From d6bef43d71d82f7ec16cba317e94c29753d54cb7 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Sun, 21 Feb 2016 20:20:04 -0800 Subject: [PATCH] add http server. --- bin/bcoin-get | 25 +++ lib/bcoin.js | 3 + lib/bcoin/blockdb.js | 2 +- lib/bcoin/chain.js | 2 +- lib/bcoin/chaindb.js | 28 ++- lib/bcoin/http.js | 467 +++++++++++++++++++++++++++++++++++++++++++ lib/bcoin/lru.js | 18 +- lib/bcoin/node.js | 8 +- 8 files changed, 543 insertions(+), 10 deletions(-) create mode 100755 bin/bcoin-get create mode 100644 lib/bcoin/http.js diff --git a/bin/bcoin-get b/bin/bcoin-get new file mode 100755 index 00000000..c1f0abb2 --- /dev/null +++ b/bin/bcoin-get @@ -0,0 +1,25 @@ +#!/bin/bash + +_utxo() { + if test "$1" = 'address'; then + curl -s "http://localhost:8080/utxo/address/$2" + else + curl -s "http://localhost:8080/utxo/$1/$2" + fi +} + +_tx() { + if test "$1" = 'address'; then + curl -s "http://localhost:8080/tx/address/$2" + else + curl -s "http://localhost:8080/tx/$1" + fi +} + +_block() { + curl -s "http://localhost:8080/block/$1" +} + +cmd=$1 +shift +"_${cmd}" "$@" diff --git a/lib/bcoin.js b/lib/bcoin.js index c5c58f86..e2426e9b 100644 --- a/lib/bcoin.js +++ b/lib/bcoin.js @@ -79,5 +79,8 @@ bcoin.peer = require('./bcoin/peer'); bcoin.pool = require('./bcoin/pool'); bcoin.hd = require('./bcoin/hd'); bcoin.miner = require('./bcoin/miner'); +bcoin.http = !bcoin.isBrowser + ? require('./bcoin/ht' + 'tp') + : null; bcoin.protocol.network.set(process.env.BCOIN_NETWORK || 'main'); diff --git a/lib/bcoin/blockdb.js b/lib/bcoin/blockdb.js index aaf9cda2..48a8111e 100644 --- a/lib/bcoin/blockdb.js +++ b/lib/bcoin/blockdb.js @@ -1079,7 +1079,7 @@ BlockDB.prototype._getEntry = function _getEntry(height, callback) { if (!bcoin.chain.global) return callback(); - return bcoin.chain.global.db.get(height, callback); + return bcoin.chain.global.db.getAsync(height, callback); }; /** diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index 190aa639..482547a6 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -386,7 +386,7 @@ Chain.prototype._preload = function _preload(callback) { // Filthy hack to avoid writing // redundant blocks to disk! if (height <= chainHeight) { - self.db._cache(entry); + self.db._cache(entry, true); self.db._populate(entry); } else { self.db.saveAsync(entry); diff --git a/lib/bcoin/chaindb.js b/lib/bcoin/chaindb.js index cf2e85b2..b92021c3 100644 --- a/lib/bcoin/chaindb.js +++ b/lib/bcoin/chaindb.js @@ -46,6 +46,7 @@ function ChainDB(chain, options) { this.height = -1; this.size = 0; this.fd = null; + this.loading = false; // Need to cache up to the retarget interval // if we're going to be checking the damn @@ -115,17 +116,29 @@ ChainDB.prototype.load = function load(start, callback) { var i = start || 0; var lastEntry; + this.loading = true; + utils.debug('Starting chain load at height: %s', i); + function finish(err) { + self.loading = false; + self.emit('load'); + + if (err) + return callback(err); + + callback(); + } + function done(height) { if (height != null) { utils.debug( 'Blockchain is corrupt after height %d. Resetting.', height); - return self.resetHeightAsync(height, callback); + return self.resetHeightAsync(height, finish); } utils.debug('Chain successfully loaded.'); - callback(); + finish(); } (function next() { @@ -136,6 +149,9 @@ ChainDB.prototype.load = function load(start, callback) { if (err) return callback(err); + // Force caching + self._cache(entry, true); + // Do some paranoid checks. if (lastEntry && entry.prevBlock !== lastEntry.hash) return done(Math.max(0, i - 2)); @@ -233,12 +249,16 @@ ChainDB.prototype.getSize = function getSize() { return len; }; -ChainDB.prototype._cache = function _cache(entry) { +ChainDB.prototype._cache = function _cache(entry, force) { + // if (!force && this.loading) + // return; + if (entry.height > this.highest) { this.highest = entry.height; delete this.cache[entry.height - this._cacheWindow]; this.cache[entry.height] = entry; - assert(Object.keys(this.cache).length <= this._cacheWindow); + if (!this.loading) + assert(Object.keys(this.cache).length <= this._cacheWindow); } }; diff --git a/lib/bcoin/http.js b/lib/bcoin/http.js new file mode 100644 index 00000000..76c73c84 --- /dev/null +++ b/lib/bcoin/http.js @@ -0,0 +1,467 @@ +/** + * http.js - http server for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * https://github.com/indutny/bcoin + */ + +var EventEmitter = require('events').EventEmitter; +var StringDecoder = require('string_decoder').StringDecoder; +var url = require('url'); + +var bcoin = require('../bcoin'); +var bn = require('bn.js'); +var constants = bcoin.protocol.constants; +var network = bcoin.protocol.network; +var utils = bcoin.utils; +var assert = utils.assert; + +/** + * HTTPServer + */ + +function HTTPServer(node, options) { + var self = this; + + if (!options) + options = {}; + + this.options = options; + this.node = node; + + this.routes = { + get: [], + post: [], + put: [], + del: [] + }; + + this.server = options.key + ? require('https').createServer(options) + : require('http').createServer(); + + this._init(); +} + +utils.inherits(HTTPServer, EventEmitter); + +HTTPServer.prototype._init = function _init() { + var self = this; + + this._initRouter(); + + this.server.on('connection', function(socket) { + socket.on('error', function(err) { + try { + socket.destroy(); + } catch (e) { + ; + } + }); + }); + + this.get('/', function(req, res, next, send) { + send(200, { version: require('../../package.json').version }); + }); + + // UTXO by address + this.get('/utxo/address/:address', function(req, res, next, send) { + var addresses = req.params.address.split(','); + self.node.getCoinByAddress(addresses, function(err, coins) { + if (err) + return next(err); + if (!coins.length) + return send(404); + send(200, coins.map(function(coin) { return coin.toJSON(); })); + }); + }); + + // UTXO by id + this.get('/utxo/:hash/:index', function(req, res, next, send) { + self.node.getCoin(req.params.hash, +req.params.index, function(err, coin) { + if (err) + return next(err); + if (!coin) + return send(404); + send(200, coin.toJSON()); + }); + }); + + // Bulk read UTXOs + this.post('/utxo/address', function(req, res, next, send) { + self.node.getCoinByAddress(req.body.addresses, function(err, coins) { + if (err) + return next(err); + if (!coins.length) + return send(404); + send(200, coins.map(function(coin) { return coin.toJSON(); })); + }); + }); + + // TX by hash + this.get('/tx/:hash', function(req, res, next, send) { + self.node.getTX(req.params.hash, function(err, tx) { + if (err) + return next(err); + if (!tx) + return send(404); + send(200, tx.toJSON()); + }); + }); + + // TX by address + this.get('/tx/address/:address', function(req, res, next, send) { + var addresses = req.params.address.split(','); + self.node.getTXByAddress(addresses, function(err, txs) { + if (err) + return next(err); + if (!txs.length) + return send(404); + send(200, txs.map(function(tx) { return tx.toJSON(); })); + }); + }); + + // Bulk read TXs + this.post('/tx/address', function(req, res, next, send) { + self.node.getTXByAddress(req.params.addresses, function(err, txs) { + if (err) + return next(err); + if (!txs.length) + return send(404); + send(200, txs.map(function(tx) { return tx.toJSON(); })); + }); + }); + + // Block by hash/height + this.get('/block/:hash', function(req, res, next, send) { + self.node.getBlock(req.params.hash, function(err, block) { + if (err) + return next(err); + if (!block) + return send(404); + send(200, block.toJSON()); + }); + }); + + // Get wallet + this.get('/wallet/:id', function(req, res, next, send) { + self.node.walletdb.getJSON(req.params.id, function(err, json) { + if (err) + return next(err); + if (!json) + return send(404); + send(200, json); + }); + }); + + // Create/get wallet + this.post('/wallet/:id', function(req, res, next, send) { + req.body.id = req.params.id; + self.node.walletdb.create(req.body, function(err, json) { + if (err) + return next(err); + if (!json) + return send(404); + send(json); + }); + }); + + // Update wallet / sync address depth + this.put('/wallet/:id', function(req, res, next, send) { + req.body.id = req.params.id; + self.node.walletdb.save(req.body.id, req.body, function(err, json) { + if (err) + return next(err); + if (!json) + return send(404); + send(json); + }); + }); + + // Wallet UTXOs + this.get('/wallet/:id/utxo', function(req, res, next) { + }); + + // Wallet TXs + this.get('/wallet/:id/tx', function(req, res, next) { + }); +}; + +HTTPServer.prototype.listen = function listen(port, host, callback) { + var self = this; + this.server.listen(port, host, function(err) { + var address; + + if (err) + throw err; + + address = self.server.address(); + + utils.debug('Listening - host=%s port=%d', + address.address, address.port); + + if (callback) + callback(); + }); +}; + +HTTPServer.prototype._initRouter = function _initRouter() { + var self = this; + + this.server.on('request', function(req, res) { + function _send(code, msg) { + send(res, code, msg); + } + + function done(err) { + if (err) { + send(res, 400, { error: err.stack + '' }); + try { + req.destroy(); + req.socket.destroy(); + } catch (e) { + ; + } + } + } + + try { + parsePath(req); + } catch (e) { + done(e); + } + + utils.debug('Request from %s path=%s', + req.socket.remoteAddress, req.pathname); + + parseBody(req, function(err) { + var method, routes, i; + + if (err) + return done(err); + + method = (req.method || 'GET').toLowerCase(); + routes = self.routes[method]; + i = 0; + + if (!routes) + return done(new Error('No routes found.')); + + (function next() { + utils.nextTick(function() { + var route, path, callback, compiled, matched; + + if (i === routes.length) + return done(new Error('Route not found.')); + + route = routes[i++]; + path = route.path; + callback = route.callback; + + if (!route.regex) { + compiled = compilePath(path); + route.regex = compiled.regex; + route.map = compiled.map; + } + + matched = route.regex.exec(req.pathname); + + if (!matched) + return next(); + + req.params = {}; + matched.slice(1).forEach(function(item, i) { + if (route.map[i]) + req.params[route.map[i]] = item; + req.params[i] = item; + }); + + try { + callback(req, res, next, _send); + } catch (e) { + done(e); + } + }); + })(); + }); + }); +}; + +HTTPServer.prototype.get = function get(path, callback) { + this.routes.get.push({ path: path, callback: callback }); +}; + +HTTPServer.prototype.post = function post(path, callback) { + this.routes.post.push({ path: path, callback: callback }); +}; + +HTTPServer.prototype.put = function put(path, callback) { + this.routes.put.push({ path: path, callback: callback }); +}; + +HTTPServer.prototype.del = function del(path, callback) { + this.routes.del.push({ path: path, callback: callback }); +}; + +/** + * Helpers + */ + +function send(res, code, msg) { + if (!msg) + msg = { error: 'No message.' }; + + try { + res.statusCode = code; + msg = JSON.stringify(msg, null, 2) + '\n'; + res.setHeader('Content-Type', 'application/json; charset=utf-8'); + res.setHeader('Content-Length', Buffer.byteLength(msg) + ''); + res.write(msg); + res.end(); + } catch (e) { + utils.debug('Write failed: %s', e.message); + } +} + +function compilePath(path) { + var map = []; + + if (path instanceof RegExp) + return { regex: path, map: map }; + + var regex = path + .replace(/([^\/]+)\?/g, '(?:$1)?') + .replace(/\.(?!\+)/g, '\\.') + .replace(/\*/g, '.*?') + .replace(/%/g, '\\') + .replace(/:(\w+)/g, function(__, name) { + map.push(name); + return '([^/]+)'; + } + ); + + regex = new RegExp('^' + regex + '$'); + + return { + map: map, + regex: regex + }; +} + +function parseBody(req, callback) { + var decode = new StringDecoder('utf8'); + var total = 0; + var body = ''; + + req.body = {}; + + if (req.method === 'GET') + return callback(); + + req.on('data', function(data) { + total += data.length; + + if (total > 20 * 1024 * 1024) + return callback(new Error('Overflow.')); + + body += decode.write(data); + }); + + req.on('error', function(err) { + try { + req.destroy(); + req.socket.destroy(); + } catch (e) { + ; + } + callback(err); + }); + + req.on('end', function() { + try { + if (body) + req.body = JSON.parse(body); + } catch (e) { + return callback(e); + } + callback(); + }); +} + +function parsePairs(str, del, eq) { + var out, s, i, parts; + + if (!str) + return {}; + + if (!del) + del = '&'; + + if (!eq) + eq = '='; + + out = {}; + s = str.split(del); + + for (i = 0; i < s.length; i++) { + parts = s[i].split(eq); + if (parts[0]) { + parts[0] = unescape(parts[0]); + parts[1] = parts[1] ? unescape(parts[1]) : ''; + out[parts[0]] = parts[1]; + } + } + + return out; +} + +function parsePath(req) { + var uri = url.parse(req.url); + var pathname = uri.pathname || '/'; + + if (pathname[pathname.length - 1] === '/') + pathname = pathname.slice(0, -1); + + pathname = unescape(pathname); + + req.path = pathname; + + if (req.path[0] === '/') + req.path = req.path.substring(1); + + req.path = req.path.split('/'); + + if (!req.path[0]) + req.path = []; + + req.pathname = pathname || '/'; + + if (req.url.indexOf('//') !== -1) { + req.url = req.url.replace(/^([^:\/]+)?\/\/[^\/]+/, ''); + if (!req.url) + req.url = '/'; + } + + if (!req.query) { + req.query = uri.query + ? parsePairs(uri.query, '&') + : {}; + } +} + +function escape(str) { + return encodeURIComponent(str).replace(/%20/g, '+'); +} + +function unescape(str) { + try { + str = decodeURIComponent(str).replace(/\+/g, ' '); + } finally { + return str.replace(/\0/g, ''); + } +} + +/** + * Expose + */ + +module.exports = HTTPServer; diff --git a/lib/bcoin/lru.js b/lib/bcoin/lru.js index dd37ec52..512ef722 100644 --- a/lib/bcoin/lru.js +++ b/lib/bcoin/lru.js @@ -82,7 +82,11 @@ LRU.prototype._compact = function _compact() { }; LRU.prototype.set = function set(key, value) { - var item = this.data[key]; + var item; + + key = key + ''; + + item = this.data[key]; if (item) { this.size -= this._getSize(item); @@ -106,7 +110,11 @@ LRU.prototype.set = function set(key, value) { }; LRU.prototype.get = function get(key) { - var item = this.data[key]; + var item; + + key = key + ''; + + item = this.data[key]; if (!item) return; @@ -122,7 +130,11 @@ LRU.prototype.has = function get(key) { }; LRU.prototype.remove = function remove(key) { - var item = this.data[key]; + var item; + + key = key + ''; + + item = this.data[key]; if (!item) return false; diff --git a/lib/bcoin/node.js b/lib/bcoin/node.js index 427fc1c0..4cf68c31 100644 --- a/lib/bcoin/node.js +++ b/lib/bcoin/node.js @@ -75,6 +75,12 @@ Node.prototype._init = function _init() { this.walletdb = new bcoin.walletdb(this.options.walletdb); + this.options.http = {}; + if (this.options.http && bcoin.http) { + this.http = new bcoin.http(this, this.options.http); + this.http.listen(this.options.http.port || 8080); + } + this.mempool.on('error', function(err) { self.emit('error', err); }); @@ -157,7 +163,7 @@ Node.prototype.getCoin = function getCoin(hash, index, callback) { }); }; -Node.prototype.getCoinByAddress = function getCoinsByAddress(addresses, callback) { +Node.prototype.getCoinByAddress = function getCoinByAddress(addresses, callback) { var self = this; var mempool;