From de0c6c444778d8d7a915659bfbd0f8454724a64e Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Fri, 1 Jul 2011 22:19:49 +0100 Subject: [PATCH 01/24] Added package.json for NPM. --- package.json | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 package.json diff --git a/package.json b/package.json new file mode 100644 index 0000000..56392e9 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "jsonrpc2", + "version": "0.0.2", + "description": "JSON-RPC server and client library", + "main": "./src/jsonrpc", + "keywords": [ + "json", + "rpc", + "server", + "client" + ], + + "author": "Eric Florenzano (eflorenzano.com)", + + "contributors": [ + "Bill Casarin (jb55.com)", + "Stefan Thomas (justmoon.net)" + ], + + "repository": { + "type": "git", + "url": "git://github.com/bitcoinjs/node-jsonrpc2.git" + } +} From e2ab1c74749eca0f01df1b18347bd5eb13282564 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Fri, 1 Jul 2011 22:20:25 +0100 Subject: [PATCH 02/24] Changed README to use Markdown and added install instructions. --- README | 30 ------------------------------ README.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 30 deletions(-) delete mode 100644 README create mode 100644 README.md diff --git a/README b/README deleted file mode 100644 index 0c65e38..0000000 --- a/README +++ /dev/null @@ -1,30 +0,0 @@ -This is a JSON-RPC server and client library for node.js , -the V8 based evented IO framework. - -Firing up an efficient JSON-RPC server becomes extremely simple: - - var rpc = require('jsonrpc'); - - function add(first, second) { - return first + second; - } - rpc.expose('add', add); - - rpc.listen(8000, 'localhost'); - - -And creating a client to speak to that server is easy too: - - var rpc = require('jsonrpc'); - var sys = require('sys'); - - var client = rpc.getClient(8000, 'localhost'); - - client.call('add', [1, 2], function(result) { - sys.puts('1 + 2 = ' + result); - }); - -To learn more, see the examples directory, peruse test/jsonrpc-test.js, or -simply "Use The Source, Luke". - -More documentation and development is on its way. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b254d7a --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# node-jsonrpc2 + +This is a JSON-RPC server and client library for node.js , +the V8 based evented IO framework. + +## Install + +To install node-jsonrpc2 in the current directory, run: + + npm install jsonrpc2 + +## Usage + +Firing up an efficient JSON-RPC server becomes extremely simple: + +``` javascript +var rpc = require('jsonrpc'); + +function add(first, second) { + return first + second; +} +rpc.expose('add', add); + +rpc.listen(8000, 'localhost'); +``` + +And creating a client to speak to that server is easy too: + +``` javascript +var rpc = require('jsonrpc'); +var sys = require('sys'); + +var client = rpc.getClient(8000, 'localhost'); + +client.call('add', [1, 2], function(result) { + sys.puts('1 + 2 = ' + result); +}); +``` + +To learn more, see the examples directory, peruse test/jsonrpc-test.js, or +simply "Use The Source, Luke". + +More documentation and development is on its way. From baf3779bf3edef15e695ddafff99f19effd8b0e5 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Mon, 4 Jul 2011 17:57:08 +0100 Subject: [PATCH 03/24] Updated README to refer to new package name. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b254d7a..6343f9b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ To install node-jsonrpc2 in the current directory, run: Firing up an efficient JSON-RPC server becomes extremely simple: ``` javascript -var rpc = require('jsonrpc'); +var rpc = require('jsonrpc2'); function add(first, second) { return first + second; @@ -27,7 +27,7 @@ rpc.listen(8000, 'localhost'); And creating a client to speak to that server is easy too: ``` javascript -var rpc = require('jsonrpc'); +var rpc = require('jsonrpc2'); var sys = require('sys'); var client = rpc.getClient(8000, 'localhost'); From 4d7cfe6701af6f074e14184e4d31cf5dc4e08e62 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Mon, 4 Jul 2011 18:11:08 +0100 Subject: [PATCH 04/24] Changed handler format to conform to Node.js norms. Callbacks should always take an "err" argument first for asynchronous error handling. Callbacks should be passed in as the last parameter (this was already the case). The parameter array should be passed in as an array. -- Now, I had quite some internal debate about this. It is nicer syntax-wise to have the arguments come in as actual arguments, not as an array. However, it does have the effect that the callback parameter moves around depending on user-submitted data. So the function will have to handle finding out which parameter is the callback, which is a lot uglier and more error-prone than just passing in the arguments as an array. Finally, I've added an additional parameter "opt" which contains the HTTP request object and the server object. This is useful as an extensible way to deliver more features and options to the handler in future versions of the library. Here is an example handler with the new syntax: server.expose('multiply', function (args, opt, callback) { var a = args[0], b = args[1]; callback(null, a*b); }); --- src/jsonrpc.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/jsonrpc.js b/src/jsonrpc.js index d694b94..86aa52a 100644 --- a/src/jsonrpc.js +++ b/src/jsonrpc.js @@ -193,14 +193,24 @@ Server.prototype.handlePOST = function(req, res) { // Try to call the method, but intercept errors and call our // onFailure handler. var method = self.functions[decoded.method]; - var args = decoded.params.push(function(resp) { - onSuccess(resp); - }); + var callback = function(err, result) { + if (err) { + onFailure(err); + } else { + onSuccess(result); + } + }; + + // Other various information we want to pass in for the handler to be + // able to access. + var opt = { + req: req, + server: self + }; try { - method.apply(null, decoded.params); - } - catch(err) { + method.call(null, decoded.params, opt, callback); + } catch (err) { return onFailure(err); } From ca45aa458f5371855715eaeb5a5943f5064c5351 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Mon, 4 Jul 2011 19:31:02 +0100 Subject: [PATCH 05/24] Version bump. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 56392e9..50754a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonrpc2", - "version": "0.0.2", + "version": "0.0.3", "description": "JSON-RPC server and client library", "main": "./src/jsonrpc", "keywords": [ From af0b801eb7a9e4c3be5b82dcdbec030e0a9726c2 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Tue, 5 Jul 2011 13:08:54 +0100 Subject: [PATCH 06/24] Let user configure the scope (per function, per module and globally). --- src/jsonrpc.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/jsonrpc.js b/src/jsonrpc.js index 86aa52a..ae8eb85 100644 --- a/src/jsonrpc.js +++ b/src/jsonrpc.js @@ -70,6 +70,8 @@ var Client = function(port, host, user, password) { function Server() { var self = this; this.functions = {}; + this.scopes = {}; + this.defaultScope = this; this.server = http.createServer(function(req, res) { Server.trace('<--', 'accepted request'); if(req.method === 'POST') { @@ -85,13 +87,17 @@ function Server() { //===----------------------------------------------------------------------===// // exposeModule //===----------------------------------------------------------------------===// -Server.prototype.exposeModule = function(mod, object) { +Server.prototype.exposeModule = function(mod, object, scope) { var funcs = []; for(var funcName in object) { var funcObj = object[funcName]; if(typeof(funcObj) == 'function') { this.functions[mod + '.' + funcName] = funcObj; funcs.push(funcName); + + if (scope) { + this.scopes[mod + '.' + funcName] = scope; + } } } Server.trace('***', 'exposing module: ' + mod + ' [funs: ' + funcs.join(', ') @@ -103,9 +109,13 @@ Server.prototype.exposeModule = function(mod, object) { //===----------------------------------------------------------------------===// // expose //===----------------------------------------------------------------------===// -Server.prototype.expose = function(name, func) { +Server.prototype.expose = function(name, func, scope) { Server.trace('***', 'exposing: ' + name); this.functions[name] = func; + + if (scope) { + this.scopes[name] = scope; + } } @@ -200,6 +210,7 @@ Server.prototype.handlePOST = function(req, res) { onSuccess(result); } }; + var scope = self.scopes[decoded.method] || this.defaultScope; // Other various information we want to pass in for the handler to be // able to access. @@ -209,7 +220,7 @@ Server.prototype.handlePOST = function(req, res) { }; try { - method.call(null, decoded.params, opt, callback); + method.call(scope, decoded.params, opt, callback); } catch (err) { return onFailure(err); } From 59f513ce55a183230db3fcd19b7198694a9849c3 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Tue, 5 Jul 2011 13:09:18 +0100 Subject: [PATCH 07/24] Version bump to 0.0.4. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 50754a7..99b9c87 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonrpc2", - "version": "0.0.3", + "version": "0.0.4", "description": "JSON-RPC server and client library", "main": "./src/jsonrpc", "keywords": [ From dc788d7283d3b30521da98ea6c84f4604377fec0 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Tue, 5 Jul 2011 13:19:20 +0100 Subject: [PATCH 08/24] Fixed bug: Incorrect reference to defaultScope. --- src/jsonrpc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jsonrpc.js b/src/jsonrpc.js index ae8eb85..c751254 100644 --- a/src/jsonrpc.js +++ b/src/jsonrpc.js @@ -210,7 +210,7 @@ Server.prototype.handlePOST = function(req, res) { onSuccess(result); } }; - var scope = self.scopes[decoded.method] || this.defaultScope; + var scope = self.scopes[decoded.method] || self.defaultScope; // Other various information we want to pass in for the handler to be // able to access. From 8fd36e6e418fa29384ad61cee929987854e0df9c Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Tue, 5 Jul 2011 13:19:36 +0100 Subject: [PATCH 09/24] Version bump to 0.0.5. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 99b9c87..c056977 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonrpc2", - "version": "0.0.4", + "version": "0.0.5", "description": "JSON-RPC server and client library", "main": "./src/jsonrpc", "keywords": [ From 1dca5074c1ef43a88a7416758ef1c6ddd548e45d Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Sat, 16 Jul 2011 20:15:50 +0100 Subject: [PATCH 10/24] Changed to standard callback format. --- src/jsonrpc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jsonrpc.js b/src/jsonrpc.js index c751254..76f454a 100644 --- a/src/jsonrpc.js +++ b/src/jsonrpc.js @@ -53,7 +53,7 @@ var Client = function(port, host, user, password) { var decoded = JSON.parse(buffer); if(decoded.hasOwnProperty('result')) { if (callback) - callback(decoded.result); + callback(null, decoded.result); } else { if (errback) From 4be3cb0270e3cebb08defa794cda1c892f66b528 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Sun, 17 Jul 2011 08:37:19 +0100 Subject: [PATCH 11/24] Fixed README, test cases and examples. --- README.md | 14 +-- examples/client.js | 35 +++---- examples/server.js | 65 +++++++------ package.json | 2 +- src/jsonrpc.js | 10 +- test/jsonrpc-test.js | 212 ++++++++++++++++++++++--------------------- test/test.js | 132 ++++++++++++++------------- 7 files changed, 244 insertions(+), 226 deletions(-) diff --git a/README.md b/README.md index 6343f9b..e0d2d3e 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,14 @@ Firing up an efficient JSON-RPC server becomes extremely simple: ``` javascript var rpc = require('jsonrpc2'); -function add(first, second) { - return first + second; +var server = new rpc.Server(); + +function add(args, opt, callback) { + callback(null, args[0] + args[1]); } -rpc.expose('add', add); +server.expose('add', add); -rpc.listen(8000, 'localhost'); +server.listen(8000, 'localhost'); ``` And creating a client to speak to that server is easy too: @@ -30,9 +32,9 @@ And creating a client to speak to that server is easy too: var rpc = require('jsonrpc2'); var sys = require('sys'); -var client = rpc.getClient(8000, 'localhost'); +var client = new rpc.Client(8000, 'localhost'); -client.call('add', [1, 2], function(result) { +client.call('add', [1, 2], function(err, result) { sys.puts('1 + 2 = ' + result); }); ``` diff --git a/examples/client.js b/examples/client.js index d8f4aaa..0481c7c 100644 --- a/examples/client.js +++ b/examples/client.js @@ -1,32 +1,35 @@ var sys = require('sys'); var rpc = require('../src/jsonrpc'); -var client = rpc.getClient(8000, 'localhost'); +var client = new rpc.Client(8088, 'localhost'); -client.call('add', [1, 2], function(result) { - sys.puts(' 1 + 2 = ' + result); +client.call('add', [1, 2], function (err, result) { + sys.puts(' 1 + 2 = ' + result); }); -client.call('multiply', [199, 2], function(result) { - sys.puts('199 * 2 = ' + result); +client.call('multiply', [199, 2], function (err, result) { + sys.puts('199 * 2 = ' + result); }); // Accessing modules is as simple as dot-prefixing. -client.call('math.power', [3, 3], function(result) { - sys.puts(' 3 ^ 3 = ' + result); +client.call('math.power', [3, 3], function (err, result) { + sys.puts(' 3 ^ 3 = ' + result); }); -// Call simply returns a promise, so we can add callbacks or errbacks at will. -var promise = client.call('add', [1, 1]); -promise.addCallback(function(result) { - sys.puts(' 1 + 1 = ' + result + ', dummy!'); +// We can handle errors the same way as anywhere else in Node +client.call('add', [1, 1], function (err, result) { + if (err) { + sys.puts('RPC Error: '+ sys.inspect(err)); + return; + } + sys.puts(' 1 + 1 = ' + result + ', dummy!'); }); /* These calls should each take 1.5 seconds to complete. */ -client.call('delayed.add', [1, 1, 1500], function(result) { - sys.puts(result); +client.call('delayed.add', [1, 1, 1500], function (err, result) { + sys.puts(result); }); -client.call('delayed.echo', ['Echo.', 1500], function(result) { - sys.puts(result); -}); \ No newline at end of file +client.call('delayed.echo', ['Echo.', 1500], function (err, result) { + sys.puts(result); +}); diff --git a/examples/server.js b/examples/server.js index 08aaa8b..bee3090 100644 --- a/examples/server.js +++ b/examples/server.js @@ -1,46 +1,53 @@ var rpc = require('../src/jsonrpc'); +var server = new rpc.Server(); + /* Create two simple functions */ -function add(first, second) { - return first + second; +function add(args, opts, callback) { + callback(null, args[0]+args[1]); } -function multiply(first, second) { - return first * second; +function multiply(args, opts, callback) { + callback(null, args[0]*args[1]); } /* Expose those methods */ -rpc.expose('add', add); -rpc.expose('multiply', multiply); +server.expose('add', add); +server.expose('multiply', multiply); /* We can expose entire modules easily */ var math = { - power: function(first, second) { return Math.pow(first, second); }, - sqrt: function(num) { return Math.sqrt(num); } + power: function(args, opts, callback) { + callback(null, Math.pow(args[0], args[1])); + }, + sqrt: function(args, opts, callback) { + callback(null, Math.sqrt(args[0])); + } } -rpc.exposeModule('math', math); +server.exposeModule('math', math); -/* Listen on port 8000 */ -rpc.listen(8000, 'localhost'); +/* Listen on port 8088 */ +server.listen(8088, 'localhost'); -/* By returning a promise, we can delay our response indefinitely, leaving the - request hanging until the promise emits success. */ +/* By using a callback, we can delay our response indefinitely, leaving the + request hanging until the callback emits success. */ var delayed = { - echo: function(data, delay) { - var promise = new process.Promise(); - setTimeout(function() { - promise.emitSuccess(data); - }, delay); - return promise; - }, - - add: function(first, second, delay) { - var promise = new process.Promise(); - setTimeout(function() { - promise.emitSuccess(first + second); - }, delay); - return promise; - } + echo: function(args, opts, callback) { + var data = args[0]; + var delay = args[1]; + setTimeout(function() { + callback(null, data); + }, delay); + }, + + add: function(args, opts, callback) { + var first = args[0]; + var second = args[1]; + var delay = args[2]; + setTimeout(function() { + callback(null, first + second); + }, delay); + } } -rpc.exposeModule('delayed', delayed); \ No newline at end of file +server.exposeModule('delayed', delayed); diff --git a/package.json b/package.json index c056977..1608f12 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonrpc2", - "version": "0.0.5", + "version": "0.0.6", "description": "JSON-RPC server and client library", "main": "./src/jsonrpc", "keywords": [ diff --git a/src/jsonrpc.js b/src/jsonrpc.js index 76f454a..23ef0f4 100644 --- a/src/jsonrpc.js +++ b/src/jsonrpc.js @@ -156,7 +156,7 @@ Server.prototype.handlePOST = function(req, res) { var self = this; var handle = function (buf) { var decoded = JSON.parse(buf); - + // Check for the required fields, and if they aren't there, then // dispatch to the handleInvalidRequest function. if(!(decoded.method && decoded.params && decoded.id)) { @@ -166,7 +166,7 @@ Server.prototype.handlePOST = function(req, res) { if(!self.functions.hasOwnProperty(decoded.method)) { return Server.handleInvalidRequest(req, res); } - + // Build our success handler var onSuccess = function(funcResp) { Server.trace('-->', 'response (id ' + decoded.id + '): ' + @@ -182,7 +182,7 @@ Server.prototype.handlePOST = function(req, res) { res.write(encoded); res.end(); }; - + // Build our failure handler (note that error must not be null) var onFailure = function(failure) { Server.trace('-->', 'failure: ' + JSON.stringify(failure)); @@ -196,10 +196,10 @@ Server.prototype.handlePOST = function(req, res) { res.write(encoded); res.end(); }; - + Server.trace('<--', 'request (id ' + decoded.id + '): ' + decoded.method + '(' + decoded.params.join(', ') + ')'); - + // Try to call the method, but intercept errors and call our // onFailure handler. var method = self.functions[decoded.method]; diff --git a/test/jsonrpc-test.js b/test/jsonrpc-test.js index 893a6b2..fbab78e 100644 --- a/test/jsonrpc-test.js +++ b/test/jsonrpc-test.js @@ -1,156 +1,158 @@ -process.mixin(GLOBAL, require('./test')); +require('./test').extend(global); var sys = require('sys'); -var jsonrpc = require('../src/jsonrpc'); +var rpc = require('../src/jsonrpc'); + +var server = new rpc.Server(); // MOCK REQUEST/RESPONSE OBJECTS var MockRequest = function(method) { - this.method = method; - process.EventEmitter.call(this); + this.method = method; + process.EventEmitter.call(this); }; sys.inherits(MockRequest, process.EventEmitter); var MockResponse = function() { - process.EventEmitter.call(this); - this.sendHeader = function(httpCode, httpHeaders) { - this.httpCode = httpCode; - this.httpHeaders = httpCode; - }; - this.sendBody = function(httpBody) { - this.httpBody = httpBody; - }; - this.finish = function() {}; + process.EventEmitter.call(this); + this.writeHead = this.sendHeader = function(httpCode, httpHeaders) { + this.httpCode = httpCode; + this.httpHeaders = httpCode; + }; + this.write = this.sendBody = function(httpBody) { + this.httpBody = httpBody; + }; + this.end = this.finish = function() {}; }; sys.inherits(MockResponse, process.EventEmitter); // A SIMPLE MODULE var TestModule = { - foo: function (a, b) { - return ['foo', 'bar', a, b]; - }, + foo: function (a, b) { + return ['foo', 'bar', a, b]; + }, - other: 'hello' + other: 'hello' }; // EXPOSING FUNCTIONS -test('jsonrpc.expose', function() { - var echo = function(data) { - return data; - }; - jsonrpc.expose('echo', echo); - assert(jsonrpc.functions.echo === echo); +test('Server.expose', function() { + var echo = function(args, opts, callback) { + callback(null, args[0]); + }; + server.expose('echo', echo); + assert(server.functions.echo === echo); }) -test('jsonrpc.exposeModule', function() { - jsonrpc.exposeModule('test', TestModule); - sys.puts(jsonrpc.functions['test.foo']); - sys.puts(TestModule.foo); - assert(jsonrpc.functions['test.foo'] == TestModule.foo); +test('Server.exposeModule', function() { + server.exposeModule('test', TestModule); + sys.puts(server.functions['test.foo']); + sys.puts(TestModule.foo); + assert(server.functions['test.foo'] == TestModule.foo); }); // INVALID REQUEST -test('GET jsonrpc.handleRequest', function() { - var req = new MockRequest('GET'); - var res = new MockResponse(); - jsonrpc.handleRequest(req, res); - assert(res.httpCode === 405); +test('GET Server.handleNonPOST', function() { + var req = new MockRequest('GET'); + var res = new MockResponse(); + rpc.Server.handleNonPOST(req, res); + assert(res.httpCode === 405); }); function testBadRequest(testJSON) { - var req = new MockRequest('POST'); - var res = new MockResponse(); - jsonrpc.handleRequest(req, res); - req.emit('body', testJSON); - req.emit('complete'); - sys.puts(res.httpCode); - assert(res.httpCode === 400); + var req = new MockRequest('POST'); + var res = new MockResponse(); + server.handlePOST(req, res); + req.emit('data', testJSON); + req.emit('end'); + sys.puts(res.httpCode); + assert(res.httpCode === 400); } test('Missing object attribute (method)', function() { - var testJSON = '{ "params": ["Hello, World!"], "id": 1 }'; - testBadRequest(testJSON); + var testJSON = '{ "params": ["Hello, World!"], "id": 1 }'; + testBadRequest(testJSON); }); test('Missing object attribute (params)', function() { - var testJSON = '{ "method": "echo", "id": 1 }'; - testBadRequest(testJSON); + var testJSON = '{ "method": "echo", "id": 1 }'; + testBadRequest(testJSON); }); test('Missing object attribute (id)', function() { - var testJSON = '{ "method": "echo", "params": ["Hello, World!"] }'; - testBadRequest(testJSON); + var testJSON = '{ "method": "echo", "params": ["Hello, World!"] }'; + testBadRequest(testJSON); }); test('Unregistered method', function() { - var testJSON = '{ "method": "notRegistered", "params": ["Hello, World!"], "id": 1 }'; - testBadRequest(testJSON); + var testJSON = '{ "method": "notRegistered", "params": ["Hello, World!"], "id": 1 }'; + testBadRequest(testJSON); }); // VALID REQUEST test('Simple synchronous echo', function() { - var testJSON = '{ "method": "echo", "params": ["Hello, World!"], "id": 1 }'; - var req = new MockRequest('POST'); - var res = new MockResponse(); - jsonrpc.handleRequest(req, res); - req.emit('body', testJSON); - req.emit('complete'); - assert(res.httpCode === 200); - var decoded = JSON.parse(res.httpBody); - assert(decoded.id === 1); - assert(decoded.error === null); - assert(decoded.result == 'Hello, World!'); + var testJSON = '{ "method": "echo", "params": ["Hello, World!"], "id": 1 }'; + var req = new MockRequest('POST'); + var res = new MockResponse(); + server.handlePOST(req, res); + req.emit('data', testJSON); + req.emit('end'); + assert(res.httpCode === 200); + var decoded = JSON.parse(res.httpBody); + assert(decoded.id === 1); + assert(decoded.error === null); + assert(decoded.result == 'Hello, World!'); }); test('Using promise', function() { - // Expose a function that just returns a promise that we can control. - var promise = new process.Promise(); - jsonrpc.expose('promiseEcho', function(data) { - return promise; - }); - // Build a request to call that function - var testJSON = '{ "method": "promiseEcho", "params": ["Hello, World!"], "id": 1 }'; - var req = new MockRequest('POST'); - var res = new MockResponse(); - // Have the server handle that request - jsonrpc.handleRequest(req, res); - req.emit('body', testJSON); - req.emit('complete'); - // Now the request has completed, and in the above synchronous test, we - // would be finished. However, this function is smarter and only completes - // when the promise completes. Therefore, we should not have a response - // yet. - assert(res['httpCode'] == null); - // We can force the promise to emit a success code, with a message. - promise.emitSuccess('Hello, World!'); - // Aha, now that the promise has finished, our request has finished as well. - assert(res.httpCode === 200); - var decoded = JSON.parse(res.httpBody); - assert(decoded.id === 1); - assert(decoded.error === null); - assert(decoded.result == 'Hello, World!'); + // Expose a function that just returns a promise that we can control. + var callbackRef = null; + server.expose('promiseEcho', function(args, opts, callback) { + callbackRef = callback; + }); + // Build a request to call that function + var testJSON = '{ "method": "promiseEcho", "params": ["Hello, World!"], "id": 1 }'; + var req = new MockRequest('POST'); + var res = new MockResponse(); + // Have the server handle that request + server.handlePOST(req, res); + req.emit('data', testJSON); + req.emit('end'); + // Now the request has completed, and in the above synchronous test, we + // would be finished. However, this function is smarter and only completes + // when the promise completes. Therefore, we should not have a response + // yet. + assert(res['httpCode'] == null); + // We can force the promise to emit a success code, with a message. + callbackRef(null, 'Hello, World!'); + // Aha, now that the promise has finished, our request has finished as well. + assert(res.httpCode === 200); + var decoded = JSON.parse(res.httpBody); + assert(decoded.id === 1); + assert(decoded.error === null); + assert(decoded.result == 'Hello, World!'); }); test('Triggering an errback', function() { - var promise = new process.Promise(); - jsonrpc.expose('errbackEcho', function(data) { - return promise; - }); - var testJSON = '{ "method": "errbackEcho", "params": ["Hello, World!"], "id": 1 }'; - var req = new MockRequest('POST'); - var res = new MockResponse(); - jsonrpc.handleRequest(req, res); - req.emit('body', testJSON); - req.emit('complete'); - assert(res['httpCode'] == null); - // This time, unlike the above test, we trigger an error and expect to see - // it in the error attribute of the object returned. - promise.emitError('This is an error'); - assert(res.httpCode === 200); - var decoded = JSON.parse(res.httpBody); - assert(decoded.id === 1); - assert(decoded.error == 'This is an error'); - assert(decoded.result == null); -}) \ No newline at end of file + var callbackRef = null; + server.expose('errbackEcho', function(args, opts, callback) { + callbackRef = callback; + }); + var testJSON = '{ "method": "errbackEcho", "params": ["Hello, World!"], "id": 1 }'; + var req = new MockRequest('POST'); + var res = new MockResponse(); + server.handlePOST(req, res); + req.emit('data', testJSON); + req.emit('end'); + assert(res['httpCode'] == null); + // This time, unlike the above test, we trigger an error and expect to see + // it in the error attribute of the object returned. + callbackRef('This is an error'); + assert(res.httpCode === 200); + var decoded = JSON.parse(res.httpBody); + assert(decoded.id === 1); + assert(decoded.error == 'This is an error'); + assert(decoded.result == null); +}) diff --git a/test/test.js b/test/test.js index 943507f..314d043 100644 --- a/test/test.js +++ b/test/test.js @@ -1,77 +1,81 @@ var sys = require('sys'); -TEST = { - passed: 0, - failed: 0, - assertions: 0, +var TEST = module.exports = { + passed: 0, + failed: 0, + assertions: 0, - test: function (desc, block) { - var _puts = sys.puts, - output = "", - result = '?', - _boom = null; - sys.puts = function (s) { output += s + "\n"; } - try { - sys.print(" " + desc + " ..."); - block(); - result = '.'; - } catch(boom) { - if ( boom == 'FAIL' ) { - result = 'F'; - } else { - result = 'E'; - _boom = boom; - sys.puts(boom.toString()); - } - } - sys.puts = _puts; - if ( result == '.' ) { - sys.print(" OK\n"); - TEST.passed += 1; + test: function (desc, block) { + var _puts = sys.puts, + output = "", + result = '?', + _boom = null; + sys.puts = function (s) { output += s + "\n"; } + try { + sys.print(" " + desc + " ..."); + block(); + result = '.'; + } catch(boom) { + if ( boom == 'FAIL' ) { + result = 'F'; } else { - sys.print(" FAIL\n"); - sys.print(output.replace(/^/, " ") + "\n"); - TEST.failed += 1; - if ( _boom ) throw _boom; + result = 'E'; + _boom = boom; + sys.puts(boom.toString()); } - }, + } + sys.puts = _puts; + if ( result == '.' ) { + sys.print(" OK\n"); + TEST.passed += 1; + } else { + sys.print(" FAIL\n"); + sys.print(output.replace(/^/, " ") + "\n"); + TEST.failed += 1; + if ( _boom ) throw _boom; + } + }, - assert: function (value, desc) { - TEST.assertions += 1; - if ( desc ) sys.puts("ASSERT: " + desc); - if ( !value ) throw 'FAIL'; - }, + assert: function (value, desc) { + TEST.assertions += 1; + if ( desc ) sys.puts("ASSERT: " + desc); + if ( !value ) throw 'FAIL'; + }, - assert_equal: function (expect, is) { - assert( - expect == is, - sys.inspect(expect) + " == " + sys.inspect(is) - ); - }, + assert_equal: function (expect, is) { + assert( + expect == is, + sys.inspect(expect) + " == " + sys.inspect(is) + ); + }, - assert_boom: function (message, block) { - var error = null; - try { block() } - catch (boom) { error = boom } + assert_boom: function (message, block) { + var error = null; + try { block() } + catch (boom) { error = boom } - if ( !error ) { - sys.puts('NO BOOM'); - throw 'FAIL' - } - if ( error != message ) { - sys.puts('BOOM: ' + sys.inspect(error) + - ' [' + sys.inspect(message) + ' expected]'); - throw 'FAIL' - } - } -}; + if ( !error ) { + sys.puts('NO BOOM'); + throw 'FAIL' + } + if ( error != message ) { + sys.puts('BOOM: ' + sys.inspect(error) + + ' [' + sys.inspect(message) + ' expected]'); + throw 'FAIL' + } + }, -process.mixin(exports, TEST); + extend: function (scope) { + Object.keys(TEST).forEach(function (key) { + scope[key] = TEST[key]; + }); + } +}; process.addListener('exit', function (code) { - if ( !TEST.exit ) { - TEST.exit = true; - sys.puts("" + TEST.passed + " passed, " + TEST.failed + " failed"); - if ( TEST.failed > 0 ) { process.exit(1) }; - } + if ( !TEST.exit ) { + TEST.exit = true; + sys.puts("" + TEST.passed + " passed, " + TEST.failed + " failed"); + if ( TEST.failed > 0 ) { process.exit(1) }; + } }); From 600be76a7551a36f0b97e84a50b7015a5104c538 Mon Sep 17 00:00:00 2001 From: Bruno Bigras Date: Mon, 17 Oct 2011 11:15:02 -0400 Subject: [PATCH 12/24] Report errors from the http client --- src/jsonrpc.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/jsonrpc.js b/src/jsonrpc.js index 23ef0f4..cb44639 100644 --- a/src/jsonrpc.js +++ b/src/jsonrpc.js @@ -37,6 +37,11 @@ var Client = function(port, host, user, password) { headers['Host'] = host; headers['Content-Length'] = requestJSON.length; + // Report errors from the http client. This also prevents crashes since an exception is thrown if we don't handle this event. + client.on('error', function(err) { + callback(err); + }); + // Now we'll make a request to the server var request = client.request('POST', path || '/', headers); request.write(requestJSON); From fab2eae4002ca9f7bc21bd187caf96f604da87d7 Mon Sep 17 00:00:00 2001 From: Stefan Date: Tue, 18 Oct 2011 08:49:51 +0200 Subject: [PATCH 13/24] Bump version to 0.0.7. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1608f12..f3c8396 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonrpc2", - "version": "0.0.6", + "version": "0.0.7", "description": "JSON-RPC server and client library", "main": "./src/jsonrpc", "keywords": [ From 09b2c8a7488b9ead36eee014df8bd88ca8df7d75 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Thu, 1 Dec 2011 06:47:30 +0100 Subject: [PATCH 14/24] Added HTTP streaming mode. --- .gitignore | 1 + examples/client.js | 12 +++ examples/server.js | 24 +++++- package.json | 4 + src/jsonrpc.js | 182 ++++++++++++++++++++++++++++++++------------- 5 files changed, 172 insertions(+), 51 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/examples/client.js b/examples/client.js index 0481c7c..4e06990 100644 --- a/examples/client.js +++ b/examples/client.js @@ -33,3 +33,15 @@ client.call('delayed.add', [1, 1, 1500], function (err, result) { client.call('delayed.echo', ['Echo.', 1500], function (err, result) { sys.puts(result); }); + +var counter = 0; +client.stream('listen', [], function (err, connection) { + connection.expose('event', function (params) { + console.log('Streaming #'+counter+': '+params[0]); + counter++; + if (counter > 4) { + connection.end(); + } + }); + console.log('start listening'); +}); diff --git a/examples/server.js b/examples/server.js index bee3090..8db2562 100644 --- a/examples/server.js +++ b/examples/server.js @@ -1,4 +1,5 @@ var rpc = require('../src/jsonrpc'); +var events = require('events'); var server = new rpc.Server(); @@ -23,7 +24,7 @@ var math = { sqrt: function(args, opts, callback) { callback(null, Math.sqrt(args[0])); } -} +}; server.exposeModule('math', math); /* Listen on port 8088 */ @@ -51,3 +52,24 @@ var delayed = { } server.exposeModule('delayed', delayed); + +// Create a message bus with random events on it +var firehose = new events.EventEmitter(); +(function emitFirehoseEvent() { + firehose.emit('foobar', {data: 'random '+Math.random()}); + setTimeout(arguments.callee, 200+Math.random()*3000); +})(); + +var listen = function (args, opts, callback) { + function handleFirehoseEvent(event) { + opts.emit('event', event.data); + }; + firehose.on('foobar', handleFirehoseEvent); + opts.stream(function () { + console.log('connection ended'); + firehose.removeListener('foobar', handleFirehoseEvent); + }); + callback(null); +}; + +server.expose('listen', listen); diff --git a/package.json b/package.json index f3c8396..37eb763 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,10 @@ "author": "Eric Florenzano (eflorenzano.com)", + "dependencies": { + "jsonparse": ">=0.0.1" + }, + "contributors": [ "Bill Casarin (jb55.com)", "Stefan Thomas (justmoon.net)" diff --git a/src/jsonrpc.js b/src/jsonrpc.js index cb44639..1e275f4 100644 --- a/src/jsonrpc.js +++ b/src/jsonrpc.js @@ -1,25 +1,36 @@ var sys = require('sys'); var http = require('http'); +var util = require('util'); +var events = require('events'); +var JsonParser = require('jsonparse'); var METHOD_NOT_ALLOWED = "Method Not Allowed\n"; var INVALID_REQUEST = "Invalid Request\n"; //===----------------------------------------------------------------------===// -// Server Client +// JSON-RPC HTTP Client //===----------------------------------------------------------------------===// -var Client = function(port, host, user, password) { +var Client = function (port, host, user, password) { this.port = port; this.host = host; this.user = user; this.password = password; - - this.call = function(method, params, callback, errback, path) { + + this.stream = function (method, params, opts, callback) { + if ("function" === typeof opts) { + callback = opts; + opts = {}; + } + opts = opts || {}; + var client = http.createClient(port, host); - + + var id = 1; + // First we encode the request into JSON var requestJSON = JSON.stringify({ - 'id': '' + (new Date()).getTime(), + 'id': id, 'method': method, 'params': params }); @@ -43,34 +54,69 @@ var Client = function(port, host, user, password) { }); // Now we'll make a request to the server - var request = client.request('POST', path || '/', headers); + var request = client.request('POST', opts.path || '/', headers); request.write(requestJSON); - request.on('response', function(response) { - // We need to buffer the response chunks in a nonblocking way. - var buffer = ''; - response.on('data', function(chunk) { - buffer = buffer + chunk; - }); - // When all the responses are finished, we decode the JSON and - // depending on whether it's got a result or an error, we call - // emitSuccess or emitError on the promise. - response.on('end', function() { - var decoded = JSON.parse(buffer); - if(decoded.hasOwnProperty('result')) { - if (callback) - callback(null, decoded.result); - } - else { - if (errback) - errback(decoded.error); - } - }); + request.on('response', function (response) { + if ("function" === typeof callback) { + var connection = new events.EventEmitter(); + connection.id = id; + connection.req = request; + connection.res = response; + connection.expose = function (method, callback) { + connection.on('call:'+method, function (data) { + callback.call(null, data.params || []); + }); + }; + connection.end = function () { + this.req.connection.end(); + }; + callback(null, connection); + + // We need to buffer the response chunks in a nonblocking way. + var parser = new JsonParser(); + parser.onValue = function (decoded) { + if (this.stack.length) return; + + connection.emit('data', decoded); + if (decoded.hasOwnProperty('result') || + decoded.hasOwnProperty('error') && + decoded.id === id && + "function" === typeof callback) { + connection.emit('result', decoded); + } else if (decoded.hasOwnProperty('method')) { + connection.emit('call:'+decoded.method, decoded); + } + }; + connection.res.on('data', function (chunk) { + parser.write(chunk); + }); + connection.res.on('end', function () { + // TODO: Issue an error if there has been no valid response message + }); + } }); }; -} + this.call = function (method, params, opts, callback) { + if ("function" === typeof opts) { + callback = opts; + opts = {}; + } + opts = opts || {}; + this.stream(method, params, opts, function (err, conn) { + if ("function" === typeof callback) { + conn.on('result', function (decoded) { + if (!decoded.error) { + decoded.error = null; + } + callback(decoded.error, decoded.result); + }); + } + }); + }; +}; //===----------------------------------------------------------------------===// -// Server +// JSON-RPC HTTP Server //===----------------------------------------------------------------------===// function Server() { var self = this; @@ -162,44 +208,55 @@ Server.prototype.handlePOST = function(req, res) { var handle = function (buf) { var decoded = JSON.parse(buf); + var isStreaming = false; + // Check for the required fields, and if they aren't there, then // dispatch to the handleInvalidRequest function. if(!(decoded.method && decoded.params && decoded.id)) { - return Server.handleInvalidRequest(req, res); + Server.handleInvalidRequest(req, res); + return; } if(!self.functions.hasOwnProperty(decoded.method)) { - return Server.handleInvalidRequest(req, res); + Server.handleInvalidRequest(req, res); + return; } + var reply = function (json) { + var encoded = JSON.stringify(json); + + if (!isStreaming) { + res.writeHead(200, {'Content-Type': 'application/json', + 'Content-Length': encoded.length}); + res.write(encoded); + res.end(); + } else { + res.writeHead(200, {'Content-Type': 'application/json'}); + res.write(encoded); + // Keep connection open + } + }; + // Build our success handler var onSuccess = function(funcResp) { Server.trace('-->', 'response (id ' + decoded.id + '): ' + JSON.stringify(funcResp)); - var encoded = JSON.stringify({ + reply({ 'result': funcResp, 'error': null, 'id': decoded.id }); - res.writeHead(200, {'Content-Type': 'application/json', - 'Content-Length': encoded.length}); - res.write(encoded); - res.end(); }; // Build our failure handler (note that error must not be null) var onFailure = function(failure) { Server.trace('-->', 'failure: ' + JSON.stringify(failure)); - var encoded = JSON.stringify({ + reply({ 'result': null, 'error': failure || 'Unspecified Failure', 'id': decoded.id }); - res.writeHead(200, {'Content-Type': 'application/json', - 'Content-Length': encoded.length}); - res.write(encoded); - res.end(); }; Server.trace('<--', 'request (id ' + decoded.id + '): ' + @@ -215,22 +272,48 @@ Server.prototype.handlePOST = function(req, res) { onSuccess(result); } }; + // Can be called before the response callback to keep the connection open. + var stream = function (onend) { + isStreaming = true; + + if ("function" === typeof onend) { + res.connection.on('end', onend); + } + }; + var emit = function (method, params) { + if (!res.connection.writable) return; + + if (!Array.isArray(params)) { + params = [params]; + } + + Server.trace('-->', 'emit (method '+method+'): ' + JSON.stringify(params)); + var data = JSON.stringify({ + method: method, + params: params, + id: null + }); + res.write(data); + }; var scope = self.scopes[decoded.method] || self.defaultScope; // Other various information we want to pass in for the handler to be // able to access. - var opt = { + var opts = { req: req, - server: self + res: res, + server: self, + callback: callback, + stream: stream, + emit: emit }; try { - method.call(scope, decoded.params, opt, callback); + method.call(scope, decoded.params, opts, callback); } catch (err) { - return onFailure(err); + onFailure(err); } - - } // function handle(buf) + }; // function handle(buf) req.addListener('data', function(chunk) { buffer = buffer + chunk; @@ -239,7 +322,7 @@ Server.prototype.handlePOST = function(req, res) { req.addListener('end', function() { handle(buffer); }); -} +}; //===----------------------------------------------------------------------===// @@ -251,8 +334,7 @@ Server.handleNonPOST = function(req, res) { 'Allow': 'POST'}); res.write(METHOD_NOT_ALLOWED); res.end(); -} - +}; module.exports.Server = Server; module.exports.Client = Client; From f0fe410bff12c10d96ef76529d10e78ae7c4bd9e Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Thu, 1 Dec 2011 18:20:57 +0100 Subject: [PATCH 15/24] Switched back to native parser for non-streaming client requests. --- src/jsonrpc.js | 64 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 16 deletions(-) diff --git a/src/jsonrpc.js b/src/jsonrpc.js index 1e275f4..e8db590 100644 --- a/src/jsonrpc.js +++ b/src/jsonrpc.js @@ -17,7 +17,7 @@ var Client = function (port, host, user, password) { this.user = user; this.password = password; - this.stream = function (method, params, opts, callback) { + this.raw = function raw(method, params, opts, callback) { if ("function" === typeof opts) { callback = opts; opts = {}; @@ -56,7 +56,17 @@ var Client = function (port, host, user, password) { // Now we'll make a request to the server var request = client.request('POST', opts.path || '/', headers); request.write(requestJSON); - request.on('response', function (response) { + request.on('response', callback.bind(this, id, request)); + }; + + this.stream = function (method, params, opts, callback) { + if ("function" === typeof opts) { + callback = opts; + opts = {}; + } + opts = opts || {}; + + this.raw(method, params, opts, function (id, request, response) { if ("function" === typeof callback) { var connection = new events.EventEmitter(); connection.id = id; @@ -70,7 +80,6 @@ var Client = function (port, host, user, password) { connection.end = function () { this.req.connection.end(); }; - callback(null, connection); // We need to buffer the response chunks in a nonblocking way. var parser = new JsonParser(); @@ -87,8 +96,20 @@ var Client = function (port, host, user, password) { connection.emit('call:'+decoded.method, decoded); } }; + // Handle headers + connection.res.once('data', function (data) { + if (connection.res.statusCode === 200) { + callback(null, connection); + } else { + callback(new Error(""+connection.res.statusCode+" "+data)); + } + }); connection.res.on('data', function (chunk) { - parser.write(chunk); + try { + parser.write(chunk); + } catch(err) { + // TODO: Is ignoring invalid data the right thing to do? + } }); connection.res.on('end', function () { // TODO: Issue an error if there has been no valid response message @@ -102,15 +123,24 @@ var Client = function (port, host, user, password) { opts = {}; } opts = opts || {}; - this.stream(method, params, opts, function (err, conn) { - if ("function" === typeof callback) { - conn.on('result', function (decoded) { + this.raw(method, params, opts, function (id, request, response) { + var data = ''; + response.on('data', function (chunk) { + data += chunk; + }); + response.on('end', function () { + if (response.statusCode !== 200) { + callback(new Error(""+response.statusCode+" "+data)); + return; + } + var decoded = JSON.parse(data); + if ("function" === typeof callback) { if (!decoded.error) { decoded.error = null; } callback(decoded.error, decoded.result); - }); - } + } + }); }); }; }; @@ -189,12 +219,12 @@ Server.prototype.listen = function(port, host) { //===----------------------------------------------------------------------===// -// handleInvalidRequest +// handleServerError //===----------------------------------------------------------------------===// -Server.handleInvalidRequest = function(req, res) { +Server.handleServerError = function(req, res, code, message) { res.writeHead(400, {'Content-Type': 'text/plain', - 'Content-Length': INVALID_REQUEST.length}); - res.write(INVALID_REQUEST); + 'Content-Length': message.length}); + res.write(message); res.end(); } @@ -211,14 +241,16 @@ Server.prototype.handlePOST = function(req, res) { var isStreaming = false; // Check for the required fields, and if they aren't there, then - // dispatch to the handleInvalidRequest function. + // dispatch to the handleServerError function. if(!(decoded.method && decoded.params && decoded.id)) { - Server.handleInvalidRequest(req, res); + Server.trace('-->', 'response (invalid request)'); + Server.handleServerError(req, res, 400, INVALID_REQUEST); return; } if(!self.functions.hasOwnProperty(decoded.method)) { - Server.handleInvalidRequest(req, res); + Server.trace('-->', 'response (unknown method "' + decoded.method + '")'); + Server.handleServerError(req, res, 400, "Unknown RPC call '"+decoded.method+"'"); return; } From 22ac2dea9864977eed8915f0ba420d23330cc851 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Sun, 4 Dec 2011 02:25:43 +0100 Subject: [PATCH 16/24] Major refactoring, added socket-based transport. --- examples/client.js | 21 +- examples/server.js | 11 +- src/jsonrpc.js | 854 ++++++++++++++++++++++++++++++--------------- 3 files changed, 602 insertions(+), 284 deletions(-) diff --git a/examples/client.js b/examples/client.js index 4e06990..069e5cb 100644 --- a/examples/client.js +++ b/examples/client.js @@ -1,6 +1,8 @@ var sys = require('sys'); var rpc = require('../src/jsonrpc'); +rpc.Endpoint.trace = function () {}; + var client = new rpc.Client(8088, 'localhost'); client.call('add', [1, 2], function (err, result) { @@ -25,7 +27,7 @@ client.call('add', [1, 1], function (err, result) { sys.puts(' 1 + 1 = ' + result + ', dummy!'); }); -/* These calls should each take 1.5 seconds to complete. */ +// These calls should each take 1.5 seconds to complete client.call('delayed.add', [1, 1, 1500], function (err, result) { sys.puts(result); }); @@ -34,8 +36,8 @@ client.call('delayed.echo', ['Echo.', 1500], function (err, result) { sys.puts(result); }); -var counter = 0; client.stream('listen', [], function (err, connection) { + var counter = 0; connection.expose('event', function (params) { console.log('Streaming #'+counter+': '+params[0]); counter++; @@ -45,3 +47,18 @@ client.stream('listen', [], function (err, connection) { }); console.log('start listening'); }); + +var socketClient = new rpc.Client(8089, 'localhost'); + +socketClient.connectSocket(function (err, conn) { + var counter = 0; + socketClient.expose('event', function (params) { + console.log('Streaming (socket) #'+counter+': '+params[0]); + counter++; + if (counter > 4) { + conn.end(); + } + }); + + conn.call('listen', []); +}); diff --git a/examples/server.js b/examples/server.js index 8db2562..c594e98 100644 --- a/examples/server.js +++ b/examples/server.js @@ -27,9 +27,6 @@ var math = { }; server.exposeModule('math', math); -/* Listen on port 8088 */ -server.listen(8088, 'localhost'); - /* By using a callback, we can delay our response indefinitely, leaving the request hanging until the callback emits success. */ var delayed = { @@ -62,7 +59,7 @@ var firehose = new events.EventEmitter(); var listen = function (args, opts, callback) { function handleFirehoseEvent(event) { - opts.emit('event', event.data); + opts.call('event', event.data); }; firehose.on('foobar', handleFirehoseEvent); opts.stream(function () { @@ -73,3 +70,9 @@ var listen = function (args, opts, callback) { }; server.expose('listen', listen); + +/* HTTP server on port 8088 */ +server.listen(8088, 'localhost'); + +/* Raw socket server on port 8089 */ +server.listenRaw(8089, 'localhost'); diff --git a/src/jsonrpc.js b/src/jsonrpc.js index e8db590..2f7e14b 100644 --- a/src/jsonrpc.js +++ b/src/jsonrpc.js @@ -1,4 +1,5 @@ var sys = require('sys'); +var net = require('net'); var http = require('http'); var util = require('util'); var events = require('events'); @@ -7,257 +8,356 @@ var JsonParser = require('jsonparse'); var METHOD_NOT_ALLOWED = "Method Not Allowed\n"; var INVALID_REQUEST = "Invalid Request\n"; +/** + * Abstract base class for RPC endpoints. + * + * Has the ability to register RPC events and expose RPC methods. + */ +var Endpoint = function () +{ + events.EventEmitter.call(this); + + this.functions = {}; + this.scopes = {}; + this.defaultScope = this; +}; +util.inherits(Endpoint, events.EventEmitter); + +/** + * Output a piece of debug information. + */ +Endpoint.trace = function(direction, message) +{ + sys.puts(' ' + direction + ' ' + message); +} + +/** + * Define a callable method on this RPC endpoint + */ +Endpoint.prototype.expose = function(name, func, scope) +{ + if ("function" === typeof func) { + Endpoint.trace('***', 'exposing: ' + name); + this.functions[name] = func; + + if (scope) { + this.scopes[name] = scope; + } + } else { + var funcs = []; + var object = func; + for(var funcName in object) { + var funcObj = object[funcName]; + if(typeof(funcObj) == 'function') { + this.functions[name + '.' + funcName] = funcObj; + funcs.push(funcName); + + if (scope) { + this.scopes[name + '.' + funcName] = scope; + } + } + } + Endpoint.trace('***', 'exposing module: ' + name + + ' [funs: ' + funcs.join(', ') + ']'); + return object; + } +} + +/** + * Handle a call to one of the endpoint's methods. + */ +Endpoint.prototype.handleCall = function handleCall(decoded, conn, callback) +{ + Endpoint.trace('<--', 'Request (id ' + decoded.id + '): ' + + decoded.method + '(' + decoded.params.join(', ') + ')'); + + if (!this.functions.hasOwnProperty(decoded.method)) { + callback(new Error("Unknown RPC call '"+decoded.method+"'")); + return; + } + + var method = this.functions[decoded.method]; + var scope = this.scopes[decoded.method] || this.defaultScope; + + // Try to call the method, but intercept errors and call our + // error handler. + try { + method.call(scope, decoded.params, conn, callback); + } catch (err) { + callback(err); + } +}; + +Endpoint.prototype.exposeModule = Endpoint.prototype.expose; + +/** + * JSON-RPC Client. + */ +var Client = function (port, host, user, password, type) +{ + Endpoint.call(this); -//===----------------------------------------------------------------------===// -// JSON-RPC HTTP Client -//===----------------------------------------------------------------------===// -var Client = function (port, host, user, password) { this.port = port; this.host = host; this.user = user; this.password = password; +}; - this.raw = function raw(method, params, opts, callback) { - if ("function" === typeof opts) { - callback = opts; - opts = {}; - } - opts = opts || {}; +util.inherits(Client, Endpoint); - var client = http.createClient(port, host); - var id = 1; +/** + * Make HTTP connection/request. + * + * In HTTP mode, we get to submit exactly one message and receive up to n + * messages. + */ +Client.prototype.connectHttp = function connectHttp(method, params, opts, callback) +{ + if ("function" === typeof opts) { + callback = opts; + opts = {}; + } + opts = opts || {}; - // First we encode the request into JSON - var requestJSON = JSON.stringify({ - 'id': id, - 'method': method, - 'params': params - }); - - var headers = {}; - - if (user && password) { - var buff = new Buffer(this.user + ":" + this.password) - .toString('base64'); - var auth = 'Basic ' + buff; - headers['Authorization'] = auth; - } + var client = http.createClient(this.port, this.host); - // Then we build some basic headers. - headers['Host'] = host; - headers['Content-Length'] = requestJSON.length; + var id = 1; - // Report errors from the http client. This also prevents crashes since an exception is thrown if we don't handle this event. - client.on('error', function(err) { - callback(err); - }); + // First we encode the request into JSON + var requestJSON = JSON.stringify({ + 'id': id, + 'method': method, + 'params': params + }); - // Now we'll make a request to the server - var request = client.request('POST', opts.path || '/', headers); - request.write(requestJSON); - request.on('response', callback.bind(this, id, request)); - }; + // Report errors from the http client. This also prevents crashes since + // an exception is thrown if we don't handle this event. + client.on('error', function(err) { + callback(err); + }); - this.stream = function (method, params, opts, callback) { - if ("function" === typeof opts) { - callback = opts; - opts = {}; - } - opts = opts || {}; + var headers = {}; - this.raw(method, params, opts, function (id, request, response) { - if ("function" === typeof callback) { - var connection = new events.EventEmitter(); - connection.id = id; - connection.req = request; - connection.res = response; - connection.expose = function (method, callback) { - connection.on('call:'+method, function (data) { - callback.call(null, data.params || []); - }); - }; - connection.end = function () { - this.req.connection.end(); - }; - - // We need to buffer the response chunks in a nonblocking way. - var parser = new JsonParser(); - parser.onValue = function (decoded) { - if (this.stack.length) return; - - connection.emit('data', decoded); - if (decoded.hasOwnProperty('result') || - decoded.hasOwnProperty('error') && - decoded.id === id && - "function" === typeof callback) { - connection.emit('result', decoded); - } else if (decoded.hasOwnProperty('method')) { - connection.emit('call:'+decoded.method, decoded); - } - }; - // Handle headers - connection.res.once('data', function (data) { - if (connection.res.statusCode === 200) { - callback(null, connection); - } else { - callback(new Error(""+connection.res.statusCode+" "+data)); - } - }); - connection.res.on('data', function (chunk) { - try { - parser.write(chunk); - } catch(err) { - // TODO: Is ignoring invalid data the right thing to do? - } - }); - connection.res.on('end', function () { - // TODO: Issue an error if there has been no valid response message - }); - } - }); - }; - this.call = function (method, params, opts, callback) { - if ("function" === typeof opts) { - callback = opts; - opts = {}; - } - opts = opts || {}; - this.raw(method, params, opts, function (id, request, response) { - var data = ''; - response.on('data', function (chunk) { - data += chunk; - }); - response.on('end', function () { - if (response.statusCode !== 200) { - callback(new Error(""+response.statusCode+" "+data)); - return; - } - var decoded = JSON.parse(data); - if ("function" === typeof callback) { - if (!decoded.error) { - decoded.error = null; - } - callback(decoded.error, decoded.result); - } - }); - }); - }; + if (this.user && this.password) { + var buff = new Buffer(this.user + ":" + this.password).toString('base64'); + var auth = 'Basic ' + buff; + headers['Authorization'] = auth; + } + + // Then we build some basic headers. + headers['Host'] = this.host; + headers['Content-Length'] = requestJSON.length; + + // Now we'll make a request to the server + var request = client.request('POST', opts.path || '/', headers); + request.write(requestJSON); + request.on('response', callback.bind(this, id, request)); }; -//===----------------------------------------------------------------------===// -// JSON-RPC HTTP Server -//===----------------------------------------------------------------------===// -function Server() { +/** + * Make Socket connection. + * + * This implements JSON-RPC over a raw socket. This mode allows us to send and + * receive as many messages as we like once the socket is established. + */ +Client.prototype.connectSocket = function connectSocket(callback) +{ var self = this; - this.functions = {}; - this.scopes = {}; - this.defaultScope = this; - this.server = http.createServer(function(req, res) { - Server.trace('<--', 'accepted request'); - if(req.method === 'POST') { - self.handlePOST(req, res); - } - else { - Server.handleNonPOST(req, res); + + var socket = net.connect(this.port, this.host, function () { + if ("function" === typeof callback) { + callback(null, conn); } }); -} - + var conn = new SocketConnection(self, socket); + var parser = new JsonParser(); + parser.onValue = function (decoded) { + if (this.stack.length) return; -//===----------------------------------------------------------------------===// -// exposeModule -//===----------------------------------------------------------------------===// -Server.prototype.exposeModule = function(mod, object, scope) { - var funcs = []; - for(var funcName in object) { - var funcObj = object[funcName]; - if(typeof(funcObj) == 'function') { - this.functions[mod + '.' + funcName] = funcObj; - funcs.push(funcName); - - if (scope) { - this.scopes[mod + '.' + funcName] = scope; - } + conn.handleMessage(decoded); + }; + socket.on('data', function (chunk) { + try { + parser.write(chunk); + } catch(err) { + Endpoint.trace('<--', err.toString()); } - } - Server.trace('***', 'exposing module: ' + mod + ' [funs: ' + funcs.join(', ') - + ']'); - return object; -} + }); + return conn; +}; -//===----------------------------------------------------------------------===// -// expose -//===----------------------------------------------------------------------===// -Server.prototype.expose = function(name, func, scope) { - Server.trace('***', 'exposing: ' + name); - this.functions[name] = func; +Client.prototype.stream = function (method, params, opts, callback) +{ + if ("function" === typeof opts) { + callback = opts; + opts = {}; + } + opts = opts || {}; + + this.connectHttp(method, params, opts, function (id, request, response) { + if ("function" === typeof callback) { + var connection = new events.EventEmitter(); + connection.id = id; + connection.req = request; + connection.res = response; + connection.expose = function (method, callback) { + connection.on('call:'+method, function (data) { + callback.call(null, data.params || []); + }); + }; + connection.end = function () { + this.req.connection.end(); + }; + + // We need to buffer the response chunks in a nonblocking way. + var parser = new JsonParser(); + parser.onValue = function (decoded) { + if (this.stack.length) return; + + connection.emit('data', decoded); + if (decoded.hasOwnProperty('result') || + decoded.hasOwnProperty('error') && + decoded.id === id && + "function" === typeof callback) { + connection.emit('result', decoded); + } else if (decoded.hasOwnProperty('method')) { + connection.emit('call:'+decoded.method, decoded); + } + }; + // Handle headers + connection.res.once('data', function (data) { + if (connection.res.statusCode === 200) { + callback(null, connection); + } else { + callback(new Error(""+connection.res.statusCode+" "+data)); + } + }); + connection.res.on('data', function (chunk) { + try { + parser.write(chunk); + } catch(err) { + // TODO: Is ignoring invalid data the right thing to do? + } + }); + connection.res.on('end', function () { + // TODO: Issue an error if there has been no valid response message + }); + } + }); +}; - if (scope) { - this.scopes[name] = scope; +Client.prototype.call = function (method, params, opts, callback) +{ + if ("function" === typeof opts) { + callback = opts; + opts = {}; } -} + opts = opts || {}; + this.connectHttp(method, params, opts, function (id, request, response) { + var data = ''; + response.on('data', function (chunk) { + data += chunk; + }); + response.on('end', function () { + if (response.statusCode !== 200) { + callback(new Error(""+response.statusCode+" "+data)); + return; + } + var decoded = JSON.parse(data); + if ("function" === typeof callback) { + if (!decoded.error) { + decoded.error = null; + } + callback(decoded.error, decoded.result); + } + }); + }); +}; +/** + * JSON-RPC Server. + */ +function Server(opts) { + Endpoint.call(this); -//===----------------------------------------------------------------------===// -// trace -//===----------------------------------------------------------------------===// -Server.trace = function(direction, message) { - sys.puts(' ' + direction + ' ' + message); + opts = opts || {}; + opts.type = opts.type || 'http'; } - - -//===----------------------------------------------------------------------===// -// listen -//===----------------------------------------------------------------------===// -Server.prototype.listen = function(port, host) { - this.server.listen(port, host); - Server.trace('***', 'Server listening on http://' + (host || '127.0.0.1') + - ':' + port + '/'); +util.inherits(Server, Endpoint); + +/** + * Start listening to incoming connections. + */ +Server.prototype.listen = function listen(port, host) +{ + var server = http.createServer(this.handleHttp.bind(this)); + server.listen(port, host); + Endpoint.trace('***', 'Server listening on http://' + + (host || '127.0.0.1') + ':' + port + '/'); + return server; } +Server.prototype.listenRaw = function listenRaw(port, host) +{ + var server = net.createServer(this.handleRaw.bind(this)); + server.listen(port, host); + Endpoint.trace('***', 'Server listening on socket://' + + (host || '127.0.0.1') + ':' + port + '/'); + return server; +}; + +Server.prototype.listenHybrid = function listenHybrid(port, host) { + var httpServer = http.createServer(this.handleHttp.bind(this)); + var server = net.createServer(this.handleHybrid.bind(this, httpServer)); + server.listen(port, host); + Endpoint.trace('***', 'Server (hybrid) listening on socket://' + + (host || '127.0.0.1') + ':' + port + '/'); + return server; +}; -//===----------------------------------------------------------------------===// -// handleServerError -//===----------------------------------------------------------------------===// -Server.handleServerError = function(req, res, code, message) { - res.writeHead(400, {'Content-Type': 'text/plain', - 'Content-Length': message.length}); +/** + * Handle a low level server error. + */ +Server.handleHttpError = function(req, res, code, message) +{ + res.writeHead(code, {'Content-Type': 'text/plain', + 'Content-Length': message.length, + 'Allow': 'POST'}); res.write(message); res.end(); } +/** + * Handle HTTP POST request. + */ +Server.prototype.handleHttp = function(req, res) +{ + Endpoint.trace('<--', 'Accepted http request'); + + if (req.method !== 'POST') { + Server.handleHttpError(req, res, 405, METHOD_NOT_ALLOWED); + return; + } -//===----------------------------------------------------------------------===// -// handlePOST -//===----------------------------------------------------------------------===// -Server.prototype.handlePOST = function(req, res) { var buffer = ''; var self = this; var handle = function (buf) { var decoded = JSON.parse(buf); - var isStreaming = false; - // Check for the required fields, and if they aren't there, then - // dispatch to the handleServerError function. - if(!(decoded.method && decoded.params && decoded.id)) { - Server.trace('-->', 'response (invalid request)'); - Server.handleServerError(req, res, 400, INVALID_REQUEST); - return; - } - - if(!self.functions.hasOwnProperty(decoded.method)) { - Server.trace('-->', 'response (unknown method "' + decoded.method + '")'); - Server.handleServerError(req, res, 400, "Unknown RPC call '"+decoded.method+"'"); + // dispatch to the handleHttpError function. + if (!(decoded.method && decoded.params && decoded.id)) { + Endpoint.trace('-->', 'Response (invalid request)'); + Server.handleHttpError(req, res, 400, INVALID_REQUEST); return; } var reply = function (json) { var encoded = JSON.stringify(json); - if (!isStreaming) { + if (!conn.isStreaming) { res.writeHead(200, {'Content-Type': 'application/json', 'Content-Length': encoded.length}); res.write(encoded); @@ -269,104 +369,302 @@ Server.prototype.handlePOST = function(req, res) { } }; - // Build our success handler - var onSuccess = function(funcResp) { - Server.trace('-->', 'response (id ' + decoded.id + '): ' + - JSON.stringify(funcResp)); + var callback = function(err, result) { + if (err) { + Endpoint.trace('-->', 'Failure (id ' + decoded.id + '): ' + + (err.stack ? err.stack : err.toString())); + err = err.toString(); + result = null; + } else { + Endpoint.trace('-->', 'Response (id ' + decoded.id + '): ' + + JSON.stringify(result)); + err = null; + } + // TODO: Not sure if we should return a message if decoded.id == null reply({ - 'result': funcResp, - 'error': null, + 'result': result, + 'error': err, 'id': decoded.id }); }; - // Build our failure handler (note that error must not be null) - var onFailure = function(failure) { - Server.trace('-->', 'failure: ' + JSON.stringify(failure)); - reply({ - 'result': null, - 'error': failure || 'Unspecified Failure', - 'id': decoded.id - }); - }; + var conn = new HttpServerConnection(self, req, res); - Server.trace('<--', 'request (id ' + decoded.id + '): ' + - decoded.method + '(' + decoded.params.join(', ') + ')'); + self.handleCall(decoded, conn, callback); + }; // function handle(buf) - // Try to call the method, but intercept errors and call our - // onFailure handler. - var method = self.functions[decoded.method]; - var callback = function(err, result) { + req.addListener('data', function(chunk) { + buffer = buffer + chunk; + }); + + req.addListener('end', function() { + handle(buffer); + }); +}; + +Server.prototype.handleRaw = function handleRaw(socket) +{ + Endpoint.trace('<--', 'Accepted socket connection'); + var conn = new SocketConnection(this, socket); + var parser = new JsonParser(); + parser.onValue = function (decoded) { + if (this.stack.length) return; + + conn.handleMessage(decoded); + }; + socket.on('data', function (chunk) { + try { + parser.write(chunk); + } catch(err) { + // TODO: Is ignoring invalid data the right thing to do? + } + }); +}; + +Server.prototype.handleHybrid = function handleHybrid(httpServer, socket) +{ + var self = this; + socket.once('data', function (chunk) { + // If first byte is a capital letter, treat connection as HTTP + if (chunk[0] >= 65 && chunk[0] <= 90) { + httpServer.emit('connection', socket); + } else { + self.handleRaw(socket); + } + // Re-emit first chunk + socket.emit('data', chunk); + }); +}; + +var Connection = function Connection(ep) { + events.EventEmitter.call(this); + + this.endpoint = ep; + this.callbacks = []; + this.latestId = 0; + + // Default error handler (prevents "uncaught error event") + this.on('error', function () {}); +}; + +util.inherits(Connection, events.EventEmitter); + +/** + * Make a standard RPC call to the other endpoint. + * + * Note that some ways to make RPC calls bypass this method, for example HTTP + * calls and responses are done in other places. + */ +Connection.prototype.call = function call(method, params, callback) +{ + if (!Array.isArray(params)) { + params = [params]; + } + + var id = null; + if ("function" === typeof callback) { + id = ++this.latestId; + this.callbacks[id] = callback; + } + + Endpoint.trace('-->', 'Call (method '+method+'): ' + JSON.stringify(params)); + var data = JSON.stringify({ + method: method, + params: params, + id: id + }); + this.write(data); +}; + +/** + * Dummy method for sending data. + * + * Connection types that support sending additional data will override this + * method. + */ +Connection.prototype.write = function write(data) +{ + throw new Error("Tried to write data on unsupported connection type."); +}; + +/** + * Keep the connection open. + * + * This method is used to tell a HttpServerConnection to stay open. In order + * to keep it compatible with other connection types, we add it here and make + * it register a connection end handler. + */ +Connection.prototype.stream = function (onend) +{ + if ("function" === typeof onend) { + this.on('end', onend); + } +}; + +Connection.prototype.handleMessage = function handleMessage(msg) +{ + if (msg.hasOwnProperty('result') || + msg.hasOwnProperty('error') && + msg.hasOwnProperty('id') && + "function" === typeof this.callbacks[msg.id]) { + try { + this.callbacks[msg.id](msg.error, msg.result); + } catch(err) { + // TODO: What do we do with erroneous callbacks? + } + } else if (msg.hasOwnProperty('method')) { + this.endpoint.handleCall(msg, this, (function (err, result) { if (err) { - onFailure(err); - } else { - onSuccess(result); + Endpoint.trace('-->', 'Failure (id ' + msg.id + '): ' + + (err.stack ? err.stack : err.toString())); } - }; - // Can be called before the response callback to keep the connection open. - var stream = function (onend) { - isStreaming = true; - if ("function" === typeof onend) { - res.connection.on('end', onend); - } - }; - var emit = function (method, params) { - if (!res.connection.writable) return; + if ("undefined" === msg.id || null === msg.id) return; - if (!Array.isArray(params)) { - params = [params]; + if (err) { + err = err.toString(); + result = null; + } else { + Endpoint.trace('-->', 'Response (id ' + msg.id + '): ' + + JSON.stringify(result)); + err = null; } - Server.trace('-->', 'emit (method '+method+'): ' + JSON.stringify(params)); var data = JSON.stringify({ - method: method, - params: params, - id: null + result: result, + error: err, + id: msg.id }); - res.write(data); - }; - var scope = self.scopes[decoded.method] || self.defaultScope; - - // Other various information we want to pass in for the handler to be - // able to access. - var opts = { - req: req, - res: res, - server: self, - callback: callback, - stream: stream, - emit: emit - }; + this.write(data); + }).bind(this)); + } +}; - try { - method.call(scope, decoded.params, opts, callback); - } catch (err) { - onFailure(err); - } - }; // function handle(buf) +var HttpServerConnection = function HttpServerConnection(server, req, res) +{ + Connection.call(this, server); - req.addListener('data', function(chunk) { - buffer = buffer + chunk; + var self = this; + + this.req = req; + this.res = res; + this.isStreaming = false; + + this.res.connection.on('end', function () { + self.emit('end'); }); +}; - req.addListener('end', function() { - handle(buffer); +util.inherits(HttpServerConnection, Connection); + +/** + * Can be called before the response callback to keep the connection open. + */ +HttpServerConnection.prototype.stream = function (onend) +{ + Connection.prototype.stream.call(this, onend); + + this.isStreaming = true; +}; + +/** + * Send the client additional data. + * + * An HTTP connection can be kept open and additional RPC calls sent through if + * the client supports it. + */ +HttpServerConnection.prototype.write = function (data) +{ + if (!this.isStreaming) { + throw new Error("Cannot send extra messages via non-streaming HTTP"); + } + + if (!this.res.connection.writable) { + // Client disconnected, we'll quietly fail + return; + } + + this.res.write(data); +}; + +/** + * Socket connection. + * + * Socket connections are mostly symmetric, so we are using a single class for + * representing both the server and client perspective. + */ +var SocketConnection = function SocketConnection(endpoint, conn) +{ + Connection.call(this, endpoint); + + var self = this; + + this.conn = conn; + this.autoReconnect = true; + this.ended = true; + + this.conn.on('connect', function () { + self.emit('connect'); + }); + + this.conn.on('end', function () { + self.emit('end'); + }); + + this.conn.on('error', function () { + self.emit('error'); + }); + + this.conn.on('close', function (hadError) { + self.emit('close', hadError); + + // Handle automatic reconnections if we are the client + if (self.endpoint instanceof Client && + self.autoReconnect && + !self.ended) { + if (hadError) { + // If there was an error, we'll wait a moment before retrying + setTimeout(self.reconnect.bind(self), 200); + } else { + self.reconnect(); + } + } }); }; +util.inherits(SocketConnection, Connection); -//===----------------------------------------------------------------------===// -// handleNonPOST -//===----------------------------------------------------------------------===// -Server.handleNonPOST = function(req, res) { - res.writeHead(405, {'Content-Type': 'text/plain', - 'Content-Length': METHOD_NOT_ALLOWED.length, - 'Allow': 'POST'}); - res.write(METHOD_NOT_ALLOWED); - res.end(); +SocketConnection.prototype.write = function write(data) +{ + if (!this.conn.writable) { + // Other side disconnected, we'll quietly fail + return; + } + + this.conn.write(data); }; -module.exports.Server = Server; -module.exports.Client = Client; +SocketConnection.prototype.end = function end() +{ + this.ended = true; + this.conn.end(); +}; + +SocketConnection.prototype.reconnect = function reconnect() +{ + this.ended = false; + if (this.endpoint instanceof Client) { + this.conn.connect(this.endpoint.port, this.endpoint.host); + } else { + throw new Error('Cannot reconnect a connection from the server-side.'); + } +}; + +exports.Endpoint = Endpoint; +exports.Server = Server; +exports.Client = Client; + +exports.Connection = Connection; +exports.HttpServerConnection = HttpServerConnection; +exports.SocketConnection = SocketConnection; From 0dde19681caf09967c845497c467c921cf29620f Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Sun, 4 Dec 2011 02:26:14 +0100 Subject: [PATCH 17/24] Bump version to 0.0.8. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 37eb763..f2fae8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonrpc2", - "version": "0.0.7", + "version": "0.0.8", "description": "JSON-RPC server and client library", "main": "./src/jsonrpc", "keywords": [ From 4e9657d2449988500608762b1603dd7c89c06d26 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Fri, 30 Dec 2011 02:22:08 +0100 Subject: [PATCH 18/24] Removed references to sys and fixed test cases. --- examples/client.js | 16 ++++++++-------- src/jsonrpc.js | 4 ++-- test/jsonrpc-test.js | 40 ++++++++++++++++++++++++++-------------- test/test.js | 44 ++++++++++++++++++++++---------------------- 4 files changed, 58 insertions(+), 46 deletions(-) diff --git a/examples/client.js b/examples/client.js index 069e5cb..98f74a4 100644 --- a/examples/client.js +++ b/examples/client.js @@ -1,4 +1,4 @@ -var sys = require('sys'); +var util = require('util'); var rpc = require('../src/jsonrpc'); rpc.Endpoint.trace = function () {}; @@ -6,34 +6,34 @@ rpc.Endpoint.trace = function () {}; var client = new rpc.Client(8088, 'localhost'); client.call('add', [1, 2], function (err, result) { - sys.puts(' 1 + 2 = ' + result); + console.log(' 1 + 2 = ' + result); }); client.call('multiply', [199, 2], function (err, result) { - sys.puts('199 * 2 = ' + result); + console.log('199 * 2 = ' + result); }); // Accessing modules is as simple as dot-prefixing. client.call('math.power', [3, 3], function (err, result) { - sys.puts(' 3 ^ 3 = ' + result); + console.log(' 3 ^ 3 = ' + result); }); // We can handle errors the same way as anywhere else in Node client.call('add', [1, 1], function (err, result) { if (err) { - sys.puts('RPC Error: '+ sys.inspect(err)); + console.error('RPC Error: '+ util.inspect(err)); return; } - sys.puts(' 1 + 1 = ' + result + ', dummy!'); + console.log(' 1 + 1 = ' + result + ', dummy!'); }); // These calls should each take 1.5 seconds to complete client.call('delayed.add', [1, 1, 1500], function (err, result) { - sys.puts(result); + console.log(result); }); client.call('delayed.echo', ['Echo.', 1500], function (err, result) { - sys.puts(result); + console.log(result); }); client.stream('listen', [], function (err, connection) { diff --git a/src/jsonrpc.js b/src/jsonrpc.js index 2f7e14b..25a6d0e 100644 --- a/src/jsonrpc.js +++ b/src/jsonrpc.js @@ -1,4 +1,4 @@ -var sys = require('sys'); +var util = require('util'); var net = require('net'); var http = require('http'); var util = require('util'); @@ -28,7 +28,7 @@ util.inherits(Endpoint, events.EventEmitter); */ Endpoint.trace = function(direction, message) { - sys.puts(' ' + direction + ' ' + message); + console.log(' ' + direction + ' ' + message); } /** diff --git a/test/jsonrpc-test.js b/test/jsonrpc-test.js index fbab78e..27abe76 100644 --- a/test/jsonrpc-test.js +++ b/test/jsonrpc-test.js @@ -1,19 +1,22 @@ require('./test').extend(global); -var sys = require('sys'); +var util = require('util'); var rpc = require('../src/jsonrpc'); +var events = require('events'); var server = new rpc.Server(); +rpc.Endpoint.trace = function () {}; + // MOCK REQUEST/RESPONSE OBJECTS var MockRequest = function(method) { this.method = method; - process.EventEmitter.call(this); + events.EventEmitter.call(this); }; -sys.inherits(MockRequest, process.EventEmitter); +util.inherits(MockRequest, events.EventEmitter); var MockResponse = function() { - process.EventEmitter.call(this); + events.EventEmitter.call(this); this.writeHead = this.sendHeader = function(httpCode, httpHeaders) { this.httpCode = httpCode; this.httpHeaders = httpCode; @@ -22,8 +25,9 @@ var MockResponse = function() { this.httpBody = httpBody; }; this.end = this.finish = function() {}; + this.connection = new events.EventEmitter(); }; -sys.inherits(MockResponse, process.EventEmitter); +util.inherits(MockResponse, events.EventEmitter); // A SIMPLE MODULE var TestModule = { @@ -46,8 +50,6 @@ test('Server.expose', function() { test('Server.exposeModule', function() { server.exposeModule('test', TestModule); - sys.puts(server.functions['test.foo']); - sys.puts(TestModule.foo); assert(server.functions['test.foo'] == TestModule.foo); }); @@ -56,17 +58,16 @@ test('Server.exposeModule', function() { test('GET Server.handleNonPOST', function() { var req = new MockRequest('GET'); var res = new MockResponse(); - rpc.Server.handleNonPOST(req, res); + server.handleHttp(req, res); assert(res.httpCode === 405); }); function testBadRequest(testJSON) { var req = new MockRequest('POST'); var res = new MockResponse(); - server.handlePOST(req, res); + server.handleHttp(req, res); req.emit('data', testJSON); req.emit('end'); - sys.puts(res.httpCode); assert(res.httpCode === 400); } @@ -87,7 +88,18 @@ test('Missing object attribute (id)', function() { test('Unregistered method', function() { var testJSON = '{ "method": "notRegistered", "params": ["Hello, World!"], "id": 1 }'; - testBadRequest(testJSON); + var req = new MockRequest('POST'); + var res = new MockResponse(); + try { + server.handleHttp(req, res); + }catch (e) {}; + req.emit('data', testJSON); + req.emit('end'); + assert(res.httpCode === 200); + var decoded = JSON.parse(res.httpBody); + assert(decoded.id === 1); + assert(decoded.error === 'Error: Unknown RPC call \'notRegistered\''); + assert(decoded.result === null); }); // VALID REQUEST @@ -96,7 +108,7 @@ test('Simple synchronous echo', function() { var testJSON = '{ "method": "echo", "params": ["Hello, World!"], "id": 1 }'; var req = new MockRequest('POST'); var res = new MockResponse(); - server.handlePOST(req, res); + server.handleHttp(req, res); req.emit('data', testJSON); req.emit('end'); assert(res.httpCode === 200); @@ -117,7 +129,7 @@ test('Using promise', function() { var req = new MockRequest('POST'); var res = new MockResponse(); // Have the server handle that request - server.handlePOST(req, res); + server.handleHttp(req, res); req.emit('data', testJSON); req.emit('end'); // Now the request has completed, and in the above synchronous test, we @@ -143,7 +155,7 @@ test('Triggering an errback', function() { var testJSON = '{ "method": "errbackEcho", "params": ["Hello, World!"], "id": 1 }'; var req = new MockRequest('POST'); var res = new MockResponse(); - server.handlePOST(req, res); + server.handleHttp(req, res); req.emit('data', testJSON); req.emit('end'); assert(res['httpCode'] == null); diff --git a/test/test.js b/test/test.js index 314d043..504316d 100644 --- a/test/test.js +++ b/test/test.js @@ -1,18 +1,19 @@ -var sys = require('sys'); +var util = require('util'); var TEST = module.exports = { passed: 0, failed: 0, assertions: 0, + output: "", + test: function (desc, block) { - var _puts = sys.puts, - output = "", - result = '?', - _boom = null; - sys.puts = function (s) { output += s + "\n"; } + var result = '?', + _boom = null; + + TEST.output = ""; try { - sys.print(" " + desc + " ..."); + TEST.output += " " + desc + " ..."; block(); result = '.'; } catch(boom) { @@ -21,16 +22,15 @@ var TEST = module.exports = { } else { result = 'E'; _boom = boom; - sys.puts(boom.toString()); + TEST.output += boom.toString(); } } - sys.puts = _puts; if ( result == '.' ) { - sys.print(" OK\n"); + process.stdout.write(TEST.output + " OK\n"); TEST.passed += 1; } else { - sys.print(" FAIL\n"); - sys.print(output.replace(/^/, " ") + "\n"); + process.stdout.write(TEST.output + " FAIL\n"); + process.stdout.write(TEST.output.replace(/^/, " ") + "\n"); TEST.failed += 1; if ( _boom ) throw _boom; } @@ -38,30 +38,30 @@ var TEST = module.exports = { assert: function (value, desc) { TEST.assertions += 1; - if ( desc ) sys.puts("ASSERT: " + desc); + if ( desc ) TEST.output += "ASSERT: " + desc; if ( !value ) throw 'FAIL'; }, assert_equal: function (expect, is) { assert( expect == is, - sys.inspect(expect) + " == " + sys.inspect(is) + util.inspect(expect) + " == " + util.inspect(is) ); }, assert_boom: function (message, block) { var error = null; - try { block() } - catch (boom) { error = boom } + try { block(); } + catch (boom) { error = boom; } if ( !error ) { - sys.puts('NO BOOM'); - throw 'FAIL' + TEST.output += 'NO BOOM'; + throw 'FAIL'; } if ( error != message ) { - sys.puts('BOOM: ' + sys.inspect(error) + - ' [' + sys.inspect(message) + ' expected]'); - throw 'FAIL' + TEST.output += 'BOOM: ' + util.inspect(error) + + ' [' + util.inspect(message) + ' expected]'; + throw 'FAIL'; } }, @@ -75,7 +75,7 @@ var TEST = module.exports = { process.addListener('exit', function (code) { if ( !TEST.exit ) { TEST.exit = true; - sys.puts("" + TEST.passed + " passed, " + TEST.failed + " failed"); + console.log("" + TEST.passed + " passed, " + TEST.failed + " failed"); if ( TEST.failed > 0 ) { process.exit(1) }; } }); From 022c93580ec4c14812aab789f18d440c26f13747 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Fri, 30 Dec 2011 04:40:43 +0100 Subject: [PATCH 19/24] Bump to version 0.0.9. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f2fae8e..ae1a3d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonrpc2", - "version": "0.0.8", + "version": "0.0.9", "description": "JSON-RPC server and client library", "main": "./src/jsonrpc", "keywords": [ From 3bc723bc2d8e8d866e6fa0d59889abc75b5f6e1b Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Fri, 30 Dec 2011 12:14:04 +0100 Subject: [PATCH 20/24] Implement authentication for HTTP and raw sockets. --- examples/client.js | 37 ++++++++++++-- examples/server.js | 2 + src/jsonrpc.js | 125 ++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 148 insertions(+), 16 deletions(-) diff --git a/examples/client.js b/examples/client.js index 98f74a4..b0a84f6 100644 --- a/examples/client.js +++ b/examples/client.js @@ -3,25 +3,37 @@ var rpc = require('../src/jsonrpc'); rpc.Endpoint.trace = function () {}; -var client = new rpc.Client(8088, 'localhost'); +var client = new rpc.Client(8088, 'localhost', "myuser", "secret123"); client.call('add', [1, 2], function (err, result) { + if (err) { + console.error('RPC Error: '+ err.toString()); + return; + } console.log(' 1 + 2 = ' + result); }); client.call('multiply', [199, 2], function (err, result) { + if (err) { + console.error('RPC Error: '+ err.toString()); + return; + } console.log('199 * 2 = ' + result); }); // Accessing modules is as simple as dot-prefixing. client.call('math.power', [3, 3], function (err, result) { + if (err) { + console.error('RPC Error: '+ err.toString()); + return; + } console.log(' 3 ^ 3 = ' + result); }); // We can handle errors the same way as anywhere else in Node client.call('add', [1, 1], function (err, result) { if (err) { - console.error('RPC Error: '+ util.inspect(err)); + console.error('RPC Error: '+ err.toString()); return; } console.log(' 1 + 1 = ' + result + ', dummy!'); @@ -29,14 +41,26 @@ client.call('add', [1, 1], function (err, result) { // These calls should each take 1.5 seconds to complete client.call('delayed.add', [1, 1, 1500], function (err, result) { + if (err) { + console.error('RPC Error: '+ err.toString()); + return; + } console.log(result); }); client.call('delayed.echo', ['Echo.', 1500], function (err, result) { + if (err) { + console.error('RPC Error: '+ err.toString()); + return; + } console.log(result); }); client.stream('listen', [], function (err, connection) { + if (err) { + console.error('RPC Error: '+ err.toString()); + return; + } var counter = 0; connection.expose('event', function (params) { console.log('Streaming #'+counter+': '+params[0]); @@ -48,7 +72,7 @@ client.stream('listen', [], function (err, connection) { console.log('start listening'); }); -var socketClient = new rpc.Client(8089, 'localhost'); +var socketClient = new rpc.Client(8089, 'localhost', "myuser", "secret123"); socketClient.connectSocket(function (err, conn) { var counter = 0; @@ -60,5 +84,10 @@ socketClient.connectSocket(function (err, conn) { } }); - conn.call('listen', []); + conn.call('listen', [], function (err) { + if (err) { + console.error('RPC Error: '+ err.toString()); + return; + } + }); }); diff --git a/examples/server.js b/examples/server.js index c594e98..0d20862 100644 --- a/examples/server.js +++ b/examples/server.js @@ -3,6 +3,8 @@ var events = require('events'); var server = new rpc.Server(); +server.enableAuth("myuser", "secret123"); + /* Create two simple functions */ function add(args, opts, callback) { callback(null, args[0]+args[1]); diff --git a/src/jsonrpc.js b/src/jsonrpc.js index 25a6d0e..87f75f6 100644 --- a/src/jsonrpc.js +++ b/src/jsonrpc.js @@ -5,6 +5,7 @@ var util = require('util'); var events = require('events'); var JsonParser = require('jsonparse'); +var UNAUTHORIZED = "Unauthorized\n"; var METHOD_NOT_ALLOWED = "Method Not Allowed\n"; var INVALID_REQUEST = "Invalid Request\n"; @@ -93,7 +94,7 @@ Endpoint.prototype.exposeModule = Endpoint.prototype.expose; /** * JSON-RPC Client. */ -var Client = function (port, host, user, password, type) +var Client = function (port, host, user, password) { Endpoint.call(this); @@ -166,6 +167,18 @@ Client.prototype.connectSocket = function connectSocket(callback) var self = this; var socket = net.connect(this.port, this.host, function () { + // Submit non-standard "auth" message for raw sockets. + if ("string" === typeof self.user && + "string" === typeof self.password) { + conn.call("auth", [self.user, self.password], function (err) { + if (err) { + callback(err); + } else { + callback(null, conn); + } + }); + return; + } if ("function" === typeof callback) { callback(null, conn); } @@ -322,12 +335,18 @@ Server.prototype.listenHybrid = function listenHybrid(port, host) { */ Server.handleHttpError = function(req, res, code, message) { - res.writeHead(code, {'Content-Type': 'text/plain', - 'Content-Length': message.length, - 'Allow': 'POST'}); + var headers = {'Content-Type': 'text/plain', + 'Content-Length': message.length, + 'Allow': 'POST'}; + + if (code === 401) { + headers['WWW-Authenticate'] = 'Basic realm="JSON-RPC"'; + } + + res.writeHead(code, headers); res.write(message); res.end(); -} +}; /** * Handle HTTP POST request. @@ -343,6 +362,21 @@ Server.prototype.handleHttp = function(req, res) var buffer = ''; var self = this; + + // Check authentication if we require it + if (this.authHandler) { + var authHeader = req.headers['authorization'] || '', // get the header + authToken = authHeader.split(/\s+/).pop() || '', // get the token + auth = new Buffer(authToken, 'base64').toString(), // base64 -> string + parts = auth.split(/:/), // split on colon + username = parts[0], + password = parts[1]; + if (!this.authHandler(username, password)) { + Server.handleHttpError(req, res, 401, UNAUTHORIZED); + return; + } + } + var handle = function (buf) { var decoded = JSON.parse(buf); @@ -406,13 +440,53 @@ Server.prototype.handleHttp = function(req, res) Server.prototype.handleRaw = function handleRaw(socket) { Endpoint.trace('<--', 'Accepted socket connection'); + + var self = this; + var conn = new SocketConnection(this, socket); var parser = new JsonParser(); + var requireAuth = !!this.authHandler; + parser.onValue = function (decoded) { if (this.stack.length) return; - conn.handleMessage(decoded); + // We're on a raw TCP socket. To enable authentication we implement a simple + // authentication scheme that is non-standard, but is easy to call from any + // client library. + // + // The authentication message is to be sent as follows: + // {"method": "auth", "params": ["myuser", "mypass"], id: 0} + if (requireAuth) { + if (decoded.method !== "auth" ) { + // Try to notify client about failure to authenticate + if ("number" === typeof decoded.id) { + conn.sendReply("Error: Unauthorized", null, decoded.id); + } + } else { + // Handle "auth" message + if (Array.isArray(decoded.params) && + decoded.params.length === 2 && + self.authHandler(decoded.params[0], decoded.params[1])) { + // Authorization completed + requireAuth = false; + + // Notify client about success + if ("number" === typeof decoded.id) { + conn.sendReply(null, true, decoded.id); + } + } else { + if ("number" === typeof decoded.id) { + conn.sendReply("Error: Invalid credentials", null, decoded.id); + } + } + } + // Make sure we explicitly return here - the client was not yet auth'd. + return; + } else { + conn.handleMessage(decoded); + } }; + socket.on('data', function (chunk) { try { parser.write(chunk); @@ -437,6 +511,29 @@ Server.prototype.handleHybrid = function handleHybrid(httpServer, socket) }); }; +/** + * Set the server to require authentication. + * + * Can be called with a custom handler function: + * server.enableAuth(function (user, password) { + * return true; // Do authentication and return result as boolean + * }); + * + * Or just with a single valid username and password: + * sever.enableAuth("myuser", "supersecretpassword"); + */ +Server.prototype.enableAuth = function enableAuth(handler, password) { + if ("function" !== typeof handler) { + var user = "" + handler; + password = "" + password; + handler = function checkAuth(suppliedUser, suppliedPassword) { + return user === suppliedUser && password === suppliedPassword; + }; + } + + this.authHandler = handler; +}; + var Connection = function Connection(ep) { events.EventEmitter.call(this); @@ -531,16 +628,20 @@ Connection.prototype.handleMessage = function handleMessage(msg) err = null; } - var data = JSON.stringify({ - result: result, - error: err, - id: msg.id - }); - this.write(data); + this.sendReply(err, result, msg.id); }).bind(this)); } }; +Connection.prototype.sendReply = function sendReply(err, result, id) { + var data = JSON.stringify({ + result: result, + error: err, + id: id + }); + this.write(data); +}; + var HttpServerConnection = function HttpServerConnection(server, req, res) { Connection.call(this, server); From ff5733984cf05d69e0afde8e1e431d5cd4db10e1 Mon Sep 17 00:00:00 2001 From: Stefan Thomas Date: Fri, 30 Dec 2011 12:14:45 +0100 Subject: [PATCH 21/24] Bump version to 0.1.0. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ae1a3d9..a34a771 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonrpc2", - "version": "0.0.9", + "version": "0.1.0", "description": "JSON-RPC server and client library", "main": "./src/jsonrpc", "keywords": [ From 95634b9f7ad087b178036e2955623f44ae30e640 Mon Sep 17 00:00:00 2001 From: egolovaniuc Date: Tue, 7 Feb 2012 14:35:59 +0100 Subject: [PATCH 22/24] set Request parameter 'jsonrpc': '2.0'; used Buffer.byteLength(str, 'utf8') to set Content-Length header. See http://www.jsonrpc.org/spec.html and http://nodejs.org/docs/v0.4.7/api/http.html#response.writeHead --- src/jsonrpc.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/jsonrpc.js b/src/jsonrpc.js index 87f75f6..f312e19 100644 --- a/src/jsonrpc.js +++ b/src/jsonrpc.js @@ -129,7 +129,8 @@ Client.prototype.connectHttp = function connectHttp(method, params, opts, callba var requestJSON = JSON.stringify({ 'id': id, 'method': method, - 'params': params + 'params': params, + 'jsonrpc': '2.0' }); // Report errors from the http client. This also prevents crashes since @@ -148,7 +149,7 @@ Client.prototype.connectHttp = function connectHttp(method, params, opts, callba // Then we build some basic headers. headers['Host'] = this.host; - headers['Content-Length'] = requestJSON.length; + headers['Content-Length'] = Buffer.byteLength(requestJSON, 'utf8'); // Now we'll make a request to the server var request = client.request('POST', opts.path || '/', headers); From 647ac72fdac63f1eb1c908f00e1bb3113c131072 Mon Sep 17 00:00:00 2001 From: justmoon Date: Tue, 7 Feb 2012 19:03:38 +0100 Subject: [PATCH 23/24] Version bump to 0.1.1. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a34a771..c28ae8c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jsonrpc2", - "version": "0.1.0", + "version": "0.1.1", "description": "JSON-RPC server and client library", "main": "./src/jsonrpc", "keywords": [ From 520beb26078361c475385a3c073286a33da7dc84 Mon Sep 17 00:00:00 2001 From: Farrin Reid Date: Sat, 23 Jun 2012 06:45:42 -0800 Subject: [PATCH 24/24] [fix] Changed require('sys') to require('util') for compatibility with node v0.8 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e0d2d3e..20173d6 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,12 @@ And creating a client to speak to that server is easy too: ``` javascript var rpc = require('jsonrpc2'); -var sys = require('sys'); +var util = require('util'); var client = new rpc.Client(8000, 'localhost'); client.call('add', [1, 2], function(err, result) { - sys.puts('1 + 2 = ' + result); + util.puts('1 + 2 = ' + result); }); ```