UNPKG

eureca.io

Version:

Nodejs transparent bidirectional remote procedure call (RPC) supporting multiple transports : WebRTC, Websocket, engine.io, faye ...etc

610 lines (437 loc) 22.2 kB
/// <reference path="transport/Primus.transport.ts" /> /// <reference path="transport/WebRTC.transport.ts" /> /// <reference path="Transport.ts" /> /// <reference path="Stub.ts" /> /// <reference path="EObject.class.ts" /> /// <reference path="Contract.class.ts" /> /** @ignore */ //declare var require: any; /** @ignore */ declare var exports: any; /** @ignore */ declare var __dirname: any; /** @ignore */ //declare var Proxy: any; var fs = require('fs'); //var EProxy = require('./EurecaProxy.class.js').Eureca.EurecaProxy; //var io = require('engine.io'); var util = require('util'); var http = require('http'); var host = ''; function getUrl(req) { var scheme = req.headers.referer !== undefined ? req.headers.referer.split(':')[0] : 'http'; host = scheme + '://' + req.headers.host; return host; } var hproxywarn = false; var clientUrl = {}; var ELog = console; module Eureca { /** * Eureca server constructor * This constructor takes an optional settings object * @constructor Server * @param {object} [settings] - have the following properties * @property {string} [settings.transport=engine.io] - can be "engine.io", "sockjs", "websockets", "faye" or "browserchannel" by default "engine.io" is used * @property {function} [settings.authenticate] - If this function is defined, the client will not be able to invoke server functions until it successfully call the client side authenticate method, which will invoke this function. * @property {function} [settings.serialize] - If defined, this function is used to serialize the request object before sending it to the client (default is JSON.stringify). This function can be useful to add custom information/meta-data to the transmitted request. * @property {function} [settings.deserialize] - If defined, this function is used to deserialize the received response string. * @example * <h4> # default instantiation</h4> * var Eureca = require('eureca.io'); * //use default transport * var server = new Eureca.Server(); * * * @example * <h4> # custom transport instantiation </h4> * var Eureca = require('eureca.io'); * //use websockets transport * var server = new Eureca.Server({transport:'websockets'}); * * @example * <h4> # Authentication </h4> * var Eureca = require('eureca.io'); * * var eurecaServer = new Eureca.Server({ * authenticate: function (authToken, next) { * console.log('Called Auth with token=', authToken); * * if (isValidToekn(authToken)) next(); // authentication success * else next('Auth failed'); //authentication fail * } * }); * * @see attach * @see getClient * * */ export class Server extends EObject { public contract: any[]; public debuglevel: number; public allowedF: any; public clients: any; private transport: any; public stub: Stub; public scriptCache: string = ''; private serialize = (v) => v; private deserialize = (v) => v; private useAuthentication:boolean; public ioServer; /** * All declared functions under this namespace become available to the clients. * @namespace Server exports * @memberOf Server * * @example * var Eureca = require('eureca.io'); * //use default transport * var server = new Eureca.Server(); * server.exports.add = function(a, b) { * return a + b; * } */ public exports: any; constructor(public settings: any = {}) { super(); //needed by primus settings.transformer = settings.transport || 'engine.io'; this.transport = Transport.get(settings.transformer); if (typeof settings.serialize == 'function' || typeof this.transport.serialize == 'function') this.serialize = settings.serialize || this.transport.serialize; settings.serialize = this.serialize; if (typeof settings.deserialize == 'function'|| typeof this.transport.deserialize == 'function') this.deserialize = settings.deserialize || this.transport.deserialize settings.deserialize = this.deserialize; this.stub = new Stub(settings); this.contract = []; this.debuglevel = settings.debuglevel || 1; //Removing need for Harmony proxies for simplification //var _exports = {}; //this.exports = Contract.proxify(_exports, this.contract); this.exports = {}; this.allowedF = []; this.clients = {}; this.useAuthentication = (typeof this.settings.authenticate == 'function'); if (this.useAuthentication) this.exports.authenticate = this.settings.authenticate; //this.registerEvents(['onConnect', 'onDisconnect', 'onMessage', 'onError']); } public onConnect(callback: (any) => void) { this.on('connect', callback); } public onDisconnect(callback: (any) => void) { this.on('disconnect', callback); } public onMessage(callback: (any) => void) { this.on('message', callback); } public onError(callback: (any) => void) { this.on('error', callback); } /** * This method is used to get the client proxy of a given connection. * it allows the server to call remote client function * * @function Server#getClient * @param {String} id - client identifier * @returns {Proxy} * * @example * //we suppose here that the clients are exposing hello() function * //onConnect event give the server an access to the client socket * server.onConnect(function (socket) { * //get client proxy by socket ID * var client = server.getClient(socket.id); * //call remote hello() function. * client.hello(); * } */ public getClient(id) { var conn = this.clients[id]; if (conn === undefined) return false; if (conn.clientProxy !== undefined) return conn.clientProxy; conn.clientProxy = {}; conn._proxy = conn.clientProxy; //this.importClientFunction(conn.client, conn, this.allowedF); this.stub.importRemoteFunction(conn.clientProxy, conn, conn.contract || this.allowedF/*, this.serialize*/); return conn.clientProxy; } /** * **!! Experimental !! **<br /> * force regeneration of client remote function signatures * this is needed if for some reason we need to dynamically update allowed client functions at runtime * @function Server#updateClientAllowedFunctions * @param {String} id - client identifier */ public updateClientAllowedFunctions(id) { var conn = this.clients[id]; if (conn === undefined) return false; conn.clientProxy = {}; conn._proxy = conn.clientProxy; //this.importClientFunction(conn.client, conn, this.allowedF); this.stub.importRemoteFunction(conn.clientProxy, conn, this.allowedF/*, this.serialize*/); } public getConnection (id) { return this.clients[id]; } private sendScript(request, response, prefix) { if (this.scriptCache != '') { response.writeHead(200); response.write(this.scriptCache); response.end(); return; } this.scriptCache = ''; if (this.transport.script){ if (this.transport.script.length < 256 && fs.existsSync(__dirname + this.transport.script)) this.scriptCache += fs.readFileSync(__dirname + this.transport.script); else this.scriptCache += this.transport.script; } this.scriptCache += '\nvar _eureca_prefix = "' + prefix + '";\n'; this.scriptCache += '\nvar _eureca_uri = "' + getUrl(request) + '";\n'; this.scriptCache += '\nvar _eureca_host = "' + getUrl(request) + '";\n'; //FIXME : override primus hardcoded pathname this.scriptCache += '\nif (typeof Primus != "undefined") Primus.prototype.pathname = "/' + prefix+'";\n'; this.scriptCache += fs.readFileSync(__dirname + '/EurecaClient.js'); response.writeHead(200); response.write(this.scriptCache); response.end(); } /** * **!! Experimental !! **<br /> * Sends exported server functions to all connected clients <br /> * This can be used if the server is designed to dynamically expose new methods. * * @function Server#updateContract */ public updateContract() { this.contract = Contract.ensureContract(this.exports, this.contract); for (var id in this.clients) { var socket = this.clients[id]; var sendObj = {}; sendObj[Eureca.Protocol.contractId] = this.contract; socket.send(this.serialize(sendObj)); } } //this function is internally used to return value for asynchronous calls in the server side private static returnFunc (result, error = null) { var retObj = {}; retObj[Eureca.Protocol.signatureId] = this['retId']; retObj[Eureca.Protocol.resultId] = result; retObj[Eureca.Protocol.errorId] = error; this['connection'].send(this['serialize'](retObj)); } private _handleServer(ioServer:IServer) { var __this = this; //ioServer.on('connection', function (socket) { ioServer.onconnect(function (eurecaClientSocket) { //socket.siggg = 'A'; eurecaClientSocket.eureca.remoteAddress = (<any>eurecaClientSocket).remoteAddress; __this.clients[eurecaClientSocket.id] = eurecaClientSocket; //Send EURECA contract var sendContract = function () { __this.contract = Contract.ensureContract(__this.exports, __this.contract); var sendObj = {}; sendObj[Eureca.Protocol.contractId] = __this.contract; if (__this.allowedF == 'all') sendObj[Eureca.Protocol.signatureId] = eurecaClientSocket.id; eurecaClientSocket.send(__this.serialize(sendObj)); } if (!__this.useAuthentication) sendContract(); //attach socket client eurecaClientSocket.clientProxy = __this.getClient(eurecaClientSocket.id); eurecaClientSocket._proxy = eurecaClientSocket.clientProxy; /** * Triggered each time a new client is connected * * @event Server#connect * @property {ISocket} socket - client socket. */ __this.trigger('connect', eurecaClientSocket); eurecaClientSocket.on('message', function (message) { /** * Triggered each time a new message is received from a client. * * @event Server#message * @property {String} message - the received message. * @property {ISocket} socket - client socket. */ __this.trigger('message', message, eurecaClientSocket); var context: any; var jobj = __this.deserialize.call(eurecaClientSocket, message); if (jobj === undefined) { __this.trigger('unhandledMessage', message, eurecaClientSocket); return; } //Handle authentication if (jobj[Eureca.Protocol.authReq] !== undefined) { if (typeof __this.settings.authenticate == 'function') { var args = jobj[Eureca.Protocol.authReq]; args.push(function (error) { if (error == null) { eurecaClientSocket.eureca.authenticated = true; sendContract(); } var authResponse = {}; authResponse[Eureca.Protocol.authResp] = [error]; eurecaClientSocket.send(__this.serialize(authResponse)); __this.trigger('authentication', error); }); var context:any = { user: { clientId: eurecaClientSocket.id }, connection: eurecaClientSocket, socket: eurecaClientSocket, request:eurecaClientSocket.request }; __this.settings.authenticate.apply(context, args); } return; } if (__this.useAuthentication && !eurecaClientSocket.eureca.authenticated) { console.log('Authentication needed for ', eurecaClientSocket.id); return; } /** Experimental : dynamic client contract*/ //if (jobj[Eureca.Protocol.contractId] !== undefined) { // socket.contract = jobj[Eureca.Protocol.contractId]; // return; //} /*****************************************/ //handle remote call if (jobj[Eureca.Protocol.functionId] !== undefined) { var context:any = { user: { clientId: eurecaClientSocket.id }, connection: eurecaClientSocket, socket: eurecaClientSocket, serialize:__this.serialize, clientProxy: eurecaClientSocket.clientProxy, async: false, retId: jobj[Eureca.Protocol.signatureId], return: Server.returnFunc }; __this.stub.invoke(context, __this, jobj, eurecaClientSocket); return; } //handle remote response if (jobj[Eureca.Protocol.signatureId] !== undefined) //invoke result { //_this.stub.doCallBack(jobj[Eureca.Protocol.signatureId], jobj[Eureca.Protocol.resultId], jobj[Eureca.Protocol.errorId]); Stub.doCallBack(jobj[Eureca.Protocol.signatureId], jobj[Eureca.Protocol.resultId], jobj[Eureca.Protocol.errorId]); return; } __this.trigger('unhandledMessage', message, eurecaClientSocket); }); eurecaClientSocket.on('error', function (e) { /** * triggered if an error occure. * * @event Server#error * @property {String} error - the error message * @property {ISocket} socket - client socket. */ __this.trigger('error', e, eurecaClientSocket); }); eurecaClientSocket.on('close', function () { /** * triggered when the client is disconneced. * * @event Server#disconnect * @property {ISocket} socket - client socket. */ __this.trigger('disconnect', eurecaClientSocket); delete __this.clients[eurecaClientSocket.id]; }); eurecaClientSocket.on('stateChange', function (s) { __this.trigger('stateChange', s); }); }); } /** * Sends exported server functions to all connected clients <br /> * This can be used if the server is designed to dynamically expose new methods. * * @function attach * @memberof Server# * @param {appServer} - a nodejs {@link https://nodejs.org/api/http.html#http_class_http_server|nodejs http server} * or {@link http://expressjs.com/api.html#application|expressjs Application} * */ public attach (appServer:any) { var __this = this; var app = undefined; //is it express application ? if (appServer._events && appServer._events.request !== undefined && appServer.routes === undefined && appServer._events.request.on) app = appServer._events.request; //is standard http server ? if (app === undefined && appServer instanceof http.Server) app = appServer //not standard http server nor express app ==> try to guess http.Server instance if (app === undefined) { var keys = Object.getOwnPropertyNames(appServer); for (let k of keys) { if (appServer[k] instanceof http.Server) { //got it ! app = appServer[k]; break; } } } //this._checkHarmonyProxies(); appServer.eurecaServer = this; this.allowedF = this.settings.allow || []; var _prefix = this.settings.prefix || 'eureca.io'; var _clientUrl = this.settings.clientScript || '/eureca.js'; var _transformer = this.settings.transformer; var _parser = this.settings.parser; //initialising server //var ioServer = io.attach(server, { path: '/'+_prefix }); this.ioServer = this.transport.createServer(appServer, { prefix: _prefix, transformer:_transformer, parser:_parser }); //console.log('Primus ? ', ioServer.primus); //var scriptLib = (typeof ioServer.primus == 'function') ? ioServer.primus.library() : null; this._handleServer(this.ioServer); //install on express //sockjs_server.installHandlers(server, {prefix:_prefix}); if (app.get && app.post) //TODO : better way to detect express { app.get(_clientUrl, function (request, response) { __this.sendScript(request, response, _prefix); }); } else //Fallback to nodejs { app.on('request', function (request, response) { if (request.method === 'GET') { if (request.url.split('?')[0] === _clientUrl) { __this.sendScript(request, response, _prefix); } } }); } //console.log('>>>>>>>>>>>> ', app.get); //Workaround : nodejs 0.10.0 have a strange behaviour making remoteAddress unavailable when connecting from a nodejs client appServer.on('request', function (request, response) { if (!request.query) return; var id = request.query.sid; var client = __this.clients[request.query.sid]; if (client) { client.eureca = client.eureca || {}; client.eureca.remoteAddress = client.eureca.remoteAddress || request.socket.remoteAddress; client.eureca.remotePort = client.eureca.remotePort || request.socket.remotePort; } //req.eureca = { // remoteAddress: req.socket.remoteAddress, // remotePort: req.socket.remotePort //} }); } } } exports.Eureca = Eureca;