diff --git a/browser/wsproxy.js b/browser/wsproxy.js index c26d05c6..0001c2c3 100644 --- a/browser/wsproxy.js +++ b/browser/wsproxy.js @@ -1,7 +1,6 @@ 'use strict'; var net = require('net'); -var dns = require('dns'); var EventEmitter = require('events').EventEmitter; var IOServer = require('socket.io'); var util = require('../lib/utils/util'); @@ -65,47 +64,6 @@ WSProxy.prototype._handleSocket = function _handleSocket(ws) { ws.on('tcp connect', function(port, host, nonce) { self._handleConnect(ws, port, host, nonce); }); - - ws.on('dns resolve', function(name, record, callback) { - self._handleResolve(ws, name, record, callback); - }); -}; - -WSProxy.prototype._handleResolve = function _handleResolve(ws, name, record, callback) { - if (typeof name !== 'string') { - ws.disconnect(); - return; - } - - if (typeof record !== 'string') { - ws.disconnect(); - return; - } - - if (typeof callback !== 'function') { - ws.disconnect(); - return; - } - - if (record !== 'A' && record !== 'AAAA') { - this.log('Client sent a bad record type: %s.', record); - ws.disconnect(); - return; - } - - if (!NAME_REGEX.test(name) || name.length > 200) { - this.log('Client sent a bad domain: %s.', name); - ws.disconnect(); - return; - } - - dns.resolve(name, record, function(err, result) { - if (err) { - callback({ message: err.message, code: err.code }); - return; - } - callback(null, result); - }); }; WSProxy.prototype._handleConnect = function _handleConnect(ws, port, host, nonce) { diff --git a/etc/sample.conf b/etc/sample.conf index 9e457b01..f9c79f2d 100644 --- a/etc/sample.conf +++ b/etc/sample.conf @@ -63,6 +63,7 @@ max-inbound: 30 # Proxy Server (browser=websockets, node=socks) # proxy: foo:bar@127.0.0.1:9050 # onion: true +# upnp: true # Custom list of DNS seeds # seeds: seed.bitcoin.sipa.be diff --git a/lib/http/rpc.js b/lib/http/rpc.js index 51437feb..7e7beb3c 100644 --- a/lib/http/rpc.js +++ b/lib/http/rpc.js @@ -386,7 +386,7 @@ RPC.prototype.getnetworkinfo = co(function* getnetworkinfo(args, help) { version: pkg.version, subversion: this.pool.options.agent, protocolversion: this.pool.options.version, - localservices: this.pool.address.services, + localservices: this.pool.options.services, timeoffset: this.network.time.offset, connections: this.pool.peers.size(), networks: [], @@ -522,7 +522,9 @@ RPC.prototype.getpeerinfo = co(function* getpeerinfo(args, help) { peers.push({ id: id++, addr: peer.hostname(), - addrlocal: peer.hostname(), + addrlocal: !peer.local.isNull() + ? peer.local.hostname + : undefined, relaytxes: !peer.noRelay, lastsend: peer.lastSend / 1000 | 0, lastrecv: peer.lastRecv / 1000 | 0, diff --git a/lib/http/server.js b/lib/http/server.js index e6450f90..9f73afbe 100644 --- a/lib/http/server.js +++ b/lib/http/server.js @@ -623,6 +623,10 @@ HTTPServer.prototype._init = function _init() { 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, @@ -633,10 +637,10 @@ HTTPServer.prototype._init = function _init() { progress: this.chain.getProgress() }, pool: { - host: this.pool.address.host, - port: this.pool.address.port, + host: addr.host, + port: addr.port, agent: this.pool.options.agent, - services: this.pool.address.services.toString(2), + services: this.pool.options.services.toString(2), outbound: this.pool.peers.outbound, inbound: this.pool.peers.inbound }, diff --git a/lib/net/bip150.js b/lib/net/bip150.js index abb2376c..f7d4cc35 100644 --- a/lib/net/bip150.js +++ b/lib/net/bip150.js @@ -467,7 +467,7 @@ function AuthDB(options) { return new AuthDB(options); this.logger = null; - this.resolve = dns.resolve; + this.resolve = dns.lookup; this.dnsKnown = []; this.known = {}; @@ -476,6 +476,26 @@ function AuthDB(options) { this._init(options); } +/** + * Open auth database (lookup known peers). + * @method + * @returns {Promise} + */ + +AuthDB.prototype.open = co(function* open() { + yield this.lookup(); +}); + +/** + * Close auth database. + * @method + * @returns {Promise} + */ + +AuthDB.prototype.close = co(function* close() { + ; +}); + /** * Initialize authdb with options. * @param {Object} options @@ -601,7 +621,7 @@ AuthDB.prototype.getKnown = function getKnown(hostname) { * @returns {Promise} */ -AuthDB.prototype.discover = co(function* discover() { +AuthDB.prototype.lookup = co(function* lookup() { var jobs = []; var i, addr; @@ -610,8 +630,6 @@ AuthDB.prototype.discover = co(function* discover() { jobs.push(this.populate(addr[0], addr[1])); } - this.dnsKnown.length = 0; - yield Promise.all(jobs); }); diff --git a/lib/net/dns-browser.js b/lib/net/dns-browser.js index 255cdeaa..4ea2953b 100644 --- a/lib/net/dns-browser.js +++ b/lib/net/dns-browser.js @@ -6,36 +6,17 @@ 'use strict'; -var ProxySocket = require('./proxysocket'); -var socket; - /** * Resolve host (no getaddrinfo). * @ignore * @param {String} host - * @param {String?} proxy - * @param {Boolean?} onion + * @param {String?} proxy - Tor socks proxy. * @returns {Promise} */ -exports.resolve = function resolve(host, proxy, onion) { +exports.resolve = function resolve(host, proxy) { return new Promise(function(resolve, reject) { - if (!socket) - socket = new ProxySocket(proxy); - - socket.resolve(host, 'A', function(err, result) { - if (err) { - reject(err); - return; - } - - if (result.length === 0) { - reject(new Error('No DNS results.')); - return; - } - - resolve(result); - }); + reject(new Error('DNS not supported.')); }); }; @@ -43,11 +24,12 @@ exports.resolve = function resolve(host, proxy, onion) { * Resolve host (getaddrinfo). * @ignore * @param {String} host - * @param {String?} proxy - * @param {Boolean?} onion + * @param {String?} proxy - Tor socks proxy. * @returns {Promise} */ -exports.lookup = function lookup(host, proxy, onion) { - return exports.resolve(host, proxy, onion); +exports.lookup = function lookup(host, proxy) { + return new Promise(function(resolve, reject) { + reject(new Error('DNS not supported.')); + }); }; diff --git a/lib/net/dns.js b/lib/net/dns.js index bac10746..599d5552 100644 --- a/lib/net/dns.js +++ b/lib/net/dns.js @@ -22,13 +22,12 @@ var options = { /** * Resolve host (no getaddrinfo). * @param {String} host - * @param {String?} proxy - * @param {Boolean?} onion + * @param {String?} proxy - Tor socks proxy. * @returns {Promise} */ -exports.resolve = function resolve(host, proxy, onion) { - if (proxy && onion) +exports.resolve = function resolve(host, proxy) { + if (proxy) return socks.resolve(proxy, host); return new Promise(function(resolve, reject) { @@ -51,13 +50,12 @@ exports.resolve = function resolve(host, proxy, onion) { /** * Resolve host (getaddrinfo). * @param {String} host - * @param {String?} proxy - * @param {Boolean?} onion + * @param {String?} proxy - Tor socks proxy. * @returns {Promise} */ -exports.lookup = function lookup(host, proxy, onion) { - if (proxy && onion) +exports.lookup = function lookup(host, proxy) { + if (proxy) return socks.resolve(proxy, host); return new Promise(function(resolve, reject) { diff --git a/lib/net/hostlist.js b/lib/net/hostlist.js index a70e70aa..4c80735c 100644 --- a/lib/net/hostlist.js +++ b/lib/net/hostlist.js @@ -7,6 +7,7 @@ 'use strict'; var assert = require('assert'); +var fs = require('fs'); var util = require('../utils/util'); var IP = require('../utils/ip'); var co = require('../utils/co'); @@ -16,8 +17,9 @@ var List = require('../utils/list'); var murmur3 = require('../utils/murmur3'); var Map = require('../utils/map'); var common = require('./common'); +var seeds = require('./seeds'); var dns = require('./dns'); -var fs = require('fs'); +var Logger = require('../node/logger'); /** * Host List @@ -30,45 +32,76 @@ function HostList(options) { if (!(this instanceof HostList)) return new HostList(options); - this.network = Network.primary; - this.logger = null; - this.address = new NetAddress(); - this.resolve = dns.resolve; - this.banTime = common.BAN_TIME; + this.options = new HostListOptions(options); + this.network = this.options.network; + this.logger = this.options.logger; + this.address = this.options.address; + this.resolve = this.options.resolve; - this.rawSeeds = this.network.seeds; this.dnsSeeds = []; - this.rawNodes = []; this.dnsNodes = []; - this.nodes = []; - - this.banned = {}; this.map = {}; this.fresh = []; - this.used = []; - this.totalFresh = 0; + this.used = []; this.totalUsed = 0; + this.nodes = []; + this.local = new Map(); + this.banned = {}; - this.maxBuckets = 20; - this.maxEntries = 50; - this.maxAddresses = this.maxBuckets * this.maxEntries; - - this.horizonDays = 30; - this.retries = 3; - this.minFailDays = 7; - this.maxFailures = 10; - this.maxRefs = 8; - - this.filename = null; - this.flushInterval = 120000; this.timer = null; - this._initOptions(options); this._init(); } +/** + * Number of days before considering + * an address stale. + * @const {Number} + * @default + */ + +HostList.HORIZON_DAYS = 30; + +/** + * Number of retries (without success) + * before considering an address stale. + * @const {Number} + * @default + */ + +HostList.RETRIES = 3; + +/** + * Number of days after reaching + * MAX_FAILURES to consider an + * address stale. + * @const {Number} + * @default + */ + +HostList.MIN_FAIL_DAYS = 7; + +/** + * Maximum number of failures + * allowed before considering + * an address stale. + * @const {Number} + * @default + */ + +HostList.MAX_FAILURES = 10; + +/** + * Maximum number of references + * in fresh buckets. + * @const {Number} + * @default + */ + +HostList.MAX_REFS = 8; + /** * Serialization version. * @const {Number} @@ -78,58 +111,19 @@ function HostList(options) { HostList.VERSION = 0; /** - * Initialize options. - * @private + * Local address scores. + * @enum {Number} + * @default */ -HostList.prototype._initOptions = function initOptions(options) { - if (!options) - return; - - if (options.network != null) { - this.network = Network.get(options.network); - this.rawSeeds = this.network.seeds; - } - - if (options.logger != null) { - assert(typeof options.logger === 'object'); - this.logger = options.logger; - } - - if (options.address != null) { - assert(options.address instanceof NetAddress); - this.address = options.address; - } - - if (options.resolve != null) { - assert(typeof options.resolve === 'function'); - this.resolve = options.resolve; - } - - if (options.banTime != null) { - assert(options.banTime >= 0); - this.banTime = options.banTime; - } - - if (options.seeds) { - assert(Array.isArray(options.seeds)); - this.rawSeeds = options.seeds; - } - - if (options.nodes) { - assert(Array.isArray(options.nodes)); - this.rawNodes = options.nodes; - } - - if (options.hostLocation != null) { - assert(typeof options.hostLocation === 'string'); - this.filename = options.hostLocation; - } - - if (options.flushInterval != null) { - assert(options.flushInterval >= 0); - this.flushInterval = options.flushInterval; - } +HostList.scores = { + NONE: 0, + IF: 1, + BIND: 2, + UPNP: 3, + HTTP: 3, + MANUAL: 4, + MAX: 5 }; /** @@ -138,16 +132,28 @@ HostList.prototype._initOptions = function initOptions(options) { */ HostList.prototype._init = function init() { - var i; + var options = this.options; + var scores = HostList.scores; + var hosts = IP.getPublic(); + var port = this.address.port; + var i, host; - for (i = 0; i < this.maxBuckets; i++) + for (i = 0; i < this.options.maxBuckets; i++) this.fresh.push(new Map()); - for (i = 0; i < this.maxBuckets; i++) + for (i = 0; i < this.options.maxBuckets; i++) this.used.push(new List()); - this.setSeeds(this.rawSeeds); - this.setNodes(this.rawNodes); + this.setSeeds(options.seeds); + this.setNodes(options.nodes); + + this.pushLocal(this.address, scores.MANUAL); + this.addLocal(options.host, options.port, scores.BIND); + + for (i = 0; i < hosts.length; i++) { + host = hosts[i]; + this.addLocal(host, port, scores.IF); + } }; /** @@ -158,14 +164,17 @@ HostList.prototype._init = function init() { HostList.prototype.open = co(function* open() { try { - yield this.read(); + yield this.loadFile(); } catch (e) { - if (this.logger) { - this.logger.warning('Hosts deserialization failed.'); - this.logger.error(e); - } + this.logger.warning('Hosts deserialization failed.'); + this.logger.error(e); } + if (this.size() === 0) + this.injectSeeds(); + + yield this.discoverNodes(); + this.start(); }); @@ -178,6 +187,7 @@ HostList.prototype.open = co(function* open() { HostList.prototype.close = co(function* close() { this.stop(); yield this.flush(); + this.reset(); }); /** @@ -185,11 +195,11 @@ HostList.prototype.close = co(function* close() { */ HostList.prototype.start = function start() { - if (!this.filename) + if (!this.options.hostLocation) return; - assert(!this.timer); - this.timer = setInterval(this.flush.bind(this), this.flushInterval); + assert(this.timer == null); + this.timer = co.setInterval(this.flush, this.options.flushInterval, this); }; /** @@ -197,28 +207,56 @@ HostList.prototype.start = function start() { */ HostList.prototype.stop = function stop() { - if (!this.filename) + if (!this.options.hostLocation) return; assert(this.timer != null); - clearInterval(this.timer); + co.clearInterval(this.timer); this.timer = null; }; /** - * Read and initialie from hosts file. + * Read and initialize from hosts file. * @method * @returns {Promise} */ -HostList.prototype.read = co(function* read() { +HostList.prototype.injectSeeds = function injectSeeds() { + var nodes = seeds.get(this.network.type); + var i, node, addr; + + for (i = 0; i < nodes.length; i++) { + node = nodes[i]; + addr = NetAddress.fromHostname(node, this.network); + + if (!addr.isRoutable()) + continue; + + if (!this.options.onion && addr.isOnion()) + continue; + + if (addr.port === 0) + continue; + + this.add(addr); + } +}; + +/** + * Read and initialize from hosts file. + * @method + * @returns {Promise} + */ + +HostList.prototype.loadFile = co(function* loadFile() { + var filename = this.options.hostLocation; var data, json; - if (!this.filename) + if (!filename) return; try { - data = yield readFile(this.filename, 'utf8'); + data = yield readFile(filename, 'utf8'); } catch (e) { if (e.code === 'ENOENT') return; @@ -237,24 +275,22 @@ HostList.prototype.read = co(function* read() { */ HostList.prototype.flush = co(function* flush() { + var filename = this.options.hostLocation; var json, data; - if (!this.filename) + if (!filename) return; - if (this.logger) - this.logger.debug('Writing hosts to %s.', this.filename); + this.logger.debug('Writing hosts to %s.', filename); json = this.toJSON(); data = JSON.stringify(json); try { - yield writeFile(this.filename, data, 'utf8'); + yield writeFile(filename, data, 'utf8'); } catch (e) { - if (this.logger) { - this.logger.warning('Writing hosts failed.'); - this.logger.error(e); - } + this.logger.warning('Writing hosts failed.'); + this.logger.error(e); } }); @@ -273,7 +309,8 @@ HostList.prototype.size = function size() { */ HostList.prototype.isFull = function isFull() { - return this.size() >= this.maxAddresses; + var max = this.options.maxBuckets * this.options.maxEntries; + return this.size() >= max; }; /** @@ -297,6 +334,8 @@ HostList.prototype.reset = function reset() { this.totalFresh = 0; this.totalUsed = 0; + + this.nodes.length = 0; }; /** @@ -337,7 +376,7 @@ HostList.prototype.isBanned = function isBanned(host) { if (time == null) return false; - if (util.now() > time + this.banTime) { + if (util.now() > time + this.options.banTime) { delete this.banned[host]; return false; } @@ -477,10 +516,10 @@ HostList.prototype.add = function add(addr, src) { // Do not update if the max // reference count is reached. - if (entry.refCount === this.maxRefs) + if (entry.refCount === HostList.MAX_REFS) return false; - assert(entry.refCount < this.maxRefs); + assert(entry.refCount < HostList.MAX_REFS); // Stochastic test: previous refCount // N: 2^N times harder to increase it. @@ -506,7 +545,7 @@ HostList.prototype.add = function add(addr, src) { if (bucket.has(entry.key())) return false; - if (bucket.size >= this.maxEntries) + if (bucket.size >= this.options.maxEntries) this.evictFresh(bucket); bucket.set(entry.key(), entry); @@ -579,14 +618,14 @@ HostList.prototype.isStale = function isStale(entry) { if (entry.addr.ts === 0) return true; - if (now - entry.addr.ts > this.horizonDays * 24 * 60 * 60) + if (now - entry.addr.ts > HostList.HORIZON_DAYS * 24 * 60 * 60) return true; - if (entry.lastSuccess === 0 && entry.attempts >= this.retries) + if (entry.lastSuccess === 0 && entry.attempts >= HostList.RETRIES) return true; - if (now - entry.lastSuccess > this.minFailDays * 24 * 60 * 60) { - if (entry.attempts >= this.maxFailures) + if (now - entry.lastSuccess > HostList.MIN_FAIL_DAYS * 24 * 60 * 60) { + if (entry.attempts >= HostList.MAX_FAILURES) return true; } @@ -713,7 +752,7 @@ HostList.prototype.markAck = function markAck(hostname, services) { // Find room in used bucket. bucket = this.usedBucket(entry); - if (bucket.size < this.maxEntries) { + if (bucket.size < this.options.maxEntries) { entry.used = true; bucket.push(entry); this.totalUsed++; @@ -725,7 +764,7 @@ HostList.prototype.markAck = function markAck(hostname, services) { fresh = this.freshBucket(evicted); // Move to entry's old bucket if no room. - if (fresh.size >= this.maxEntries) + if (fresh.size >= this.options.maxEntries) fresh = old; // Swap to evicted's used bucket. @@ -855,27 +894,131 @@ HostList.prototype.setNodes = function setNodes(nodes) { }; /** - * Discover hosts from seeds and nodes. + * Add a local address. + * @param {String} host + * @param {Number} port + * @param {Number} score + * @returns {Boolean} + */ + +HostList.prototype.addLocal = function addLocal(host, port, score) { + var addr = NetAddress.fromHost(host, port, this.network); + addr.services = this.options.services; + return this.pushLocal(addr, score); +}; + +/** + * Add a local address. + * @param {NetAddress} addr + * @param {Number} score + * @returns {Boolean} + */ + +HostList.prototype.pushLocal = function pushLocal(addr, score) { + var local; + + if (!addr.isRoutable()) + return false; + + if (this.local.has(addr.hostname)) + return false; + + local = new LocalAddress(addr, score); + + this.local.set(addr.hostname, local); + + return true; +}; + +/** + * Get local address based on reachability. + * @param {NetAddress?} src + * @returns {NetAddress} + */ + +HostList.prototype.getLocal = function getLocal(src) { + var keys = this.local.keys(); + var bestReach = -1; + var bestScore = -1; + var bestDest = null; + var i, key, dest, reach; + + if (!src) + src = this.address; + + if (keys.length === 0) + return null; + + for (i = 0; i < keys.length; i++) { + key = keys[i]; + dest = this.local.get(key); + reach = src.getReachability(dest.addr); + + if (reach < bestReach) + continue; + + if (reach > bestReach || dest.score > bestScore) { + bestReach = reach; + bestScore = dest.score; + bestDest = dest.addr; + } + } + + bestDest.ts = this.network.now(); + + return bestDest; +}; + +/** + * Mark local address as seen during a handshake. + * @param {NetAddress} addr + * @returns {Boolean} + */ + +HostList.prototype.markLocal = function markLocal(addr) { + var local = this.local.get(addr.hostname); + + if (!local) + return false; + + local.score++; + + return true; +}; + +/** + * Discover hosts from seeds. * @method * @returns {Promise} */ -HostList.prototype.discover = co(function* discover() { +HostList.prototype.discoverSeeds = co(function* discoverSeeds() { var jobs = []; - var i, node, seed; + var i, seed; for (i = 0; i < this.dnsSeeds.length; i++) { seed = this.dnsSeeds[i]; jobs.push(this.populateSeed(seed)); } + yield Promise.all(jobs); +}); + +/** + * Discover hosts from nodes. + * @method + * @returns {Promise} + */ + +HostList.prototype.discoverNodes = co(function* discoverNodes() { + var jobs = []; + var i, node; + for (i = 0; i < this.dnsNodes.length; i++) { node = this.dnsNodes[i]; jobs.push(this.populateNode(node)); } - this.dnsNodes.length = 0; - yield Promise.all(jobs); }); @@ -926,14 +1069,12 @@ HostList.prototype.populate = co(function* populate(target) { assert(target.type === IP.types.DNS, 'Resolved host passed.'); - if (this.logger) - this.logger.info('Resolving host: %s.', target.host); + this.logger.info('Resolving host: %s.', target.host); try { hosts = yield this.resolve(target.host); } catch (e) { - if (this.logger) - this.logger.error(e); + this.logger.error(e); return addrs; } @@ -1027,6 +1168,8 @@ HostList.prototype.fromJSON = function fromJSON(json) { } assert(Array.isArray(json.fresh)); + assert(json.fresh.length <= this.options.maxBuckets, + 'Buckets mismatch.'); for (i = 0; i < json.fresh.length; i++) { keys = json.fresh[i]; @@ -1042,7 +1185,7 @@ HostList.prototype.fromJSON = function fromJSON(json) { bucket.set(key, entry); } - assert(bucket.size <= this.maxEntries, + assert(bucket.size <= this.options.maxEntries, 'Bucket size mismatch.'); fresh.push(bucket); @@ -1052,6 +1195,8 @@ HostList.prototype.fromJSON = function fromJSON(json) { 'Buckets mismatch.'); assert(Array.isArray(json.used)); + assert(json.used.length <= this.options.maxBuckets, + 'Buckets mismatch.'); for (i = 0; i < json.used.length; i++) { keys = json.used[i]; @@ -1068,7 +1213,7 @@ HostList.prototype.fromJSON = function fromJSON(json) { bucket.push(entry); } - assert(bucket.size <= this.maxEntries, + assert(bucket.size <= this.options.maxEntries, 'Bucket size mismatch.'); used.push(bucket); @@ -1285,6 +1430,161 @@ HostEntry.fromJSON = function fromJSON(json, network) { return new HostEntry().fromJSON(json, network); }; +/** + * LocalAddress + * @alias module:net.LocalAddress + * @constructor + * @param {NetAddress} addr + * @param {Number?} score + */ + +function LocalAddress(addr, score) { + this.addr = addr; + this.score = score || 0; +} + +/** + * Host List Options + * @alias module:net.HostListOptions + * @constructor + * @param {Object?} options + */ + +function HostListOptions(options) { + if (!(this instanceof HostListOptions)) + return new HostListOptions(options); + + this.network = Network.primary; + this.logger = Logger.global; + this.resolve = dns.lookup; + this.host = '0.0.0.0'; + this.port = this.network.port; + this.services = common.LOCAL_SERVICES; + this.onion = false; + this.banTime = common.BAN_TIME; + + this.address = new NetAddress(); + this.address.services = this.services; + this.address.ts = this.network.now(); + + this.seeds = this.network.seeds; + this.nodes = []; + + this.maxBuckets = 20; + this.maxEntries = 50; + + this.hostLocation = null; + this.flushInterval = 120000; + + if (options) + this.fromOptions(options); +} + +/** + * Inject properties from options. + * @private + * @param {Object} options + */ + +HostListOptions.prototype.fromOptions = function fromOptions(options) { + var raw; + + assert(options, 'Options are required.'); + + if (options.network != null) { + this.network = Network.get(options.network); + this.seeds = this.network.seeds; + this.address.port = this.network.port; + this.port = this.network.port; + } + + if (options.logger != null) { + assert(typeof options.logger === 'object'); + this.logger = options.logger; + } + + if (options.resolve != null) { + assert(typeof options.resolve === 'function'); + this.resolve = options.resolve; + } + + if (options.banTime != null) { + assert(options.banTime >= 0); + this.banTime = options.banTime; + } + + if (options.seeds) { + assert(Array.isArray(options.seeds)); + this.seeds = options.seeds; + } + + if (options.nodes) { + assert(Array.isArray(options.nodes)); + this.nodes = options.nodes; + } + + if (options.host != null) { + assert(typeof options.host === 'string'); + raw = IP.toBuffer(options.host); + this.host = IP.toString(raw); + if (IP.isRoutable(raw)) + this.address.setHost(this.host); + } + + if (options.port != null) { + assert(typeof options.port === 'number'); + assert(options.port > 0 && options.port <= 0xffff); + this.port = options.port; + this.address.setPort(this.port); + } + + if (options.publicHost != null) { + assert(typeof options.publicHost === 'string'); + this.address.setHost(options.publicHost); + } + + if (options.publicPort != null) { + assert(typeof options.publicPort === 'number'); + assert(options.publicPort > 0 && options.publicPort <= 0xffff); + this.address.setPort(options.publicPort); + } + + if (options.services != null) { + assert(typeof options.services === 'number'); + this.services = options.services; + } + + if (options.onion != null) { + assert(typeof options.onion === 'boolean'); + this.onion = options.onion; + } + + if (options.maxBuckets != null) { + assert(typeof options.maxBuckets === 'number'); + this.maxBuckets = options.maxBuckets; + } + + if (options.maxEntries != null) { + assert(typeof options.maxEntries === 'number'); + this.maxEntries = options.maxEntries; + } + + if (options.hostLocation != null) { + assert(typeof options.hostLocation === 'string'); + this.hostLocation = options.hostLocation; + } + + if (options.flushInterval != null) { + assert(options.flushInterval >= 0); + this.flushInterval = options.flushInterval; + } + + this.address.ts = this.network.now(); + this.address.services = this.services; + + return this; +}; + /* * Helpers */ diff --git a/lib/net/packets.js b/lib/net/packets.js index e5bcb0ce..709809ed 100644 --- a/lib/net/packets.js +++ b/lib/net/packets.js @@ -169,8 +169,8 @@ function VersionPacket(options) { this.version = common.PROTOCOL_VERSION; this.services = common.LOCAL_SERVICES; this.ts = util.now(); - this.recv = new NetAddress(); - this.from = new NetAddress(); + this.remote = new NetAddress(); + this.local = new NetAddress(); this.nonce = encoding.ZERO_U64; this.agent = common.USER_AGENT; this.height = 0; @@ -201,11 +201,11 @@ VersionPacket.prototype.fromOptions = function fromOptions(options) { if (options.ts != null) this.ts = options.ts; - if (options.recv) - this.recv.fromOptions(options.recv); + if (options.remote) + this.remote.fromOptions(options.remote); - if (options.from) - this.from.fromOptions(options.from); + if (options.local) + this.local.fromOptions(options.local); if (options.nonce) this.nonce = options.nonce; @@ -240,8 +240,8 @@ VersionPacket.fromOptions = function fromOptions(options) { VersionPacket.prototype.getSize = function getSize() { var size = 0; size += 20; - size += this.recv.getSize(false); - size += this.from.getSize(false); + size += this.remote.getSize(false); + size += this.local.getSize(false); size += 8; size += encoding.sizeVarString(this.agent, 'ascii'); size += 5; @@ -258,8 +258,8 @@ VersionPacket.prototype.toWriter = function toWriter(bw) { bw.writeU32(this.services); bw.writeU32(0); bw.write64(this.ts); - this.recv.toWriter(bw, false); - this.from.toWriter(bw, false); + this.remote.toWriter(bw, false); + this.local.toWriter(bw, false); bw.writeBytes(this.nonce); bw.writeVarString(this.agent, 'ascii'); bw.write32(this.height); @@ -292,10 +292,10 @@ VersionPacket.prototype.fromReader = function fromReader(br) { br.readU32(); this.ts = br.read53(); - this.recv.fromReader(br, false); + this.remote.fromReader(br, false); if (br.left() > 0) { - this.from.fromReader(br, false); + this.local.fromReader(br, false); this.nonce = br.readBytes(8); } diff --git a/lib/net/peer.js b/lib/net/peer.js index 15bf0b4b..f3796343 100644 --- a/lib/net/peer.js +++ b/lib/net/peer.js @@ -84,6 +84,7 @@ function Peer(options) { this.outbound = false; this.loader = false; this.address = new NetAddress(); + this.local = new NetAddress(); this.connected = false; this.destroyed = false; this.ack = false; @@ -979,8 +980,9 @@ Peer.prototype.sendVersion = function sendVersion() { packet.version = this.options.version; packet.services = this.options.services; packet.ts = this.network.now(); - packet.recv = this.address; - packet.from = this.options.address; + packet.remote = this.address; + packet.local.setNull(); + packet.local.services = this.options.services; packet.nonce = this.options.createNonce(this.hostname()); packet.agent = this.options.agent; packet.height = this.options.getHeight(); @@ -1703,6 +1705,7 @@ Peer.prototype.handleVersion = co(function* handleVersion(packet) { this.height = packet.height; this.agent = packet.agent; this.noRelay = packet.noRelay; + this.local = packet.remote; if (!this.network.selfConnect) { if (this.options.hasNonce(packet.nonce)) @@ -2275,7 +2278,6 @@ function PeerOptions(options) { this.createSocket = tcp.createSocket; this.version = common.PROTOCOL_VERSION; this.services = common.LOCAL_SERVICES; - this.address = new NetAddress(); this.agent = common.USER_AGENT; this.noRelay = false; this.spv = false; @@ -2327,11 +2329,6 @@ PeerOptions.prototype.fromOptions = function fromOptions(options) { this.services = options.services; } - if (options.address != null) { - assert(typeof options.address === 'object'); - this.address = options.address; - } - if (options.agent != null) { assert(typeof options.agent === 'string'); this.agent = options.agent; diff --git a/lib/net/pool.js b/lib/net/pool.js index 1d734dbf..c80e1469 100644 --- a/lib/net/pool.js +++ b/lib/net/pool.js @@ -15,7 +15,6 @@ var IP = require('../utils/ip'); var co = require('../utils/co'); var common = require('./common'); var chainCommon = require('../blockchain/common'); -var NetAddress = require('../primitives/netaddress'); var Address = require('../primitives/address'); var BIP150 = require('./bip150'); var BIP151 = require('./bip151'); @@ -30,12 +29,14 @@ var List = require('../utils/list'); var tcp = require('./tcp'); var dns = require('./dns'); var HostList = require('./hostlist'); +var UPNP = require('./upnp'); var InvItem = require('../primitives/invitem'); var Map = require('../utils/map'); var packets = require('./packets'); var services = common.services; var invTypes = InvItem.types; var packetTypes = packets.types; +var scores = HostList.scores; /** * A pool of peers for handling all network activity. @@ -85,7 +86,6 @@ function Pool(options) { this.mempool = this.options.mempool; this.server = this.options.createServer(); this.nonces = this.options.nonces; - this.address = this.options.address; this.locker = new Lock(true); this.connected = false; @@ -127,7 +127,15 @@ util.inherits(Pool, AsyncObject); * @default */ -Pool.MAX_HEADER_FAILS = 500; +Pool.MAX_HEADER_FAILS = 1000; + +/** + * Discovery interval for UPNP and DNS seeds. + * @const {Number} + * @default + */ + +Pool.DISCOVERY_INTERVAL = 120000; /** * Initialize the pool. @@ -286,38 +294,24 @@ Pool.prototype.connect = co(function* connect() { */ Pool.prototype._connect = co(function* connect() { - var ip; - assert(this.loaded, 'Pool is not loaded.'); if (this.connected) return; - if (this.address.isNull() && !this.options.proxy) { - try { - ip = yield this.getIP(); - } catch (e) { - this.logger.error(e); - } - if (ip) { - this.address.setHost(ip); - this.logger.info('External IP found: %s.', ip); - } - } + yield this.hosts.open(); + yield this.authdb.open(); - yield this.authdb.discover(); - - yield this.hosts.discover(); - - if (this.hosts.size() === 0) - throw new Error('No hosts available. Do you have an internet connection?'); - - this.logger.info('Resolved %d hosts from DNS seeds.', this.hosts.size()); + yield this.discoverGateway(); + yield this.discoverExternal(); + yield this.discoverSeeds(); this.fillOutbound(); yield this.listen(); + this.startTimer(); + this.connected = true; }); @@ -380,6 +374,11 @@ Pool.prototype._disconnect = co(function* disconnect() { this.headerChain.reset(); this.headerNext = null; + this.stopTimer(); + + yield this.authdb.close(); + yield this.hosts.close(); + yield this.unlisten(); this.disconnecting = false; @@ -423,6 +422,160 @@ Pool.prototype.unlisten = co(function* unlisten() { yield this.server.close(); }); +/** + * Start discovery timer. + * @private + */ + +Pool.prototype.startTimer = function startTimer() { + assert(this.timer == null, 'Timer already started.'); + this.timer = co.setInterval(this.discover, Pool.DISCOVERY_INTERVAL, this); +}; + +/** + * Stop discovery timer. + * @private + */ + +Pool.prototype.stopTimer = function stopTimer() { + assert(this.timer != null, 'Timer already stopped.'); + co.clearInterval(this.timer); + this.timer = null; +}; + +/** + * Rediscover seeds and internet gateway. + * Attempt to add port mapping once again. + * @returns {Promise} + */ + +Pool.prototype.discover = co(function* discover() { + yield this.discoverGateway(); + yield this.discoverSeeds(true); +}); + +/** + * Attempt to add port mapping (i.e. + * remote:8333->local:8333) via UPNP. + * @returns {Promise} + */ + +Pool.prototype.discoverGateway = co(function* discoverGateway() { + var src = this.options.publicPort; + var dest = this.options.port; + var wan, host; + + // Pointless if we're not listening. + if (!this.options.listen) + return; + + // UPNP is always optional, since + // it's likely to not work anyway. + if (!this.options.upnp) + return; + + try { + this.logger.debug('Discovering internet gateway (upnp).'); + wan = yield UPNP.discover(); + } catch (e) { + this.logger.debug('UPNP error:'); + this.logger.error(e); + return false; + } + + try { + host = yield wan.getExternalIP(); + } catch (e) { + this.logger.debug('Could not find external IP.'); + this.logger.error(e); + return false; + } + + if (this.hosts.addLocal(host, src, scores.UPNP)) + this.logger.info('External IP found (upnp): %s.', host); + + this.logger.debug( + 'Adding port mapping %d->%d.', + src, dest); + + try { + yield wan.addPortMapping(host, src, dest); + } catch (e) { + this.logger.debug('Could not add port mapping.'); + this.logger.error(e); + return false; + } + + return true; +}); + +/** + * Attempt to resolve DNS seeds if necessary. + * @param {Boolean} checkPeers + * @returns {Promise} + */ + +Pool.prototype.discoverSeeds = co(function* discoverSeeds(checkPeers) { + var total = 0; + var max = Math.min(2, this.options.maxOutbound); + var size = this.hosts.size(); + var peer; + + for (peer = this.peers.head(); peer; peer = peer.next) { + if (peer.handshake) { + if (++total > max) + break; + } + } + + if (size === 0 || (checkPeers && total < max)) { + this.logger.warning('Could not find enough peers.'); + this.logger.warning('Hitting DNS seeds...'); + + yield this.hosts.discoverSeeds(); + + this.logger.info( + 'Resolved %d hosts from DNS seeds.', + this.hosts.size() - size); + + this.refill(); + } +}); + +/** + * Attempt to discover external IP via HTTP. + * @returns {Promise} + */ + +Pool.prototype.discoverExternal = co(function* discoverExternal() { + var port = this.options.publicPort; + var host; + + // Pointless if we're not listening. + if (!this.options.listen) + return; + + // Never hit an HTTP server if + // we're using an outbound proxy. + if (this.options.proxy) + return; + + // Try not to hit this if we can avoid it. + if (this.hosts.local.size > 0) + return; + + try { + host = yield this.getIP(); + } catch (e) { + this.logger.debug('Could not find external IP.'); + this.logger.error(e); + return; + } + + if (this.hosts.addLocal(host, port, scores.HTTP)) + this.logger.info('External IP found (http): %s.', host); +}); + /** * Handle incoming connection. * @private @@ -1118,11 +1271,13 @@ Pool.prototype.handleConnect = co(function* handleConnect(peer) { */ Pool.prototype.handleOpen = co(function* handleOpen(peer) { + var addr; + // Advertise our address. - if (!this.address.isNull() - && !this.options.selfish - && this.options.listen) { - peer.send(new packets.AddrPacket([this.address])); + if (!this.options.selfish && this.options.listen) { + addr = this.hosts.getLocal(peer.address); + if (addr) + peer.send(new packets.AddrPacket([addr])); } // We want compact blocks! @@ -1239,6 +1394,9 @@ Pool.prototype.handleVersion = co(function* handleVersion(peer, packet) { this.network.time.add(peer.hostname(), packet.ts); this.nonces.remove(peer.hostname()); + + if (!peer.outbound && packet.remote.isRoutable()) + this.hosts.markLocal(packet.remote); }); /** @@ -2492,7 +2650,7 @@ Pool.prototype.handleSendCmpct = co(function* handleSendCmpct(peer, packet) { Pool.prototype.handleCmpctBlock = co(function* handleCmpctBlock(peer, packet) { var block = packet.block; var hash = block.hash('hex'); - var witness = peer.hasWitness(); + var witness = peer.compactWitness; var flags = chainCommon.flags.VERIFY_BODY; var result; @@ -2507,7 +2665,7 @@ Pool.prototype.handleCmpctBlock = co(function* handleCmpctBlock(peer, packet) { return; } - if (!peer.hasCompactSupport()) { + if (!peer.hasCompactSupport() && !peer.hasCompact()) { this.logger.info( 'Peer sent unsolicited cmpctblock (%s).', peer.hostname()); @@ -3394,8 +3552,6 @@ function PoolOptions(options) { this.mempool = null; this.nonces = new NonceList(); - this.address = new NetAddress(); - this.address.port = this.network.port; this.checkpoints = true; this.spv = false; @@ -3405,6 +3561,8 @@ function PoolOptions(options) { this.noRelay = false; this.host = '0.0.0.0'; this.port = this.network.port; + this.publicHost = '0.0.0.0'; + this.publicPort = this.network.port; this.maxOutbound = 8; this.maxInbound = 8; this.createSocket = this._createSocket.bind(this); @@ -3412,6 +3570,7 @@ function PoolOptions(options) { this.resolve = this._resolve.bind(this); this.proxy = null; this.onion = false; + this.upnp = false; this.selfish = false; this.version = common.PROTOCOL_VERSION; this.agent = common.USER_AGENT; @@ -3441,6 +3600,8 @@ function PoolOptions(options) { */ PoolOptions.prototype.fromOptions = function fromOptions(options) { + var raw; + assert(options, 'Pool requires options.'); assert(options.chain && typeof options.chain === 'object', 'Pool options require a blockchain.'); @@ -3451,7 +3612,8 @@ PoolOptions.prototype.fromOptions = function fromOptions(options) { this.port = this.network.port; this.seeds = this.network.seeds; - this.address.port = this.network.port; + this.port = this.network.port; + this.publicPort = this.network.port; if (options.logger != null) { assert(typeof options.logger === 'object'); @@ -3501,30 +3663,33 @@ PoolOptions.prototype.fromOptions = function fromOptions(options) { if (options.host != null) { assert(typeof options.host === 'string'); - this.host = options.host; - this.address.setHost(this.host); - if (!this.address.isRoutable()) - this.address.setNull(); + raw = IP.toBuffer(options.host); + this.host = IP.toString(raw); + if (IP.isRoutable(raw)) + this.publicHost = this.host; } if (options.port != null) { assert(typeof options.port === 'number'); + assert(options.port > 0 && options.port <= 0xffff); this.port = options.port; - this.address.setPort(this.port); + this.publicPort = options.port; } if (options.publicHost != null) { assert(typeof options.publicHost === 'string'); - this.address.setHost(options.publicHost); + this.publicHost = IP.normalize(options.publicHost); } if (options.publicPort != null) { - assert(typeof options.port === 'number'); - this.address.setPort(options.publicPort); + assert(typeof options.publicPort === 'number'); + assert(options.publicPort > 0 && options.publicPort <= 0xffff); + this.publicPort = options.publicPort; } if (options.maxOutbound != null) { assert(typeof options.maxOutbound === 'number'); + assert(options.maxOutbound > 0); this.maxOutbound = options.maxOutbound; } @@ -3558,6 +3723,11 @@ PoolOptions.prototype.fromOptions = function fromOptions(options) { this.onion = options.onion; } + if (options.upnp != null) { + assert(typeof options.upnp === 'boolean'); + this.upnp = options.upnp; + } + if (options.selfish) { assert(typeof options.selfish === 'boolean'); this.selfish = options.selfish; @@ -3669,9 +3839,6 @@ PoolOptions.prototype.fromOptions = function fromOptions(options) { this.requiredServices = options.requiredServices; } - this.address.services = this.services; - this.address.ts = this.network.now(); - return this; }; @@ -3791,7 +3958,10 @@ PoolOptions.prototype._createSocket = function createSocket(port, host) { */ PoolOptions.prototype._resolve = function resolve(name) { - return dns.resolve(name, this.proxy, this.onion); + if (this.onion) + return dns.lookup(name, this.proxy); + + return dns.lookup(name); }; /** diff --git a/lib/net/proxysocket.js b/lib/net/proxysocket.js index b9df286d..2b1dc691 100644 --- a/lib/net/proxysocket.js +++ b/lib/net/proxysocket.js @@ -96,19 +96,6 @@ ProxySocket.prototype._init = function _init() { }); }; -ProxySocket.prototype.resolve = function resolve(name, record, callback) { - var e; - this.socket.emit('dns resolve', name, record, function(err, results) { - if (err) { - e = new Error(err.message); - e.code = err.code || null; - callback(e); - return; - } - callback(null, results); - }); -}; - ProxySocket.prototype.connect = function connect(port, host) { var nonce = 0; var i, pow; diff --git a/lib/net/upnp-browser.js b/lib/net/upnp-browser.js new file mode 100644 index 00000000..58be40de --- /dev/null +++ b/lib/net/upnp-browser.js @@ -0,0 +1,41 @@ +/*! + * upnp-browser.js - upnp for bcoin + * Copyright (c) 2017, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +/** + * UPNP + * @constructor + * @ignore + * @param {String?} host - Multicast IP. + * @param {Number?} port - Multicast port. + * @param {String?} gateway - Gateway name. + */ + +function UPNP(host, port, gateway) { + throw new Error('UPNP not supported.'); +} + +/** + * Discover gateway and resolve service. + * @param {String?} host - Multicast IP. + * @param {Number?} port - Multicast port. + * @param {String?} gateway - Gateway type. + * @param {String[]?} targets - Target service types. + * @returns {Promise} Service. + */ + +UPNP.discover = function discover(host, port, gateway, targets) { + return new Promise(function(resolve, reject) { + reject(new Error('UPNP not supported.')); + }); +}; + +/* + * Expose + */ + +module.exports = UPNP; diff --git a/lib/net/upnp.js b/lib/net/upnp.js index 29a628f0..a372ad04 100644 --- a/lib/net/upnp.js +++ b/lib/net/upnp.js @@ -60,7 +60,7 @@ UPNP.WAN_SERVICES = [ * @default */ -UPNP.RESPONSE_TIMEOUT = 3000; +UPNP.RESPONSE_TIMEOUT = 1000; /** * Clean up current job. @@ -430,14 +430,14 @@ UPNPService.prototype.addPortMapping = co(function* addPortMapping(remote, src, var local = IP.getPrivate(); var xml, child; - if (!local) + if (local.length === 0) throw new Error('Cannot determine local IP.'); xml = yield this.soapRequest(action, [ ['NewRemoteHost', remote], ['NewExternalPort', src], ['NewProtocol', 'TCP'], - ['NewInternalClient', local], + ['NewInternalClient', local[0]], ['NewInternalPort', dest], ['NewEnabled', 'True'], ['NewPortMappingDescription', 'upnp:bcoin'], diff --git a/lib/node/config.js b/lib/node/config.js index 5b943b05..ae099111 100644 --- a/lib/node/config.js +++ b/lib/node/config.js @@ -267,6 +267,7 @@ config.toOptions = function toOptions(data) { options.identityKey = key(data.identitykey); options.proxy = str(data.proxy); options.onion = bool(data.onion); + options.upnp = bool(data.upnp); options.seeds = list(data.seeds); options.nodes = list(data.nodes); options.maxOutbound = num(data.maxoutbound); diff --git a/lib/node/fullnode.js b/lib/node/fullnode.js index 32ba4bfb..72fb997e 100644 --- a/lib/node/fullnode.js +++ b/lib/node/fullnode.js @@ -102,6 +102,7 @@ function FullNode(options) { maxInbound: this.options.maxInbound, proxy: this.options.proxy, onion: this.options.onion, + upnp: this.options.upnp, seeds: this.options.seeds, nodes: this.options.nodes, publicHost: this.options.publicHost, diff --git a/lib/node/spvnode.js b/lib/node/spvnode.js index 8aec8abd..1486a488 100644 --- a/lib/node/spvnode.js +++ b/lib/node/spvnode.js @@ -62,6 +62,7 @@ function SPVNode(options) { chain: this.chain, proxy: this.options.proxy, onion: this.options.onion, + upnp: this.options.upnp, seeds: this.options.seeds, nodes: this.options.nodes, bip151: this.options.bip151, diff --git a/lib/primitives/netaddress.js b/lib/primitives/netaddress.js index a58e74b1..cddf9232 100644 --- a/lib/primitives/netaddress.js +++ b/lib/primitives/netaddress.js @@ -168,6 +168,29 @@ NetAddress.prototype.isOnion = function isOnion() { return IP.isOnion(this.raw); }; +/** + * Compare against another network address. + * @returns {Boolean} + */ + +NetAddress.prototype.equal = function equal(addr) { + return this.compare(addr) === 0; +}; + +/** + * Compare against another network address. + * @returns {Number} + */ + +NetAddress.prototype.compare = function compare(addr) { + var cmp = util.cmp(this.raw, addr.raw); + + if (cmp !== 0) + return cmp; + + return this.port - addr.port; +}; + /** * Get reachable score to destination. * @param {NetAddress} dest @@ -205,6 +228,7 @@ NetAddress.prototype.setHost = function setHost(host) { */ NetAddress.prototype.setPort = function setPort(port) { + assert(port >= 0 && port <= 0xffff); this.port = port; this.hostname = IP.toHostname(this.host, port); }; diff --git a/lib/protocol/networks.js b/lib/protocol/networks.js index 1c707710..60bea886 100644 --- a/lib/protocol/networks.js +++ b/lib/protocol/networks.js @@ -43,17 +43,18 @@ main = network.main = {}; main.type = 'main'; /** - * Default seeds. + * Default DNS seeds. * @const {String[]} * @default */ main.seeds = [ + 'seed.bitcoin.sipa.be', // Pieter Wuille 'dnsseed.bluematt.me', // Matt Corallo 'dnsseed.bitcoin.dashjr.org', // Luke Dashjr 'seed.bitcoinstats.com', // Christian Decker - 'bitseed.xf2.org', // Jeff Garzik - 'seed.bitcoin.jonasschnelli.ch' // Jonas Schnelli + 'seed.bitcoin.jonasschnelli.ch', // Jonas Schnelli + 'seed.btc.petertodd.org' // Peter Todd ]; /** @@ -469,9 +470,10 @@ testnet = network.testnet = {}; testnet.type = 'testnet'; testnet.seeds = [ - 'testnet-seed.bitcoin.petertodd.org', - 'testnet-seed.bluematt.me', - 'testnet-seed.bitcoin.schildbach.de' + 'testnet-seed.bitcoin.jonasschnelli.ch', // Jonas Schnelli + 'seed.tbtc.petertodd.org', // Peter Todd + 'testnet-seed.bluematt.me', // Matt Corallo + 'testnet-seed.bitcoin.schildbach.de' // Andreas Schildbach ]; testnet.magic = 0x0709110b; diff --git a/lib/utils/co.js b/lib/utils/co.js index 3787e47e..4324e2b8 100644 --- a/lib/utils/co.js +++ b/lib/utils/co.js @@ -11,6 +11,7 @@ * @module utils/co */ +var assert = require('assert'); var nextTick = require('./nexttick'); var every; @@ -230,6 +231,83 @@ every = co(function* every(jobs) { return true; }); +/** + * Start an interval. Wait for promise + * to resolve on each iteration. + * @param {Function} func + * @param {Number?} time + * @param {Object?} self + * @returns {Object} + */ + +function startInterval(func, time, self) { + var cb, ctx; + + ctx = { + timer: null, + stopped: false + }; + + cb = co(function* () { + assert(ctx.timer != null); + ctx.timer = null; + + try { + yield func.call(self); + } finally { + if (!ctx.stopped) + ctx.timer = setTimeout(cb, time); + } + }); + + ctx.timer = setTimeout(cb, time); + + return ctx; +} + +/** + * Clear an interval. + * @param {Object} ctx + */ + +function stopInterval(ctx) { + assert(ctx); + if (ctx.timer != null) { + clearTimeout(ctx.timer); + ctx.timer = null; + } + ctx.stopped = true; +} + +/** + * Start a timeout. + * @param {Function} func + * @param {Number?} time + * @param {Object?} self + * @returns {Object} + */ + +function startTimeout(func, time, self) { + return { + timer: setTimeout(func.bind(self), time), + stopped: false + }; +} + +/** + * Clear a timeout. + * @param {Object} ctx + */ + +function stopTimeout(ctx) { + assert(ctx); + if (ctx.timer != null) { + clearTimeout(ctx.timer); + ctx.timer = null; + } + ctx.stopped = true; +} + /** * Create a job object. * @returns {Job} @@ -282,6 +360,10 @@ exports.timeout = timeout; exports.wrap = wrap; exports.promisify = promisify; exports.every = every; +exports.setInterval = startInterval; +exports.clearInterval = stopInterval; +exports.setTimeout = startTimeout; +exports.clearTimeout = stopTimeout; exports.job = job; module.exports = exports; diff --git a/lib/utils/ip.js b/lib/utils/ip.js index cff7f2b5..84f75a26 100644 --- a/lib/utils/ip.js +++ b/lib/utils/ip.js @@ -984,15 +984,16 @@ IP.isEqual = function isEqual(a, b) { /** * Get IP address from network interfaces. - * @param {String} name - `public` or `private`. - * @param {String} family - IP family name. + * @param {String?} name - `public` or `private`. + * @param {String?} family - IP family name. * @returns {String} */ -IP.getInterface = function getInterface(name, family) { +IP.getInterfaces = function _getInterfaces(name, family) { var interfaces = getInterfaces(); var keys = Object.keys(interfaces); - var i, j, key, items, details, type, ip; + var result = []; + var i, j, key, items, details, type, raw; for (i = 0; i < keys.length; i++) { key = keys[i]; @@ -1003,57 +1004,57 @@ IP.getInterface = function getInterface(name, family) { type = details.family.toLowerCase(); - if (type !== family) + if (family && type !== family) continue; if (details.internal) continue; try { - ip = IP.toBuffer(details.address); + raw = IP.toBuffer(details.address); } catch (e) { continue; } - if (IP.isLocal(ip)) + if (IP.isNull(raw)) + continue; + + if (IP.isLocal(raw)) continue; if (name === 'public') { - if (!IP.isRoutable(ip)) + if (!IP.isRoutable(raw)) + continue; + } else if (name === 'private') { + if (IP.isRoutable(raw)) continue; } - return IP.toString(ip); + result.push(IP.toString(raw)); } } + + return result; }; /** * Get private IP from network interfaces. + * @param {String?} family - IP family name. * @returns {String} */ -IP.getPrivate = function getPrivate() { - var ip = IP.getInterface('private', 'ipv4'); - - if (ip) - return ip; - - return IP.getInterface('private', 'ipv6'); +IP.getPrivate = function getPrivate(family) { + return IP.getInterfaces('private', family); }; /** * Get public IP from network interfaces. + * @param {String?} family - IP family name. * @returns {String} */ -IP.getPublic = function getPublic() { - var ip = IP.getInterface('public', 'ipv4'); - - if (ip) - return ip; - - return IP.getInterface('public', 'ipv6'); +IP.getPublic = function getPublic(family) { + return IP.getInterfaces('public', family); }; /** diff --git a/package.json b/package.json index ae0b345d..6db26570 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "./lib/mempool/layout": "./lib/mempool/layout-browser.js", "./lib/net/dns": "./lib/net/dns-browser.js", "./lib/net/tcp": "./lib/net/tcp-browser.js", + "./lib/net/upnp": "./lib/net/upnp-browser.js", "./lib/utils/native": "./browser/empty.js", "./lib/utils/nfkd": "./lib/utils/nfkd-browser.js", "./lib/utils/nexttick": "./lib/utils/nexttick-browser.js", @@ -87,6 +88,7 @@ "bcoin-native": "./browser/empty.js", "child_process": "./browser/empty.js", "crypto": "./browser/empty.js", + "dgram": "./browser/empty.js", "fs": "./browser/empty.js", "net": "./browser/empty.js", "os": "./browser/empty.js",