discovery-swarm
Version:
A network swarm that uses discovery-channel to find peers
555 lines (457 loc) • 15.1 kB
JavaScript
var discovery = require('discovery-channel')
var pump = require('pump')
var events = require('events')
var util = require('util')
var net = require('net')
var toBuffer = require('to-buffer')
var crypto = require('crypto')
var lpmessage = require('length-prefixed-message')
var connections = require('connections')
var debug = require('debug')('discovery-swarm')
try {
var utp = require('utp-native')
} catch (err) {
// do nothing
}
var PEER_SEEN = 1
var PEER_BANNED = 2
var HANDSHAKE_TIMEOUT = 5000
var CONNECTION_TIMEOUT = 3000
var RECONNECT_WAIT = [1000, 1000, 5000, 15000]
// var DEFAULT_SIZE = 100 // TODO enable max connections
module.exports = Swarm
function Swarm (opts) {
if (!(this instanceof Swarm)) return new Swarm(opts)
if (!opts) opts = {}
events.EventEmitter.call(this)
var self = this
this.maxConnections = opts.maxConnections || 0
this.totalConnections = 0
this.connections = []
this.id = opts.id || crypto.randomBytes(32)
this.destroyed = false
this._stream = opts.stream
this._options = opts || {}
this._whitelist = opts.whitelist || []
this._discovery = null
this._tcp = opts.tcp === false ? null : net.createServer().on('connection', onconnection)
this._utp = opts.utp === false || !utp ? null : utp().on('connection', onconnection)
this._tcpConnections = this._tcp && connections(this._tcp)
this._adding = null
this._listening = false
this._keepExistingConnections = (opts.keepExistingConnections === true)
this._peersIds = {}
this._peersSeen = {}
this._peersQueued = []
if (this._options.discovery !== false) {
this.on('listening', this._ondiscover)
}
function onconnection (connection) {
var type = this === self._tcp ? 'tcp' : 'utp'
debug('inbound connection type=%s ip=%s:%d', type, connection.remoteAddress, connection.remotePort)
connection.on('error', onerror)
self.totalConnections++
self._onconnection(connection, type, null)
}
}
util.inherits(Swarm, events.EventEmitter)
Swarm.prototype.close =
Swarm.prototype.destroy = function (onclose) {
if (this.destroyed) return process.nextTick(onclose || noop)
if (onclose) this.once('close', onclose)
if (this._listening && this._adding) return this.once('listening', this.destroy)
this.destroyed = true
if (this._discovery) this._discovery.destroy()
var self = this
var missing = 0
if (this._utp) {
missing++
for (var i = 0; i < this._utp.connections.length; i++) {
this._utp.connections[i].destroy()
}
}
if (this._tcp) {
missing++
this._tcpConnections.destroy()
}
if (this._listening) {
if (this._tcp) this._tcp.close(onserverclose)
if (this._utp) this._utp.close(onserverclose)
} else {
this.emit('close')
}
function onserverclose () {
if (!--missing) self.emit('close')
}
}
Swarm.prototype.__defineGetter__('queued', function () {
return this._peersQueued.length
})
Swarm.prototype.__defineGetter__('connecting', function () {
return this.totalConnections - this.connections.length
})
Swarm.prototype.__defineGetter__('connected', function () {
return this.connections.length
})
Swarm.prototype.join = function (name, opts, cb) {
if (typeof opts === 'function') return this.join(name, {}, opts)
name = toBuffer(name)
if (!opts) opts = {}
if (typeof opts.announce === 'undefined') opts.announce = true
if (!this._listening && !this._adding) this._listenNext()
if (this._adding) {
this._adding.push({ name: name, opts: opts, cb: cb })
} else {
var port
if (opts.announce) port = this.address().port
this._discovery.join(name, port, { impliedPort: opts.announce && !!this._utp }, cb)
}
}
Swarm.prototype.leave = function (name) {
name = toBuffer(name)
if (this._adding) {
for (var i = 0; i < this._adding.length; i++) {
if (name.equals(this._adding[i].name)) {
this._adding.splice(i, 1)
return
}
}
} else {
this._discovery.leave(name, this.address() ? this.address().port : 0)
}
}
Swarm.prototype.addPeer = function (name, peer) {
peer = peerify(peer, toBuffer(name))
if (this._peersSeen[peer.id]) return
if (this._whitelist.length && this._whitelist.indexOf(peer.host) === -1) return
this._peersSeen[peer.id] = PEER_SEEN
this._peersQueued.push(peer)
this.emit('peer', peer)
this._kick()
}
Swarm.prototype.removePeer = function (name, peer) {
peer = peerify(peer, toBuffer(name))
this._peersSeen[peer.id] = PEER_BANNED
this.emit('peer-banned', peer, { reason: 'application' })
}
Swarm.prototype._dropPeer = function (peer) {
delete this._peersSeen[peer.id]
this.emit('drop', peer)
}
Swarm.prototype.address = function () {
return this._tcp ? this._tcp.address() : this._utp.address()
}
Swarm.prototype._ondiscover = function () {
var self = this
var joins = this._adding
if (this._options.dns !== false) {
if (!this._options.dns || this._options.dns === true) this._options.dns = {}
this._options.dns.socket = this._utp
}
if (this._options.dht !== false) {
if (!this._options.dht || this._options.dht === true) this._options.dht = {}
this._options.dht.socket = this._utp
}
this._discovery = discovery(this._options)
this._discovery.on('peer', onpeer)
this._discovery.on('whoami', onwhoami)
this._adding = null
if (!joins) return
for (var i = 0; i < joins.length; i++) this.join(joins[i].name, joins[i].opts, joins[i].cb)
function onwhoami (me) {
self._peersSeen[me.host + ':' + me.port] = PEER_BANNED
}
function onpeer (channel, peer) {
var id = peer.host + ':' + peer.port
var longId = id + '@' + (channel ? channel.toString('hex') : '')
if (self._whitelist.length && self._whitelist.indexOf(peer.host) === -1) {
self.emit('peer-rejected', peer, { reason: 'whitelist' })
return
}
var peerSeen = self._peersSeen[id] || self._peersSeen[longId]
var peerConnected = self._peersIds[id] || self._peersIds[longId]
if (peerSeen && peerSeen === PEER_BANNED) {
self.emit('peer-rejected', peer, { reason: 'banned' })
return
} else if (peerSeen && peerConnected) {
self.emit('peer-rejected', peer, { reason: 'duplicate' })
return
}
self._peersSeen[longId] = PEER_SEEN
self._peersQueued.push(peerify(peer, channel))
self.emit('peer', peer)
self._kick()
}
}
Swarm.prototype._kick = function () {
if (this.maxConnections && this.totalConnections >= this.maxConnections) return
if (this.destroyed) return
var self = this
var connected = false
var didTimeOut = false
var next = this._peersQueued.shift()
while (next && this._peersSeen[next.id] === PEER_BANNED) {
next = this._peersQueued.shift()
}
if (!next) return
this.totalConnections++
this.emit('connecting', next)
debug('connecting %s retries=%d', next.id, next.retries)
var tcpSocket = null
var utpSocket = null
var tcpClosed = true
var utpClosed = true
if (this._tcp) {
tcpClosed = false
tcpSocket = net.connect(next.port, next.host)
tcpSocket.on('connect', onconnect)
tcpSocket.on('error', onerror)
tcpSocket.on('close', onclose)
this._tcpConnections.add(tcpSocket)
}
if (this._utp) {
utpClosed = false
utpSocket = this._utp.connect(next.port, next.host)
utpSocket.on('connect', ondeferredconnect)
utpSocket.on('error', onerror)
utpSocket.on('close', onclose)
}
var timeout = setTimeoutUnref(ontimeout, CONNECTION_TIMEOUT)
function ondeferredconnect () {
if (!self._tcp || tcpClosed) return onconnect.call(utpSocket)
setTimeout(function () {
if (!utpClosed && !connected) onconnect.call(utpSocket)
}, 500)
}
function ontimeout () {
debug('timeout %s', next.id)
didTimeOut = true
if (utpSocket) utpSocket.destroy()
if (tcpSocket) tcpSocket.destroy()
}
function cleanup () {
clearTimeout(timeout)
if (utpSocket) utpSocket.removeListener('close', onclose)
if (tcpSocket) tcpSocket.removeListener('close', onclose)
}
function onclose () {
if (this === utpSocket) utpClosed = true
if (this === tcpSocket) tcpClosed = true
if (tcpClosed && utpClosed) {
debug('onclose utp+tcp %s will-requeue=%d', next.id, !connected)
cleanup()
if (!connected) {
self.totalConnections--
self.emit('connect-failed', next, { timedout: didTimeOut })
self._requeue(next)
}
}
}
function onconnect () {
connected = true
cleanup()
var type = this === utpSocket ? 'utp' : 'tcp'
debug('onconnect %s type=%s', next.id, type)
if (type === 'utp' && tcpSocket) tcpSocket.destroy()
if (type === 'tcp' && utpSocket) utpSocket.destroy()
self._onconnection(this, type, next)
}
}
Swarm.prototype._requeue = function (peer) {
if (this.destroyed) return
var self = this
var wait = peer.retries >= RECONNECT_WAIT.length ? 0 : RECONNECT_WAIT[peer.retries++]
if (wait) setTimeoutUnref(requeue, wait)
else this._dropPeer(peer)
function requeue () {
self._peersQueued.push(peer)
self._kick()
}
}
var connectionDebugIdCounter = 0
Swarm.prototype._onconnection = function (connection, type, peer) {
var self = this
var idHex = this.id.toString('hex')
var remoteIdHex
// internal variables used for debugging
connection._debugId = ++connectionDebugIdCounter
connection._debugStartTime = Date.now()
var info = {
type: type,
initiator: !!peer,
id: null,
host: peer ? peer.host : connection.remoteAddress,
port: peer ? peer.port : connection.remotePort,
channel: peer ? peer.channel : null
}
this.emit('handshaking', connection, info)
connection.on('close', onclose)
if (this._stream) {
var wire = connection
connection = this._stream(info)
connection._debugId = wire._debugId
connection._debugStartTime = wire._debugStartTime
if (connection.id) idHex = connection.id.toString('hex')
connection.on('handshake', onhandshake)
if (this._options.connect) this._options.connect(connection, wire)
else pump(wire, connection, wire)
} else {
handshake(connection, this.id, onhandshake)
}
var wrap = {
info: info,
connection: connection
}
var timeout = setTimeoutUnref(ontimeout, HANDSHAKE_TIMEOUT)
if (this.destroyed) connection.destroy()
function ontimeout () {
self.emit('handshake-timeout', connection, info)
connection.destroy()
}
function onclose () {
clearTimeout(timeout)
self.totalConnections--
self.emit('connection-closed', connection, info)
var i = self.connections.indexOf(connection)
if (i > -1) {
var last = self.connections.pop()
if (last !== connection) self.connections[i] = last
}
if (remoteIdHex && self._peersIds[remoteIdHex] && self._peersIds[remoteIdHex].connection === connection) {
delete self._peersIds[remoteIdHex]
if (peer) self._requeue(peer)
}
}
function onhandshake (remoteId) {
if (!remoteId) remoteId = connection.remoteId
clearTimeout(timeout)
remoteIdHex = remoteId.toString('hex')
if (Buffer.isBuffer(connection.discoveryKey) || Buffer.isBuffer(connection.channel)) {
var suffix = '@' + (connection.discoveryKey || connection.channel).toString('hex')
remoteIdHex += suffix
idHex += suffix
}
if (peer) peer.retries = 0
if (idHex === remoteIdHex) {
if (peer) {
self._peersSeen[peer.id] = PEER_BANNED
self.emit('peer-banned', { peer: peer, reason: 'detected-self' })
}
connection.destroy()
return
}
var oldWrap = self._peersIds[remoteIdHex]
var old = oldWrap && oldWrap.connection
var oldType = oldWrap && oldWrap.info.type
if (old && self._keepExistingConnections) {
self.emit('redundant-connection', connection, info)
connection.destroy()
return
}
if (old) {
debug('duplicate connections detected in handshake, dropping one')
if (!(oldType === 'utp' && type === 'tcp')) {
if ((peer && remoteIdHex < idHex) || (!peer && remoteIdHex > idHex) || (type === 'utp' && oldType === 'tcp')) {
self.emit('redundant-connection', connection, info)
connection.destroy()
return
}
}
self.emit('redundant-connection', old, info)
delete self._peersIds[remoteIdHex] // delete to not trigger re-queue
old.destroy()
old = null // help gc
}
self._peersIds[remoteIdHex] = wrap
self.connections.push(connection)
info.id = remoteId
self.emit('connection', connection, info)
}
}
Swarm.prototype._listenNext = function () {
var self = this
if (!this._adding) this._adding = []
process.nextTick(function () {
if (!self._listening) self.listen()
})
}
Swarm.prototype.listen = function (port, onlistening) {
if (this.destroyed) return
if (this._tcp && this._utp) return this._listenBoth(port, onlistening)
if (!port) port = 0
if (onlistening) this.once('listening', onlistening)
var self = this
var server = this._tcp || this._utp
if (!this._listening) {
this._listening = true
server.on('error', onerror)
server.on('listening', onlisten)
}
if (!this._adding) this._adding = []
server.listen(port)
function onerror (err) {
self.emit('error', err)
}
function onlisten () {
self.emit('listening')
}
}
Swarm.prototype._listenBoth = function (port, onlistening) {
if (typeof port === 'function') return this.listen(0, port)
if (!port) port = 0
if (onlistening) this.once('listening', onlistening)
var self = this
if (!this._adding) this._adding = []
this._listening = true
this._utp.on('error', onerror)
this._utp.on('listening', onutplisten)
this._tcp.on('listening', ontcplisten)
this._tcp.on('error', onerror)
this._tcp.listen(port)
function cleanup () {
self._utp.removeListener('error', onerror)
self._tcp.removeListener('error', onerror)
self._utp.removeListener('listening', onutplisten)
self._tcp.removeListener('listening', ontcplisten)
}
function onerror (err) {
cleanup()
self._tcp.close(function () {
if (!port) return self.listen() // retry
self.emit('error', err)
})
}
function onutplisten () {
cleanup()
self._utp.on('error', forward)
self._tcp.on('error', forward)
self.emit('listening')
}
function ontcplisten () {
self._utp.listen(this.address().port)
}
function forward (err) {
self.emit('error', err)
}
}
function handshake (socket, id, cb) {
lpmessage.write(socket, id)
lpmessage.read(socket, cb)
}
function onerror () {
this.destroy()
}
function peerify (peer, channel) {
if (typeof peer === 'number') peer = { port: peer }
if (!peer.host) peer.host = '127.0.0.1'
peer.id = peer.host + ':' + peer.port + '@' + (channel ? channel.toString('hex') : '')
peer.retries = 0
peer.channel = channel
return peer
}
function setTimeoutUnref (fn, time) {
var timeout = setTimeout(fn, time)
if (timeout.unref) timeout.unref()
return timeout
}
function noop () {}