fcoin/lib/http/server.js
2017-03-14 06:10:34 -07:00

1080 lines
24 KiB
JavaScript

/*!
* server.js - http server for bcoin
* Copyright (c) 2014-2015, Fedor Indutny (MIT License)
* Copyright (c) 2014-2017, Christopher Jeffrey (MIT License).
* https://github.com/bcoin-org/bcoin
*/
'use strict';
var assert = require('assert');
var EventEmitter = require('events').EventEmitter;
var HTTPBase = require('./base');
var util = require('../utils/util');
var co = require('../utils/co');
var base58 = require('../utils/base58');
var Amount = require('../btc/amount');
var Bloom = require('../utils/bloom');
var TX = require('../primitives/tx');
var Outpoint = require('../primitives/outpoint');
var crypto = require('../crypto/crypto');
var Network = require('../protocol/network');
var Validator = require('../utils/validator');
var pkg = require('../pkg');
var RPC = require('./rpc');
/**
* HTTPServer
* @alias module:http.Server
* @constructor
* @param {Object} options
* @param {Fullnode} options.node
* @see HTTPBase
* @emits HTTPServer#websocket
*/
function HTTPServer(options) {
if (!(this instanceof HTTPServer))
return new HTTPServer(options);
options = new HTTPOptions(options);
HTTPBase.call(this, options);
this.options = options;
this.network = this.options.network;
this.logger = this.options.logger;
this.node = this.options.node;
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.rpc = new RPC(this.node);
this.init();
}
util.inherits(HTTPServer, HTTPBase);
/**
* Initialize routes.
* @private
*/
HTTPServer.prototype.init = function init() {
var self = this;
this.on('request', function(req, res) {
if (req.method === 'POST' && req.pathname === '/')
return;
self.logger.debug('Request for method=%s path=%s (%s).',
req.method, req.pathname, req.socket.remoteAddress);
});
this.on('listening', function(address) {
self.logger.info('Node HTTP server listening on %s (port=%d).',
address.address, address.port);
});
this.initRouter();
this.initSockets();
};
/**
* Initialize routes.
* @private
*/
HTTPServer.prototype.initRouter = function initRouter() {
this.use(this.cors());
if (!this.options.noAuth) {
this.use(this.basicAuth({
username: 'bitcoinrpc',
password: this.options.apiKey,
realm: 'node'
}));
}
this.use(this.bodyParser({
contentType: 'json'
}));
// JSON RPC
this.post('/', co(function* (req, res) {
var json = yield this.rpc.call(req.body, req.query);
json = JSON.stringify(json);
json += '\n';
res.setHeader('X-Long-Polling', '/?longpoll=1');
res.send(200, json, 'json');
}));
this.get('/', co(function* (req, res) {
var totalTX = this.mempool ? this.mempool.totalTX : 0;
var size = this.mempool ? this.mempool.getSize() : 0;
var addr = this.pool.hosts.getLocal();
if (!addr)
addr = this.pool.hosts.address;
res.send(200, {
version: pkg.version,
network: this.network.type,
chain: {
height: this.chain.height,
tip: this.chain.tip.rhash(),
progress: this.chain.getProgress()
},
pool: {
host: addr.host,
port: addr.port,
agent: this.pool.options.agent,
services: this.pool.options.services.toString(2),
outbound: this.pool.peers.outbound,
inbound: this.pool.peers.inbound
},
mempool: {
tx: totalTX,
size: size
},
time: {
uptime: this.node.uptime(),
system: util.now(),
adjusted: this.network.now(),
offset: this.network.time.offset
},
memory: util.memoryUsage()
});
}));
// UTXO by address
this.get('/coin/address/:address', co(function* (req, res) {
var valid = req.valid();
var address = valid.str('address');
var result = [];
var i, coins, coin;
enforce(address, 'Address is required.');
enforce(!this.chain.options.spv, 'Cannot get coins in SPV mode.');
coins = yield this.node.getCoinsByAddress(address);
for (i = 0; i < coins.length; i++) {
coin = coins[i];
result.push(coin.getJSON(this.network));
}
res.send(200, result);
}));
// UTXO by id
this.get('/coin/:hash/:index', co(function* (req, res) {
var valid = req.valid();
var hash = valid.hash('hash');
var index = valid.num('index');
var coin;
enforce(hash, 'Hash is required.');
enforce(index != null, 'Index is required.');
enforce(!this.chain.options.spv, 'Cannot get coins in SPV mode.');
coin = yield this.node.getCoin(hash, index);
if (!coin) {
res.send(404);
return;
}
res.send(200, coin.getJSON(this.network));
}));
// Bulk read UTXOs
this.post('/coin/address', co(function* (req, res) {
var valid = req.valid();
var address = valid.array('addresses');
var result = [];
var i, coins, coin;
enforce(address, 'Address is required.');
enforce(!this.chain.options.spv, 'Cannot get coins in SPV mode.');
coins = yield this.node.getCoinsByAddress(address);
for (i = 0; i < coins.length; i++) {
coin = coins[i];
result.push(coin.getJSON(this.network));
}
res.send(200, result);
}));
// TX by hash
this.get('/tx/:hash', co(function* (req, res) {
var valid = req.valid();
var hash = valid.hash('hash');
var meta, view;
enforce(hash, 'Hash is required.');
enforce(!this.chain.options.spv, 'Cannot get TX in SPV mode.');
meta = yield this.node.getMeta(hash);
if (!meta) {
res.send(404);
return;
}
view = yield this.node.getMetaView(meta);
res.send(200, meta.getJSON(this.network, view));
}));
// TX by address
this.get('/tx/address/:address', co(function* (req, res) {
var valid = req.valid();
var address = valid.str('address');
var result = [];
var i, metas, meta, view;
enforce(address, 'Address is required.');
enforce(!this.chain.options.spv, 'Cannot get TX in SPV mode.');
metas = yield this.node.getMetaByAddress(address);
for (i = 0; i < metas.length; i++) {
meta = metas[i];
view = yield this.node.getMetaView(meta);
result.push(meta.getJSON(this.network, view));
}
res.send(200, result);
}));
// Bulk read TXs
this.post('/tx/address', co(function* (req, res) {
var valid = req.valid();
var address = valid.array('address');
var result = [];
var i, metas, meta, view;
enforce(req.options.address, 'Address is required.');
enforce(!this.chain.options.spv, 'Cannot get TX in SPV mode.');
metas = yield this.node.getMetaByAddress(address);
for (i = 0; i < metas.length; i++) {
meta = metas[i];
view = yield this.node.getMetaView(meta);
result.push(meta.getJSON(this.network, view));
}
res.send(200, result);
}));
// Block by hash/height
this.get('/block/:block', co(function* (req, res) {
var valid = req.valid();
var hash = valid.get('block');
var block, view, height;
enforce(typeof hash === 'number' || typeof hash === 'string',
'Hash or height required.');
enforce(!this.chain.options.spv, 'Cannot get block in SPV mode.');
block = yield this.chain.db.getBlock(hash);
if (!block) {
res.send(404);
return;
}
view = yield this.chain.db.getBlockView(block);
if (!view) {
res.send(404);
return;
}
height = yield this.chain.db.getHeight(hash);
res.send(200, block.getJSON(this.network, view, height));
}));
// Mempool snapshot
this.get('/mempool', co(function* (req, res) {
var result = [];
var i, hash, hashes;
enforce(this.mempool, 'No mempool available.');
hashes = this.mempool.getSnapshot();
for (i = 0; i < hashes.length; i++) {
hash = hashes[i];
result.push(util.revHex(hash));
}
res.send(200, result);
}));
// Broadcast TX
this.post('/broadcast', co(function* (req, res) {
var valid = req.valid();
var tx = valid.buf('tx');
enforce(tx, 'TX is required.');
yield this.node.sendTX(req.options.tx);
res.send(200, { success: true });
}));
// Estimate fee
this.get('/fee', function(req, res) {
var valid = req.valid();
var blocks = valid.num('blocks');
var fee;
if (!this.fees) {
res.send(200, { rate: Amount.btc(this.network.feeRate) });
return;
}
fee = this.fees.estimateFee(blocks);
res.send(200, { rate: Amount.btc(fee) });
});
// Reset chain
this.post('/reset', co(function* (req, res) {
var valid = req.valid();
var height = valid.num('height');
enforce(height != null, 'Hash or height is required.');
yield this.chain.reset(height);
res.send(200, { success: true });
}));
};
/**
* Initialize websockets.
* @private
*/
HTTPServer.prototype.initSockets = function initSockets() {
var self = this;
if (!this.io)
return;
this.on('socket', function(ws) {
self.handleSocket(ws);
});
};
/**
* Handle new websocket.
* @private
* @param {WebSocket} socket
*/
HTTPServer.prototype.handleSocket = function handleSocket(ws) {
var self = this;
var socket = new ClientSocket(this, ws);
socket.start();
socket.on('disconnect', function() {
socket.destroy();
});
socket.hook('auth', function(args) {
var valid = new Validator([args]);
var key = valid.str(0);
var hash;
if (socket.auth)
throw new Error('Already authed.');
socket.stop();
if (!self.options.noAuth) {
hash = hash256(key);
if (!crypto.ccmp(hash, self.options.apiHash))
throw new Error('Bad key.');
}
socket.auth = true;
self.logger.info('Successful auth from %s.', socket.host);
self.handleAuth(socket);
return null;
});
socket.emit('version', {
version: pkg.version,
network: self.network.type
});
};
/**
* Handle new auth'd websocket.
* @private
* @param {WebSocket} socket
*/
HTTPServer.prototype.handleAuth = function handleAuth(socket) {
var self = this;
socket.hook('options', function (args) {
var options = args[0];
socket.setOptions(options);
});
socket.hook('watch chain', function(args) {
if (!socket.auth)
throw new Error('Not authorized.');
socket.watchChain();
});
socket.hook('unwatch chain', function(args) {
if (!socket.auth)
throw new Error('Not authorized.');
socket.unwatchChain();
});
socket.hook('set filter', function(args) {
var data = args[0];
var filter;
if (!util.isHex(data) && !Buffer.isBuffer(data))
throw new Error('Invalid parameter.');
if (!socket.auth)
throw new Error('Not authorized.');
filter = Bloom.fromRaw(data, 'hex');
socket.setFilter(filter);
});
socket.hook('get tip', function(args) {
return socket.frameEntry(self.chain.tip);
});
socket.hook('get entry', co(function* (args) {
var block = args[0];
var entry;
if (typeof block === 'string') {
if (!util.isHex256(block))
throw new Error('Invalid parameter.');
block = util.revHex(block);
} else {
if (!util.isUInt32(block))
throw new Error('Invalid parameter.');
}
entry = yield self.chain.db.getEntry(block);
if (!(yield entry.isMainChain()))
entry = null;
if (!entry)
return null;
return socket.frameEntry(entry);
}));
socket.hook('add filter', function(args) {
var chunks = args[0];
if (!Array.isArray(chunks))
throw new Error('Invalid parameter.');
if (!socket.auth)
throw new Error('Not authorized.');
socket.addFilter(chunks);
});
socket.hook('reset filter', function(args) {
if (!socket.auth)
throw new Error('Not authorized.');
socket.resetFilter();
});
socket.hook('estimate fee', function(args) {
var blocks = args[0];
var rate;
if (blocks != null && !util.isNumber(blocks))
throw new Error('Invalid parameter.');
if (!self.fees) {
rate = self.network.feeRate;
rate = Amount.btc(rate);
return rate;
}
rate = self.fees.estimateFee(blocks);
rate = Amount.btc(rate);
return rate;
});
socket.hook('send', function(args) {
var data = args[0];
var tx;
if (!util.isHex(data) && !Buffer.isBuffer(data))
throw new Error('Invalid parameter.');
tx = TX.fromRaw(data, 'hex');
self.node.send(tx);
});
socket.hook('rescan', function(args) {
var start = args[0];
if (!util.isHex256(start) && !util.isUInt32(start))
throw new Error('Invalid parameter.');
if (!socket.auth)
throw new Error('Not authorized.');
if (typeof start === 'string')
start = util.revHex(start);
return socket.scan(start);
});
};
/**
* HTTPOptions
* @alias module:http.HTTPOptions
* @constructor
* @param {Object} options
*/
function HTTPOptions(options) {
if (!(this instanceof HTTPOptions))
return new HTTPOptions(options);
this.network = Network.primary;
this.logger = null;
this.node = null;
this.apiKey = base58.encode(crypto.randomBytes(20));
this.apiHash = hash256(this.apiKey);
this.noAuth = false;
this.walletAuth = false;
this.prefix = null;
this.host = '127.0.0.1';
this.port = 8080;
this.ssl = false;
this.keyFile = null;
this.certFile = null;
this.fromOptions(options);
}
/**
* Inject properties from object.
* @private
* @param {Object} options
* @returns {HTTPOptions}
*/
HTTPOptions.prototype.fromOptions = function fromOptions(options) {
assert(options);
assert(options.node && typeof options.node === 'object',
'HTTP Server requires a Node.');
this.node = options.node;
this.network = options.node.network;
this.logger = options.node.logger;
this.port = this.network.rpcPort;
if (options.logger != null) {
assert(typeof options.logger === 'object');
this.logger = options.logger;
}
if (options.apiKey != null) {
assert(typeof options.apiKey === 'string',
'API key must be a string.');
assert(options.apiKey.length <= 200,
'API key must be under 200 bytes.');
this.apiKey = options.apiKey;
this.apiHash = hash256(this.apiKey);
}
if (options.noAuth != null) {
assert(typeof options.noAuth === 'boolean');
this.noAuth = options.noAuth;
}
if (options.walletAuth != null) {
assert(typeof options.walletAuth === 'boolean');
this.walletAuth = options.walletAuth;
}
if (options.prefix != null) {
assert(typeof options.prefix === 'string');
this.prefix = options.prefix;
this.keyFile = this.prefix + '/key.pem';
this.certFile = this.prefix + '/cert.pem';
}
if (options.host != null) {
assert(typeof options.host === 'string');
this.host = options.host;
}
if (options.port != null) {
assert(typeof options.port === 'number', 'Port must be a number.');
assert(options.port > 0 && options.port <= 0xffff);
this.port = options.port;
}
if (options.ssl != null) {
assert(typeof options.ssl === 'boolean');
this.ssl = options.ssl;
}
if (options.keyFile != null) {
assert(typeof options.keyFile === 'string');
this.keyFile = options.keyFile;
}
if (options.certFile != null) {
assert(typeof options.certFile === 'string');
this.certFile = options.certFile;
}
// Allow no-auth implicitly
// if we're listening locally.
if (!options.apiKey) {
if (this.host === '127.0.0.1' || this.host === '::1')
this.noAuth = true;
}
return this;
};
/**
* Instantiate http options from object.
* @param {Object} options
* @returns {HTTPOptions}
*/
HTTPOptions.fromOptions = function fromOptions(options) {
return new HTTPOptions().fromOptions(options);
};
/**
* ClientSocket
* @constructor
* @ignore
* @param {HTTPServer} server
* @param {SocketIO.Socket}
*/
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.remoteAddress;
this.timeout = null;
this.auth = false;
this.filter = null;
this.raw = false;
this.watching = false;
this.network = this.server.network;
this.node = this.server.node;
this.chain = this.server.chain;
this.mempool = this.server.mempool;
this.pool = this.server.pool;
this.logger = this.server.logger;
this.events = [];
this.init();
}
util.inherits(ClientSocket, EventEmitter);
ClientSocket.prototype.init = function init() {
var self = this;
var socket = this.socket;
var emit = EventEmitter.prototype.emit;
socket.on('error', function(err) {
emit.call(self, 'error', err);
});
socket.on('disconnect', function() {
emit.call(self, 'disconnect');
});
};
ClientSocket.prototype.setOptions = function setOptions(options) {
assert(options && typeof options === 'object', 'Invalid parameter.');
if (options.raw != null) {
assert(typeof options.raw === 'boolean', 'Invalid parameter.');
this.raw = options.raw;
}
};
ClientSocket.prototype.setFilter = function setFilter(filter) {
this.filter = filter;
};
ClientSocket.prototype.addFilter = function addFilter(chunks) {
var i, data;
if (!this.filter)
throw new Error('No filter set.');
for (i = 0; i < chunks.length; i++) {
data = chunks[i];
if (!util.isHex(data) && !Buffer.isBuffer(data))
throw new Error('Not a hex string.');
this.filter.add(data, 'hex');
if (this.pool.options.spv)
this.pool.watch(data, 'hex');
}
};
ClientSocket.prototype.resetFilter = function resetFilter() {
if (!this.filter)
throw new Error('No filter set.');
this.filter.reset();
};
ClientSocket.prototype.bind = function bind(obj, event, listener) {
this.events.push([obj, event, listener]);
obj.on(event, listener);
};
ClientSocket.prototype.unbind = function unbind(obj, event) {
var i, item;
for (i = this.events.length - 1; i >= 0; i--) {
item = this.events[i];
if (item[0] === obj && item[1] === event) {
obj.removeListener(event, item[2]);
this.events.splice(i, 1);
}
}
};
ClientSocket.prototype.unbindAll = function unbindAll() {
var i, event;
for (i = 0; i < this.events.length; i++) {
event = this.events[i];
event[0].removeListener(event[1], event[2]);
}
this.events.length = 0;
};
ClientSocket.prototype.watchChain = function watchChain() {
var self = this;
var pool = this.mempool || this.pool;
if (this.watching)
throw new Error('Already watching chain.');
this.watching = true;
this.bind(this.chain, 'connect', function(entry, block, view) {
self.connectBlock(entry, block, view);
});
this.bind(this.chain, 'disconnect', function(entry, block, view) {
self.disconnectBlock(entry, block, view);
});
this.bind(this.chain, 'reset', function(tip) {
self.emit('chain reset', self.frameEntry(tip));
});
this.bind(pool, 'tx', function(tx) {
self.sendTX(tx);
});
};
ClientSocket.prototype.onError = function onError(err) {
var emit = EventEmitter.prototype.emit;
emit.call(this, 'error', err);
};
ClientSocket.prototype.unwatchChain = function unwatchChain() {
var pool = this.mempool || this.pool;
if (!this.watching)
throw new Error('Not watching chain.');
this.watching = false;
this.unbind(this.chain, 'connect');
this.unbind(this.chain, 'disconnect');
this.unbind(this.chain, 'reset');
this.unbind(pool, 'tx');
};
ClientSocket.prototype.connectBlock = function connectBlock(entry, block, view) {
var raw = this.frameEntry(entry);
var txs;
this.emit('entry connect', raw);
if (!this.filter)
return;
txs = this.filterBlock(entry, block, view);
this.emit('block connect', raw, txs);
};
ClientSocket.prototype.disconnectBlock = function disconnectBlock(entry, block, view) {
var raw = this.frameEntry(entry);
this.emit('entry disconnect', raw);
if (!this.filter)
return;
this.emit('block disconnect', raw);
};
ClientSocket.prototype.sendTX = function sendTX(tx) {
var raw;
if (!this.filterTX(tx))
return;
raw = this.frameTX(tx);
this.emit('tx', raw);
};
ClientSocket.prototype.rescanBlock = function rescanBlock(entry, txs) {
var self = this;
return new Promise(function(resolve, reject) {
var cb = TimedCB.wrap(resolve, reject);
self.emit('block rescan', entry, txs, cb);
});
};
ClientSocket.prototype.filterBlock = function filterBlock(entry, block, view) {
var txs = [];
var i, tx;
if (!this.filter)
return;
for (i = 0; i < block.txs.length; i++) {
tx = block.txs[i];
if (this.filterTX(tx))
txs.push(this.frameTX(tx, view, entry, i));
}
return txs;
};
ClientSocket.prototype.filterTX = function filterTX(tx) {
var found = false;
var i, hash, input, prevout, output;
if (!this.filter)
return false;
for (i = 0; i < tx.outputs.length; i++) {
output = tx.outputs[i];
hash = output.getHash();
if (!hash)
continue;
if (this.filter.test(hash)) {
prevout = Outpoint.fromTX(tx, i);
this.filter.add(prevout.toRaw());
found = true;
}
}
if (found)
return true;
if (!tx.isCoinbase()) {
for (i = 0; i < tx.inputs.length; i++) {
input = tx.inputs[i];
prevout = input.prevout;
if (this.filter.test(prevout.toRaw()))
return true;
}
}
return false;
};
ClientSocket.prototype.scan = co(function* scan(start) {
var scanner = this.scanner.bind(this);
yield this.node.scan(start, this.filter, scanner);
});
ClientSocket.prototype.scanner = function scanner(entry, txs) {
var block = this.frameEntry(entry);
var raw = new Array(txs.length);
var i;
for (i = 0; i < txs.length; i++)
raw[i] = this.frameTX(txs[i], null, entry, i);
return this.rescanBlock(block, raw);
};
ClientSocket.prototype.frameEntry = function frameEntry(entry) {
if (this.raw)
return entry.toRaw();
return entry.toJSON();
};
ClientSocket.prototype.frameTX = function frameTX(tx, view, entry, index) {
if (this.raw)
return tx.toRaw();
return tx.getJSON(this.network, view, entry, index);
};
ClientSocket.prototype.join = function join(id) {
this.socket.join(id);
};
ClientSocket.prototype.leave = function leave(id) {
this.socket.leave(id);
};
ClientSocket.prototype.hook = function hook(type, handler) {
this.socket.hook(type, handler);
};
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.stop = function stop() {
if (this.timeout != null) {
clearTimeout(this.timeout);
this.timeout = null;
}
};
ClientSocket.prototype.destroy = function() {
this.unbindAll();
this.stop();
this.socket.disconnect();
};
/*
* Helpers
*/
function hash256(data) {
if (typeof data !== 'string')
return new Buffer(0);
if (data.length > 200)
return new Buffer(0);
return crypto.hash256(new Buffer(data, 'utf8'));
}
function enforce(value, msg) {
var err;
if (!value) {
err = new Error(msg);
err.statusCode = 400;
throw err;
}
}
/**
* TimedCB
* @constructor
* @ignore
*/
function TimedCB(resolve, reject) {
this.resolve = resolve;
this.reject = reject;
this.done = false;
}
TimedCB.wrap = function wrap(resolve, reject) {
return new TimedCB(resolve, reject).start();
};
TimedCB.prototype.start = function start() {
var self = this;
var timeout;
timeout = setTimeout(function() {
self.cleanup(timeout);
self.reject(new Error('Callback timed out.'));
}, 5000);
return function(err) {
self.cleanup(timeout);
if (err) {
self.reject(err);
return;
}
self.resolve();
};
};
TimedCB.prototype.cleanup = function cleanup(timeout) {
assert(!this.done);
this.done = true;
clearTimeout(timeout);
};
/*
* Expose
*/
module.exports = HTTPServer;