http: more validation.

This commit is contained in:
Christopher Jeffrey 2016-08-13 05:33:29 -07:00
parent f42d60769b
commit 59d4fa22ce
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
6 changed files with 292 additions and 147 deletions

View File

@ -233,6 +233,7 @@ Address.fromRaw = function fromRaw(data) {
*/ */
Address.prototype.fromBase58 = function fromBase58(data) { Address.prototype.fromBase58 = function fromBase58(data) {
assert(typeof data === 'string');
return this.fromRaw(utils.fromBase58(data)); return this.fromRaw(utils.fromBase58(data));
}; };

View File

@ -306,19 +306,8 @@ Fullnode.prototype._open = function open(callback) {
self.walletdb.rescan(self.chain.db, next); self.walletdb.rescan(self.chain.db, next);
}, },
function(next) { function(next) {
var i; // Rebroadcast pending transactions.
self.wallet.getUnconfirmed(function(err, txs) { self.wallet.resend(next);
if (err)
return next(err);
if (txs.length > 0)
self.logger.info('Rebroadcasting %d transactions.', txs.length);
for (i = 0; i < txs.length; i++)
self.pool.broadcast(txs[i]);
next();
});
}, },
function(next) { function(next) {
if (!self.http) if (!self.http)

View File

@ -287,7 +287,7 @@ HTTPClient.prototype.getInfo = function getInfo(callback) {
*/ */
HTTPClient.prototype.getCoinsByAddress = function getCoinsByAddress(address, callback) { HTTPClient.prototype.getCoinsByAddress = function getCoinsByAddress(address, callback) {
var body = { addresses: address }; var body = { address: address };
return this._post('/coin/address', body, callback); return this._post('/coin/address', body, callback);
}; };
@ -312,7 +312,7 @@ HTTPClient.prototype.getCoin = function getCoin(hash, index, callback) {
*/ */
HTTPClient.prototype.getTXByAddress = function getTXByAddress(address, callback) { HTTPClient.prototype.getTXByAddress = function getTXByAddress(address, callback) {
var body = { addresses: address }; var body = { address: address };
return this._post('/tx/address', body, callback); return this._post('/tx/address', body, callback);
}; };

View File

@ -43,9 +43,13 @@ function HTTPServer(options) {
assert(this.node, 'HTTP requires a Node.'); assert(this.node, 'HTTP requires a Node.');
this.network = this.node.network; this.network = this.node.network;
this.walletdb = this.node.walletdb; this.chain = this.node.chain;
this.mempool = this.node.mempool; this.mempool = this.node.mempool;
this.pool = this.node.pool; this.pool = this.node.pool;
this.fees = this.node.fees;
this.miner = this.node.miner;
this.wallet = this.node.wallet;
this.walletdb = this.node.walletdb;
this.logger = options.logger || this.node.logger; this.logger = options.logger || this.node.logger;
this.loaded = false; this.loaded = false;
this.apiKey = options.apiKey; this.apiKey = options.apiKey;
@ -102,8 +106,8 @@ HTTPServer.prototype._init = function _init() {
res.setHeader('X-Bcoin-Version', constants.USER_VERSION); res.setHeader('X-Bcoin-Version', constants.USER_VERSION);
res.setHeader('X-Bcoin-Agent', constants.USER_AGENT); res.setHeader('X-Bcoin-Agent', constants.USER_AGENT);
res.setHeader('X-Bcoin-Network', self.network.type); res.setHeader('X-Bcoin-Network', self.network.type);
res.setHeader('X-Bcoin-Height', self.node.chain.height + ''); res.setHeader('X-Bcoin-Height', self.chain.height + '');
res.setHeader('X-Bcoin-Tip', utils.revHex(self.node.chain.tip.hash)); res.setHeader('X-Bcoin-Tip', utils.revHex(self.chain.tip.hash));
next(); next();
}); });
@ -157,109 +161,184 @@ HTTPServer.prototype._init = function _init() {
}); });
this.use(function(req, res, next, send) { this.use(function(req, res, next, send) {
var params, options; var i, params, options, output, address;
if (req.method === 'POST' if (req.method === 'POST' && req.pathname === '/') {
&& req.pathname === '/') { assert(typeof req.body.method === 'string', 'Method must be a string.');
assert(Array.isArray(req.body.params), 'Params must be an array.');
req.options = {}; req.options = {};
return next(); return next();
} }
params = utils.merge({}, req.params, req.query, req.body); params = {};
options = {}; options = {};
softMerge(params, req.params, true);
softMerge(params, req.query, true);
softMerge(params, req.body);
self.logger.debug('Params:'); self.logger.debug('Params:');
self.logger.debug(params); self.logger.debug(params);
if (params.id) { if (params.id) {
assert(params.id !== '!all'); assert(typeof params.id === 'string', 'ID must be a string.');
options.id = params.id; options.id = params.id;
} }
if (params.hash) { if (params.hash) {
if (params.hash.length !== 64) assert(typeof params.hash === 'string', 'Hash must be a string.');
options.height = params.hash >>> 0; if (params.hash.length !== 64) {
else options.height = Number(params.hash);
assert(utils.isUInt32(options.height), 'Hash must be a number.');
} else {
options.hash = utils.revHex(params.hash); options.hash = utils.revHex(params.hash);
}
} }
if (params.index != null) if (params.index != null) {
options.index = params.index >>> 0; options.index = Number(params.index);
assert(utils.isUInt32(options.index), 'Index must be a number.');
}
if (params.height != null) if (params.height != null) {
options.height = params.height >>> 0; options.height = Number(params.height);
assert(utils.isUInt32(options.height), 'Height must be a number.');
}
if (params.start != null) if (params.start != null) {
options.start = params.start >>> 0; options.start = Number(params.start);
assert(utils.isUInt32(options.height), 'Start must be a number.');
}
if (params.end != null) if (params.end != null) {
options.end = params.end >>> 0; options.end = Number(params.end);
assert(utils.isUInt32(options.end), 'End must be a number.');
}
if (params.limit != null) if (params.limit != null) {
options.limit = params.limit >>> 0; options.limit = Number(params.limit);
assert(utils.isUInt32(options.limit), 'Limit must be a number.');
}
if (params.address) { if (params.age != null) {
params.addresses = params.address; options.age = Number(params.age);
options.address = params.address; assert(utils.isUInt32(options.age), 'Age must be a number.');
} }
if (params.rate) if (params.rate)
options.rate = utils.satoshi(params.rate); options.rate = utils.satoshi(params.rate);
if (params.subtractFee) if (params.m != null) {
options.subtractFee = params.subtractFee; options.m = Number(params.m);
assert(utils.isUInt32(options.m), 'm must be a number.');
if (Array.isArray(params.outputs)) {
options.outputs = params.outputs.map(function(output) {
return {
address: output.address,
script: decodeScript(output.script),
value: utils.satoshi(output.value)
};
});
} }
if (params.addresses) { if (params.n != null) {
if (typeof params.addresses === 'string') options.n = Number(params.n);
options.addresses = params.addresses.split(','); assert(utils.isUInt32(options.n), 'n must be a number.');
else
options.addresses = params.addresses;
} }
if (params.tx) { if (params.blocks != null) {
try { options.blocks = Number(params.blocks);
if (typeof params.tx === 'object') assert(utils.isUInt32(options.blocks), 'Blocks must be a number.');
options.tx = bcoin.tx.fromJSON(params.tx); }
else
options.tx = bcoin.tx.fromRaw(params.tx, 'hex'); if (params.subtractFee != null) {
} catch (e) { if (typeof params.subtractFee === 'number') {
return next(e); options.subtractFee = params.subtractFee;
assert(utils.isUInt32(options.subtractFee), 'subtractFee must be a number.');
} else if (params.subtractFee === 'true') {
options.subtractFee = true;
} else {
assert(typeof options.subtractFee === 'boolean', 'subtractFee must be a boolean.');
options.subtractFee = params.subtractFee;
} }
} }
if (typeof params.account === 'string') if (params.outputs) {
options.account = params.account || null; assert(Array.isArray(params.outputs), 'Outputs must be an array.');
else if (typeof params.account === 'number') options.outputs = [];
options.account = params.account; for (i = 0; i < params.outputs.length; i++) {
output = params.outputs[i];
if (params.name) if (output.address)
options.name = params.name; assert(typeof output.address === 'string', 'Address must be a string.');
else if (output.script)
assert(typeof output.script === 'string', 'Script must be a string.');
else
assert(false, 'No address or script present.');
if (params.age) options.outputs.push({
options.age = params.age >>> 0; address: output.address
? bcoin.address.fromBase58(output.address)
if (params.key) : null,
params.keys = params.key; script: output.script
? bcoin.script.fromRaw(output.script, 'hex')
if (params.keys) { : null,
if (typeof params.keys === 'string') value: utils.satoshi(output.value)
options.keys = params.keys.split(','); });
else }
options.keys = params.keys;
} }
if (params.passphrase) if (params.address) {
if (Array.isArray(options.address)) {
options.address = [];
for (i = 0; i < params.address.length; i++) {
address = params.address[i];
assert(typeof address === 'string', 'Address must be a string.');
address = bcoin.address.fromBase58(address);
}
} else {
assert(typeof params.address === 'string', 'Address must be a string.');
options.address = bcoin.address.fromBase58(params.address);
}
}
if (params.tx) {
if (typeof params.tx === 'object') {
options.tx = bcoin.tx.fromJSON(params.tx);
} else {
assert(typeof params.tx === 'string', 'TX must be a hex string.');
options.tx = bcoin.tx.fromRaw(params.tx, 'hex');
}
}
if (params.account != null) {
if (typeof params.account === 'number') {
options.account = params.account;
assert(utils.isUInt32(options.account), 'Account must be a number.');
} else {
assert(typeof params.account === 'string', 'Account must be a string.');
options.account = params.account;
}
}
if (params.type) {
assert(typeof params.type === 'string', 'Type must be a string.');
options.type = params.type;
}
if (params.name) {
assert(typeof params.name === 'string', 'Name must be a string.');
options.name = params.name;
}
if (params.key) {
assert(typeof params.key === 'string', 'Key must be a string.');
options.key = params.key;
}
if (params.old) {
assert(typeof params.old === 'string', 'Passphrase must be a string.');
assert(params.old.length > 0, 'Passphrase must be a string.');
options.old = params.old;
}
if (params.passphrase) {
assert(typeof params.passphrase === 'string', 'Passphrase must be a string.');
assert(params.passphrase.length > 0, 'Passphrase must be a string.');
options.passphrase = params.passphrase; options.passphrase = params.passphrase;
}
if (params.token) { if (params.token) {
assert(utils.isHex(params.token), 'API key must be a hex string.'); assert(utils.isHex(params.token), 'API key must be a hex string.');
@ -307,19 +386,8 @@ HTTPServer.prototype._init = function _init() {
}); });
}); });
function decodeScript(script) {
if (!script)
return;
if (typeof script === 'string')
return bcoin.script.fromRaw(script, 'hex');
return new bcoin.script(script);
}
// JSON RPC // JSON RPC
this.post('/', function(req, res, next, send) { this.post('/', function(req, res, next, send) {
if (!(req.body.method && req.body.params))
return next(new Error('Method not found.'));
if (!self.rpc) { if (!self.rpc) {
RPC = require('./rpc'); RPC = require('./rpc');
self.rpc = new RPC(self.node); self.rpc = new RPC(self.node);
@ -359,23 +427,21 @@ HTTPServer.prototype._init = function _init() {
send(200, { send(200, {
version: constants.USER_VERSION, version: constants.USER_VERSION,
agent: constants.USER_AGENT, agent: constants.USER_AGENT,
services: self.pool.services,
network: self.network.type, network: self.network.type,
height: self.node.chain.height, height: self.chain.height,
tip: utils.revHex(self.node.chain.tip.hash), tip: self.chain.tip.rhash,
peers: self.node.pool.peers.all.length, peers: self.pool.peers.all.length,
progress: self.node.chain.getProgress() progress: self.chain.getProgress()
}); });
}); });
// UTXO by address // UTXO by address
this.get('/coin/address/:address', function(req, res, next, send) { this.get('/coin/address/:address', function(req, res, next, send) {
self.node.getCoinsByAddress(req.options.addresses, function(err, coins) { self.node.getCoinsByAddress(req.options.address, function(err, coins) {
if (err) if (err)
return next(err); return next(err);
if (!coins.length)
return send(404);
send(200, coins.map(function(coin) { send(200, coins.map(function(coin) {
return coin.toJSON(); return coin.toJSON();
})); }));
@ -397,13 +463,10 @@ HTTPServer.prototype._init = function _init() {
// Bulk read UTXOs // Bulk read UTXOs
this.post('/coin/address', function(req, res, next, send) { this.post('/coin/address', function(req, res, next, send) {
self.node.getCoinsByAddress(req.options.addresses, function(err, coins) { self.node.getCoinsByAddress(req.options.address, function(err, coins) {
if (err) if (err)
return next(err); return next(err);
if (!coins.length)
return send(404);
send(200, coins.map(function(coin) { send(200, coins.map(function(coin) {
return coin.toJSON(); return coin.toJSON();
})); }));
@ -430,7 +493,7 @@ HTTPServer.prototype._init = function _init() {
// TX by address // TX by address
this.get('/tx/address/:address', function(req, res, next, send) { this.get('/tx/address/:address', function(req, res, next, send) {
self.node.getTXByAddress(req.options.addresses, function(err, txs) { self.node.getTXByAddress(req.options.address, function(err, txs) {
if (err) if (err)
return next(err); return next(err);
@ -449,13 +512,10 @@ HTTPServer.prototype._init = function _init() {
// Bulk read TXs // Bulk read TXs
this.post('/tx/address', function(req, res, next, send) { this.post('/tx/address', function(req, res, next, send) {
self.node.getTXByAddress(req.options.addresses, function(err, txs) { self.node.getTXByAddress(req.options.address, function(err, txs) {
if (err) if (err)
return next(err); return next(err);
if (!txs.length)
return send(404);
utils.forEachSerial(txs, function(tx, next) { utils.forEachSerial(txs, function(tx, next) {
self.node.fillHistory(tx, next); self.node.fillHistory(tx, next);
}, function(err) { }, function(err) {
@ -485,13 +545,10 @@ HTTPServer.prototype._init = function _init() {
// Mempool snapshot // Mempool snapshot
this.get('/mempool', function(req, res, next, send) { this.get('/mempool', function(req, res, next, send) {
self.node.mempool.getHistory(function(err, txs) { self.mempool.getHistory(function(err, txs) {
if (err) if (err)
return next(err); return next(err);
if (!txs.length)
return send(404);
utils.forEachSerial(txs, function(tx, next) { utils.forEachSerial(txs, function(tx, next) {
self.node.fillHistory(tx, next); self.node.fillHistory(tx, next);
}, function(err) { }, function(err) {
@ -515,6 +572,12 @@ HTTPServer.prototype._init = function _init() {
}); });
}); });
// Estimate fee
this.get('/fee', function(req, res, next, send) {
var fee = self.fees.estimateFee(req.options.blocks);
send(200, { rate: utils.btc(fee) });
});
// Get wallet // Get wallet
this.get('/wallet/:id', function(req, res, next, send) { this.get('/wallet/:id', function(req, res, next, send) {
send(200, req.wallet.toJSON()); send(200, req.wallet.toJSON());
@ -645,6 +708,19 @@ HTTPServer.prototype._init = function _init() {
}); });
}); });
// Abandon Wallet TX
this.del('/wallet/:id/tx/:hash', function(req, res, next, send) {
var hash = req.options.hash;
var account = req.options.account;
req.wallet.abandon(hash, function(err) {
if (err)
return next(err);
send(200, { success: true });
});
});
// Add key // Add key
this.put('/wallet/:id/key', function(req, res, next, send) { this.put('/wallet/:id/key', function(req, res, next, send) {
var account = req.options.account; var account = req.options.account;
@ -672,7 +748,7 @@ HTTPServer.prototype._init = function _init() {
// Create address // Create address
this.post('/wallet/:id/address', function(req, res, next, send) { this.post('/wallet/:id/address', function(req, res, next, send) {
var account = req.options.account; var account = req.options.account;
req.wallet.createAddress(account, false, function(err, address) { req.wallet.createReceive(account, function(err, address) {
if (err) if (err)
return next(err); return next(err);
@ -832,29 +908,32 @@ HTTPServer.prototype._initIO = function _initIO() {
if (!this.server.io) if (!this.server.io)
return; return;
this.server.on('websocket', function(socket) { this.server.on('websocket', function(ws) {
socket.bcoin = new ClientSocket(self, socket); var socket = new ClientSocket(self, ws);
socket.bcoin.startTimeout();
socket.start();
socket.on('error', function(err) { socket.on('error', function(err) {
self.emit('error', err); self.emit('error', err);
}); });
socket.on('auth', function(apiKey, callback) { socket.once('auth', function(apiKey, callback) {
callback = utils.ensure(callback); if (typeof callback !== 'function')
return socket.destroy();
if (!self.apiHash) { socket.stop();
self.logger.info('Successful auth.');
socket.bcoin.stopTimeout(); if (self.apiHash) {
self.emit('websocket', socket); if (!utils.ccmp(hash256(apiKey), self.apiHash)) {
return callback(); socket.destroy();
return callback({ error: 'Bad key.' });
}
} }
if (!utils.ccmp(hash256(apiKey), self.apiHash)) socket.auth = true;
return callback({ error: 'Bad key.' });
self.logger.info('Successful auth from %s.', socket.host);
self.logger.info('Successful auth.');
socket.bcoin.stopTimeout();
self.emit('websocket', socket); self.emit('websocket', socket);
return callback(); return callback();
@ -869,13 +948,20 @@ HTTPServer.prototype._initIO = function _initIO() {
this.on('websocket', function(socket) { this.on('websocket', function(socket) {
socket.on('wallet join', function(id, token, callback) { socket.on('wallet join', function(id, token, callback) {
callback = utils.ensure(callback); if (typeof callback !== 'function')
return socket.destroy();
if (typeof id !== 'string')
return callback({ error: 'Invalid parameter.' });
if (!self.options.walletAuth) { if (!self.options.walletAuth) {
socket.join(id); socket.join(id);
return callback(); return callback();
} }
if (typeof token !== 'string')
return callback({ error: 'Invalid parameter.' });
self.walletdb.auth(id, token, function(err, wallet) { self.walletdb.auth(id, token, function(err, wallet) {
if (err) { if (err) {
self.logger.info('Wallet auth failure for %s: %s.', id, err.message); self.logger.info('Wallet auth failure for %s: %s.', id, err.message);
@ -886,14 +972,22 @@ HTTPServer.prototype._initIO = function _initIO() {
return callback({ error: 'Wallet does not exist.' }); return callback({ error: 'Wallet does not exist.' });
self.logger.info('Successful wallet auth for %s.', id); self.logger.info('Successful wallet auth for %s.', id);
socket.join(id); socket.join(id);
return callback(); return callback();
}); });
}); });
socket.on('wallet leave', function(id, callback) { socket.on('wallet leave', function(id, callback) {
callback = utils.ensure(callback); if (typeof callback !== 'function')
return socket.destroy();
if (typeof id !== 'string')
return callback({ error: 'Invalid parameter.' });
socket.leave(id); socket.leave(id);
return callback(); return callback();
}); });
}); });
@ -1034,20 +1128,57 @@ HTTPServer.prototype.listen = function listen(port, host, callback) {
*/ */
function ClientSocket(server, socket) { function ClientSocket(server, socket) {
if (!(this instanceof ClientSocket))
return new ClientSocket(server, socket);
EventEmitter.call(this);
this.server = server; this.server = server;
this.socket = socket; this.socket = socket;
this.host = socket.conn.remoteAddress;
this.timeout = null; this.timeout = null;
this.auth = false;
this._init();
} }
ClientSocket.prototype.startTimeout = function startTimeout() { utils.inherits(ClientSocket, EventEmitter);
ClientSocket.prototype._init = function _init() {
var self = this; var self = this;
this.stopTimeout(); var socket = this.socket;
var emit = EventEmitter.prototype.emit;
var onevent = socket.onevent.bind(socket);
socket.onevent = function(packet) {
var result = onevent(packet);
var args = packet.data || [];
emit.apply(self, args);
return result;
};
};
ClientSocket.prototype.join = function join(id) {
this.socket.join(id);
};
ClientSocket.prototype.leave = function leave(id) {
this.socket.leave(id);
};
ClientSocket.prototype.emit = function emit() {
this.socket.emit.apply(this.socket, arguments);
};
ClientSocket.prototype.start = function start() {
var self = this;
this.stop();
this.timeout = setTimeout(function() { this.timeout = setTimeout(function() {
self.timeout = null;
self.destroy(); self.destroy();
}, 60000); }, 60000);
}; };
ClientSocket.prototype.stopTimeout = function stopTimeout() { ClientSocket.prototype.stop = function stop() {
if (this.timeout != null) { if (this.timeout != null) {
clearTimeout(this.timeout); clearTimeout(this.timeout);
this.timeout = null; this.timeout = null;
@ -1055,6 +1186,7 @@ ClientSocket.prototype.stopTimeout = function stopTimeout() {
}; };
ClientSocket.prototype.destroy = function() { ClientSocket.prototype.destroy = function() {
this.stop();
this.socket.disconnect(); this.socket.disconnect();
}; };
@ -1070,6 +1202,17 @@ function hash256(data) {
return utils.hash256(new Buffer(data, 'utf8')); return utils.hash256(new Buffer(data, 'utf8'));
} }
function softMerge(a, b, soft) {
var keys = Object.keys(b);
var i, key, value;
for (i = 0; i < keys.length; i++) {
key = keys[i];
value = b[key];
if (!soft || value)
a[key] = value;
}
}
/* /*
* Expose * Expose
*/ */

View File

@ -212,19 +212,8 @@ SPVNode.prototype._open = function open(callback) {
}); });
}, },
function(next) { function(next) {
var i; // Rebroadcast pending transactions.
self.wallet.getUnconfirmed(function(err, txs) { self.wallet.resend(next);
if (err)
return next(err);
if (txs.length > 0)
self.logger.info('Rebroadcasting %d transactions.', txs.length);
for (i = 0; i < txs.length; i++)
self.pool.broadcast(txs[i]);
next();
});
}, },
function(next) { function(next) {
if (!self.http) if (!self.http)

View File

@ -915,11 +915,34 @@ Wallet.prototype.send = function send(options, callback) {
}, true); }, true);
}; };
/**
* Resend pending wallet transactions.
* @param {Function} callback
*/
Wallet.prototype.resend = function resend(callback) {
var self = this;
var i;
this.getUnconfirmed(function(err, txs) {
if (err)
return callback(err);
if (txs.length > 0)
self.logger.info('Rebroadcasting %d transactions.', txs.length);
for (i = 0; i < txs.length; i++)
self.db.emit('send', txs[i]);
return callback();
});
};
/** /**
* Derive necessary addresses for signing a transaction. * Derive necessary addresses for signing a transaction.
* @param {TX|Input} tx * @param {TX|Input} tx
* @param {Number?} index - Input index. * @param {Number?} index - Input index.
* @returns {KeyRing[]} * @param {Function} callback - Returns [Error, {@link KeyRing}[]].
*/ */
Wallet.prototype.deriveInputs = function deriveInputs(tx, callback) { Wallet.prototype.deriveInputs = function deriveInputs(tx, callback) {