From 94fd001e886479454608ca7a109621ffc67a56ee Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Thu, 28 Dec 2017 11:20:13 -0800 Subject: [PATCH] wallet/http: require admin token. --- lib/wallet/http.js | 148 ++++++++++++++++++++++++++++++++----------- lib/wallet/node.js | 3 +- lib/wallet/plugin.js | 3 +- lib/wallet/rpc.js | 46 +++++++++----- test/http-test.js | 33 ++++++---- 5 files changed, 164 insertions(+), 69 deletions(-) diff --git a/lib/wallet/http.js b/lib/wallet/http.js index 5e6b8467..334dc081 100644 --- a/lib/wallet/http.js +++ b/lib/wallet/http.js @@ -92,6 +92,26 @@ class HTTP extends Server { type: 'json' })); + this.use(async (req, res) => { + if (!this.options.walletAuth) { + req.admin = true; + return; + } + + const valid = Validator.fromRequest(req); + const token = valid.buf('token'); + + if (token && ccmp(token, this.options.adminToken)) { + req.admin = true; + return; + } + + if (req.method === 'POST' && req.path.length === 0) { + res.json(403); + return; + } + }); + this.use(this.jsonRPC()); this.use(this.router()); @@ -107,9 +127,7 @@ class HTTP extends Server { }); this.hook(async (req, res) => { - const valid = Validator.fromRequest(req); - - if (req.path.length === 0) + if (req.path.length < 2) return; if (req.path[0] !== 'wallet') @@ -118,6 +136,7 @@ class HTTP extends Server { if (req.method === 'PUT' && req.path.length === 2) return; + const valid = Validator.fromRequest(req); const id = valid.str('id'); const token = valid.buf('token'); @@ -126,7 +145,7 @@ class HTTP extends Server { return; } - if (!this.options.walletAuth) { + if (req.admin || !this.options.walletAuth) { const wallet = await this.wdb.get(id); if (!wallet) { @@ -165,6 +184,11 @@ class HTTP extends Server { // Rescan this.post('/rescan', async (req, res) => { + if (!req.admin) { + res.json(403); + return; + } + const valid = Validator.fromRequest(req); const height = valid.u32('height'); @@ -175,6 +199,11 @@ class HTTP extends Server { // Resend this.post('/resend', async (req, res) => { + if (!req.admin) { + res.json(403); + return; + } + await this.wdb.resend(); res.json(200, { success: true }); @@ -182,6 +211,11 @@ class HTTP extends Server { // Backup WalletDB this.post('/backup', async (req, res) => { + if (!req.admin) { + res.json(403); + return; + } + const valid = Validator.fromRequest(req); const path = valid.str('path'); @@ -193,7 +227,12 @@ class HTTP extends Server { }); // List wallets - this.get('/wallets', async (req, res) => { + this.get('/wallet', async (req, res) => { + if (!req.admin) { + res.json(403); + return; + } + const wallets = await this.wdb.getWallets(); res.json(200, wallets); }); @@ -206,6 +245,11 @@ class HTTP extends Server { // Get wallet master key this.get('/wallet/:id/master', (req, res) => { + if (!req.admin) { + res.json(403); + return; + } + res.json(200, req.wallet.master.toJSON(this.network, true)); }); @@ -809,60 +853,56 @@ class HTTP extends Server { */ initSockets() { - this.wdb.on('tx', (wallet, tx, details) => { + const handleTX = (event, wallet, tx, details) => { const name = `w:${wallet.id}`; - if (!this.channel(name)) + if (!this.channel(name) && !this.channel('w:*')) return; const json = details.toJSON(this.network, this.wdb.height); - this.to(name, 'wallet tx', json); + + if (this.channel(name)) + this.to(name, event, wallet.id, json); + + if (this.channel('w:*')) + this.to('w:*', event, wallet.id, json); + }; + + this.wdb.on('tx', (wallet, tx, details) => { + handleTX('tx', wallet, tx, details); }); this.wdb.on('confirmed', (wallet, tx, details) => { - const name = `w:${wallet.id}`; - - if (!this.channel(name)) - return; - - const json = details.toJSON(this.network, this.wdb.height); - this.to(name, 'wallet confirmed', json); + handleTX('confirmed', wallet, tx, details); }); this.wdb.on('unconfirmed', (wallet, tx, details) => { - const name = `w:${wallet.id}`; - - if (!this.channel(name)) - return; - - const json = details.toJSON(this.network, this.wdb.height); - this.to(name, 'wallet unconfirmed', json); + handleTX('unconfirmed', wallet, tx, details); }); this.wdb.on('conflict', (wallet, tx, details) => { - const name = `w:${wallet.id}`; - - if (!this.channel(name)) - return; - - const json = details.toJSON(this.network, this.wdb.height); - this.to(name, 'wallet conflict', json); + handleTX('conflict', wallet, tx, details); }); this.wdb.on('balance', (wallet, balance) => { const name = `w:${wallet.id}`; - if (!this.channel(name)) + if (!this.channel(name) && !this.channel('w:*')) return; const json = balance.toJSON(); - this.to(name, 'wallet balance', json); + + if (this.channel(name)) + this.to(name, 'balance', wallet.id, json); + + if (this.channel('w:*')) + this.to('w:*', 'balance', wallet.id, json); }); this.wdb.on('address', (wallet, receive) => { const name = `w:${wallet.id}`; - if (!this.channel(name)) + if (!this.channel(name) && !this.channel('w:*')) return; const json = []; @@ -870,7 +910,11 @@ class HTTP extends Server { for (const addr of receive) json.push(addr.toJSON(this.network)); - this.to(name, 'wallet address', json); + if (this.channel(name)) + this.to(name, 'address', wallet.id, json); + + if (this.channel('w:*')) + this.to('w:*', 'address', wallet.id, json); }); } @@ -881,8 +925,8 @@ class HTTP extends Server { */ handleSocket(socket) { - socket.hook('wallet auth', (...args) => { - if (socket.channel('wallet auth')) + socket.hook('auth', (...args) => { + if (socket.channel('auth')) throw new Error('Already authed.'); if (!this.options.noAuth) { @@ -899,7 +943,7 @@ class HTTP extends Server { throw new Error('Invalid API key.'); } - socket.join('wallet auth'); + socket.join('auth'); this.logger.info('Successful auth from %s.', socket.host); @@ -916,7 +960,7 @@ class HTTP extends Server { */ handleAuth(socket) { - socket.hook('wallet join', async (...args) => { + socket.hook('join', async (...args) => { const valid = new Validator(args); const id = valid.str(0, ''); const token = valid.buf(1); @@ -925,10 +969,20 @@ class HTTP extends Server { throw new Error('Invalid parameter.'); if (!this.options.walletAuth) { + socket.join('admin'); + } else if (token) { + if (ccmp(token, this.options.adminToken)) + socket.join('admin'); + } + + if (socket.channel('admin') || !this.options.walletAuth) { socket.join(`w:${id}`); return null; } + if (id === '*') + throw new Error('Bad token.'); + if (!token) throw new Error('Invalid parameter.'); @@ -950,7 +1004,7 @@ class HTTP extends Server { return null; }); - socket.hook('wallet leave', (...args) => { + socket.hook('leave', (...args) => { const valid = new Validator(args); const id = valid.str(0, ''); @@ -978,6 +1032,7 @@ class HTTPOptions { this.node = null; this.apiKey = base58.encode(random.randomBytes(20)); this.apiHash = sha256.digest(Buffer.from(this.apiKey, 'ascii')); + this.adminToken = random.randomBytes(32); this.serviceHash = this.apiHash; this.noAuth = false; this.walletAuth = false; @@ -1023,6 +1078,23 @@ class HTTPOptions { this.apiHash = sha256.digest(Buffer.from(this.apiKey, 'ascii')); } + if (options.adminToken != null) { + if (typeof options.adminToken === 'string') { + assert(options.adminToken.length === 64, + 'Admin token must be a 32 byte hex string.'); + const token = Buffer.from(options.adminToken, 'hex'); + assert(token.length === 32, + 'Admin token must be a 32 byte hex string.'); + this.adminToken = token; + } else { + assert(Buffer.isBuffer(options.adminToken), + 'Admin token must be a hex string or buffer.'); + assert(options.adminToken.length === 32, + 'Admin token must be 32 bytes.'); + this.adminToken = options.adminToken; + } + } + if (options.noAuth != null) { assert(typeof options.noAuth === 'boolean'); this.noAuth = options.noAuth; diff --git a/lib/wallet/node.js b/lib/wallet/node.js index 528a1bb1..ae34d105 100644 --- a/lib/wallet/node.js +++ b/lib/wallet/node.js @@ -68,7 +68,8 @@ class WalletNode extends Node { port: this.config.uint('http-port'), apiKey: this.config.str('api-key'), walletAuth: this.config.bool('wallet-auth'), - noAuth: this.config.bool('no-auth') + noAuth: this.config.bool('no-auth'), + adminToken: this.config.str('admin-token') }); this.init(); diff --git a/lib/wallet/plugin.js b/lib/wallet/plugin.js index 0f957360..9ea0e35d 100644 --- a/lib/wallet/plugin.js +++ b/lib/wallet/plugin.js @@ -69,7 +69,8 @@ class Plugin extends EventEmitter { port: this.config.uint('http-port'), apiKey: this.config.str('api-key', node.config.str('api-key')), walletAuth: this.config.bool('wallet-auth'), - noAuth: this.config.bool('no-auth') + noAuth: this.config.bool('no-auth'), + adminToken: this.config.str('admin-token') }); this.init(); diff --git a/lib/wallet/rpc.js b/lib/wallet/rpc.js index 0a3bd3cf..36620d6b 100644 --- a/lib/wallet/rpc.js +++ b/lib/wallet/rpc.js @@ -1032,6 +1032,7 @@ class RPC extends RPCBase { return []; let height = -1; + if (block) { const entry = await this.client.getEntry(block); if (entry) @@ -1042,9 +1043,10 @@ class RPC extends RPCBase { height = this.chain.height; const txs = await wallet.getHistory(); - const out = []; - let highest; + + let highest = null; + for (const wtx of txs) { if (wtx.height < height) continue; @@ -1085,7 +1087,11 @@ class RPC extends RPCBase { let sent = 0; let received = 0; - let sendMember, recMember, sendIndex, recIndex; + let sendMember = null; + let recMember = null; + let sendIndex = -1; + let recIndex = -1; + for (let i = 0; i < details.outputs.length; i++) { const member = details.outputs[i]; @@ -1103,21 +1109,30 @@ class RPC extends RPCBase { sendIndex = i; } - let member, index; + let member = null; + let index = -1; + if (receive) { + assert(recMember); member = recMember; index = recIndex; } else { - member = sendMember; - index = sendIndex; + if (sendMember) { + member = sendMember; + index = sendIndex; + } else { + // In the odd case where we send to ourselves. + receive = true; + received = 0; + member = recMember; + index = recIndex; + } } - // In the odd case where we send to ourselves. - if (!member) { - assert(!receive); - member = recMember; - index = recIndex; - } + let rbf = false; + + if (wtx.height === -1 && wtx.tx.isRBF()) + rbf = true; return { account: member.path ? member.path.name : '', @@ -1128,15 +1143,16 @@ class RPC extends RPCBase { amount: Amount.btc(receive ? received : -sent, true), label: member.path ? member.path.name : undefined, vout: index, - confirmations: details.getDepth(), + confirmations: details.getDepth(this.wdb.height), blockhash: details.block ? util.revHex(details.block) : null, - blockindex: details.index, + blockindex: -1, blocktime: details.time, + blockheight: details.height, txid: util.revHex(details.hash), walletconflicts: [], time: details.mtime, timereceived: details.mtime, - 'bip125-replaceable': 'no' + 'bip125-replaceable': rbf ? 'yes' : 'no' }; } diff --git a/test/http-test.js b/test/http-test.js index 553f4688..68dd3671 100644 --- a/test/http-test.js +++ b/test/http-test.js @@ -25,16 +25,18 @@ const node = new FullNode({ const {NodeClient, WalletClient} = require('bclient'); -const client = new NodeClient({ +const nclient = new NodeClient({ port: network.rpcPort, apiKey: 'foo' }); -const wallet = new WalletClient({ +const wclient = new WalletClient({ port: network.walletPort, apiKey: 'foo' }); +let wallet = null; + const {wdb} = node.require('walletdb'); let addr = null; @@ -46,16 +48,19 @@ describe('HTTP', function() { it('should open node', async () => { consensus.COINBASE_MATURITY = 0; await node.open(); - await client.open(); + await nclient.open(); + await wclient.open(); }); it('should create wallet', async () => { - const info = await wallet.create({ id: 'test' }); + const info = await wclient.createWallet('test'); assert.strictEqual(info.id, 'test'); + wallet = wclient.wallet('test', info.token); + await wallet.open(); }); it('should get info', async () => { - const info = await client.getInfo(); + const info = await nclient.getInfo(); assert.strictEqual(info.network, node.network.type); assert.strictEqual(info.version, pkg.version); assert.typeOf(info.pool, 'object'); @@ -150,9 +155,9 @@ describe('HTTP', function() { it('should generate new api key', async () => { const old = wallet.token.toString('hex'); - const token = await wallet.retoken(null); - assert.strictEqual(token.length, 64); - assert.notStrictEqual(token, old); + const result = await wallet.retoken(null); + assert.strictEqual(result.token.length, 64); + assert.notStrictEqual(result.token, old); }); it('should get balance', async () => { @@ -161,12 +166,12 @@ describe('HTTP', function() { }); it('should execute an rpc call', async () => { - const info = await client.execute('getblockchaininfo', []); + const info = await nclient.execute('getblockchaininfo', []); assert.strictEqual(info.blocks, 0); }); it('should execute an rpc call with bool parameter', async () => { - const info = await client.execute('getrawmempool', [true]); + const info = await nclient.execute('getrawmempool', [true]); assert.deepStrictEqual(info, {}); }); @@ -195,7 +200,7 @@ describe('HTTP', function() { }); it('should get a block template', async () => { - const json = await client.execute('getblocktemplate', []); + const json = await nclient.execute('getblocktemplate', []); assert.deepStrictEqual(json, { capabilities: ['proposal'], mutable: ['time', 'transactions', 'prevblock'], @@ -230,7 +235,7 @@ describe('HTTP', function() { const attempt = await node.miner.createBlock(); const block = attempt.toBlock(); const hex = block.toRaw().toString('hex'); - const json = await client.execute('getblocktemplate', [{ + const json = await nclient.execute('getblocktemplate', [{ mode: 'proposal', data: hex }]); @@ -238,7 +243,7 @@ describe('HTTP', function() { }); it('should validate an address', async () => { - const json = await client.execute('validateaddress', [ + const json = await nclient.execute('validateaddress', [ addr.toString(node.network) ]); assert.deepStrictEqual(json, { @@ -253,7 +258,7 @@ describe('HTTP', function() { it('should cleanup', async () => { consensus.COINBASE_MATURITY = 100; await wallet.close(); - await client.close(); + await nclient.close(); await node.close(); }); });