From 1ac9e7d201b843221a05cf4b82c1398d7942783a Mon Sep 17 00:00:00 2001 From: freewil Date: Fri, 8 Mar 2013 08:09:10 -0500 Subject: [PATCH] add SSL support closes #2 --- Makefile | 23 +++++++++++++++++- Readme.md | 30 ++++++++++++++++++----- lib/bitcoin/client.js | 31 ++++++++++++++++++------ lib/jsonrpc.js | 23 ++++++++++++++---- package.json | 7 ++++-- test/index.js | 6 ++++- test/ssl.js | 56 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 155 insertions(+), 21 deletions(-) create mode 100644 test/ssl.js diff --git a/Makefile b/Makefile index 18381f1..2992318 100644 --- a/Makefile +++ b/Makefile @@ -2,19 +2,40 @@ MOCHA=./node_modules/.bin/mocha BOX=test/testnet-box test: + $(MAKE) test-ssl-no + sleep 15 + $(MAKE) clean + $(MAKE) test-ssl + +test-ssl-no: $(MAKE) start sleep 15 $(MAKE) run-test $(MAKE) stop + +test-ssl: + $(MAKE) start-ssl + sleep 15 + $(MAKE) run-test-ssl + $(MAKE) stop-ssl start: $(MAKE) -C $(BOX) start + +start-ssl: + $(MAKE) -C $(BOX) start B1_FLAGS=-rpcssl=1 B2_FLAGS=-rpcssl=1 stop: $(MAKE) -C $(BOX) stop + +stop-ssl: + $(MAKE) -C $(BOX) stop B1_FLAGS=-rpcssl=1 B2_FLAGS=-rpcssl=1 run-test: - $(MOCHA) + $(MOCHA) --invert --grep SSL + +run-test-ssl: + $(MOCHA) --grep SSL clean: $(MAKE) -C $(BOX) clean diff --git a/Readme.md b/Readme.md index aeea5e6..d7dd66b 100644 --- a/Readme.md +++ b/Readme.md @@ -16,12 +16,6 @@ object, or you may call the API directly using the `cmd` method. ### Create client ```js -var bitcoin = require('bitcoin'); -var client = new bitcoin.Client('localhost', 8332, 'username', 'password'); -``` - -### Create client with single object -```js var client = new bitcoin.Client({ host: 'localhost', port: 8332, @@ -62,3 +56,27 @@ client.cmd(batch, function(err, address) { console.log('Address:', address); }); ``` + +## SSL +See [Enabling SSL on original client](https://en.bitcoin.it/wiki/Enabling_SSL_on_original_client_daemon). + +If you're using this to connect to bitcoind across a network it is highly +recommended to use SSL, otherwise an attacker may interecept your RPC credentials +resulting in theft of your Bitcoins. + +When enabling `ssl` by setting the configuration option to `true`, the `sslStrict` +option will also be enabled by default. It is highly recommended to specify the +CA as well to ensure you are not connecting to an attacker's bitcoind attempting +to impersonate your own bitcoind. + +```js +var client = new bitcoin.Client({ + host: 'localhost', + port: 8332, + user: 'username', + pass: 'password', + ssl: true + sslStrict: true, + sslCa: fs.readFileSync(__dirname + '/myca.cert') +}); +``` diff --git a/lib/bitcoin/client.js b/lib/bitcoin/client.js index d42afb6..fe94116 100644 --- a/lib/bitcoin/client.js +++ b/lib/bitcoin/client.js @@ -1,4 +1,5 @@ -var rpc = require('../jsonrpc'); +var rpc = require('../jsonrpc'), + deprecate = require('deprecate'); //===----------------------------------------------------------------------===// // jsonrpc wrappers @@ -69,16 +70,32 @@ var bitcoinAPI = { //===----------------------------------------------------------------------===// // Client -// Either pass in 4 arguments, or a single object with host, port, user, pass +// Pass in an object with host, port, user, pass //===----------------------------------------------------------------------===// function Client() { var args = [].slice.call(arguments); - this.host = args[0].host || args[0]; - this.port = args[0].port || args[1]; - this.user = args[0].user || args[2]; - this.pass = args[0].pass || args[3]; + + if (args.length > 1) { + deprecate('calling bitcoin.Client with more than one argument is deprecated'); + this.host = args[0]; + this.port = args[1]; + this.user = args[2]; + this.pass = args[3]; + this.ssl = false; + this.sslStrict = false; + this.sslCa = null; + } else { + this.host = args[0].host; + this.port = args[0].port; + this.user = args[0].user; + this.pass = args[0].pass; + this.ssl = args[0].ssl ? true : false; + this.sslStrict = (typeof args[0].sslStrict === 'undefined' || args[0].sslStrict); + this.sslCa = args[0].sslCa; + } - this.rpc = new rpc.Client(this.port, this.host, this.user, this.pass); + this.rpc = new rpc.Client(this.port, this.host, this.user, this.pass, + this.ssl, this.sslStrict, this.sslCa); } diff --git a/lib/jsonrpc.js b/lib/jsonrpc.js index 10bd0ac..1dea942 100644 --- a/lib/jsonrpc.js +++ b/lib/jsonrpc.js @@ -1,10 +1,16 @@ -var http = require('http'); +var http = require('http'), + https = require('https'); -var Client = function(port, host, user, password) { +var Client = function(port, host, user, password, ssl, sslStrict, sslCa) { this.port = port; this.host = host; this.user = user; this.password = password; + + this.ssl = ssl ? true : false; + this.sslStrict = sslStrict ? true : false; + this.http = this.ssl ? https : http; + this.sslCa = sslCa; }; Client.prototype.call = function(method, params, callback, errback, path) { @@ -42,8 +48,17 @@ Client.prototype.call = function(method, params, callback, errback, path) { headers: { 'Host': this.host, 'Content-Length': requestJSON.length - } + }, + agent: false }; + + if (this.ssl && this.sslStrict) { + requestOptions.rejectUnauthorized = true; + } + + if (this.ssl && this.sslCa) { + requestOptions.ca = this.sslCa; + } // use HTTP auth if user and password set if (this.user && this.password) { @@ -51,7 +66,7 @@ Client.prototype.call = function(method, params, callback, errback, path) { } // Now we'll make a request to the server - var request = http.request(requestOptions); + var request = this.http.request(requestOptions); request.on('error', errback); diff --git a/package.json b/package.json index 0ad1ec0..9f0c212 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,12 @@ "contributors": [ "Sean Lavine " ], - "dependencies": {}, + "dependencies": { + "deprecate": "~0.1.0" + }, "devDependencies": { - "mocha": "~1.8.1" + "mocha": "~1.8.1", + "clone": "~0.1.6" }, "repository": { "type": "git", diff --git a/test/index.js b/test/index.js index c3c82bc..58edc56 100644 --- a/test/index.js +++ b/test/index.js @@ -1,6 +1,10 @@ var assert = require('assert'), bitcoin = require('../lib/bitcoin'), - config = require('./config'); + config = require('./config'), + deprecate = require('deprecate'); + +// hide deprecation warnings +deprecate.silence = true; var test = { account: 'test' diff --git a/test/ssl.js b/test/ssl.js new file mode 100644 index 0000000..fe02123 --- /dev/null +++ b/test/ssl.js @@ -0,0 +1,56 @@ +var assert = require('assert'), + fs = require('fs'), + clone = require('clone'), + bitcoin = require('../lib/bitcoin'), + config = require('./config'); + +var getInfo = function(opts, cb) { + var client = new bitcoin.Client(opts); + client.getInfo(cb); +}; + +describe('Client SSL', function() { + it('use sslStrict by default', function(done) { + var opts = clone(config); + opts.ssl = true; + getInfo(opts, function(err, info) { + assert.ok(err instanceof Error); + assert.equal(err.message, 'DEPTH_ZERO_SELF_SIGNED_CERT'); + done(); + }); + }); + + it('strictSSL should fail with self-signed certificate', function(done) { + var opts = clone(config); + opts.ssl = true; + opts.sslStrict = true; + getInfo(opts, function(err, info) { + assert.ok(err instanceof Error); + assert.equal(err.message, 'DEPTH_ZERO_SELF_SIGNED_CERT'); + done(); + }); + }); + + it('self-signed certificate with sslStrict false', function(done) { + var opts = clone(config); + opts.ssl = true; + opts.sslStrict = false; + getInfo(opts, function(err, info) { + assert.ifError(err); + assert.ok(info); + done(); + }); + }); + + it('self-signed certificate with sslStrict and CA specified', function(done) { + var opts = clone(config); + opts.ssl = true; + opts.sslCa = fs.readFileSync(__dirname + '/testnet-box/1/testnet3/server.cert'); + getInfo(opts, function(err, info) { + assert.ifError(err); + assert.ok(info); + done(); + }); + }); + +});