UNPKG

@hyperswarm/network

Version:

The networking guts of Hyperswarm

277 lines (233 loc) 6.92 kB
'use strict' const utp = require('utp-native') const net = require('net') const Nanoresource = require('nanoresource') const discovery = require('@hyperswarm/discovery') const CONNECTION_TIMEOUT = 10000 // TODO: make configurable module.exports = (opts) => new NetworkResource(opts) class NetworkResource extends Nanoresource { constructor (opts) { if (!opts) opts = {} super() this.id = opts.id || null this.port = opts.port || 0 this.announcePort = opts.announcePort || 0 this.preferredPort = opts.preferredPort || 0 this.tcp = net.createServer() this.utp = utp() this.discovery = null this.options = opts this.sockets = new Set() this._announceLocalAddress = !!opts.announceLocalAddress this._onbind = opts.bind || noop this._onclose = opts.close || noop this._onsocket = opts.socket || noop this._bootstrap = opts.bootstrap this._ephemeral = opts.ephemeral this.utp.on('connection', this._onincoming.bind(this, false)) this.tcp.on('connection', this._onincoming.bind(this, true)) } _onincoming (isTCP, socket) { if (isTCP) socket.setNoDelay(true) else socket.on('end', noHalfOpen) // utp defaults to halfOpen so end->finish like tcp this.sockets.add(socket) socket.on('close', this._removeSocket.bind(this, socket)) this._onsocket(socket, isTCP) } address () { return this.tcp.address() } connect (peer, cb) { const self = this const tcp = net.connect(peer.port, peer.host) const timeout = setTimeout(ontimeout, CONNECTION_TIMEOUT) let timedout = false let connected = false let active = [tcp] let closes = 1 tcp.setNoDelay(true) tcp.on('error', ontcperror) tcp.on('connect', onconnect) tcp.on('close', onclose) if (!peer.referrer) return closes++ this.open(onopen) function onopen (err) { if (err) return onerror(err) self.discovery.holepunch(peer, onholepunch) } function ontimeout () { timedout = true for (const socket of active) socket.destroy() cb(new Error('Timeout')) } function onholepunch (err) { if (connected || timedout) return if (err) return onerror(err) const utp = self.utp.connect(peer.port, peer.host) utp.on('error', onutperror) utp.on('connect', onconnect) utp.on('close', onclose) active.push(utp) } function onconnect () { const socket = this if (self.closed || connected || timedout) return socket.destroy() // eagerly destroy dead sockets by now for (const a of active) { if (a !== socket) a.destroy() } clearTimeout(timeout) connected = true self.sockets.add(socket) cb(null, socket, tcp === socket) } function onerror (err) { if (!--closes && !connected && !timedout) { clearTimeout(timeout) cb(err || new Error('All sockets failed')) } } function onclose () { self.sockets.delete(this) // only one of the sockets are added but this still works onerror(null) } } announce (key, { lookup = false, port = this.announcePort } = {}) { if (!this.discovery) throw new Error('Bind before announcing') const localPort = port || this.tcp.address().port const localAddress = this._localAddress() return this.discovery.announce(key, { port, localPort, localAddress, lookup }) } lookupOne (key, cb) { if (!this.discovery) throw new Error('Bind before doing a lookup') const localAddress = this._localAddress() this.discovery.lookupOne(key, { localAddress }, cb) } lookup (key) { if (!this.discovery) throw new Error('Bind before doing a lookup') const localAddress = this._localAddress() return this.discovery.lookup(key, { localAddress }) } bind (preferredPort, cb) { if (typeof preferredPort === 'function') { return this.open(preferredPort) } this.preferredPort = preferredPort || 0 this.open(cb) } _localAddress () { if (!this._announceLocalAddress) return null const ip = localIp() if (!ip) return null return { host: ip, port: this.tcp.address().port } } _open (cb) { const self = this const ports = this.port !== 0 ? [ this.port ] : [ this.preferredPort, this.preferredPort ? this.preferredPort + 1 : 0, this.preferredPort ? this.preferredPort + 2 : 0, 0, 0, 0, 0 ] let tries = 0 listenBoth(this.tcp, this.utp, ports[0], retry) function retry (err) { if (!err) return onlisten() if (++tries >= ports.length) return cb(err) listenBoth(self.tcp, self.utp, ports[tries], retry) } function onlisten () { self.discovery = discovery({ id: self.id, bootstrap: self._bootstrap, // pass original opts.ephemeral (stored on self._ephemeral) // to preserve potential `undefined` value for adaptive ephemerality ephemeral: self._ephemeral, socket: self.utp }) self._onbind() cb(null) } } _removeSocket (socket) { this.sockets.delete(socket) } _close (cb) { const self = this this.discovery.destroy() this.discovery.on('close', ondiscoveryclose) this.utp.maxConnections = -1 this.tcp.maxConnections = -1 function ondiscoveryclose () { let missing = 2 for (const socket of self.sockets) socket.destroy() self.sockets.clear() self.tcp.close() self.utp.close() self.tcp.on('close', onclose) self.utp.on('close', onclose) function onclose () { if (--missing) return self.discovery = null self._onclose() cb(null) } } } } function listenBoth (tcp, utp, port, cb) { listen(tcp, port, function (err) { if (err) return cb(err) listen(utp, tcp.address().port, function (err) { if (err) { tcp.once('close', () => cb(err)) tcp.close() return } cb(null) }) }) } function ontcperror (err) { if (this.destroyed === false) this.destroy(err) } function onutperror (err) { if (this.destroyed === false) this.destroy(err) } function listen (server, port, cb) { server.on('listening', done) server.on('error', done) server.listen(port) function done (err) { server.removeListener('listening', done) server.removeListener('error', done) cb(err) } } function noop () {} function localIp () { const os = require('os') const nets = os.networkInterfaces() for (const name of Object.keys(nets)) { const addrs = nets[name] for (const addr of addrs) { if (!addr.internal && addr.family === 'IPv4') { return addr.address } } } return null } function noHalfOpen () { this.end() } module.exports.NetworkResource = NetworkResource