├── .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 | [](https://travis-ci.org/pocesar/node-jsonrpc2)
2 |
3 | [](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 |
--------------------------------------------------------------------------------