diff --git a/browser/wsproxy.js b/browser/wsproxy.js index 670e593e..c65d8ab7 100644 --- a/browser/wsproxy.js +++ b/browser/wsproxy.js @@ -151,6 +151,7 @@ WSProxy.prototype._handleConnect = function _handleConnect(ws, port, host, nonce try { raw = IP.toBuffer(host); + host = IP.toString(raw); } catch (e) { this.log('Client gave a bad host: %s (%s).', host, state.host); ws.emit('tcp error', { @@ -161,7 +162,7 @@ WSProxy.prototype._handleConnect = function _handleConnect(ws, port, host, nonce return; } - if (!IP.isRoutable(raw)) { + if (!IP.isRoutable(raw) || IP.isTor(raw)) { this.log( 'Client is trying to connect to a bad ip: %s (%s).', host, state.host); @@ -257,7 +258,7 @@ SocketState.prototype.toInfo = function toInfo() { SocketState.prototype.connect = function connect(port, host) { this.socket = net.connect(port, host); - this.remoteHost = IP.hostname(host, port); + this.remoteHost = IP.toHostname(host, port); return this.socket; }; diff --git a/lib/net/bip150.js b/lib/net/bip150.js index 84ce9fcf..61cfa984 100644 --- a/lib/net/bip150.js +++ b/lib/net/bip150.js @@ -529,7 +529,7 @@ AuthDB.prototype.addKnown = function addKnown(host, key) { addr = IP.parseHost(host); - if (addr.version === -1) { + if (addr.type === IP.types.DNS) { // Defer this for resolution. this.dnsKnown.push([addr, key]); return; @@ -632,7 +632,7 @@ AuthDB.prototype.discover = co(function* discover() { AuthDB.prototype.populate = co(function* populate(addr, key) { var i, hosts, host; - assert(addr.version === -1, 'Resolved host passed.'); + assert(addr.type === IP.types.DNS, 'Resolved host passed.'); if (this.logger) this.logger.info('Resolving authorized hosts from: %s.', addr.host); @@ -649,7 +649,7 @@ AuthDB.prototype.populate = co(function* populate(addr, key) { host = hosts[i]; if (addr.port !== 0) - host = IP.hostname(host, addr.port); + host = IP.toHostname(host, addr.port); this.known[host] = key; } diff --git a/lib/net/hostlist.js b/lib/net/hostlist.js index 4c04cad2..0f734e50 100644 --- a/lib/net/hostlist.js +++ b/lib/net/hostlist.js @@ -664,7 +664,7 @@ HostList.prototype.toArray = function toArray() { HostList.prototype.addSeed = function addSeed(host) { var addr = IP.parseHost(host, this.network.port); - if (addr.version === -1) { + if (addr.type === IP.types.DNS) { // Defer for resolution. this.dnsSeeds.push(addr); return; @@ -685,7 +685,7 @@ HostList.prototype.addSeed = function addSeed(host) { HostList.prototype.addNode = function addNode(host) { var addr = IP.parseHost(host, this.network.port); - if (addr.version === -1) { + if (addr.type === IP.types.DNS) { // Defer for resolution. this.dnsNodes.push(addr); return; @@ -798,7 +798,7 @@ HostList.prototype.populate = co(function* populate(target) { var addrs = []; var i, addr, hosts, host; - assert(target.version === -1, 'Resolved host passed.'); + assert(target.type === IP.types.DNS, 'Resolved host passed.'); if (this.logger) this.logger.info('Resolving host: %s.', target.host); diff --git a/lib/net/pool.js b/lib/net/pool.js index d93f8c71..7c5177c1 100644 --- a/lib/net/pool.js +++ b/lib/net/pool.js @@ -439,7 +439,7 @@ Pool.prototype.handleSocket = function handleSocket(socket) { return; } - host = IP.hostname(host, socket.remotePort); + host = IP.toHostname(host, socket.remotePort); assert(!this.peers.map[host], 'Port collision.'); diff --git a/lib/primitives/netaddress.js b/lib/primitives/netaddress.js index a47200a4..f927d960 100644 --- a/lib/primitives/netaddress.js +++ b/lib/primitives/netaddress.js @@ -80,7 +80,7 @@ NetAddress.prototype.fromOptions = function fromOptions(options) { this.ts = options.ts; } - this.hostname = IP.hostname(this.host, this.port); + this.hostname = IP.toHostname(this.host, this.port); return this; }; @@ -175,7 +175,7 @@ NetAddress.prototype.isTor = function isTor() { NetAddress.prototype.setNull = function setNull() { this.raw = IP.ZERO_IP; this.host = '0.0.0.0'; - this.hostname = IP.hostname(this.host, this.port); + this.hostname = IP.toHostname(this.host, this.port); }; /** @@ -186,7 +186,7 @@ NetAddress.prototype.setNull = function setNull() { NetAddress.prototype.setHost = function setHost(host) { this.raw = IP.toBuffer(host); this.host = IP.toString(this.raw); - this.hostname = IP.hostname(this.host, this.port); + this.hostname = IP.toHostname(this.host, this.port); }; /** @@ -196,7 +196,7 @@ NetAddress.prototype.setHost = function setHost(host) { NetAddress.prototype.setPort = function setPort(port) { this.port = port; - this.hostname = IP.hostname(this.host, port); + this.hostname = IP.toHostname(this.host, port); }; /** @@ -216,7 +216,7 @@ NetAddress.prototype.fromHost = function fromHost(host, port, network) { this.services = NetAddress.DEFAULT_SERVICES; this.ts = network.now(); - this.hostname = IP.hostname(this.host, this.port); + this.hostname = IP.toHostname(this.host, this.port); return this; }; @@ -306,7 +306,7 @@ NetAddress.prototype.fromReader = function fromReader(br, full) { this.raw = br.readBytes(16); this.host = IP.toString(this.raw); this.port = br.readU16BE(); - this.hostname = IP.hostname(this.host, this.port); + this.hostname = IP.toHostname(this.host, this.port); return this; }; @@ -414,7 +414,7 @@ NetAddress.prototype.fromJSON = function fromJSON(json) { this.port = json.port; this.services = json.services; this.ts = json.ts; - this.hostname = IP.hostname(this.host, this.port); + this.hostname = IP.toHostname(this.host, this.port); return this; }; diff --git a/lib/utils/base32.js b/lib/utils/base32.js new file mode 100644 index 00000000..e9359169 --- /dev/null +++ b/lib/utils/base32.js @@ -0,0 +1,187 @@ +/*! + * base32.js - base32 for bcoin + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +/* + * Base32 + */ + +var base32 = 'abcdefghijklmnopqrstuvwxyz234567'; +var padding = [0, 6, 4, 3, 1]; +var unbase32 = {}; + +for (var i = 0; i < base32.length; i++) + unbase32[base32[i]] = i; + +/** + * Encode a base32 string. + * @param {Buffer} data + * @returns {String} + */ + +exports.encode = function(data) { + var str = ''; + var mode = 0; + var left = 0; + var i, ch; + + for (i = 0; i < data.length; i++) { + ch = data[i]; + switch (mode) { + case 0: + str += base32[ch >>> 3]; + left = (ch & 7) << 2; + mode = 1; + break; + case 1: + str += base32[left | (ch >>> 6)]; + str += base32[(ch >>> 1) & 31]; + left = (ch & 1) << 4; + mode = 2; + break; + case 2: + str += base32[left | (ch >>> 4)]; + left = (ch & 15) << 1; + mode = 3; + break; + case 3: + str += base32[left | (ch >>> 7)]; + str += base32[(ch >>> 2) & 31]; + left = (ch & 3) << 3; + mode = 4; + break; + case 4: + str += base32[left | (ch >>> 5)]; + str += base32[ch & 31]; + mode = 0; + break; + } + } + + if (mode > 0) { + str += base32[left]; + for (i = 0; i < padding[mode]; i++) + str += '='; + } + + return str; +}; + +/** + * Decode a base32 string. + * @param {String} str + * @returns {Buffer} + */ + +exports.decode = function decode(str) { + var data = new Buffer(str.length * 5 / 8 | 0); + var mode = 0; + var left = 0; + var j = 0; + var i, ch; + + for (i = 0; i < str.length; i++) { + ch = unbase32[str[i]]; + + if (ch == null) + break; + + switch (mode) { + case 0: + left = ch; + mode = 1; + break; + case 1: + data[j++] = (left << 3) | (ch >>> 2); + left = ch & 3; + mode = 2; + break; + case 2: + left = left << 5 | ch; + mode = 3; + break; + case 3: + data[j++] = (left << 1) | (ch >>> 4); + left = ch & 15; + mode = 4; + break; + case 4: + data[j++] = (left << 4) | (ch >>> 1); + left = ch & 1; + mode = 5; + break; + case 5: + left = left << 5 | ch; + mode = 6; + break; + case 6: + data[j++] = (left << 2) | (ch >>> 3); + left = ch & 7; + mode = 7; + break; + case 7: + data[j++] = (left << 5) | ch; + mode = 0; + break; + } + } + + switch (mode) { + case 0: + break; + case 1: + case 3: + case 6: + throw new Error('Invalid base32 string.'); + case 2: + if (left > 0) + throw new Error('Invalid padding.'); + + if (str.slice(i, i + 6) !== '======') + throw new Error('Invalid base32 character.'); + + if (unbase32[str[i + 6]] != null) + throw new Error('Invalid padding.'); + + break; + case 4: + if (left > 0) + throw new Error('Invalid padding.'); + + if (str.slice(i, i + 4) !== '====') + throw new Error('Invalid base32 character.'); + + if (unbase32[str[i + 4]] != null) + throw new Error('Invalid padding.'); + + break; + case 5: + if (left > 0) + throw new Error('Invalid padding.'); + + if (str.slice(i, i + 3) !== '===') + throw new Error('Invalid base32 character.'); + + if (unbase32[str[i + 3]] != null) + throw new Error('Invalid padding.'); + + break; + case 7: + if (left > 0) + throw new Error('Invalid padding.'); + + if (str[i] !== '=') + throw new Error('Invalid base32 character.'); + + if (unbase32[str[i + 1]] != null) + throw new Error('Invalid padding.'); + + break; + } + + return data.slice(0, j); +}; diff --git a/lib/utils/index.js b/lib/utils/index.js index ceeb7666..be78346f 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -2,6 +2,7 @@ exports.ASN1 = require('./asn1'); exports.AsyncObject = require('./async'); +exports.base32 = require('./base32'); exports.base58 = require('./base58'); exports.Bloom = require('./bloom'); exports.RollingFilter = exports.Bloom.Rolling; diff --git a/lib/utils/ip.js b/lib/utils/ip.js index 81ffa50f..669c498c 100644 --- a/lib/utils/ip.js +++ b/lib/utils/ip.js @@ -12,6 +12,7 @@ 'use strict'; var assert = require('assert'); +var base32 = require('./base32'); var IP = exports; /* @@ -37,15 +38,27 @@ var IPV6_REGEX = IP.ZERO_IP = ZERO_IP; +/** + * Address types. + * @enum {Number} + */ + +IP.types = { + DNS: -1, + IPV4: 4, + IPV6: 6, + TOR: 10 +}; + /** * Parse a hostname. * @param {String} addr * @param {Number?} fallback - Fallback port. - * @returns {Object} Contains `host`, `port`, and `version`. + * @returns {Object} Contains `host`, `port`, and `type`. */ IP.parseHost = function parseHost(addr, fallback) { - var parts, host, port, version, hostname, raw; + var parts, host, port, type, hostname, raw; assert(typeof addr === 'string'); @@ -104,21 +117,21 @@ IP.parseHost = function parseHost(addr, fallback) { port = fallback || 0; } - version = IP.version(host); + type = IP.getType(host); - if (version !== -1) { + if (type !== IP.types.DNS) { raw = IP.toBuffer(host); host = IP.toString(raw); } hostname = host; - if (version === 6) + if (type === IP.types.IPV6) hostname = '[' + hostname + ']'; hostname += ':' + port; - return new Address(host, port, version, hostname, raw); + return new Address(host, port, type, hostname, raw); }; /** @@ -128,8 +141,8 @@ IP.parseHost = function parseHost(addr, fallback) { * @returns {String} */ -IP.hostname = function hostname(host, port) { - var version; +IP.toHostname = function toHostname(host, port) { + var type; assert(typeof host === 'string'); assert(host.length > 0); @@ -138,34 +151,37 @@ IP.hostname = function hostname(host, port) { assert(!/[\[\]]/.test(host), 'Bad host.'); - version = IP.version(host); + type = IP.getType(host); if (host.indexOf(':') !== -1) - assert(version === 6, 'Bad host.'); + assert(type === IP.types.IPV6, 'Bad host.'); - if (version !== -1) + if (type !== IP.types.DNS) host = IP.normalize(host); - if (version === 6) + if (type === IP.types.IPV6) host = '[' + host + ']'; return host + ':' + port; }; /** - * Test whether a string is an IP address. + * Get address type (-1=dns, 4=ipv4, 6=ipv6, 10=tor). * @param {String?} str - * @returns {Number} IP version (4 or 6). + * @returns {Number} */ -IP.version = function version(str) { +IP.getType = function getType(str) { if (IP.isV4Format(str)) - return 4; + return IP.types.IPV4; if (IP.isV6Format(str)) - return 6; + return IP.types.IPV6; - return -1; + if (IP.isTorFormat(str)) + return IP.types.TOR; + + return IP.types.DNS; }; /** @@ -204,6 +220,21 @@ IP.isV6Format = function isV6Format(str) { return IPV6_REGEX.test(str); }; +/** + * Test whether a string is an onion address. + * @param {String?} str + * @returns {Boolean} + */ + +IP.isTorFormat = function isTorFormat(str) { + assert(typeof str === 'string'); + + if (str.length < 7) + return false; + + return str.slice(-6) === '.onion'; +}; + /** * Test whether a buffer is an ipv4-mapped ipv6 address. * @param {Buffer} raw @@ -236,6 +267,7 @@ IP.isMapped = function isMapped(raw) { IP.toBuffer = function toBuffer(str) { var raw = new Buffer(16); + var data; assert(typeof str === 'string'); @@ -246,6 +278,15 @@ IP.toBuffer = function toBuffer(str) { return IP.parseV4(str, raw, 12); } + if (IP.isTorFormat(str)) { + data = TOR_ONION; + data.copy(raw, 0); + data = base32.decode(str.slice(0, -6)); + assert(data.length === 10, 'Invalid onion address.'); + data.copy(raw, 6); + return raw; + } + return IP.parseV6(str, raw, 0); }; @@ -357,68 +398,49 @@ IP.parseV6 = function parseV6(str, raw, offset) { */ IP.toString = function toString(raw) { - var str = ''; + var host = ''; var i; assert(Buffer.isBuffer(raw)); if (raw.length === 4) { - str += raw[0]; - str += '.' + raw[1]; - str += '.' + raw[2]; - str += '.' + raw[3]; - return str; + host += raw[0]; + host += '.' + raw[1]; + host += '.' + raw[2]; + host += '.' + raw[3]; + return host; } if (raw.length === 16) { if (IP.isMapped(raw)) { - str += raw[12]; - str += '.' + raw[13]; - str += '.' + raw[14]; - str += '.' + raw[15]; - return str; + host += raw[12]; + host += '.' + raw[13]; + host += '.' + raw[14]; + host += '.' + raw[15]; + return host; } - str += raw.readUInt16BE(0, true).toString(16); + if (IP.isTor(raw)) { + host = base32.encode(raw.slice(6)); + return host + '.onion'; + } + + host += raw.readUInt16BE(0, true).toString(16); for (i = 2; i < 16; i += 2) { - str += ':'; - str += raw.readUInt16BE(i, true).toString(16); + host += ':'; + host += raw.readUInt16BE(i, true).toString(16); } - str = str.replace(/(^|:)0(:0)*:0(:|$)/, '$1::$3'); - str = str.replace(/:{3,4}/, '::'); + host = host.replace(/(^|:)0(:0)*:0(:|$)/, '$1::$3'); + host = host.replace(/:{3,4}/, '::'); - return str; + return host; } throw Error('Invalid IP address: ' + raw.toString('hex')); }; -/** - * Convert a buffer to an ip string or .onion. - * @param {Buffer} raw - * @returns {String} - */ - -IP.toHost = function toHost(raw) { - var i, ch, host; - - if (IP.isTor(raw)) { - host = ''; - for (i = 6; i < raw.length; i += 2) { - ch = raw[i]; - ch = ch.toString(32); - while (ch.length < 4) - ch = '0' + ch; - host += ch; - } - return host + '.onion'; - } - - return IP.toString(raw); -}; - /** * Normalize an ip. * @param {String} str @@ -836,15 +858,15 @@ IP.isEqual = function isEqual(a, b) { * Represents a parsed address. * @param {String} host * @param {Number} port - * @param {Number} version + * @param {Number} type * @param {String} hostname * @param {Buffer} raw */ -function Address(host, port, version, hostname, raw) { +function Address(host, port, type, hostname, raw) { this.host = host; this.port = port; - this.version = version; + this.type = type; this.hostname = hostname; this.raw = raw || ZERO_IP; }