wallet/http: require admin token.

This commit is contained in:
Christopher Jeffrey 2017-12-28 11:20:13 -08:00
parent 8f44352f63
commit 94fd001e88
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
5 changed files with 164 additions and 69 deletions

View File

@ -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;

View File

@ -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();

View File

@ -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();

View File

@ -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'
};
}

View File

@ -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();
});
});