From 383c8f085f86ef63391325171a80762fa72f3351 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Wed, 5 Oct 2016 05:51:32 -0700 Subject: [PATCH] http: add admin key for dos-able calls. --- bin/cli | 13 +++++++++- etc/sample.conf | 1 + lib/http/client.js | 11 ++++++++ lib/http/rpc.js | 18 ++++++++++++- lib/http/server.js | 61 +++++++++++++++++++++++++++++++++++++++++--- lib/node/config.js | 1 + lib/node/fullnode.js | 1 + lib/node/spvnode.js | 1 + 8 files changed, 101 insertions(+), 6 deletions(-) diff --git a/bin/cli b/bin/cli index de80a0a7..0961774f 100755 --- a/bin/cli +++ b/bin/cli @@ -303,6 +303,14 @@ CLI.prototype.rescan = co(function* rescan() { this.log('Rescanning...'); }); +CLI.prototype.backup = co(function* backup() { + var path = this.argv[0]; + + yield this.client.backup(path); + + this.log('Backup complete.'); +}); + CLI.prototype.importKey = co(function* importKey() { var key = this.argv[0]; @@ -485,6 +493,8 @@ CLI.prototype.handleNode = co(function* handleNode() { return yield this.getBlock(); case 'rescan': return yield this.rescan(); + case 'backup': + return yield this.backup(); case 'rpc': return yield this.rpc(); default: @@ -496,7 +506,8 @@ CLI.prototype.handleNode = co(function* handleNode() { this.log(' $ tx [hash/address]: View transactions.'); this.log(' $ coin [hash+index/address]: View coins.'); this.log(' $ block [hash/height]: View block.'); - this.log(' $ rescan [height/hash]: Rescan for transactions.'); + this.log(' $ rescan [height/hash]: Rescan for transactions (admin-only).'); + this.log(' $ backup [path]: Backup the wallet db (admin-only).'); this.log(' $ rpc [command] [args]: Execute RPC command.'); return; } diff --git a/etc/sample.conf b/etc/sample.conf index 444087e5..d8fce512 100644 --- a/etc/sample.conf +++ b/etc/sample.conf @@ -59,5 +59,6 @@ known-peers: ./known-peers # http-port: 8332 # http-host: 0.0.0.0 api-key: bikeshed +admin-key: bikeshed2 wallet-auth: false # no-auth: false diff --git a/lib/http/client.js b/lib/http/client.js index 7f4fc84d..02c195e1 100644 --- a/lib/http/client.js +++ b/lib/http/client.js @@ -358,6 +358,17 @@ HTTPClient.prototype.rescan = function rescan(hash) { return this._post('/rescan', options); }; +/** + * Backup the walletdb. + * @param {String} path + * @returns {Promise} + */ + +HTTPClient.prototype.backup = function backup(path) { + var options = { path: path }; + return this._post('/backup', options); +}; + /** * Listen for events on wallet id. * @param {WalletID} id diff --git a/lib/http/rpc.js b/lib/http/rpc.js index f36de44d..6a6dc06b 100644 --- a/lib/http/rpc.js +++ b/lib/http/rpc.js @@ -69,9 +69,11 @@ function RPC(node) { utils.inherits(RPC, EventEmitter); -RPC.prototype.execute = function execute(json) { +RPC.prototype.execute = function execute(json, admin) { switch (json.method) { case 'stop': + if (!admin) + return Promise.resolve('Not authorized.'); return this.stop(json.params); case 'help': return this.help(json.params); @@ -107,6 +109,8 @@ RPC.prototype.execute = function execute(json) { case 'gettxoutsetinfo': return this.gettxoutsetinfo(json.params); case 'verifychain': + if (!admin) + return Promise.resolve('Not authorized.'); return this.verifychain(json.params); case 'invalidateblock': @@ -130,12 +134,18 @@ RPC.prototype.execute = function execute(json) { return this.submitblock(json.params); case 'setgenerate': + if (!admin) + return Promise.resolve('Not authorized.'); return this.setgenerate(json.params); case 'getgenerate': return this.getgenerate(json.params); case 'generate': + if (!admin) + return Promise.resolve('Not authorized.'); return this.generate(json.params); case 'generatetoaddress': + if (!admin) + return Promise.resolve('Not authorized.'); return this.generatetoaddress(json.params); case 'estimatefee': @@ -215,10 +225,14 @@ RPC.prototype.execute = function execute(json) { case 'addwitnessaddress': return this.addwitnessaddress(json.params); case 'backupwallet': + if (!admin) + return Promise.resolve('Not authorized.'); return this.backupwallet(json.params); case 'dumpprivkey': return this.dumpprivkey(json.params); case 'dumpwallet': + if (!admin) + return Promise.resolve('Not authorized.'); return this.dumpwallet(json.params); case 'encryptwallet': return this.encryptwallet(json.params); @@ -247,6 +261,8 @@ RPC.prototype.execute = function execute(json) { case 'importprivkey': return this.importprivkey(json.params); case 'importwallet': + if (!admin) + return Promise.resolve('Not authorized.'); return this.importwallet(json.params); case 'importaddress': return this.importaddress(json.params); diff --git a/lib/http/server.js b/lib/http/server.js index 0508d3fc..68b3c64a 100644 --- a/lib/http/server.js +++ b/lib/http/server.js @@ -62,6 +62,7 @@ function HTTPServer(options) { this.loaded = false; this.apiKey = options.apiKey; this.apiHash = null; + this.adminHash = null; this.rpc = null; if (!this.apiKey) @@ -71,6 +72,13 @@ function HTTPServer(options) { assert(this.apiKey.length <= 200, 'API key must be under 200 bytes.'); this.apiHash = hash256(this.apiKey); + this.adminHash = this.apiHash; + + if (options.adminKey) { + assert(typeof options.adminKey === 'string', 'API key must be a string.'); + assert(options.adminKey.length <= 200, 'API key must be under 200 bytes.'); + this.adminHash = hash256(options.adminKey); + } if (options.noAuth) { this.apiKey = null; @@ -135,6 +143,7 @@ HTTPServer.prototype._init = function _init() { if (!auth) { req.username = null; req.password = null; + req.admin = false; return next(); } @@ -147,15 +156,25 @@ HTTPServer.prototype._init = function _init() { req.username = parts.shift(); req.password = parts.join(':'); + req.admin = false; next(); }); this.use(function(req, res, send, next) { + var hash; + if (!this.apiHash) return next(); - if (crypto.ccmp(hash256(req.password), this.apiHash)) + hash = hash256(req.password); + + if (crypto.ccmp(hash, this.adminHash)) { + req.admin = true; + return next(); + } + + if (crypto.ccmp(hash, this.apiHash)) return next(); res.setHeader('WWW-Authenticate', 'Basic realm="node"'); @@ -413,6 +432,11 @@ HTTPServer.prototype._init = function _init() { options.token = new Buffer(params.token, 'hex'); } + if (params.path) { + enforce(typeof params.path === 'string', 'Passphrase must be a string.'); + options.path = params.path; + } + req.options = options; next(); @@ -477,7 +501,7 @@ HTTPServer.prototype._init = function _init() { } try { - json = yield this.rpc.execute(req.body); + json = yield this.rpc.execute(req.body, req.admin); } catch (err) { this.logger.error(err); @@ -669,11 +693,30 @@ HTTPServer.prototype._init = function _init() { this.post('/rescan', con(function* (req, res, send, next) { var options = req.options; var height = options.hash || options.height; + enforce(height != null, 'Hash or height is required.'); + + if (!req.admin) + throw new Error('Cannot scan.'); + send(200, { success: true }); yield this.node.scan(height); })); + // Backup WalletDB + this.post('/backup', con(function* (req, res, send, next) { + var options = req.options; + var path = options.path; + + enforce(path, 'Path is required.'); + + if (!req.admin) + throw new Error('Cannot backup.'); + + yield this.walletdb.backup(path); + send(200, { success: true }); + })); + // Get wallet this.get('/wallet/:id', function(req, res, send, next) { send(200, req.wallet.toJSON()); @@ -1047,6 +1090,7 @@ HTTPServer.prototype._initIO = function _initIO() { socket.on('auth', function(args, callback) { var apiKey = args[0]; + var hash; if (socket.auth) return callback({ error: 'Already authed.' }); @@ -1054,8 +1098,13 @@ HTTPServer.prototype._initIO = function _initIO() { socket.stop(); if (self.apiHash) { - if (!crypto.ccmp(hash256(apiKey), self.apiHash)) - return callback({ error: 'Bad key.' }); + hash = hash256(apiKey); + if (crypto.ccmp(hash, self.adminHash)) { + socket.admin = true; + } else { + if (!crypto.ccmp(hash, self.apiHash)) + return callback({ error: 'Bad key.' }); + } } socket.auth = true; @@ -1159,6 +1208,9 @@ HTTPServer.prototype._initIO = function _initIO() { socket.on('scan chain', function(args, callback) { var start = args[0]; + if (!socket.admin) + return callback({ error: 'Cannot scan.' }); + if (!utils.isHex256(start) && !utils.isUInt32(start)) return callback({ error: 'Invalid parameter.' }); @@ -1305,6 +1357,7 @@ function ClientSocket(server, socket) { this.auth = false; this.filter = {}; this.filterCount = 0; + this.admin = false; this.chain = this.server.chain; this.mempool = this.server.mempool; diff --git a/lib/node/config.js b/lib/node/config.js index 07885290..4f6b037d 100644 --- a/lib/node/config.js +++ b/lib/node/config.js @@ -199,6 +199,7 @@ config.parseData = function parseData(data, prefix, dirname) { options.httpPort = num(data.httpport); options.httpHost = str(data.httphost); options.apiKey = str(data.apikey); + options.adminKey = str(data.adminkey); options.walletAuth = bool(data.walletauth); options.noAuth = bool(data.noauth); diff --git a/lib/node/fullnode.js b/lib/node/fullnode.js index e36405c8..6f17e7ef 100644 --- a/lib/node/fullnode.js +++ b/lib/node/fullnode.js @@ -162,6 +162,7 @@ function Fullnode(options) { port: this.options.httpPort || this.network.rpcPort, host: this.options.httpHost || '0.0.0.0', apiKey: this.options.apiKey, + adminKey: this.options.adminKey, walletAuth: this.options.walletAuth, noAuth: this.options.noAuth }); diff --git a/lib/node/spvnode.js b/lib/node/spvnode.js index 173ec5f6..5ccdb3ca 100644 --- a/lib/node/spvnode.js +++ b/lib/node/spvnode.js @@ -99,6 +99,7 @@ function SPVNode(options) { port: this.options.httpPort || this.network.rpcPort, host: this.options.httpHost || '0.0.0.0', apiKey: this.options.apiKey, + adminKey: this.options.adminKey, walletAuth: this.options.walletAuth, noAuth: this.options.noAuth });