This commit is contained in:
Daniel Hübleitner
2019-09-18 02:58:51 +02:00
parent 2f32d7c9cd
commit 76dee64a54
29 changed files with 567 additions and 596 deletions
+8 -14
View File
@@ -1,20 +1,14 @@
import { ExtendedRpcInfo, UnhookFunction, callbackFunction, AsyncFunction, Socket } from "../backend/RPCSocketServer";
var bsock = require('bsock')
import { ExtendedRpcInfo, UnhookFunction, CallbackFunction, AsyncFunction } from "./Types";
import { Socket } from './interfaces/Socket'
//fix args with defaults like "force = true" -> "force"
function stripAfterEquals(str:string){
return str.split("=")[0]
}
/**
* Dynamic library to communicate with FrontblockService remotely
*
* This will be automatically injected into the webpages served by FrontblockService
* Will ask it's service for available RPCs and parse them into methods of this object
* for convenient access.
*/
export class RPCSocket implements Socket{
export class Client implements Socket{
private socket: Socket
constructor(public port:number, private server: string, private tls: boolean = false){
}
@@ -54,13 +48,13 @@ export class RPCSocket implements Socket{
info.forEach(i => {
let f: any
switch (i.type) {
case 'call':
case 'Call':
f = this.callGenerator(i.uniqueName, i.argNames)
break
case 'hook':
case 'Hook':
f = this.hookGenerator(i.uniqueName, i.argNames)
break
case 'unhook':
case 'Unhook':
f = this.unhookGenerator(i.uniqueName, i.argNames)
break
}
@@ -81,7 +75,7 @@ export class RPCSocket implements Socket{
return eval( '( () => async ('+headerArgs+') => { return await this.socket.call("'+fnName+'", '+argParams+')} )()' )
}
private hookGenerator(fnName, fnArgs:string[]): callbackFunction{
private hookGenerator(fnName, fnArgs:string[]): CallbackFunction{
const headerArgs = fnArgs.join(",")
const argParams = fnArgs.map(stripAfterEquals).join(",")
return eval( `( () => async (`+headerArgs+(headerArgs.length!==0?",":"")+` callback) => {
+38
View File
@@ -0,0 +1,38 @@
export type Outcome = "Success" | "Error"
/* Responses */
export class Response{
constructor(
public message?:string
){}
}
export class SuccessResponse extends Response{
result:Outcome = "Success"
constructor(
message?:string
){
super(message)
}
}
export class ErrorResponse extends Response{
result:Outcome = "Error"
constructor(
message: string = "Unknown error"
){
super(message)
}
}
export class SubscriptionResponse extends SuccessResponse{
constructor(
public uid: string,
message?:string
){
super(message)
}
}
+60
View File
@@ -0,0 +1,60 @@
import http = require('http');
import bsock = require('bsock');
import { ExtendedRpcInfo, SocketConf, SocketioRPC } from './Types';
import { rpcHooker } from './Util';
import { Exporter } from './interfaces/Exporter';
import { Socket } from "./interfaces/Socket";
export class Server{
private io = bsock.createServer()
private wsServer = http.createServer()
constructor(
private port:number,
private rpcExporters: Exporter[] = [],
private conf: SocketConf = {
errorHandler: (socket:Socket) => (error:any) => { socket.destroy(); console.error(error) },
closeHandler: (socket:Socket) => () => { console.log("Socket closing") },
connectionHandler: (socket:Socket) => { console.log("New websocket connection in port "+socket.port)},
visibility: "127.0.0.1"
}
){
this.startWebsocket()
}
private startWebsocket(){
try{
this.io.attach(this.wsServer)
this.io.on('socket', (socket:Socket) => {
socket.on('error', this.conf.errorHandler(socket))
socket.on('close', this.conf.closeHandler(socket))
if(this.conf.visibility === "127.0.0.1")
this.initRPCs(socket)
else
this.initPublicRPCs(socket)
})
this.wsServer.listen(this.port, this.conf.visibility)
}catch(e){
//@ts-ignore
this.errorHandler(undefined)("Unable to connect to socket")
}
}
protected initRPCs(socket:Socket){
const infoRPC:SocketioRPC[] = [
{
name: 'info',
type: 'Call',
func: async () => rpcInfos
}
]
const rpcInfos:ExtendedRpcInfo[] = [
...rpcHooker(socket, "RPC", infoRPC, false),
...this.rpcExporters.flatMap(exporter => rpcHooker(socket, exporter.name, [...exporter.exportPublicRPCs(), ...exporter.exportRPCs()]))
]
}
protected initPublicRPCs(socket:Socket){}
}
+66
View File
@@ -0,0 +1,66 @@
import { SuccessResponse, ErrorResponse, SubscriptionResponse } from "./Response";
import { Socket } from "./interfaces/Socket";
export type Visibility = "127.0.0.1" | "0.0.0.0"
export type Name = string
export type SocketConf = {
connectionHandler: (socket:Socket) => void
errorHandler: (socket:Socket) => (error:any) => void
closeHandler: (socket:Socket) => () => void
visibility: Visibility
}
export type rpcType = 'Hook' | 'Unhook' | 'Call'
export type BaseRPC = {
type: rpcType
name: string
}
export type HookRPC = BaseRPC & {
type: 'Hook'
func: CallbackFunction
unhook: UnhookFunction
}
export type UnhookRPC = BaseRPC & {
type: 'Unhook'
func: UnhookFunction
}
export type CallRPC = BaseRPC & {
type: 'Call'
func: (...args) => Promise<any>
}
export type SocketioRPC = CallRPC | UnhookRPC | HookRPC
export type BaseInfo = {
owner: string,
argNames: string[],
}
export type HookInfo = BaseRPC & BaseInfo & {
type: 'Hook',
generator: (socket) => CallbackFunction
unhook: UnhookFunction
}
export type UnhookInfo = BaseRPC & BaseInfo & {
type: 'Unhook',
func: UnhookFunction
}
export type CallInfo = BaseRPC & BaseInfo & {
type: 'Call',
func: AsyncFunction
}
export type RpcInfo = HookInfo | UnhookInfo | CallInfo
export type ExtendedRpcInfo = RpcInfo & { uniqueName: string }
export type OnFunction = (type: 'error' | 'close', f: (e?:any)=>void) => Socket
export type UnhookFunction = (uid:string) => Promise<SuccessResponse | ErrorResponse>
export type CallbackFunction = (...args) => Promise<SubscriptionResponse | ErrorResponse>
export type AsyncFunction = (...args) => Promise<any>
+82
View File
@@ -0,0 +1,82 @@
import * as uuid from "uuid/v4"
import { HookRPC, HookInfo, SocketioRPC, RpcInfo, ExtendedRpcInfo } from "./Types";
import { Socket } from "./interfaces/Socket";
export const rpcToRpcinfo = (rpc : SocketioRPC, owner: string):RpcInfo => {
switch(rpc.type){
case "Call" :
return {
owner: owner,
argNames: extractArgs(rpc.func),
type: rpc.type,
name: rpc.name,
func: rpc.func,
}
case "Unhook" :
return {
owner: owner,
argNames: extractArgs(rpc.func),
type: rpc.type,
name: rpc.name,
func: rpc.func,
}
case "Hook" :
const generator = hookGenerator(rpc)
return {
owner: owner,
argNames: extractArgs(generator(undefined)),
type: rpc.type,
name: rpc.name,
unhook: rpc.unhook,
generator: generator,
}
}
}
export const rpcHooker = (socket: Socket, owner:string, RPCs: SocketioRPC[], makeUnique = true):ExtendedRpcInfo[] => {
const suffix = makeUnique?"-"+uuid().substr(0,4):""
return RPCs.map(rpc => rpcToRpcinfo(rpc, owner))
.map(info => {
const ret:any = info
ret.uniqueName = info.name+suffix
switch(info.type){
case "Hook":
socket.hook(ret.uniqueName, info.generator(socket))
break;
default:
socket.hook(ret.uniqueName, info.func)
}
socket.on('close', () => socket.unhook(info.name))
return ret
})
}
const hookGenerator = (rpc:HookRPC): HookInfo['generator'] => {
const argsArr = extractArgs(rpc.func)
argsArr.pop()
const args = argsArr.join(',')
return eval(`(socket) => async (`+args+`) => {
const res = await rpc.func(`+args+(args.length!==0?',':'')+` (x) => {
socket.call(res.uid, x)
})
if(res.result == 'Success'){
socket.on('close', async () => {
const unhookRes = await rpc.unhook(res.uid)
console.log("Specific close handler for", rpc.name, res.uid, unhookRes)
})
}
return res
}`)
}
const extractArgs = (f:Function):string[] => {
let fn = String(f)
let args = fn.substr(0, fn.indexOf(")"))
args = args.substr(fn.indexOf("(")+1)
let ret = args.split(",")
return ret
}
-267
View File
@@ -1,267 +0,0 @@
import http = require('http');
import bsock = require('bsock');
import * as uuid from "uuid/v4"
import { Socket } from "./RPCSocketServer"
type rpcType = 'hook' | 'unhook' | 'call'
export type Outcome = "Success" | "Error"
export type Visibility = "127.0.0.1" | "0.0.0.0"
/* Responses */
export class Response{
constructor(
public message?:string
){}
}
export class SuccessResponse extends Response{
result:Outcome = "Success"
constructor(
message?:string
){
super(message)
}
}
export class ErrorResponse extends Response{
result:Outcome = "Error"
constructor(
message: string = "Unknown error"
){
super(message)
}
}
export class SubscriptionResponse extends SuccessResponse{
constructor(
public uid: string,
message?:string
){
super(message)
}
}
export type UnhookFunction = (uid:string) => Promise<SuccessResponse | ErrorResponse>
export type callbackFunction = (...args) => Promise<SubscriptionResponse | ErrorResponse>
export type AsyncFunction = (...args) => Promise<any>
export interface RPCExporter{
name: string
exportRPCs() : socketioRPC[]
exportPublicRPCs() : socketioRPC[]
}
type baseRPC = {
type: rpcType
name: string
}
type hookRPC = baseRPC & {
type: 'hook'
func: callbackFunction
unhook: UnhookFunction
}
type unhookRPC = baseRPC & {
type: 'unhook'
func: UnhookFunction
}
type callRPC = baseRPC & {
type: 'call'
func: (...args) => Promise<any>
}
export type socketioRPC = callRPC | unhookRPC | hookRPC
export type baseInfo = {
owner: string,
argNames: string[],
}
type HookInfo = baseRPC & baseInfo & {
type: 'hook',
generator: (socket) => callbackFunction
unhook: UnhookFunction
}
type UnhookInfo = baseRPC & baseInfo & {
type: 'unhook',
func: UnhookFunction
}
type CallInfo = baseRPC & baseInfo & {
type: 'call',
func: AsyncFunction
}
type RpcInfo = HookInfo | UnhookInfo | CallInfo
export type ExtendedRpcInfo = RpcInfo & { uniqueName: string }
export const rpcToRpcinfo = (rpc : socketioRPC, owner: string):RpcInfo => {
switch(rpc.type){
case "call" :
return {
owner: owner,
argNames: extractArgs(rpc.func),
type: rpc.type,
name: rpc.name,
func: rpc.func,
}
case "unhook" :
return {
owner: owner,
argNames: extractArgs(rpc.func),
type: rpc.type,
name: rpc.name,
func: rpc.func,
}
case "hook" :
const generator = hookGenerator(rpc)
return {
owner: owner,
argNames: extractArgs(generator(undefined)),
type: rpc.type,
name: rpc.name,
unhook: rpc.unhook,
generator: generator,
}
}
}
export const rpcHooker = (socket: Socket, owner:string, RPCs: socketioRPC[], makeUnique = true):ExtendedRpcInfo[] => {
const suffix = makeUnique?"-"+uuid().substr(0,4):""
return RPCs.map(rpc => rpcToRpcinfo(rpc, owner))
.map(info => {
const ret:any = info
ret.uniqueName = info.name+suffix
switch(info.type){
case "hook":
socket.hook(ret.uniqueName, info.generator(socket))
break;
default:
socket.hook(ret.uniqueName, info.func)
}
socket.on('close', () => socket.unhook(info.name))
return ret
})
}
const hookGenerator = (rpc:hookRPC): HookInfo['generator'] => {
const argsArr = extractArgs(rpc.func)
argsArr.pop()
const args = argsArr.join(',')
return eval(`(socket) => async (`+args+`) => {
const res = await rpc.func(`+args+(args.length!==0?',':'')+` (x) => {
socket.call(res.uid, x)
})
if(res.result == 'Success'){
socket.on('close', async () => {
const unhookRes = await rpc.unhook(res.uid)
console.log("Specific close handler for", rpc.name, res.uid, unhookRes)
})
}
return res
}`)
}
const extractArgs = (f:Function):string[] => {
let fn = String(f)
let args = fn.substr(0, fn.indexOf(")"))
args = args.substr(fn.indexOf("(")+1)
let ret = args.split(",")
return ret
}
type OnFunction = (type: 'error' | 'close', f: (e?:any)=>void) => Socket
export interface Socket {
port: number
hook: (rpcname: string, ...args: any[]) => Socket
unhook: (rpcname:string) => Socket
call: (rpcname:string, ...args: any[]) => Promise<any>
fire: (rpcname:string, ...args: any[]) => Promise<any>
on: OnFunction
destroy: ()=>void
close: ()=>void
}
export type RPCSocketConf = {
connectionHandler: (socket:Socket) => void
errorHandler: (socket:Socket) => (error:any) => void
closeHandler: (socket:Socket) => () => void
}
export class RPCSocketServer{
private io = bsock.createServer()
private wsServer = http.createServer()
constructor(
private port:number,
private rpcExporters: RPCExporter[] = [],
private visibility: Visibility = "127.0.0.1",
private conf: RPCSocketConf = {
errorHandler: (socket:Socket) => (error:any) => { socket.destroy(); console.error(error) },
closeHandler: (socket:Socket) => () => { console.log("Socket closing") },
connectionHandler: (socket:Socket) => { console.log("New websocket connection in port "+socket.port) }
}
){
this.startWebsocket()
}
private startWebsocket(){
try{
this.io.attach(this.wsServer)
this.io.on('socket', (socket:Socket) => {
socket.on('error', this.conf.errorHandler(socket))
socket.on('close', this.conf.closeHandler(socket))
if(this.visibility === "127.0.0.1")
this.initRPCs(socket)
else
this.initPublicRPCs(socket)
})
this.wsServer.listen(this.port, this.visibility)
}catch(e){
//@ts-ignore
this.errorHandler(undefined)("Unable to connect to socket")
}
}
protected initRPCs(socket:Socket){
const infoRPC:socketioRPC[] = [
{
name: 'info',
type: 'call',
func: async () => rpcInfos
}
]
const rpcInfos:ExtendedRpcInfo[] = [
...rpcHooker(socket, "RPC", infoRPC, false),
...this.rpcExporters.flatMap(exporter => rpcHooker(socket, exporter.name, [...exporter.exportPublicRPCs(), ...exporter.exportRPCs()]))
]
}
protected initPublicRPCs(socket:Socket){
const adminRPCs:socketioRPC[] = [
{
name: 'info',
type: 'call',
func: async () => rpcInfos
}
]
const rpcInfos:ExtendedRpcInfo[] = [
...rpcHooker(socket, "Admin", adminRPCs, false),
...this.rpcExporters.flatMap(exporter => rpcHooker(socket, exporter.name, exporter.exportPublicRPCs()))
]
}
}
+7
View File
@@ -0,0 +1,7 @@
import { SocketioRPC, Name } from "../Types";
export interface Exporter{
name: Name
exportRPCs() : SocketioRPC[]
exportPublicRPCs() : SocketioRPC[]
}
+12
View File
@@ -0,0 +1,12 @@
import { OnFunction } from "../Types";
export interface Socket {
port: number
hook: (rpcname: string, ...args: any[]) => Socket
unhook: (rpcname:string) => Socket
call: (rpcname:string, ...args: any[]) => Promise<any>
fire: (rpcname:string, ...args: any[]) => Promise<any>
on: OnFunction
destroy: ()=>void
close: ()=>void
}
@@ -4,10 +4,10 @@ const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
mode: 'production',
target: "web",
entry: path.resolve(__dirname, 'RPCSocket.ts'),
entry: path.resolve(__dirname, 'Client.ts'),
output: {
path: path.resolve(__dirname, '../../lib'),
filename: 'RPCaller.min.js',
filename: 'Frontend.min.js',
libraryTarget: 'commonjs',
},
resolve: {