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) {
assert(typeof data === 'string');
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);
},
function(next) {
var i;
self.wallet.getUnconfirmed(function(err, txs) {
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();
});
// Rebroadcast pending transactions.
self.wallet.resend(next);
},
function(next) {
if (!self.http)

View File

@ -287,7 +287,7 @@ HTTPClient.prototype.getInfo = function getInfo(callback) {
*/
HTTPClient.prototype.getCoinsByAddress = function getCoinsByAddress(address, callback) {
var body = { addresses: address };
var body = { address: address };
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) {
var body = { addresses: address };
var body = { address: address };
return this._post('/tx/address', body, callback);
};

View File

@ -43,9 +43,13 @@ function HTTPServer(options) {
assert(this.node, 'HTTP requires a Node.');
this.network = this.node.network;
this.walletdb = this.node.walletdb;
this.chain = this.node.chain;
this.mempool = this.node.mempool;
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.loaded = false;
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-Agent', constants.USER_AGENT);
res.setHeader('X-Bcoin-Network', self.network.type);
res.setHeader('X-Bcoin-Height', self.node.chain.height + '');
res.setHeader('X-Bcoin-Tip', utils.revHex(self.node.chain.tip.hash));
res.setHeader('X-Bcoin-Height', self.chain.height + '');
res.setHeader('X-Bcoin-Tip', utils.revHex(self.chain.tip.hash));
next();
});
@ -157,109 +161,184 @@ HTTPServer.prototype._init = function _init() {
});
this.use(function(req, res, next, send) {
var params, options;
var i, params, options, output, address;
if (req.method === 'POST'
&& req.pathname === '/') {
if (req.method === 'POST' && 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 = {};
return next();
}
params = utils.merge({}, req.params, req.query, req.body);
params = {};
options = {};
softMerge(params, req.params, true);
softMerge(params, req.query, true);
softMerge(params, req.body);
self.logger.debug('Params:');
self.logger.debug(params);
if (params.id) {
assert(params.id !== '!all');
assert(typeof params.id === 'string', 'ID must be a string.');
options.id = params.id;
}
if (params.hash) {
if (params.hash.length !== 64)
options.height = params.hash >>> 0;
else
assert(typeof params.hash === 'string', 'Hash must be a string.');
if (params.hash.length !== 64) {
options.height = Number(params.hash);
assert(utils.isUInt32(options.height), 'Hash must be a number.');
} else {
options.hash = utils.revHex(params.hash);
}
}
if (params.index != null)
options.index = params.index >>> 0;
if (params.index != null) {
options.index = Number(params.index);
assert(utils.isUInt32(options.index), 'Index must be a number.');
}
if (params.height != null)
options.height = params.height >>> 0;
if (params.height != null) {
options.height = Number(params.height);
assert(utils.isUInt32(options.height), 'Height must be a number.');
}
if (params.start != null)
options.start = params.start >>> 0;
if (params.start != null) {
options.start = Number(params.start);
assert(utils.isUInt32(options.height), 'Start must be a number.');
}
if (params.end != null)
options.end = params.end >>> 0;
if (params.end != null) {
options.end = Number(params.end);
assert(utils.isUInt32(options.end), 'End must be a number.');
}
if (params.limit != null)
options.limit = params.limit >>> 0;
if (params.limit != null) {
options.limit = Number(params.limit);
assert(utils.isUInt32(options.limit), 'Limit must be a number.');
}
if (params.address) {
params.addresses = params.address;
options.address = params.address;
if (params.age != null) {
options.age = Number(params.age);
assert(utils.isUInt32(options.age), 'Age must be a number.');
}
if (params.rate)
options.rate = utils.satoshi(params.rate);
if (params.subtractFee)
options.subtractFee = params.subtractFee;
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.m != null) {
options.m = Number(params.m);
assert(utils.isUInt32(options.m), 'm must be a number.');
}
if (params.addresses) {
if (typeof params.addresses === 'string')
options.addresses = params.addresses.split(',');
else
options.addresses = params.addresses;
if (params.n != null) {
options.n = Number(params.n);
assert(utils.isUInt32(options.n), 'n must be a number.');
}
if (params.tx) {
try {
if (typeof params.tx === 'object')
options.tx = bcoin.tx.fromJSON(params.tx);
else
options.tx = bcoin.tx.fromRaw(params.tx, 'hex');
} catch (e) {
return next(e);
if (params.blocks != null) {
options.blocks = Number(params.blocks);
assert(utils.isUInt32(options.blocks), 'Blocks must be a number.');
}
if (params.subtractFee != null) {
if (typeof params.subtractFee === 'number') {
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')
options.account = params.account || null;
else if (typeof params.account === 'number')
options.account = params.account;
if (params.outputs) {
assert(Array.isArray(params.outputs), 'Outputs must be an array.');
options.outputs = [];
for (i = 0; i < params.outputs.length; i++) {
output = params.outputs[i];
if (params.name)
options.name = params.name;
if (output.address)
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.age = params.age >>> 0;
if (params.key)
params.keys = params.key;
if (params.keys) {
if (typeof params.keys === 'string')
options.keys = params.keys.split(',');
else
options.keys = params.keys;
options.outputs.push({
address: output.address
? bcoin.address.fromBase58(output.address)
: null,
script: output.script
? bcoin.script.fromRaw(output.script, 'hex')
: null,
value: utils.satoshi(output.value)
});
}
}
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;
}
if (params.token) {
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
this.post('/', function(req, res, next, send) {
if (!(req.body.method && req.body.params))
return next(new Error('Method not found.'));
if (!self.rpc) {
RPC = require('./rpc');
self.rpc = new RPC(self.node);
@ -359,23 +427,21 @@ HTTPServer.prototype._init = function _init() {
send(200, {
version: constants.USER_VERSION,
agent: constants.USER_AGENT,
services: self.pool.services,
network: self.network.type,
height: self.node.chain.height,
tip: utils.revHex(self.node.chain.tip.hash),
peers: self.node.pool.peers.all.length,
progress: self.node.chain.getProgress()
height: self.chain.height,
tip: self.chain.tip.rhash,
peers: self.pool.peers.all.length,
progress: self.chain.getProgress()
});
});
// UTXO by address
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)
return next(err);
if (!coins.length)
return send(404);
send(200, coins.map(function(coin) {
return coin.toJSON();
}));
@ -397,13 +463,10 @@ HTTPServer.prototype._init = function _init() {
// Bulk read UTXOs
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)
return next(err);
if (!coins.length)
return send(404);
send(200, coins.map(function(coin) {
return coin.toJSON();
}));
@ -430,7 +493,7 @@ HTTPServer.prototype._init = function _init() {
// TX by address
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)
return next(err);
@ -449,13 +512,10 @@ HTTPServer.prototype._init = function _init() {
// Bulk read TXs
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)
return next(err);
if (!txs.length)
return send(404);
utils.forEachSerial(txs, function(tx, next) {
self.node.fillHistory(tx, next);
}, function(err) {
@ -485,13 +545,10 @@ HTTPServer.prototype._init = function _init() {
// Mempool snapshot
this.get('/mempool', function(req, res, next, send) {
self.node.mempool.getHistory(function(err, txs) {
self.mempool.getHistory(function(err, txs) {
if (err)
return next(err);
if (!txs.length)
return send(404);
utils.forEachSerial(txs, function(tx, next) {
self.node.fillHistory(tx, next);
}, 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
this.get('/wallet/:id', function(req, res, next, send) {
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
this.put('/wallet/:id/key', function(req, res, next, send) {
var account = req.options.account;
@ -672,7 +748,7 @@ HTTPServer.prototype._init = function _init() {
// Create address
this.post('/wallet/:id/address', function(req, res, next, send) {
var account = req.options.account;
req.wallet.createAddress(account, false, function(err, address) {
req.wallet.createReceive(account, function(err, address) {
if (err)
return next(err);
@ -832,29 +908,32 @@ HTTPServer.prototype._initIO = function _initIO() {
if (!this.server.io)
return;
this.server.on('websocket', function(socket) {
socket.bcoin = new ClientSocket(self, socket);
socket.bcoin.startTimeout();
this.server.on('websocket', function(ws) {
var socket = new ClientSocket(self, ws);
socket.start();
socket.on('error', function(err) {
self.emit('error', err);
});
socket.on('auth', function(apiKey, callback) {
callback = utils.ensure(callback);
socket.once('auth', function(apiKey, callback) {
if (typeof callback !== 'function')
return socket.destroy();
if (!self.apiHash) {
self.logger.info('Successful auth.');
socket.bcoin.stopTimeout();
self.emit('websocket', socket);
return callback();
socket.stop();
if (self.apiHash) {
if (!utils.ccmp(hash256(apiKey), self.apiHash)) {
socket.destroy();
return callback({ error: 'Bad key.' });
}
}
if (!utils.ccmp(hash256(apiKey), self.apiHash))
return callback({ error: 'Bad key.' });
socket.auth = true;
self.logger.info('Successful auth from %s.', socket.host);
self.logger.info('Successful auth.');
socket.bcoin.stopTimeout();
self.emit('websocket', socket);
return callback();
@ -869,13 +948,20 @@ HTTPServer.prototype._initIO = function _initIO() {
this.on('websocket', function(socket) {
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) {
socket.join(id);
return callback();
}
if (typeof token !== 'string')
return callback({ error: 'Invalid parameter.' });
self.walletdb.auth(id, token, function(err, wallet) {
if (err) {
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.' });
self.logger.info('Successful wallet auth for %s.', id);
socket.join(id);
return 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);
return callback();
});
});
@ -1034,20 +1128,57 @@ HTTPServer.prototype.listen = function listen(port, host, callback) {
*/
function ClientSocket(server, socket) {
if (!(this instanceof ClientSocket))
return new ClientSocket(server, socket);
EventEmitter.call(this);
this.server = server;
this.socket = socket;
this.host = socket.conn.remoteAddress;
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;
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() {
self.timeout = null;
self.destroy();
}, 60000);
};
ClientSocket.prototype.stopTimeout = function stopTimeout() {
ClientSocket.prototype.stop = function stop() {
if (this.timeout != null) {
clearTimeout(this.timeout);
this.timeout = null;
@ -1055,6 +1186,7 @@ ClientSocket.prototype.stopTimeout = function stopTimeout() {
};
ClientSocket.prototype.destroy = function() {
this.stop();
this.socket.disconnect();
};
@ -1070,6 +1202,17 @@ function hash256(data) {
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
*/

View File

@ -212,19 +212,8 @@ SPVNode.prototype._open = function open(callback) {
});
},
function(next) {
var i;
self.wallet.getUnconfirmed(function(err, txs) {
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();
});
// Rebroadcast pending transactions.
self.wallet.resend(next);
},
function(next) {
if (!self.http)

View File

@ -915,11 +915,34 @@ Wallet.prototype.send = function send(options, callback) {
}, 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.
* @param {TX|Input} tx
* @param {Number?} index - Input index.
* @returns {KeyRing[]}
* @param {Function} callback - Returns [Error, {@link KeyRing}[]].
*/
Wallet.prototype.deriveInputs = function deriveInputs(tx, callback) {