wallet/http: require admin token.
This commit is contained in:
parent
8f44352f63
commit
94fd001e88
@ -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;
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user