You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

314 lines
8.0 KiB

var udp = require('udp-request')
var crypto = require('crypto')
var KBucket = require('k-bucket')
var inherits = require('inherits')
var events = require('events')
var peers = require('ipv4-peers')
var bufferEquals = require('buffer-equals')
var nodes = peers.idLength(32)
var messages = require('./messages')
module.exports = DHT
function DHT (opts) {
if (!(this instanceof DHT)) return new DHT(opts)
if (!opts) opts = {}
events.EventEmitter.call(this)
var self = this
this.concurrency = opts.concurrency || 16
this.bootstrap = [].concat(opts.bootstrap || []).map(parseAddr)
this.id = opts.id || crypto.randomBytes(32)
this.nodes = new KBucket({localNodeId: this.id})
this.nodes.on('ping', onnodeping)
this.socket = udp({
requestEncoding: messages.Request,
responseEncoding: messages.Response
})
this.socket.on('request', onrequest)
this.socket.on('response', onresponse)
this.socket.on('close', onclose)
this._bootstrapped = false
this._pendingRequests = []
this._secrets = [crypto.randomBytes(32), crypto.randomBytes(32)]
this._interval = setInterval(rotateSecrets, 5 * 60 * 1000)
process.nextTick(function () {
self._bootstrap()
})
function rotateSecrets () {
self._rotateSecrets()
}
function onrequest (request, peer) {
self._onrequest(request, peer)
}
function onresponse (response, peer) {
self._onresponse(response, peer)
}
function onnodeping (oldContacts, newContact) {
self._onnodeping(oldContacts, newContact)
}
function onclose () {
while (self._pendingRequests.length) {
self._pendingRequests.shift().callback(new Error('Request cancelled'))
}
self.emit('close')
}
}
inherits(DHT, events.EventEmitter)
DHT.prototype.ping = function (peer, cb) {
this._ping(parseAddr(peer), function (err, res, peer) {
if (err) return cb(err)
var rinfo = decodePeer(res.value)
if (!rinfo) return cb(new Error('Invalid pong'))
cb(null, rinfo, {port: peer.port, host: peer.host, id: res.id})
})
}
DHT.prototype.destroy = function () {
this.socket.destroy()
}
DHT.prototype._rotateSecrets = function () {
var secret = crypto.randomBytes(32)
this._secrets[1] = this._secrets[0]
this._secrets[0] = secret
}
DHT.prototype._bootstrap = function () {
// TODO: run in the background
// TODO: check stats, to determine wheather to rerun?
var self = this
this._closest({command: '_find_node', target: this.id, id: this.id}, null, function (err) {
if (err) return self.emit('error', err)
self._bootstrapped = true
self.emit('ready')
})
}
DHT.prototype._closest = function (request, onresponse, cb) {
if (!cb) cb = noop
var self = this
var target = request.target
var stats = {responses: 0, errors: 0}
// var table = new KBucket({localNodeId: target})
var table = require('./table')(target)
var requested = {}
var inflight = 0
var bootstrap = this.nodes.closest(target, 20)
if (bootstrap.length < this.bootstrap.length) bootstrap.push.apply(bootstrap, this.bootstrap)
bootstrap.forEach(send)
if (!inflight) cb(null, stats, table)
function send (peer) {
var addr = peer.host + ':' + peer.port
if (requested[addr]) return
requested[addr] = true
inflight++
self._request(request, peer, false, next)
}
function next (err, res, peer) {
inflight--
if (err) {
stats.errors++
} else {
stats.responses++
if (res.id) {
// var prev = table.get(res.id)
// if (prev) prev.roundtripToken = res.roundtripToken
}
// TODO: do not add nodes to table.
// instead merge-sort with table so we only add nodes that actually respond
var n = decodeNodes(res.nodes)
for (var i = 0; i < n.length; i++) {
if (!bufferEquals(n[i].id, self.id)) table.add(n[i])
}
if (onresponse) onresponse(res, peer)
}
table.closest(20).forEach(send)
if (!inflight) {
cb(null, stats, table)
}
}
}
DHT.prototype._ping = function (peer, cb) {
this._request({command: '_ping', id: this.id}, peer, false, cb)
}
DHT.prototype._request = function (request, peer, important, cb) {
if (this.socket.inflight >= this.concurrency || this._pendingRequests.length) {
this._pendingRequests.push({request: request, peer: peer, callback: cb})
} else {
this.socket.request(request, peer, cb)
}
}
DHT.prototype._onrequest = function (request, peer) {
if (validateId(request.id)) this._addNode(request.id, peer, request.roundtripToken)
var forwardRequest = decodePeer(request.forwardRequest)
if (forwardRequest) { // TODO: security stuff
console.error('TODO: [forward request]', forwardRequest)
}
var forwardResponse = decodePeer(request.forwardResponse)
if (forwardResponse) {
console.error('TODO: [forward response]', forwardResponse)
}
if (request.roundtripToken) {
if (!bufferEquals(request.roundtripToken, this._token(peer, 0))) {
if (!bufferEquals(request.roundtripToken, this._token(peer, 1))) {
request.roundtripToken = null
}
}
}
switch (request.command) {
case '_ping': return this._onping(request, peer)
case '_find_node': return this._onfindnode(request, peer)
}
this._onquery(request, peer)
}
DHT.prototype._onquery = function (request, peer) {
var self = this
var query = {
node: {
id: request.id,
port: peer.port,
host: peer.host
},
command: request.command,
target: request.target,
value: request.value,
roundtripToken: request.roundtripToken
}
if (!this.emit('query', query, callback)) callback()
function callback (err, value) {
// TODO: support errors?
var res = {
id: self.id,
value: value || null,
nodes: nodes.encode(self.nodes.closest(request.target, 20)),
roundtripToken: self._token(peer, 0)
}
self.socket.response(res, peer)
}
}
DHT.prototype._onresponse = function (response, peer) {
if (validateId(response.id)) this._addNode(response.id, peer, response.roundtripToken)
while (this.socket.inflight < this.concurrency && this._pendingRequests.length) {
var next = this._pendingRequests.shift()
this.socket.request(next.request, next.peer, next.callback)
}
}
DHT.prototype._onping = function (request, peer) {
var res = {
id: this.id,
value: peers.encode([peer]),
roundtripToken: this._token(peer, 0)
}
this.socket.response(res, peer)
}
DHT.prototype._onfindnode = function (request, peer) {
if (!validateId(request.target)) return
var res = {
id: this.id,
nodes: nodes.encode(this.nodes.closest(request.target, 20)),
roundtripToken: this._token(peer, 0)
}
this.socket.response(res, peer)
}
DHT.prototype._onnodeping = function (oldContacts, newContact) {
if (!this._bootstrapped) return // bootstrapping, we've recently pinged all nodes
// TODO: record if we've recently pinged oldContacts, no need to flood them with new pings then
// console.log('onnodeping', this.bootstrap.length, this._bootstrapped, oldContacts.length)
for (var i = 0; i < oldContacts.length; i++) {
this.nodes.add(oldContacts[i])
}
}
DHT.prototype._token = function (peer, i) {
return crypto.createHash('sha256').update(this._secrets[i]).update(peer.host).digest()
}
DHT.prototype._addNode = function (id, peer, token) {
if (bufferEquals(id, this.id)) return
this.nodes.add({id: id, roundtripToken: token, port: peer.port, host: peer.host})
}
DHT.prototype.listen = function (port, cb) {
this.socket.listen(port, cb)
}
function noop () {}
function decodeNodes (buf) {
if (!buf) return []
try {
return nodes.decode(buf)
} catch (err) {
return []
}
}
function encodePeer (peer) {
return peer && peers.encode([peer])
}
function decodePeer (buf) {
try {
return buf && peers.decode(buf)[0]
} catch (err) {
return null
}
}
function parseAddr (addr) {
if (typeof addr === 'number') return parseAddr(':' + addr)
if (addr[0] === ':') return parseAddr('127.0.0.1' + addr)
return {port: Number(addr.split(':')[1] || 3282), host: addr.split(':')[0]}
}
function validateId (id) {
return id && id.length === 32
}