diff --git a/lib/src/backend/RPCSocketServer.d.ts b/lib/src/backend/RPCSocketServer.d.ts new file mode 100644 index 0000000..70b2fa1 --- /dev/null +++ b/lib/src/backend/RPCSocketServer.d.ts @@ -0,0 +1,97 @@ +import { Socket } from "./RPCSocketServer"; +declare type rpcType = 'hook' | 'unhook' | 'call'; +export declare type Outcome = "Success" | "Error"; +export declare type Visibility = "127.0.0.1" | "0.0.0.0"; +export declare class Response { + message?: string | undefined; + constructor(message?: string | undefined); +} +export declare class SuccessResponse extends Response { + result: Outcome; + constructor(message?: string); +} +export declare class ErrorResponse extends Response { + result: Outcome; + constructor(message?: string); +} +export declare class SubscriptionResponse extends SuccessResponse { + uid: string; + constructor(uid: string, message?: string); +} +export declare type UnhookFunction = (uid: string) => Promise; +export declare type callbackFunction = (...args: any[]) => Promise; +export declare type AsyncFunction = (...args: any[]) => Promise; +export interface RPCExporter { + name: string; + exportRPCs(): socketioRPC[]; + exportPublicRPCs(): socketioRPC[]; +} +declare type baseRPC = { + type: rpcType; + name: string; +}; +declare type hookRPC = baseRPC & { + type: 'hook'; + func: callbackFunction; + unhook: UnhookFunction; +}; +declare type unhookRPC = baseRPC & { + type: 'unhook'; + func: UnhookFunction; +}; +declare type callRPC = baseRPC & { + type: 'call'; + func: (...args: any[]) => Promise; +}; +export declare type socketioRPC = callRPC | unhookRPC | hookRPC; +export declare type baseInfo = { + owner: string; + argNames: string[]; +}; +declare type HookInfo = baseRPC & baseInfo & { + type: 'hook'; + generator: (socket: any) => callbackFunction; + unhook: UnhookFunction; +}; +declare type UnhookInfo = baseRPC & baseInfo & { + type: 'unhook'; + func: UnhookFunction; +}; +declare type CallInfo = baseRPC & baseInfo & { + type: 'call'; + func: AsyncFunction; +}; +declare type RpcInfo = HookInfo | UnhookInfo | CallInfo; +export declare type ExtendedRpcInfo = RpcInfo & { + uniqueName: string; +}; +export declare const rpcToRpcinfo: (rpc: socketioRPC, owner: string) => RpcInfo; +declare 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; + fire: (rpcname: string, ...args: any[]) => Promise; + on: OnFunction; + destroy: () => void; + close: () => void; +} +export declare type RPCSocketConf = { + connectionHandler: (socket: Socket) => void; + errorHandler: (socket: Socket) => (error: any) => void; + closeHandler: (socket: Socket) => () => void; +}; +export declare class RPCSocketServer { + private port; + private rpcExporters; + private visibility; + private conf; + private io; + private wsServer; + constructor(port: number, rpcExporters?: RPCExporter[], visibility?: Visibility, conf?: RPCSocketConf); + private startWebsocket; + protected initRPCs(socket: Socket): void; + protected initPublicRPCs(socket: Socket): void; +} +export {}; diff --git a/lib/src/backend/RPCSocketServer.js b/lib/src/backend/RPCSocketServer.js new file mode 100644 index 0000000..a7db1e2 --- /dev/null +++ b/lib/src/backend/RPCSocketServer.js @@ -0,0 +1,153 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const http = require("http"); +const bsock = require("bsock"); +const uuid = require("uuid/v4"); +/* Responses */ +class Response { + constructor(message) { + this.message = message; + } +} +exports.Response = Response; +class SuccessResponse extends Response { + constructor(message) { + super(message); + this.result = "Success"; + } +} +exports.SuccessResponse = SuccessResponse; +class ErrorResponse extends Response { + constructor(message = "Unknown error") { + super(message); + this.result = "Error"; + } +} +exports.ErrorResponse = ErrorResponse; +class SubscriptionResponse extends SuccessResponse { + constructor(uid, message) { + super(message); + this.uid = uid; + } +} +exports.SubscriptionResponse = SubscriptionResponse; +exports.rpcToRpcinfo = (rpc, owner) => { + 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, + }; + } +}; +function rpcHooker(socket, exporter, makeUnique = true) { + const owner = exporter.name; + const RPCs = [...exporter.exportPublicRPCs(), ...exporter.exportRPCs()]; + const suffix = makeUnique ? "-" + uuid().substr(0, 4) : ""; + return RPCs.map(rpc => exports.rpcToRpcinfo(rpc, owner)) + .map(info => { + const ret = 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) => { + 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) => { + let fn = String(f); + let args = fn.substr(0, fn.indexOf(")")); + args = args.substr(fn.indexOf("(") + 1); + let ret = args.split(","); + return ret; +}; +class RPCSocketServer { + constructor(port, rpcExporters = [], visibility = "127.0.0.1", conf = { + errorHandler: (socket) => (error) => { socket.destroy(); console.error(error); }, + closeHandler: (socket) => () => { console.log("Socket closing"); }, + connectionHandler: (socket) => { console.log("New websocket connection in port " + socket.port); } + }) { + this.port = port; + this.rpcExporters = rpcExporters; + this.visibility = visibility; + this.conf = conf; + this.io = bsock.createServer(); + this.wsServer = http.createServer(); + this.startWebsocket(); + } + startWebsocket() { + try { + this.io.attach(this.wsServer); + this.io.on('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"); + } + } + initRPCs(socket) { + socket.hook('info', () => rpcInfos); + const rpcInfos = [ + ...this.rpcExporters.flatMap(exporter => rpcHooker(socket, exporter)) + ]; + } + initPublicRPCs(socket) { + socket.hook('info', () => rpcInfos); + const rpcInfos = [ + ...this.rpcExporters.flatMap(exporter => rpcHooker(socket, exporter)) + ]; + } +} +exports.RPCSocketServer = RPCSocketServer; diff --git a/lib/test/test.js b/lib/test/test.js index 9969bd8..2e353e6 100644 --- a/lib/test/test.js +++ b/lib/test/test.js @@ -12,7 +12,11 @@ new Server_1.Server(20000, [{ func: async (s) => s, }], }]); +<<<<<<< HEAD const caller = new Client_1.Client(20000, 'localhost'); +======= +const caller = new RPCSocket_1.RPCSocket(20000, 'localhost'); +>>>>>>> 17dc58c5b3fd3c76113d592d895400498578affa caller.connect().then(_ => { caller.info().then(console.log); caller["HelloWorldRPCGroup"].echo("x").then(console.log); diff --git a/src/backend/RPCSocketServer.ts b/src/backend/RPCSocketServer.ts new file mode 100644 index 0000000..71ca88e --- /dev/null +++ b/src/backend/RPCSocketServer.ts @@ -0,0 +1,256 @@ +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 +export type callbackFunction = (...args) => Promise +export type AsyncFunction = (...args) => Promise + +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 +} + +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, + } + } +} + + +function rpcHooker(socket: Socket, exporter:RPCExporter, makeUnique = true):ExtendedRpcInfo[]{ + const owner = exporter.name + const RPCs = [...exporter.exportPublicRPCs(), ...exporter.exportRPCs()] + 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 + fire: (rpcname:string, ...args: any[]) => Promise + 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){ + socket.hook('info', () => rpcInfos) + + const rpcInfos:ExtendedRpcInfo[] = [ + ...this.rpcExporters.flatMap(exporter => rpcHooker(socket, exporter)) + ] + } + + protected initPublicRPCs(socket:Socket){ + socket.hook('info', () => rpcInfos) + + const rpcInfos:ExtendedRpcInfo[] = [ + ...this.rpcExporters.flatMap(exporter => rpcHooker(socket, exporter)) + ] + } +} \ No newline at end of file