├── .gitignore ├── .npmignore ├── .travis.yml ├── .mocharc.json ├── .jshintrc ├── src ├── jsonrpc.js ├── event-emitter.js ├── websocket-connection.js ├── error.js ├── http-server-connection.js ├── socket-connection.js ├── endpoint.js ├── connection.js ├── client.js └── server.js ├── examples ├── stream-server.js ├── stream-client.js ├── server.js └── client.js ├── LICENSE ├── package.json ├── README.md └── test └── jsonrpc-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | coverage -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | coverage -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "lts/*" 5 | - "10" -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": "list", 3 | "ui": "exports", 4 | "check-leaks": true 5 | } -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise" : true, 3 | "curly" : true, 4 | "eqeqeq" : true, 5 | "forin" : true, 6 | "noarg" : true, 7 | "noempty" : true, 8 | "nonew" : true, 9 | "undef" : true, 10 | "unused" : true, 11 | "devel" : true, 12 | "node" : true, 13 | "sub" : true, 14 | "esversion": 6, 15 | "quotmark": "single" 16 | } -------------------------------------------------------------------------------- /src/jsonrpc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports._ = require('lodash'); 4 | exports.ES5Class = require('es5class'); 5 | exports.Websocket = require('faye-websocket'); 6 | exports.Error = require('./error.js')(exports); 7 | 8 | exports.EventEmitter = require('./event-emitter.js')(exports); 9 | 10 | exports.Endpoint = require('./endpoint.js')(exports); 11 | exports.Connection = require('./connection.js')(exports); 12 | 13 | exports.HttpServerConnection = require('./http-server-connection.js')(exports); 14 | exports.SocketConnection = require('./socket-connection.js')(exports); 15 | exports.WebSocketConnection = require('./websocket-connection.js')(exports); 16 | 17 | 18 | exports.Server = require('./server.js')(exports); 19 | exports.Client = require('./client.js')(exports); 20 | -------------------------------------------------------------------------------- /src/event-emitter.js: -------------------------------------------------------------------------------- 1 | module.exports = function (classes){ 2 | 'use strict'; 3 | 4 | var debug = require('debug')('jsonrpc'); 5 | 6 | var EventEmitter = classes.ES5Class.$define('EventEmitter', {}, { 7 | /** 8 | * Output a piece of debug information. 9 | */ 10 | trace : function (direction, message){ 11 | var msg = ' ' + direction + ' ' + message; 12 | debug(msg); 13 | return msg; 14 | }, 15 | /** 16 | * Check if current request has an id adn it is of type integer (non fractional) or string. 17 | * 18 | * @param {Object} request 19 | * @return {Boolean} 20 | */ 21 | hasId : function (request) { 22 | return request && typeof request['id'] !== 'undefined' && 23 | ( 24 | (typeof(request['id']) === 'number' && /^\-?\d+$/.test(request['id'])) || 25 | (typeof(request['id']) === 'string') || (request['id'] === null) 26 | ); 27 | } 28 | }).$inherit(require('events').EventEmitter, []); 29 | 30 | return EventEmitter; 31 | }; 32 | 33 | -------------------------------------------------------------------------------- /examples/stream-server.js: -------------------------------------------------------------------------------- 1 | var rpc = require('../src/jsonrpc'); 2 | var events = require('events'); 3 | 4 | var server = rpc.Server.$create(); 5 | 6 | server.on('error', function (err){ 7 | console.log(err.toString()); 8 | }); 9 | 10 | // Create a message bus with random events on it 11 | var firehose = new events.EventEmitter(); 12 | (function emitFirehoseEvent(){ 13 | firehose.emit('foobar', {data: 'random ' + Math.random()}); 14 | setTimeout(emitFirehoseEvent, 200 + Math.random() * 3000); 15 | })(); 16 | 17 | var listen = function (args, opts, callback){ 18 | var handleFirehoseEvent = function (event){ 19 | opts.call('event', event.data); 20 | }; 21 | 22 | firehose.on('foobar', handleFirehoseEvent); 23 | opts.stream(function (){ 24 | console.log('connection ended'); 25 | firehose.removeListener('foobar', handleFirehoseEvent); 26 | }); 27 | callback(null); 28 | }; 29 | 30 | server.expose('listen', listen); 31 | 32 | /* HTTP server on port 8088 */ 33 | server.listen(8088, 'localhost'); 34 | 35 | /* Raw socket server on port 8089 */ 36 | server.listenRaw(8089, 'localhost'); 37 | -------------------------------------------------------------------------------- /src/websocket-connection.js: -------------------------------------------------------------------------------- 1 | module.exports = function (classes){ 2 | 'use strict'; 3 | 4 | var 5 | Connection = classes.Connection, 6 | 7 | /** 8 | * Websocket connection. 9 | * 10 | * Socket connections are mostly symmetric, so we are using a single class for 11 | * representing both the server and client perspective. 12 | */ 13 | WebSocketConnection = Connection.$define('WebSocketConnection', { 14 | construct: function ($super, endpoint, conn){ 15 | var self = this; 16 | 17 | $super(endpoint); 18 | 19 | self.conn = conn; 20 | self.ended = false; 21 | 22 | self.conn.on('close', function websocketClose(hadError){ 23 | self.emit('close', hadError); 24 | }); 25 | }, 26 | write: function(data){ 27 | if (!this.conn.writable) { 28 | // Other side disconnected, we'll quietly fail 29 | return; 30 | } 31 | 32 | this.conn.write(data); 33 | }, 34 | end: function(){ 35 | this.conn.close(); 36 | this.ended = true; 37 | } 38 | }); 39 | 40 | return WebSocketConnection; 41 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2009 Eric Florenzano and 2 | Ryan Tomayko 3 | 4 | Permission is hereby granted, free of charge, to any person ob- 5 | taining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without restric- 7 | tion, including without limitation the rights to use, copy, modi- 8 | fy, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is fur- 10 | nished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONIN- 18 | FRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 20 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /src/error.js: -------------------------------------------------------------------------------- 1 | module.exports = function (classes){ 2 | 'use strict'; 3 | 4 | /* 5 | JSON-RPC 2.0 Specification Errors codes by dcharbonnier 6 | */ 7 | var Errors = {}; 8 | 9 | Errors.AbstractError = classes.ES5Class.$define('AbstractError', { 10 | construct: function(message, extra){ 11 | this.name = this.$className; 12 | this.extra = extra || {}; 13 | this.message = message || this.$className; 14 | 15 | Error.captureStackTrace(this, this.$class); 16 | }, 17 | toString: function(){ 18 | return this.message; 19 | } 20 | }).$inherit(Error, []); 21 | 22 | Errors.ParseError = Errors.AbstractError.$define('ParseError', { 23 | code: -32700 24 | }); 25 | 26 | Errors.InvalidRequest = Errors.AbstractError.$define('InvalidRequest', { 27 | code: -32600 28 | }); 29 | 30 | Errors.MethodNotFound = Errors.AbstractError.$define('MethodNotFound', { 31 | code: -32601 32 | }); 33 | 34 | Errors.InvalidParams = Errors.AbstractError.$define('InvalidParams', { 35 | code: -32602 36 | }); 37 | 38 | Errors.InternalError = Errors.AbstractError.$define('InternalError', { 39 | code: -32603 40 | }); 41 | 42 | Errors.ServerError = Errors.AbstractError.$define('ServerError', { 43 | code: -32000 44 | }); 45 | 46 | return Errors; 47 | }; -------------------------------------------------------------------------------- /src/http-server-connection.js: -------------------------------------------------------------------------------- 1 | module.exports = function (classes){ 2 | 'use strict'; 3 | 4 | var 5 | Connection = classes.Connection, 6 | HttpServerConnection = Connection.$define('HttpServerConnection', { 7 | construct: function ($super, server, req, res){ 8 | var self = this; 9 | 10 | $super(server); 11 | 12 | this.req = req; 13 | this.res = res; 14 | this.isStreaming = false; 15 | 16 | this.res.on('finish', function responseEnd(){ 17 | self.emit('end'); 18 | }); 19 | 20 | this.res.on('close', function responseEnd(){ 21 | self.emit('end'); 22 | }); 23 | }, 24 | 25 | /** 26 | * Can be called before the response callback to keep the connection open. 27 | */ 28 | stream: function ($super, onend){ 29 | $super(onend); 30 | 31 | this.isStreaming = true; 32 | }, 33 | 34 | /** 35 | * Send the client additional data. 36 | * 37 | * An HTTP connection can be kept open and additional RPC calls sent through if 38 | * the client supports it. 39 | */ 40 | write: function (data){ 41 | if (!this.isStreaming) { 42 | throw new Error('Cannot send extra messages via non - streaming HTTP'); 43 | } 44 | 45 | if (!this.res.connection.writable) { 46 | // Client disconnected, we'll quietly fail 47 | return; 48 | } 49 | 50 | this.res.write(data); 51 | } 52 | }); 53 | 54 | return HttpServerConnection; 55 | }; -------------------------------------------------------------------------------- /examples/stream-client.js: -------------------------------------------------------------------------------- 1 | var rpc = require('../src/jsonrpc'); 2 | 3 | /* 4 | Each uses a different syntax 5 | Responses can not be assigned to a source request if the socket version 6 | ... it sucks 7 | Moreover jsonrpc2 knows no stream ... it may be helpful, but it is not JSON-RPC2 8 | 9 | Michal 10 | */ 11 | 12 | /* 13 | Connect to HTTP server 14 | */ 15 | var client = rpc.Client.$create(8088, 'localhost'); 16 | 17 | client.stream('listen', [], function (err, connection){ 18 | if (err) { 19 | return printError(err); 20 | } 21 | var counter = 0; 22 | connection.expose('event', function (params){ 23 | console.log('Streaming #' + counter + ': ' + params[0]); 24 | counter++; 25 | if (counter > 4) { 26 | connection.end(); 27 | } 28 | }); 29 | console.log('start listening'); 30 | }); 31 | 32 | /* 33 | Connect to Raw socket server 34 | */ 35 | var socketClient = rpc.Client.$create(8089, 'localhost'); 36 | 37 | socketClient.connectSocket(function (err, conn){ 38 | if (err) { 39 | return printError(err); 40 | } 41 | var counter = 0; 42 | socketClient.expose('event', function (params){ 43 | console.log('Streaming (socket) #' + counter + ': ' + params[0]); 44 | counter++; 45 | if (counter > 4) { 46 | conn.end(); 47 | } 48 | }); 49 | 50 | conn.call('listen', [], function (err){ 51 | if (err) { 52 | return printError(err); 53 | } 54 | }); 55 | }); 56 | 57 | function printError(err){ 58 | console.error('RPC Error: ' + err.toString()); 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "json-rpc2", 3 | "version": "2.0.0", 4 | "description": "JSON-RPC 2.0 server and client library, with HTTP, TCP and Websocket endpoints", 5 | "main": "./src/jsonrpc.js", 6 | "keywords": [ 7 | "json", 8 | "rpc", 9 | "rpc2", 10 | "json-rpc", 11 | "json-rpc2", 12 | "jsonrpc", 13 | "jsonrpc2", 14 | "server", 15 | "client", 16 | "tcp", 17 | "websocket", 18 | "http" 19 | ], 20 | "license": "MIT", 21 | "author": { 22 | "name": "Eric Florenzano", 23 | "url": "eflorenzano.com" 24 | }, 25 | "maintainers": [ 26 | { 27 | "name": "Paulo Cesar", 28 | "url": "https://github.com/pocesar" 29 | } 30 | ], 31 | "dependencies": { 32 | "jsonparse": "^1.3.1", 33 | "debug": "^4.1.1", 34 | "lodash": "^4.17.15", 35 | "es5class": "^2.3.1", 36 | "faye-websocket": "^0.11.3", 37 | "object-assign": "^4.1.1" 38 | }, 39 | "engines": { 40 | "node": ">= 10" 41 | }, 42 | "contributors": [ 43 | "Bill Casarin (jb55.com)", 44 | "Stefan Thomas (justmoon.net)", 45 | "Paulo Cesar (github.com/pocesar)" 46 | ], 47 | "repository": { 48 | "type": "git", 49 | "url": "git://github.com/pocesar/node-jsonrpc2.git" 50 | }, 51 | "bugs": { 52 | "url": "https://github.com/pocesar/node-jsonrpc2/issues" 53 | }, 54 | "devDependencies": { 55 | "mocha": "^7.1.1", 56 | "expect.js": "^0.3.1", 57 | "jshint": "^2.11.0", 58 | "istanbul": "^0.4.5", 59 | "@types/node": "^13.13.0" 60 | }, 61 | "scripts": { 62 | "test": "jshint examples src test && mocha test/jsonrpc-test.js", 63 | "coverage": "node ./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha -- -t 5000 test/jsonrpc-test.js" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | var rpc = require('../src/jsonrpc'); 2 | 3 | var server = rpc.Server.$create({ 4 | websocket: true 5 | }); 6 | 7 | server.on('error', function (err){ 8 | console.log(err); 9 | }); 10 | 11 | /* Expose two simple functions */ 12 | server.expose('add', function (args, opts, callback){ 13 | callback(null, args[0] + args[1]); 14 | } 15 | ); 16 | 17 | server.expose('multiply', function (args, opts, callback){ 18 | callback(null, args[0] * args[1]); 19 | } 20 | ); 21 | 22 | /* We can expose entire modules easily */ 23 | server.exposeModule('math', { 24 | power: function (args, opts, callback){ 25 | callback(null, Math.pow(args[0], args[1])); 26 | }, 27 | sqrt : function (args, opts, callback){ 28 | callback(null, Math.sqrt(args[0])); 29 | } 30 | }); 31 | 32 | /* By using a callback, we can delay our response indefinitely, leaving the 33 | request hanging until the callback emits success. */ 34 | server.exposeModule('delayed', { 35 | echo: function (args, opts, callback){ 36 | var data = args[0]; 37 | var delay = args[1]; 38 | setTimeout(function (){ 39 | callback(null, data); 40 | }, delay); 41 | }, 42 | 43 | add: function (args, opts, callback){ 44 | var first = args[0]; 45 | var second = args[1]; 46 | var delay = args[2]; 47 | setTimeout(function (){ 48 | callback(null, first + second); 49 | }, delay); 50 | } 51 | } 52 | ); 53 | 54 | // or server.enableAuth('myuser', 'secret123'); 55 | server.enableAuth(function(user, password){ 56 | return user === 'myuser' && password === 'secret123'; 57 | }); 58 | 59 | /* HTTP/Websocket server on port 8088 */ 60 | server.listen(8088, 'localhost'); 61 | 62 | /* Raw socket server on port 8089 */ 63 | server.listenRaw(8089, 'localhost'); 64 | 65 | /* can handle everything in one port using */ 66 | // server.listenHybrid(8888, 'localhost'); 67 | -------------------------------------------------------------------------------- /src/socket-connection.js: -------------------------------------------------------------------------------- 1 | module.exports = function (classes){ 2 | 'use strict'; 3 | 4 | var 5 | Connection = classes.Connection, 6 | 7 | /** 8 | * Socket connection. 9 | * 10 | * Socket connections are mostly symmetric, so we are using a single class for 11 | * representing both the server and client perspective. 12 | */ 13 | SocketConnection = Connection.$define('SocketConnection', { 14 | construct: function ($super, endpoint, conn){ 15 | var self = this; 16 | 17 | $super(endpoint); 18 | 19 | self.conn = conn; 20 | self.autoReconnect = true; 21 | self.ended = true; 22 | 23 | self.conn.on('connect', function socketConnect(){ 24 | self.emit('connect'); 25 | }); 26 | 27 | self.conn.on('end', function socketEnd(){ 28 | self.emit('end'); 29 | }); 30 | 31 | self.conn.on('error', function socketError(event){ 32 | self.emit('error', event); 33 | }); 34 | 35 | self.conn.on('close', function socketClose(hadError){ 36 | self.emit('close', hadError); 37 | 38 | if ( 39 | self.endpoint.$className === 'Client' && 40 | self.autoReconnect && !self.ended 41 | ) { 42 | if (hadError) { 43 | // If there was an error, we'll wait a moment before retrying 44 | setTimeout(function reconnectTimeout(){ 45 | self.reconnect(); 46 | }, 200); 47 | } else { 48 | self.reconnect(); 49 | } 50 | } 51 | }); 52 | }, 53 | write : function (data){ 54 | if (!this.conn.writable) { 55 | // Other side disconnected, we'll quietly fail 56 | return; 57 | } 58 | 59 | this.conn.write(data); 60 | }, 61 | 62 | end: function (){ 63 | this.ended = true; 64 | this.conn.end(); 65 | }, 66 | 67 | reconnect: function (){ 68 | this.ended = false; 69 | if (this.endpoint.$className === 'Client') { 70 | this.conn.connect(this.endpoint.port, this.endpoint.host); 71 | } else { 72 | throw new Error('Cannot reconnect a connection from the server-side.'); 73 | } 74 | } 75 | }); 76 | 77 | return SocketConnection; 78 | }; -------------------------------------------------------------------------------- /src/endpoint.js: -------------------------------------------------------------------------------- 1 | module.exports = function (classes){ 2 | 'use strict'; 3 | 4 | var 5 | _ = classes._, 6 | EventEmitter = classes.EventEmitter, 7 | Error = classes.Error, 8 | /** 9 | * Abstract base class for RPC endpoints. 10 | * 11 | * Has the ability to register RPC events and expose RPC methods. 12 | */ 13 | Endpoint = EventEmitter.$define('Endpoint', { 14 | construct : function ($super){ 15 | $super(); 16 | 17 | this.functions = {}; 18 | this.scopes = {}; 19 | this.defaultScope = this; 20 | this.exposeModule = this.expose; 21 | this.on('error', () => { }); 22 | }, 23 | /** 24 | * Define a callable method on this RPC endpoint 25 | */ 26 | expose : function (name, func, scope){ 27 | if (_.isFunction(func)) { 28 | EventEmitter.trace('***', 'exposing: ' + name); 29 | this.functions[name] = func; 30 | 31 | if (scope) { 32 | this.scopes[name] = scope; 33 | } 34 | } else { 35 | var funcs = []; 36 | 37 | for (var funcName in func) { 38 | if (Object.prototype.hasOwnProperty.call(func, funcName)) { 39 | var funcObj = func[funcName]; 40 | if (_.isFunction(funcObj)) { 41 | this.functions[name + '.' + funcName] = funcObj; 42 | funcs.push(funcName); 43 | 44 | if (scope) { 45 | this.scopes[name + '.' + funcName] = scope; 46 | } 47 | } 48 | } 49 | } 50 | 51 | EventEmitter.trace('***', 'exposing module: ' + name + ' [funs: ' + funcs.join(', ') + ']'); 52 | } 53 | return func; 54 | }, 55 | handleCall: function (decoded, conn, callback){ 56 | EventEmitter.trace('<--', 'Request (id ' + decoded.id + '): ' + 57 | decoded.method + '(' + JSON.stringify(decoded.params) + ')'); 58 | 59 | if (!this.functions.hasOwnProperty(decoded.method)) { 60 | callback(new Error.MethodNotFound('Unknown RPC call "' + decoded.method + '"')); 61 | return; 62 | } 63 | 64 | var method = this.functions[decoded.method]; 65 | var scope = this.scopes[decoded.method] || this.defaultScope; 66 | 67 | // Try to call the method, but intercept errors and call our 68 | // error handler. 69 | try { 70 | method.call(scope, decoded.params, conn, callback); 71 | } catch (err) { 72 | callback(err); 73 | } 74 | } 75 | }); 76 | 77 | return Endpoint; 78 | }; 79 | -------------------------------------------------------------------------------- /examples/client.js: -------------------------------------------------------------------------------- 1 | var 2 | rpc = require('../src/jsonrpc'); 3 | 4 | /* 5 | Connect to HTTP server 6 | */ 7 | var client = rpc.Client.$create(8088, 'localhost', 'myuser', 'secret123'); 8 | 9 | client.call('add', [1, 2], function (err, result){ 10 | if (err) { 11 | return printError(err); 12 | } 13 | console.log(' 1 + 2 = ' + result + ' (http)'); 14 | }); 15 | 16 | client.call('multiply', [199, 2], function (err, result){ 17 | if (err) { 18 | return printError(err); 19 | } 20 | console.log('199 * 2 = ' + result + ' (http)'); 21 | }); 22 | 23 | // These calls should each take 1.5 seconds to complete 24 | client.call('delayed.echo', ['Echo.', 1500], function (err, result){ 25 | if (err) { 26 | return printError(err); 27 | } 28 | console.log(' ' + result + ', delay 1500 ms' + ' (http)'); 29 | }); 30 | 31 | /* 32 | Connect to Raw socket server 33 | */ 34 | var socketClient = rpc.Client.$create(8089, 'localhost', 'myuser', 'secret123'); 35 | 36 | socketClient.connectSocket(function (err, conn){ 37 | if (err) { 38 | return printError(err); 39 | } 40 | 41 | if (conn) { 42 | // Accessing modules is as simple as dot-prefixing. 43 | conn.call('math.power', [3, 3], function (err, result){ 44 | if (err) { 45 | return printError(err); 46 | } 47 | console.log(' 3 ^ 3 = ' + result + ' (socket)'); 48 | }); 49 | 50 | // These calls should each take 1 seconds to complete 51 | conn.call('delayed.add', [1, 1, 1000], function (err, result){ 52 | if (err) { 53 | return printError(err); 54 | } 55 | console.log(' 1 + 1 = ' + result + ', delay 1000 ms (socket)'); 56 | }); 57 | 58 | conn.call('delayed.echo', ['echo back 0 timeout', 0], function (err, result){ 59 | if (err) { 60 | return printError(err); 61 | } 62 | console.log(' ' + result + ' (socket)'); 63 | }); 64 | } 65 | }); 66 | 67 | /* 68 | Connect to Websocket server 69 | */ 70 | var WebsocketClient = rpc.Client.$create(8088, 'localhost', 'myuser', 'secret123'); 71 | 72 | WebsocketClient.connectWebsocket(function (err, conn){ 73 | if (err) { 74 | return printError(err); 75 | } 76 | 77 | if (conn) { 78 | // Accessing modules is as simple as dot-prefixing. 79 | conn.call('math.power', [64, 2], function (err, result){ 80 | if (err) { 81 | return printError(err); 82 | } 83 | console.log(' 64 ^ 2 = ' + result + ' (websocket)'); 84 | }); 85 | 86 | // These calls should each take 1 seconds to complete 87 | conn.call('delayed.add', [155, 155, 4000], function (err, result){ 88 | if (err) { 89 | return printError(err); 90 | } 91 | console.log(' 155 + 155 = ' + result + ', delay 4000 ms (websocket)'); 92 | }); 93 | 94 | conn.call('delayed.echo', ['echo back 0 timeout', 0], function (err, result){ 95 | if (err) { 96 | return printError(err); 97 | } 98 | console.log(' ' + result + ' (websocket)'); 99 | }); 100 | } 101 | }); 102 | 103 | function printError(err){ 104 | console.error('RPC Error: ' + err.toString()); 105 | } 106 | -------------------------------------------------------------------------------- /src/connection.js: -------------------------------------------------------------------------------- 1 | module.exports = function (classes){ 2 | 'use strict'; 3 | 4 | var 5 | _ = classes._, 6 | EventEmitter = classes.EventEmitter, 7 | Connection = EventEmitter.$define('Connection', { 8 | construct: function ($super, ep){ 9 | $super(); 10 | 11 | this.endpoint = ep; 12 | this.callbacks = {}; 13 | this.latestId = 0; 14 | 15 | // Default error handler (prevents ''uncaught error event'') 16 | this.on('error', () => { }); 17 | }, 18 | /** 19 | * Make a standard RPC call to the other endpoint. 20 | * 21 | * Note that some ways to make RPC calls bypass this method, for example HTTP 22 | * calls and responses are done in other places. 23 | */ 24 | call : function (method, params, callback){ 25 | if (!_.isArray(params)) { 26 | params = [params]; 27 | } 28 | 29 | var id = null; 30 | if (_.isFunction(callback)) { 31 | id = ++this.latestId; 32 | this.callbacks[id] = callback; 33 | } 34 | 35 | EventEmitter.trace('-->', 'Connection call (method ' + method + '): ' + JSON.stringify(params)); 36 | 37 | var data = JSON.stringify({ 38 | jsonrpc: '2.0', 39 | method : method, 40 | params : params, 41 | id : id 42 | }); 43 | this.write(data); 44 | }, 45 | 46 | /** 47 | * Dummy method for sending data. 48 | * 49 | * Connection types that support sending additional data will override this 50 | * method. 51 | */ 52 | write: function (){ 53 | throw new Error('Tried to write data on unsupported connection type.'); 54 | }, 55 | 56 | /** 57 | * Keep the connection open. 58 | * 59 | * This method is used to tell a HttpServerConnection to stay open. In order 60 | * to keep it compatible with other connection types, we add it here and make 61 | * it register a connection end handler. 62 | */ 63 | stream: function (onend){ 64 | if (_.isFunction(onend)) { 65 | this.on('end', function(){ 66 | onend(); 67 | }); 68 | } 69 | }, 70 | 71 | handleMessage: function (msg){ 72 | var self = this; 73 | 74 | if (msg) { 75 | if ( 76 | (msg.hasOwnProperty('result') || msg.hasOwnProperty('error')) && 77 | msg.hasOwnProperty('id') 78 | ) { 79 | // Are we in the client? 80 | try { 81 | this.callbacks[msg.id](msg.error, msg.result); 82 | delete this.callbacks[msg.id]; 83 | } catch (err) { 84 | EventEmitter.trace('<---', 'Callback not found ' + msg.id + ': ' + (err.stack ? err.stack : err.toString())); 85 | } 86 | } else if (msg.hasOwnProperty('method')) { 87 | // Are we in the server? 88 | this.endpoint.handleCall(msg, this, function handleCall(err, result){ 89 | if (err) { 90 | self.emit('error', err); 91 | 92 | EventEmitter.trace('-->', 93 | 'Failure ' + (EventEmitter.hasId(msg) ? '(id ' + msg.id + ')' : '') + ': ' + 94 | (err.stack ? err.stack : err.toString()) 95 | ); 96 | } 97 | 98 | // Return if it's just a notification (no id) 99 | if (!EventEmitter.hasId(msg)) { 100 | return; 101 | } 102 | 103 | if (err) { 104 | err = err.toString(); 105 | result = null; 106 | } else { 107 | EventEmitter.trace('-->', 'Response (id ' + msg.id + '): ' + 108 | JSON.stringify(result)); 109 | err = null; 110 | } 111 | 112 | self.sendReply(err, result, msg.id); 113 | }); 114 | } 115 | } 116 | }, 117 | 118 | sendReply: function (err, result, id){ 119 | var data = JSON.stringify({ 120 | jsonrpc: '2.0', 121 | result : result, 122 | error : err, 123 | id : id 124 | }); 125 | 126 | this.write(data); 127 | } 128 | }); 129 | 130 | return Connection; 131 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/pocesar/node-jsonrpc2.svg?branch=master)](https://travis-ci.org/pocesar/node-jsonrpc2) 2 | 3 | [![NPM](https://nodei.co/npm/json-rpc2.svg?downloads=true)](https://nodei.co/npm/json-rpc2/) 4 | 5 | # node-jsonrpc2 6 | 7 | JSON-RPC 2.0 server and client library, with `HTTP` (with `Websocket` support) and `TCP` endpoints 8 | 9 | This fork is a rewrite with proper testing framework, linted code, compatible with node 0.8.x and 0.10.x, class inheritance, and added functionalities 10 | 11 | ## Tools 12 | 13 | Check [jsonrpc2-tools](https://www.npmjs.org/package/jsonrpc2-tools) for some nice additions to this module. 14 | 15 | ## Install 16 | 17 | To install node-jsonrpc2 in the current directory, run: 18 | 19 | ```bash 20 | npm install json-rpc2 --save 21 | ``` 22 | 23 | ## Changes from 1.x 24 | 25 | * Uses native EventEmitter instead of EventEmitter3 26 | 27 | ## Changes from 0.x 28 | 29 | * Before, the `id` member was permissive and wouldn't actually adhere to the RFC, allowing anything besides `undefined`. 30 | * If your application relied on weird id constructs other than `String`, `Number` or `null`, it might break if you update to 1.x 31 | 32 | ## Usage 33 | 34 | Firing up an efficient JSON-RPC server becomes extremely simple: 35 | 36 | ```js 37 | var rpc = require('json-rpc2'); 38 | 39 | var server = rpc.Server.$create({ 40 | 'websocket': true, // is true by default 41 | 'headers': { // allow custom headers is empty by default 42 | 'Access-Control-Allow-Origin': '*' 43 | } 44 | }); 45 | 46 | function add(args, opt, callback) { 47 | callback(null, args[0] + args[1]); 48 | } 49 | 50 | server.expose('add', add); 51 | 52 | // you can expose an entire object as well: 53 | 54 | server.expose('namespace', { 55 | 'function1': function(){}, 56 | 'function2': function(){}, 57 | 'function3': function(){} 58 | }); 59 | // expects calls to be namespace.function1, namespace.function2 and namespace.function3 60 | 61 | // listen creates an HTTP server on localhost only 62 | server.listen(8000, 'localhost'); 63 | ``` 64 | 65 | And creating a client to speak to that server is easy too: 66 | 67 | ```js 68 | var rpc = require('json-rpc2'); 69 | 70 | var client = rpc.Client.$create(8000, 'localhost'); 71 | 72 | // Call add function on the server 73 | 74 | client.call('add', [1, 2], function(err, result) { 75 | console.log('1 + 2 = ' + result); 76 | }); 77 | ``` 78 | 79 | Create a raw (socket) server using: 80 | 81 | ```js 82 | var rpc = require('json-rpc2'); 83 | 84 | var server = rpc.Server.$create(); 85 | 86 | // non-standard auth for RPC, when using this module using both client and server, works out-of-the-box 87 | server.enableAuth('user', 'pass'); 88 | 89 | // Listen on socket 90 | server.listenRaw(8080, 'localhost'); 91 | ``` 92 | 93 | ## Extend, overwrite, overload 94 | 95 | Any class can be extended, or used as a mixin for new classes, since it uses [ES5Class](http://github.com/pocesar/ES5-Class) module. 96 | 97 | For example, you may extend the `Endpoint` class, that automatically extends `Client` and `Server` classes. 98 | Extending `Connection` automatically extends `SocketConnection` and `HttpServerConnection`. 99 | 100 | ```js 101 | var rpc = require('json-rpc2'); 102 | 103 | rpc.Endpoint.$include({ 104 | 'newFunction': function(){ 105 | 106 | } 107 | }); 108 | 109 | var 110 | server = rpc.Server.$create(), 111 | client = rpc.Client.$create(); 112 | 113 | server.newFunction(); // already available 114 | client.newFunction(); // already available 115 | ``` 116 | 117 | To implement a new class method (that can be called without an instance, like `rpc.Endpoint.newFunction`): 118 | 119 | ```js 120 | var rpc = require('json-rpc2'); 121 | 122 | rpc.Endpoint.$implement({ 123 | 'newFunction': function(){ 124 | } 125 | }); 126 | 127 | rpc.Endpoint.newFunction(); // available 128 | rpc.Client.newFunction(); // every 129 | rpc.Server.newFunction(); // where 130 | ``` 131 | 132 | Don't forget, when you are overloading an existing function, you can call the original function using `$super` 133 | 134 | ```js 135 | var rpc = require('json-rpc2'); 136 | 137 | rpc.Endpoint.$implement({ 138 | 'trace': function($super, direction, message){ 139 | $super(' (' + direction + ')', message); //call the last defined function 140 | } 141 | }); 142 | ``` 143 | 144 | And you can start your classes directly from any of the classes 145 | 146 | ```js 147 | var MyCoolServer = require('json-rpc2').Server.$define('MyCoolServer', { 148 | myOwnFunction: function(){ 149 | }, 150 | }, { 151 | myOwnClassMethod: function(){ 152 | } 153 | }); // MyCoolServer will contain all class and instance functions from Server 154 | 155 | MyCoolServer.myOwnClassMethod(); // class function 156 | MyCoolServer.$create().myOwnFunction(); // instance function 157 | ``` 158 | 159 | ## Debugging 160 | 161 | This module uses the [debug](http://github.com/visionmedia/debug) package, to debug it, you need to set the Node 162 | environment variable to jsonrpc, by setting it in command line as `set DEBUG=jsonrpc` or `export DEBUG=jsonrpc` 163 | 164 | ## Examples 165 | 166 | To learn more, see the `examples` directory, peruse `test/jsonrpc-test.js`, or 167 | simply "Use The Source, Luke". 168 | 169 | More documentation and development is on its way. 170 | 171 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | module.exports = function (classes){ 2 | 'use strict'; 3 | 4 | var 5 | net = require('net'), 6 | http = require('http'), 7 | https = require('https'), 8 | WebSocket = classes.Websocket, 9 | JsonParser = require('jsonparse'), 10 | EventEmitter = classes.EventEmitter, 11 | Endpoint = classes.Endpoint, 12 | _ = classes._, 13 | SocketConnection = classes.SocketConnection, 14 | WebSocketConnection = classes.WebSocketConnection, 15 | /** 16 | * JSON-RPC Client. 17 | */ 18 | Client = Endpoint.$define('Client', { 19 | construct : function ($super, port, host, user, password){ 20 | $super(); 21 | 22 | this.port = port; 23 | this.host = host; 24 | this.user = user; 25 | this.password = password; 26 | this.on('error', () => {}); 27 | }, 28 | _authHeader: function(headers){ 29 | if (this.user && this.password) { 30 | var buff = Buffer.from(this.user + ':' + this.password).toString('base64'); 31 | headers['Authorization'] = 'Basic ' + buff; 32 | } 33 | }, 34 | /** 35 | * Make HTTP connection/request. 36 | * 37 | * In HTTP mode, we get to submit exactly one message and receive up to n 38 | * messages. 39 | */ 40 | connectHttp : function (method, params, opts, callback){ 41 | if (_.isFunction(opts)) { 42 | callback = opts; 43 | opts = {}; 44 | } 45 | opts = opts || {}; 46 | 47 | var id = 1, self = this; 48 | 49 | // First we encode the request into JSON 50 | var requestJSON = JSON.stringify({ 51 | 'id' : id, 52 | 'method' : method, 53 | 'params' : params, 54 | 'jsonrpc': '2.0' 55 | }); 56 | 57 | var headers = {}; 58 | 59 | this._authHeader(headers); 60 | 61 | // Then we build some basic headers. 62 | headers['Host'] = this.host; 63 | headers['Content-Length'] = Buffer.byteLength(requestJSON, 'utf8'); 64 | 65 | // Now we'll make a request to the server 66 | var options = { 67 | hostname: this.host, 68 | port : this.port, 69 | path : opts.path || '/', 70 | method : 'POST', 71 | headers : headers 72 | }; 73 | var request; 74 | if(opts.https === true) { 75 | if(opts.rejectUnauthorized !== undefined) { 76 | options.rejectUnauthorized = opts.rejectUnauthorized; 77 | } 78 | request = https.request(options); 79 | } else { 80 | request = http.request(options); 81 | } 82 | 83 | 84 | // Report errors from the http client. This also prevents crashes since 85 | // an exception is thrown if we don't handle this event. 86 | request.on('error', function requestError(err){ 87 | callback(err); 88 | }); 89 | request.write(requestJSON); 90 | request.on('response', function requestResponse(response){ 91 | callback.call(self, id, request, response); 92 | }); 93 | }, 94 | connectWebsocket: function(callback){ 95 | var self = this, conn, socket, parser, headers = {}; 96 | 97 | if (!/^wss?:\/\//i.test(self.host)) { 98 | self.host = 'ws://' + self.host + ':' + self.port + '/'; 99 | } 100 | 101 | this._authHeader(headers); 102 | 103 | socket = new WebSocket.Client(self.host, null, {headers: headers}); 104 | 105 | conn = new WebSocketConnection(self, socket); 106 | 107 | parser = new JsonParser(); 108 | 109 | parser.onValue = function parseOnValue(decoded){ 110 | if (this.stack.length) { 111 | return; 112 | } 113 | 114 | conn.handleMessage(decoded); 115 | }; 116 | 117 | socket.on('error', function socketError(event){ 118 | callback(event.reason); 119 | }); 120 | 121 | socket.on('open', function socketOpen(){ 122 | callback(null, conn); 123 | }); 124 | 125 | socket.on('message', function socketMessage(event){ 126 | try { 127 | parser.write(event.data); 128 | } catch (err) { 129 | EventEmitter.trace('<--', err.toString()); 130 | } 131 | }); 132 | 133 | return conn; 134 | }, 135 | /** 136 | * Make Socket connection. 137 | * 138 | * This implements JSON-RPC over a raw socket. This mode allows us to send and 139 | * receive as many messages as we like once the socket is established. 140 | */ 141 | connectSocket: function (callback){ 142 | var self = this, socket, conn, parser; 143 | 144 | socket = net.connect(this.port, this.host, function netConnect(){ 145 | // Submit non-standard 'auth' message for raw sockets. 146 | if (!_.isEmpty(self.user) && !_.isEmpty(self.password)) { 147 | conn.call('auth', [self.user, self.password], function connectionAuth(err){ 148 | if (err) { 149 | callback(err); 150 | } else { 151 | callback(null, conn); 152 | } 153 | }); 154 | return; 155 | } 156 | 157 | if (_.isFunction(callback)) { 158 | callback(null, conn); 159 | } 160 | }); 161 | 162 | conn = new SocketConnection(self, socket); 163 | parser = new JsonParser(); 164 | 165 | parser.onValue = function parseOnValue(decoded){ 166 | if (this.stack.length) { 167 | return; 168 | } 169 | 170 | conn.handleMessage(decoded); 171 | }; 172 | 173 | socket.on('data', function socketData(chunk){ 174 | try { 175 | parser.write(chunk); 176 | } catch (err) { 177 | EventEmitter.trace('<--', err.toString()); 178 | } 179 | }); 180 | 181 | return conn; 182 | }, 183 | stream : function (method, params, opts, callback){ 184 | if (_.isFunction(opts)) { 185 | callback = opts; 186 | opts = {}; 187 | } 188 | opts = opts || {}; 189 | 190 | this.connectHttp(method, params, opts, function connectHttp(id, request, response){ 191 | if (_.isFunction(callback)) { 192 | var connection = new EventEmitter(); 193 | 194 | connection.id = id; 195 | connection.req = request; 196 | connection.res = response; 197 | 198 | connection.expose = function connectionExpose(method, callback){ 199 | connection.on('call:' + method, function connectionCall(data){ 200 | callback.call(null, data.params || []); 201 | }); 202 | }; 203 | 204 | connection.end = function connectionEnd(){ 205 | this.req.connection.end(); 206 | }; 207 | 208 | // We need to buffer the response chunks in a nonblocking way. 209 | var parser = new JsonParser(); 210 | parser.onValue = function (decoded){ 211 | if (this.stack.length) { 212 | return; 213 | } 214 | 215 | connection.emit('data', decoded); 216 | if ( 217 | decoded.hasOwnProperty('result') || 218 | decoded.hasOwnProperty('error') && 219 | decoded.id === id && _.isFunction(callback) 220 | ) { 221 | connection.emit('result', decoded); 222 | } 223 | else if (decoded.hasOwnProperty('method')) { 224 | connection.emit('call:' + decoded.method, decoded); 225 | } 226 | }; 227 | 228 | if (response) { 229 | // Handle headers 230 | connection.res.once('data', function connectionOnce(data){ 231 | if (connection.res.statusCode === 200) { 232 | callback(null, connection); 233 | } else { 234 | callback(new Error('"' + connection.res.statusCode + '"' + data)); 235 | } 236 | }); 237 | 238 | connection.res.on('data', function connectionData(chunk){ 239 | try { 240 | parser.write(chunk); 241 | } catch (err) { 242 | // TODO: Is ignoring invalid data the right thing to do? 243 | } 244 | }); 245 | 246 | connection.res.on('end', function connectionEnd(){ 247 | // TODO: Issue an error if there has been no valid response message 248 | }); 249 | } 250 | } 251 | }); 252 | }, 253 | call : function (method, params, opts, callback){ 254 | if (_.isFunction(opts)) { 255 | callback = opts; 256 | opts = {}; 257 | } 258 | opts = opts || {}; 259 | EventEmitter.trace('-->', 'Http call (method ' + method + '): ' + JSON.stringify(params)); 260 | 261 | this.connectHttp(method, params, opts, function connectHttp(id, request, response){ 262 | // Check if response object exists. 263 | if (!response) { 264 | callback(new Error('Have no response object')); 265 | return; 266 | } 267 | 268 | var data = ''; 269 | 270 | response.on('data', function responseData(chunk){ 271 | data += chunk; 272 | }); 273 | 274 | response.on('end', function responseEnd(){ 275 | if (response.statusCode !== 200) { 276 | callback(new Error('"' + response.statusCode + '"' + data)) 277 | ; 278 | return; 279 | } 280 | var decoded = JSON.parse(data); 281 | if (_.isFunction(callback)) { 282 | if (!decoded.error) { 283 | decoded.error = null; 284 | } 285 | callback(decoded.error, decoded.result); 286 | } 287 | }); 288 | }); 289 | } 290 | }); 291 | 292 | return Client; 293 | }; 294 | -------------------------------------------------------------------------------- /test/jsonrpc-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var 4 | expect = require('expect.js'), 5 | rpc = require('../src/jsonrpc'), 6 | server, MockRequest, MockResponse, testBadRequest, TestModule, echo; 7 | 8 | module.exports = { 9 | beforeEach : function (){ 10 | server = rpc.Server.$create(); 11 | 12 | // MOCK REQUEST/RESPONSE OBJECTS 13 | MockRequest = rpc.EventEmitter.$define('MockRequest', { 14 | construct: function($super, method){ 15 | $super(); 16 | this.method = method; 17 | } 18 | }); 19 | 20 | echo = function (args, opts, callback){ 21 | callback(null, args[0]); 22 | }; 23 | server.expose('echo', echo); 24 | 25 | var throw_error = function (){ 26 | throw new rpc.Error.InternalError(); 27 | }; 28 | server.expose('throw_error', throw_error); 29 | 30 | var json_rpc_error = function (args, opts, callback){ 31 | callback(new rpc.Error.InternalError(), args[0]); 32 | }; 33 | server.expose('json_rpc_error', json_rpc_error); 34 | 35 | var text_error = function (args, opts, callback){ 36 | callback('error', args[0]); 37 | }; 38 | server.expose('text_error', text_error); 39 | 40 | var javascript_error = function (args, opts, callback){ 41 | callback(new Error(), args[0]); 42 | }; 43 | 44 | server.expose('javascript_error', javascript_error); 45 | 46 | MockResponse = rpc.EventEmitter.$define('MockResponse', { 47 | construct: function($super){ 48 | $super(); 49 | 50 | this.writeHead = this.sendHeader = function (httpCode){ 51 | this.httpCode = httpCode; 52 | this.httpHeaders = httpCode; 53 | }; 54 | this.write = this.sendBody = function (httpBody){ 55 | this.httpBody = httpBody; 56 | }; 57 | this.end = this.finish = function (){}; 58 | this.connection = new rpc.EventEmitter(); 59 | } 60 | }); 61 | 62 | 63 | // A SIMPLE MODULE 64 | TestModule = { 65 | foo: function (a, b){ 66 | return ['foo', 'bar', a, b]; 67 | }, 68 | 69 | other: 'hello' 70 | }; 71 | 72 | testBadRequest = function (testJSON, done){ 73 | var req = new MockRequest('POST'); 74 | var res = new MockResponse(); 75 | server.handleHttp(req, res); 76 | req.emit('data', testJSON); 77 | req.emit('end'); 78 | var decoded = JSON.parse(res.httpBody); 79 | expect(decoded.id).equal(null); 80 | expect(decoded.error.message).equal('Invalid Request'); 81 | expect(decoded.error.code).equal(-32600); 82 | done(); 83 | }; 84 | }, 85 | afterEach: function(){ 86 | server = null; 87 | MockRequest = null; 88 | MockResponse = null; 89 | testBadRequest = null; 90 | TestModule = null; 91 | }, 92 | 'json-rpc2': { 93 | 'Server expose': function (){ 94 | expect(server.functions.echo).eql(echo); 95 | }, 96 | 97 | 'Server exposeModule': function (){ 98 | server.exposeModule('test', TestModule); 99 | expect(server.functions['test.foo']).eql(TestModule.foo); 100 | }, 101 | 102 | 'GET Server handle NonPOST': function (){ 103 | var req = new MockRequest('GET'); 104 | var res = new MockResponse(); 105 | server.handleHttp(req, res); 106 | var decoded = JSON.parse(res.httpBody); 107 | expect(decoded.id).equal(null); 108 | expect(decoded.error.message).equal('Invalid Request'); 109 | expect(decoded.error.code).equal(-32600); 110 | }, 111 | 'Method throw an error' : function() { 112 | var req = new MockRequest('POST'); 113 | var res = new MockResponse(); 114 | server.handleHttp(req, res); 115 | req.emit('data', '{ "method": "throw_error", "params": [], "id": 1 }'); 116 | req.emit('end'); 117 | var decoded = JSON.parse(res.httpBody); 118 | expect(decoded.id).equal(1); 119 | expect(decoded.error.message).equal('InternalError'); 120 | expect(decoded.error.code).equal(-32603); 121 | }, 122 | 'Method return an rpc error' : function() { 123 | var req = new MockRequest('POST'); 124 | var res = new MockResponse(); 125 | server.handleHttp(req, res); 126 | req.emit('data', '{ "method": "json_rpc_error", "params": [], "id": 1 }'); 127 | req.emit('end'); 128 | var decoded = JSON.parse(res.httpBody); 129 | expect(decoded.id).equal(1); 130 | expect(decoded.error.message).equal('InternalError'); 131 | expect(decoded.error.code).equal(-32603); 132 | }, 133 | // text_error javascript_error 134 | 135 | 'Missing object attribute (method)': function (done){ 136 | var testJSON = '{ "params": ["Hello, World!"], "id": 1 }'; 137 | testBadRequest(testJSON, done); 138 | }, 139 | 140 | 'Missing object attribute (params)': function (done){ 141 | var testJSON = '{ "method": "echo", "id": 1 }'; 142 | testBadRequest(testJSON, done); 143 | }, 144 | 145 | 'Unregistered method': function (){ 146 | var testJSON = '{ "method": "notRegistered", "params": ["Hello, World!"], "id": 1 }'; 147 | var req = new MockRequest('POST'); 148 | var res = new MockResponse(); 149 | try { 150 | server.handleHttp(req, res); 151 | } catch (e) {} 152 | req.emit('data', testJSON); 153 | req.emit('end'); 154 | expect(res.httpCode).equal(200); 155 | var decoded = JSON.parse(res.httpBody); 156 | expect(decoded.id).equal(1); 157 | expect(decoded.error.message).equal('Unknown RPC call "notRegistered"'); 158 | expect(decoded.error.code).equal(-32601); 159 | }, 160 | 161 | // VALID REQUEST 162 | 163 | 'Simple synchronous echo': function (){ 164 | var testJSON = '{ "method": "echo", "params": ["Hello, World!"], "id": 1 }'; 165 | var req = new MockRequest('POST'); 166 | var res = new MockResponse(); 167 | server.handleHttp(req, res); 168 | req.emit('data', testJSON); 169 | req.emit('end'); 170 | expect(res.httpCode).equal(200); 171 | var decoded = JSON.parse(res.httpBody); 172 | expect(decoded.id).equal(1); 173 | expect(decoded.error).equal(undefined); 174 | expect(decoded.result).equal('Hello, World!'); 175 | }, 176 | 177 | 'Simple synchronous echo with id as null': function (){ 178 | var testJSON = '{ "method": "echo", "params": ["Hello, World!"], "id": null }'; 179 | var req = new MockRequest('POST'); 180 | var res = new MockResponse(); 181 | server.handleHttp(req, res); 182 | req.emit('data', testJSON); 183 | req.emit('end'); 184 | expect(res.httpCode).equal(200); 185 | var decoded = JSON.parse(res.httpBody); 186 | expect(decoded.id).equal(null); 187 | expect(decoded.error).equal(undefined); 188 | expect(decoded.result).equal('Hello, World!'); 189 | }, 190 | 191 | 'Simple synchronous echo with string as id': function (){ 192 | var testJSON = '{ "method": "echo", "params": ["Hello, World!"], "id": "test" }'; 193 | var req = new MockRequest('POST'); 194 | var res = new MockResponse(); 195 | server.handleHttp(req, res); 196 | req.emit('data', testJSON); 197 | req.emit('end'); 198 | expect(res.httpCode).equal(200); 199 | var decoded = JSON.parse(res.httpBody); 200 | expect(decoded.id).equal('test'); 201 | expect(decoded.error).equal(undefined); 202 | expect(decoded.result).equal('Hello, World!'); 203 | }, 204 | 205 | 'Using promise': function (){ 206 | // Expose a function that just returns a promise that we can control. 207 | var callbackRef = null; 208 | server.expose('promiseEcho', function (args, opts, callback){ 209 | callbackRef = callback; 210 | }); 211 | // Build a request to call that function 212 | var testJSON = '{ "method": "promiseEcho", "params": ["Hello, World!"], "id": 1 }'; 213 | var req = new MockRequest('POST'); 214 | var res = new MockResponse(); 215 | // Have the server handle that request 216 | server.handleHttp(req, res); 217 | req.emit('data', testJSON); 218 | req.emit('end'); 219 | // Now the request has completed, and in the above synchronous test, we 220 | // would be finished. However, this function is smarter and only completes 221 | // when the promise completes. Therefore, we should not have a response 222 | // yet. 223 | expect(res.httpCode).be(undefined); 224 | // We can force the promise to emit a success code, with a message. 225 | callbackRef(null, 'Hello, World!'); 226 | // Aha, now that the promise has finished, our request has finished as well. 227 | expect(res.httpCode).equal(200); 228 | var decoded = JSON.parse(res.httpBody); 229 | expect(decoded.id).equal(1); 230 | expect(decoded.error).equal(undefined); 231 | expect(decoded.result).equal('Hello, World!'); 232 | }, 233 | 234 | 'Triggering an errback': function (){ 235 | var callbackRef = null; 236 | server.expose('errbackEcho', function (args, opts, callback){ 237 | callbackRef = callback; 238 | }); 239 | var testJSON = '{ "method": "errbackEcho", "params": ["Hello, World!"], "id": 1 }'; 240 | var req = new MockRequest('POST'); 241 | var res = new MockResponse(); 242 | server.handleHttp(req, res); 243 | req.emit('data', testJSON); 244 | req.emit('end'); 245 | expect(res.httpCode).be(undefined); 246 | // This time, unlike the above test, we trigger an error and expect to see 247 | // it in the error attribute of the object returned. 248 | callbackRef('This is an error'); 249 | expect(res.httpCode).equal(200); 250 | var decoded = JSON.parse(res.httpBody); 251 | expect(decoded.id).equal(1); 252 | expect(decoded.error.message).equal('This is an error'); 253 | expect(decoded.error.code).equal(-32603); 254 | expect(decoded.result).equal(undefined); 255 | }, 256 | 'Notification request': function () { 257 | var testJSON = '{ "method": "notify_test", "params": ["Hello, World!"] }'; 258 | var req = new MockRequest('POST'); 259 | var res = new MockResponse(); 260 | server.handleHttp(req, res); 261 | req.emit('data', testJSON); 262 | req.emit('end'); 263 | // although it shouldn't return a response, we are dealing with HTTP, that MUST 264 | // return something, in most cases, 0 length body 265 | expect(res.httpCode).equal(200); 266 | expect(res.httpBody).equal(''); 267 | } 268 | } 269 | }; 270 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | module.exports = function (classes) { 2 | 'use strict'; 3 | 4 | var 5 | net = require('net'), 6 | http = require('http'), 7 | extend = require('object-assign'), 8 | JsonParser = require('jsonparse'), 9 | 10 | UNAUTHORIZED = 'Unauthorized', 11 | METHOD_NOT_ALLOWED = 'Invalid Request', 12 | INVALID_REQUEST = 'Invalid Request', 13 | _ = classes._, 14 | Endpoint = classes.Endpoint, 15 | WebSocket = classes.Websocket, 16 | Error = classes.Error, 17 | 18 | /** 19 | * JSON-RPC Server. 20 | */ 21 | Server = Endpoint.$define('Server', { 22 | construct: function ($super, opts) { 23 | $super(); 24 | 25 | this.opts = opts || {}; 26 | this.opts.type = typeof this.opts.type !== 'undefined' ? this.opts.type : 'http'; 27 | this.opts.headers = this.opts.headers || {}; 28 | this.opts.websocket = typeof this.opts.websocket !== 'undefined' ? this.opts.websocket : true; 29 | this.on('error', () => {}); 30 | }, 31 | _checkAuth: function (req, res) { 32 | var self = this; 33 | 34 | if (self.authHandler) { 35 | var 36 | authHeader = req.headers['authorization'] || '', // get the header 37 | authToken = authHeader.split(/\s+/).pop() || '', // get the token 38 | auth = Buffer.from(authToken, 'base64').toString(), // base64 -> string 39 | parts = auth.split(/:/), // split on colon 40 | username = parts[0], 41 | password = parts[1]; 42 | 43 | if (!this.authHandler(username, password)) { 44 | if (res) { 45 | classes.EventEmitter.trace('<--', 'Unauthorized request'); 46 | Server.handleHttpError(req, res, new Error.InvalidParams(UNAUTHORIZED), self.opts.headers); 47 | } 48 | return false; 49 | } 50 | } 51 | return true; 52 | }, 53 | /** 54 | * Start listening to incoming connections. 55 | */ 56 | listen: function (port, host) { 57 | var 58 | self = this, 59 | server = http.createServer(); 60 | 61 | server.on('request', function onRequest(req, res) { 62 | self.handleHttp(req, res); 63 | }); 64 | 65 | if (port) { 66 | server.listen(port, host); 67 | Endpoint.trace('***', 'Server listening on http://' + 68 | (host || '127.0.0.1') + ':' + port + '/'); 69 | } 70 | 71 | if (this.opts.websocket === true) { 72 | server.on('upgrade', function onUpgrade(req, socket, body) { 73 | if (WebSocket.isWebSocket(req)) { 74 | if (self._checkAuth(req, socket)) { 75 | self.handleWebsocket(req, socket, body); 76 | } 77 | } 78 | }); 79 | } 80 | 81 | return server; 82 | }, 83 | 84 | listenRaw: function (port, host) { 85 | var 86 | self = this, 87 | server = net.createServer(function createServer(socket) { 88 | self.handleRaw(socket); 89 | }); 90 | 91 | server.listen(port, host); 92 | 93 | Endpoint.trace('***', 'Server listening on tcp://' + 94 | (host || '127.0.0.1') + ':' + port + '/'); 95 | 96 | return server; 97 | }, 98 | 99 | listenHybrid: function (port, host) { 100 | var 101 | self = this, 102 | httpServer = self.listen(), 103 | server = net.createServer(function createServer(socket) { 104 | self.handleHybrid(httpServer, socket); 105 | }); 106 | 107 | server.listen(port, host); 108 | 109 | Endpoint.trace('***', 'Server (hybrid) listening on http+tcp://' + 110 | (host || '127.0.0.1') + ':' + port + '/'); 111 | 112 | return server; 113 | }, 114 | 115 | /** 116 | * Handle HTTP POST request. 117 | */ 118 | handleHttp: function (req, res) { 119 | var buffer = '', self = this; 120 | var headers; 121 | 122 | if (req.method === 'OPTIONS') { 123 | headers = { 124 | 'Content-Length': 0, 125 | 'Access-Control-Allow-Headers': 'Accept, Authorization, Content-Type' 126 | }; 127 | headers = extend({}, headers, self.opts.headers); 128 | res.writeHead(200, headers); 129 | res.end(); 130 | return; 131 | } 132 | 133 | if (!self._checkAuth(req, res)) { 134 | return; 135 | } 136 | 137 | Endpoint.trace('<--', 'Accepted http request'); 138 | 139 | if (req.method !== 'POST') { 140 | Server.handleHttpError(req, res, new Error.InvalidRequest(METHOD_NOT_ALLOWED), self.opts.headers); 141 | return; 142 | } 143 | 144 | var handle = function handle(buf) { 145 | // Check if json is valid JSON document 146 | var decoded; 147 | 148 | try { 149 | decoded = JSON.parse(buf); 150 | } catch (error) { 151 | Server.handleHttpError(req, res, new Error.ParseError(INVALID_REQUEST), self.opts.headers); 152 | return; 153 | } 154 | 155 | // Check for the required fields, and if they aren't there, then 156 | // dispatch to the handleHttpError function. 157 | if (!decoded.method || !decoded.params) { 158 | Endpoint.trace('-->', 'Response (invalid request)'); 159 | Server.handleHttpError(req, res, new Error.InvalidRequest(INVALID_REQUEST), self.opts.headers); 160 | return; 161 | } 162 | 163 | var reply = function reply(json) { 164 | var encoded; 165 | headers = { 166 | 'Content-Type': 'application/json' 167 | }; 168 | 169 | if (json) { 170 | encoded = JSON.stringify(json); 171 | headers['Content-Length'] = Buffer.byteLength(encoded, 'utf-8'); 172 | } else { 173 | encoded = ''; 174 | headers['Content-Length'] = 0; 175 | } 176 | 177 | headers = extend({}, headers, self.opts.headers); 178 | 179 | if (!conn.isStreaming) { 180 | res.writeHead(200, headers); 181 | res.write(encoded); 182 | res.end(); 183 | } else { 184 | res.writeHead(200, headers); 185 | res.write(encoded); 186 | // Keep connection open 187 | } 188 | }; 189 | 190 | var callback = function callback(err, result) { 191 | var response; 192 | if (err) { 193 | 194 | self.emit('error', err); 195 | 196 | Endpoint.trace('-->', 'Failure (id ' + decoded.id + '): ' + 197 | (err.stack ? err.stack : err.toString())); 198 | 199 | result = null; 200 | 201 | if (!(err instanceof Error.AbstractError)) { 202 | err = new Error.InternalError(err.toString()); 203 | } 204 | 205 | response = { 206 | 'jsonrpc': '2.0', 207 | 'error': {code: err.code, message: err.message } 208 | }; 209 | 210 | } else { 211 | Endpoint.trace('-->', 'Response (id ' + decoded.id + '): ' + 212 | JSON.stringify(result)); 213 | 214 | response = { 215 | 'jsonrpc': '2.0', 216 | 'result': typeof(result) === 'undefined' ? null : result 217 | }; 218 | } 219 | 220 | // Don't return a message if it doesn't have an ID 221 | if (Endpoint.hasId(decoded)) { 222 | response.id = decoded.id; 223 | reply(response); 224 | } else { 225 | reply(); 226 | } 227 | }; 228 | 229 | var conn = new classes.HttpServerConnection(self, req, res); 230 | 231 | self.handleCall(decoded, conn, callback); 232 | }; // function handle(buf) 233 | 234 | req.on('data', function requestData(chunk) { 235 | buffer = buffer + chunk; 236 | }); 237 | 238 | req.on('end', function requestEnd() { 239 | handle(buffer); 240 | }); 241 | }, 242 | 243 | handleRaw: function (socket) { 244 | var self = this, conn, parser, requireAuth; 245 | 246 | Endpoint.trace('<--', 'Accepted socket connection'); 247 | 248 | conn = new classes.SocketConnection(self, socket); 249 | parser = new JsonParser(); 250 | requireAuth = !!this.authHandler; 251 | 252 | parser.onValue = function (decoded) { 253 | if (this.stack.length) { 254 | return; 255 | } 256 | 257 | // We're on a raw TCP socket. To enable authentication we implement a simple 258 | // authentication scheme that is non-standard, but is easy to call from any 259 | // client library. 260 | // 261 | // The authentication message is to be sent as follows: 262 | // {'method': 'auth', 'params': ['myuser', 'mypass'], id: 0} 263 | if (requireAuth) { 264 | if (decoded.method !== 'auth') { 265 | // Try to notify client about failure to authenticate 266 | if (Endpoint.hasId(decoded)) { 267 | conn.sendReply('Error: Unauthorized', null, decoded.id); 268 | } 269 | } else { 270 | // Handle 'auth' message 271 | if (_.isArray(decoded.params) && 272 | decoded.params.length === 2 && 273 | self.authHandler(decoded.params[0], decoded.params[1])) { 274 | // Authorization completed 275 | requireAuth = false; 276 | 277 | // Notify client about success 278 | if (Endpoint.hasId(decoded)) { 279 | conn.sendReply(null, true, decoded.id); 280 | } 281 | } else { 282 | if (Endpoint.hasId(decoded)) { 283 | conn.sendReply('Error: Invalid credentials', null, decoded.id); 284 | } 285 | } 286 | } 287 | // Make sure we explicitly return here - the client was not yet auth'd. 288 | return; 289 | } else { 290 | conn.handleMessage(decoded); 291 | } 292 | }; 293 | 294 | socket.on('data', function (chunk) { 295 | try { 296 | parser.write(chunk); 297 | } catch (err) { 298 | // TODO: Is ignoring invalid data the right thing to do? 299 | } 300 | }); 301 | }, 302 | 303 | handleWebsocket: function (request, socket, body) { 304 | var self = this, conn, parser; 305 | 306 | socket = new WebSocket(request, socket, body); 307 | 308 | Endpoint.trace('<--', 'Accepted Websocket connection'); 309 | 310 | conn = new classes.WebSocketConnection(self, socket); 311 | parser = new JsonParser(); 312 | 313 | parser.onValue = function (decoded) { 314 | if (this.stack.length) { 315 | return; 316 | } 317 | 318 | conn.handleMessage(decoded); 319 | }; 320 | 321 | socket.on('message', function (event) { 322 | try { 323 | parser.write(event.data); 324 | } catch (err) { 325 | // TODO: Is ignoring invalid data the right thing to do? 326 | } 327 | }); 328 | }, 329 | 330 | handleHybrid: function (httpServer, socket) { 331 | var self = this; 332 | 333 | socket.once('data', function (chunk) { 334 | // If first byte is a capital letter, treat connection as HTTP 335 | if (chunk[0] >= 65 && chunk[0] <= 90) { 336 | // TODO: need to find a better way to do this 337 | http._connectionListener.call(httpServer, socket); 338 | socket.ondata(chunk, 0, chunk.length); 339 | } else { 340 | self.handleRaw(socket); 341 | // Re-emit first chunk 342 | socket.emit('data', chunk); 343 | } 344 | }); 345 | }, 346 | 347 | /** 348 | * Set the server to require authentication. 349 | * 350 | * Can be called with a custom handler function: 351 | * server.enableAuth(function (user, password) { 352 | * return true; // Do authentication and return result as boolean 353 | * }); 354 | * 355 | * Or just with a single valid username and password: 356 | * sever.enableAuth(''myuser'', ''supersecretpassword''); 357 | */ 358 | enableAuth: function (handler, password) { 359 | if (!_.isFunction(handler)) { 360 | var user = '' + handler; 361 | password = '' + password; 362 | 363 | handler = function checkAuth(suppliedUser, suppliedPassword) { 364 | return user === suppliedUser && password === suppliedPassword; 365 | }; 366 | } 367 | 368 | this.authHandler = handler; 369 | } 370 | }, { 371 | /** 372 | * Handle a low level server error. 373 | */ 374 | handleHttpError: function (req, res, error, custom_headers) { 375 | var message = JSON.stringify({ 376 | 'jsonrpc': '2.0', 377 | 'error': {code: error.code, message: error.message}, 378 | 'id': null 379 | }); 380 | custom_headers = custom_headers || {}; 381 | var headers = extend({ 382 | 'Content-Type': 'application/json', 383 | 'Content-Length': Buffer.byteLength(message), 384 | 'Access-Control-Allow-Headers': 'Content-Type', 385 | 'Allow': 'POST' 386 | }, custom_headers); 387 | 388 | /*if (code === 401) { 389 | headers['WWW-Authenticate'] = 'Basic realm=' + 'JSON-RPC' + ''; 390 | }*/ 391 | 392 | if (res.writeHead) { 393 | res.writeHead(200, headers); 394 | res.write(message); 395 | } else { 396 | headers['Content-Length'] += 3; 397 | res.write(headers + '\n\n' + message + '\n'); 398 | } 399 | res.end(); 400 | } 401 | }); 402 | 403 | return Server; 404 | }; 405 | --------------------------------------------------------------------------------