Browse Source

Next version (#2)

* rebase on top old history

* fix dep

* more missing deps

* updated docs

* impl readme api

* final tweaks

* more tests

* revert version

* remove dead code
v4
Mathias Buus 7 years ago
committed by GitHub
parent
commit
3a05e9c03e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 175
      README.md
  3. 74
      example.js
  4. 564
      index.js
  5. 0
      lib/blake2b.js
  6. 299
      lib/io.js
  7. 272
      lib/messages.js
  8. 264
      lib/query-stream.js
  9. 69
      lib/query-table.js
  10. 45
      package.json
  11. 304
      query-stream.js
  12. 37
      schema.proto
  13. 221
      test.js

1
.gitignore

@ -1 +1,2 @@
node_modules node_modules
sandbox.js

175
README.md

@ -5,8 +5,15 @@ Make RPC calls over a [Kademlia](https://pdos.csail.mit.edu/~petar/papers/maymou
``` ```
npm install dht-rpc npm install dht-rpc
``` ```
[![build status](http://img.shields.io/travis/mafintosh/dht-rpc.svg?style=flat)](http://travis-ci.org/mafintosh/dht-rpc) [![build status](http://img.shields.io/travis/mafintosh/dht-rpc.svg?style=flat)](http://travis-ci.org/mafintosh/dht-rpc)
## Key Features
* UDP hole punching support
* Easily add any command to your DHT
* Streaming queries and updates
## Usage ## Usage
Here is an example implementing a simple key value store Here is an example implementing a simple key value store
@ -14,10 +21,10 @@ Here is an example implementing a simple key value store
First spin up a bootstrap node. You can make multiple if you want for redundancy. First spin up a bootstrap node. You can make multiple if you want for redundancy.
``` js ``` js
var dht = require('dht-rpc') const dht = require('dht-rpc')
// Set ephemeral: true so other peers do not add us to the peer list, simply bootstrap // Set ephemeral: true so other peers do not add us to the peer list, simply bootstrap
var bootstrap = dht({ephemeral: true}) const bootstrap = dht({ ephemeral: true })
bootstrap.listen(10001) bootstrap.listen(10001)
``` ```
@ -25,39 +32,42 @@ bootstrap.listen(10001)
Now lets make some dht nodes that can store values in our key value store. Now lets make some dht nodes that can store values in our key value store.
``` js ``` js
var dht = require('dht-rpc') const dht = require('dht-rpc')
var crypto = require('crypto') const crypto = require('crypto')
// Let's create 100 dht nodes for our example. // Let's create 100 dht nodes for our example.
for (var i = 0; i < 100; i++) createNode() for (var i = 0; i < 100; i++) createNode()
function createNode () { function createNode () {
var node = dht({ const node = dht({
bootstrap: ['localhost:10001'] bootstrap: [
'localhost:10001'
]
}) })
var values = {} const values = new Map()
node.command('values', {
// When we are the closest node and someone is sending us a "store" command // When we are the closest node and someone is sending us a "store" command
node.on('update:values', function (query, cb) { update (query, cb) {
if (!query.value) return cb() if (!query.value) return cb()
// Use the hash of the value as the key // Use the hash of the value as the key
var key = sha256(query.value).toString('hex') const key = sha256(query.value).toString('hex')
values[key] = query.value values.set(key, query.value)
console.log('Storing', key, '-->', query.value.toString()) console.log('Storing', key, '-->', query.value.toString())
cb() cb()
}) },
// When someone is querying for a "lookup" command // When someone is querying for a "lookup" command
node.on('query:values', function (query, cb) { query (query, cb) {
var value = values[query.target.toString('hex')] const value = values[query.target.toString('hex')]
cb(null, value) cb(null, value)
}
}) })
} }
function sha256 (val) { function sha256 (val) {
return crypto.createHash('sha256').update(val).digest('hex') return crypto.createHash('sha256').update(val).digest()
} }
``` ```
@ -65,9 +75,9 @@ To insert a value into this dht make another script that does this following
``` js ``` js
// Set ephemeral: true as we are not part of the network. // Set ephemeral: true as we are not part of the network.
var node = dht({ephemeral: true}) const node = dht({ ephemeral: true })
node.update({command: 'values', target: sha256(val), value: val}, function (err, res) { node.update('values', sha256(val), value, function (err, res) {
if (err) throw err if (err) throw err
console.log('Inserted', sha256(val).toString('hex')) console.log('Inserted', sha256(val).toString('hex'))
}) })
@ -76,7 +86,7 @@ node.update({command: 'values', target: sha256(val), value: val}, function (err,
Then after inserting run this script to query for a value Then after inserting run this script to query for a value
``` js ``` js
node.query({command: 'values', target: new Buffer(hexFromAbove, 'hex')}) node.query('values', Buffer.from(hexFromAbove, 'hex'))
.on('data', function (data) { .on('data', function (data) {
if (data.value && sha256(data.value).toString('hex') === hexFromAbove) { if (data.value && sha256(data.value).toString('hex') === hexFromAbove) {
// We found the value! Destroy the query stream as there is no need to continue. // We found the value! Destroy the query stream as there is no need to continue.
@ -91,74 +101,135 @@ node.query({command: 'values', target: new Buffer(hexFromAbove, 'hex')})
## API ## API
#### `var node = dht([options])` #### `const node = dht([options])`
Create a new DHT node.
Create a new DHT node. Options include Options include:
```js ```js
{ {
id: nodeId, // id of the node // Whether or not this node is ephemeral or should join the routing table
ephemeral: false, // will this node answer queries? ephemeral: false,
bootstrap: ['host:port'], // bootstrap nodes // A list of bootstrap nodes
socket: udpSocket // optional udp socket bootstrap: [ 'bootstrap-node.com:24242', ... ],
// Optionally pass in your own UDP socket to use.
socket: udpSocket
} }
``` ```
#### `var stream = node.query(query, [options], [callback])` #### `node.command(name, cmd)`
Create a new query. Query should look like this Define a new RPC command. `cmd` should look like this
```js ```js
{ {
command: 'command-to-run', // Query handler
target: new Buffer('32 byte target'), query (query, cb),
value: new Buffer('some payload') // Update handler. only triggered when we are one of the closest nodes to the target
update (query, cb),
// Optional value encoding for the query/update incoming value. Defaults to binary.
inputEncoding: 'json', 'utf-8', object,
// Optional value encoding for the query/update outgoing value. Defaults to binary.
outputEncoding: (same as above),
valueEncoding: (sets both input/output encoding to this)
} }
``` ```
And options include The `query` object in the query/update function looks like this:
```js ```js
{ {
nodes: [{host: 'example.com', port: 4224}], // only contact these nodes // always the same as your command def
holepunching: true // set to false to disable hole punching command: 'command-name',
// the node who sent the query/update
node: { port, host, id },
// the query/update target (32 byte target)
target: Buffer,
// the query/update payload decoded with the inputEncoding
value
} }
``` ```
The stream will emit query results as they arrive. If you backpressure the query it will backpressure the query as well. You should call the query/update callback with `(err, value)` where
Call `.destroy()` on the stream to cancel the query. If you pass the callback the streams payload will be buffered and passed to that. value will be encoded using the outputEncoding and returned to the node.
#### `var stream = node.update(query, [options], [callback])` #### `const stream = node.query(name, target, [value], [callback])`
Same as a query but will trigger an update query on the 20 closest nodes (distance between node ids and target) after the query finishes. Send a query command.
Per default the stream will only contain results from the closest query. To include the query results also pass the `query: true` option.
If you set a valueEncoding when defining the command the value will be encoded.
Returns a result stream that emits data that looks like this:
```js
{
// was this a query/update response
type: dht.QUERY,
// who sent this response
node: { peer, host, id },
// the response payload decoded using the outputEncoding
value
}
```
#### `node.on('query:{command}', data, callback)` If you pass a callback the stream will be error handled and buffered
and the content passed as an array.
Called when a specific query is invoked on a node. `data` contains the same values as in the query above and also a `.node` property with info about the node invoking the query. #### `const stream = node.update(name, target, [value], [callback])`
Call the callback with `(err, value)` to respond. Send a update command
#### `node.on('update:{command}', data, callback)` Same options/results as above but the response data will have `type`
set to `dht.UPDATE`.
Called when an update query is invoked. The `data.node` is also guaranteed to have roundtripped to this dht before, meaning that you can trust that the host, port was not spoofed. #### `const stream = node.queryAndUpdate(name, target, [value], [callback])`
#### `node.ready(callback)` Send a combined query and update command.
Makes sure the initial bootstrap table has been built. You do not need to wait for this before querying. Will keep querying until it finds the closest nodes to the target and then
issue an update. More efficient than doing a query/update yourself.
Same options/results as above but the response data will include both
query and update results.
#### `node.destroy(onclose)`
Fully destroys the dht node.
#### `node.bootstrap(cb)`
Re-bootstrap the DHT node. Normally you shouldn't have to call this.
#### `node.holepunch(peer, cb)`
UDP holepunch to another peer. The DHT does this automatically
when it cannot reach another peer but you can use this yourself also.
Peer should look like this:
```js
{
port,
host,
// referrer should be the node/peer that
// told you about this node.
referrer: { port, host }
}
```
#### `node.bootstrap([callback])` #### `node.listen([port], [address], [onlistening])`
Rebootstrap your node. Call this at regular intervals if you aren't doing any other queries. Explicitly bind the dht node to a certain port/address.
#### `node.holepunch(peer, referrer, callback)` #### `node.on('listening')`
UDP hole punch to another peer using the `referrer` as a STUN server. Emitted when the node starts listening on a udp port.
#### `node.destroy()` #### `node.on('close')`
Destroy the dht node. Releases all resources. Emitted when the node is fully closed.
## License #### `node.on('holepunch', fromPeer, toPeer)`
MIT Emitted when the node is helping `fromPeer` udp holepunch to `toPeer`.

74
example.js

@ -1,46 +1,50 @@
var dht = require('./') const dht = require('./')
var blake2b = require('./blake2b')
var node = dht({ const bootstrap = dht()
bootstrap: 'localhost:49737', bootstrap.listen(10001)
ephemeral: !!process.argv[2]
})
var values = {} const nodes = []
var swarm = 1000
loop(null)
node.on('update:store', function (query, cb) { function loop (err) {
console.log('(onupdate)') if (err) throw err
if (!query.value) return cb() if (swarm--) addNode(loop)
var key = blake2b(query.value).toString('hex') else done()
values[key] = query.value }
console.log('Storing', key, '-->', query.value.toString())
cb()
})
node.on('query:lookup', function (query, cb) { function done () {
console.log('(onquery)') console.log('executing hi update')
var value = values[query.target.toString('hex')]
cb(null, value)
})
if (process.argv.length > 3) { const i = Math.floor(Math.random() * nodes.length)
var val = process.argv.slice(3).join(' ') const rs = nodes[i].update('hi', Buffer.alloc(32))
if (process.argv[2] === 'put') {
node.update({command: 'store', target: blake2b(Buffer.from(val)), value: val}, function (err) { rs.resume()
if (err) throw err rs.on('end', function () {
console.log('Inserted', blake2b(Buffer.from(val)).toString('hex')) setTimeout(done, 2000)
}) })
} }
if (process.argv[2] === 'get') {
node.query({command: 'lookup', target: Buffer.from(val, 'hex')}) function addNode (cb) {
.on('data', function (data) { const node = dht({
if (data.value && blake2b(data.value).toString('hex') === val) { bootstrap: [
console.log(val, '-->', data.value.toString()) 10001
this.destroy() ]
})
var hits = 0
node.command('hi', {
update (query, cb) {
console.log('hi', ++hits)
cb(null)
},
query (query, cb) {
cb(null)
} }
}) })
.on('end', function () {
console.log('(query finished)') node.once('ready', function () {
nodes.push(node)
cb()
}) })
} }
}

564
index.js

@ -1,446 +1,384 @@
var udp = require('udp-request') const { EventEmitter } = require('events')
var KBucket = require('k-bucket') const peers = require('ipv4-peers')
var inherits = require('inherits') const dgram = require('dgram')
var events = require('events') const sodium = require('sodium-universal')
var peers = require('ipv4-peers') const KBucket = require('k-bucket')
var collect = require('stream-collector') const tos = require('time-ordered-set')
var sodium = require('sodium-universal') const collect = require('stream-collector')
var tos = require('time-ordered-set') const codecs = require('codecs')
var nodes = peers.idLength(32) const { Message, Holepunch } = require('./lib/messages')
var messages = require('./messages') const IO = require('./lib/io')
var queryStream = require('./query-stream') const QueryStream = require('./lib/query-stream')
var blake2b = require('./blake2b') const blake2b = require('./lib/blake2b')
module.exports = DHT const UNSUPPORTED_COMMAND = new Error('Unsupported command')
const nodes = peers.idLength(32)
function DHT (opts) {
if (!(this instanceof DHT)) return new DHT(opts) exports = module.exports = opts => new DHT(opts)
class DHT extends EventEmitter {
constructor (opts) {
if (!opts) opts = {} if (!opts) opts = {}
events.EventEmitter.call(this) super()
var self = this this.bootstrapped = false
this.destroyed = false
this.concurrency = opts.concurrency || 16 this.concurrency = 16
this.id = opts.id || randomBytes(32) this.socket = dgram.createSocket('udp4')
this.ephemeral = !!opts.ephemeral this.id = randomBytes(32)
this.bucket = new KBucket({localNodeId: this.id, arbiter: arbiter})
this.bucket.on('ping', onnodeping)
this.inflightQueries = 0 this.inflightQueries = 0
this.ephemeral = !!opts.ephemeral
this.socket = udp({ this.nodes = tos()
socket: opts.socket, this.bucket = new KBucket({ localNodeId: this.id })
requestEncoding: messages.Request, this.bucket.on('ping', this._onnodeping.bind(this))
responseEncoding: messages.Response this.bootstrapNodes = [].concat(opts.bootstrap || []).map(parsePeer)
})
this.socket.on('request', onrequest) this.socket.on('listening', this.emit.bind(this, 'listening'))
this.socket.on('response', onresponse) this.socket.on('close', this.emit.bind(this, 'close'))
this.socket.on('close', onclose)
this.nodes = tos() const queryId = this.ephemeral ? null : this.id
const io = new IO(this.socket, queryId, this)
this._bootstrap = [].concat(opts.bootstrap || []).map(parseAddr) this._io = io
this._queryId = this.ephemeral ? null : this.id this._commands = new Map()
this._bootstrapped = false
this._pendingRequests = []
this._tick = 0 this._tick = 0
this._secrets = [randomBytes(32), randomBytes(32)] this._tickInterval = setInterval(this._ontick.bind(this), 5000)
this._secretsInterval = setInterval(rotateSecrets, 5 * 60 * 1000)
this._tickInterval = setInterval(tick, 5 * 1000)
if (opts.nodes) { process.nextTick(this.bootstrap.bind(this))
for (var i = 0; i < opts.nodes.length; i++) {
this._addNode(opts.nodes[i].id, opts.nodes[i])
} }
}
process.nextTick(function () {
self.bootstrap()
})
function rotateSecrets () { _ontick () {
self._rotateSecrets() this._tick++
if ((this._tick & 7) === 0) this._pingSome()
} }
function onrequest (request, peer) { address () {
self._onrequest(request, peer) return this.socket.address()
} }
function onresponse (response, peer) { command (name, opts) {
self._onresponse(response, peer) this._commands.set(name, {
inputEncoding: codecs(opts.inputEncoding || opts.valueEncoding),
outputEncoding: codecs(opts.outputEncoding || opts.valueEncoding),
query: opts.query || queryNotSupported,
update: opts.update || updateNotSupported
})
} }
function onnodeping (oldContacts, newContact) { ready (onready) {
self._onnodeping(oldContacts, newContact) if (!this.bootstrapped) this.once('ready', onready)
else onready()
} }
function onclose () { onrequest (type, message, peer) {
while (self._pendingRequests.length) { if (validateId(message.id)) {
self._pendingRequests.shift().callback(new Error('Request cancelled')) this._addNode(message.id, peer, null)
}
self.emit('close')
} }
function tick () { switch (message.command) {
self._tick++ case '_ping':
if ((self._tick & 7) === 0) self._pingSome() return this._onping(message, peer)
}
}
inherits(DHT, events.EventEmitter) case '_find_node':
return this._onfindnode(message, peer)
DHT.prototype.ready = function (cb) { case '_holepunch':
if (!this._bootstrapped) this.once('ready', cb) return this._onholepunch(message, peer)
else cb()
}
DHT.prototype.query = function (query, opts, cb) { default:
if (typeof opts === 'function') return this.query(query, null, opts) return this._oncommand(type, message, peer)
return collect(queryStream(this, query, opts), cb)
} }
DHT.prototype.update = function (query, opts, cb) {
if (typeof opts === 'function') return this.update(query, null, opts)
if (!opts) opts = {}
if (opts.query) opts.verbose = true
opts.token = true
return collect(queryStream(this, query, opts), cb)
} }
DHT.prototype._pingSome = function () { _onping (message, peer) {
var cnt = this.inflightQueries > 2 ? 1 : 3 if (message.value && !this.id.equals(message.value)) return
var oldest = this.nodes.oldest this._io.response(message, peers.encode([ peer ]), null, peer)
while (cnt--) {
if (!oldest || this._tick - oldest.tick < 3) continue
this._check(oldest)
oldest = oldest.next
}
} }
DHT.prototype.holepunch = function (peer, referrer, cb) { _onholepunch (message, peer) {
peer = parseAddr(peer) const value = decodeHolepunch(message.value)
referrer = parseAddr(referrer) if (!value) return
this._ping(peer, noop)
this._holepunch(peer, referrer, cb)
}
DHT.prototype.ping = function (peer, cb) { if (value.to) {
this._ping(parseAddr(peer), function (err, res, peer) { const to = decodePeer(value.to)
if (err) return cb(err) if (!to || samePeer(to, peer)) return
var rinfo = decodePeer(res.value) message.id = this._io.id
if (!rinfo) return cb(new Error('Invalid pong')) message.value = Holepunch.encode({ from: peers.encode([ peer ]) })
cb(null, rinfo, {port: peer.port, host: peer.host, id: res.id}) this.emit('holepunch', peer, to)
}) this._io.send(Message.encode(message), to)
return
} }
DHT.prototype.toArray = function () { if (value.from) {
return this.bucket.toArray() const from = decodePeer(value.from)
if (from) peer = from
} }
DHT.prototype.destroy = function () { this._io.response(message, null, null, peer)
clearInterval(this._secretsInterval)
clearInterval(this._tickInterval)
this.socket.destroy()
} }
DHT.prototype.address = function () { _onfindnode (message, peer) {
return this.socket.address() if (!validateId(message.target)) return
}
DHT.prototype._rotateSecrets = function () { const closerNodes = nodes.encode(this.bucket.closest(message.target, 20))
var secret = randomBytes(32) this._io.response(message, null, closerNodes, peer)
this._secrets[1] = this._secrets[0]
this._secrets[0] = secret
} }
DHT.prototype.bootstrap = function (cb) { _oncommand (type, message, peer) {
var self = this if (!message.target) return
if (!this._bootstrap.length) return process.nextTick(done)
var backgroundCon = Math.min(self.concurrency, Math.max(2, Math.floor(self.concurrency / 8)))
var qs = this.query({
command: '_find_node',
target: this.id
})
qs.on('data', update) const self = this
qs.on('error', onerror) const cmd = this._commands.get(message.command)
qs.on('end', done)
update() if (!cmd) return reply(UNSUPPORTED_COMMAND)
function onerror (err) { const query = {
if (cb) cb(err) type,
command: message.command,
node: peer,
target: message.target,
value: cmd.inputEncoding.decode(message.value)
} }
function done () { if (type === IO.UPDATE) cmd.update(query, reply)
if (!self._bootstrapped) { else cmd.query(query, reply)
self._bootstrapped = true
self.emit('ready') function reply (err, value) {
const closerNodes = nodes.encode(self.bucket.closest(message.target, 20))
if (err) return self._io.error(message, err, closerNodes, peer)
self._io.response(message, value && cmd.outputEncoding.encode(value), closerNodes, peer)
} }
if (cb) cb()
} }
function update () { onresponse (message, peer) {
qs._concurrency = self.inflightQueries === 1 ? self.concurrency : backgroundCon if (validateId(message.id)) {
this._addNode(message.id, peer, message.roundtripToken)
} }
} }
DHT.prototype._ping = function (peer, cb) { holepunch (peer, cb) {
this._request({command: '_ping', id: this._queryId}, peer, false, cb) if (!peer.referrer) throw new Error('peer.referrer is required')
this._io.query('_holepunch', null, null, peer, cb)
} }
DHT.prototype._holepunch = function (peer, referrer, cb) { destroy () {
// Expects the caller to have already sent a message to peer to open the firewall session this.destroyed = true
this._request({command: '_ping', id: this._queryId, forwardRequest: encodePeer(peer)}, referrer, false, cb) this._io.destroy()
clearInterval(this._tickInterval)
} }
DHT.prototype._request = function (request, peer, important, cb) { ping (peer, cb) {
if (this.socket.inflight >= this.concurrency || this._pendingRequests.length) { this._io.query('_ping', null, peer.id, peer, function (err, res) {
this._pendingRequests.push({request: request, peer: peer, callback: cb}) if (err) return cb(err)
} else { if (res.error) return cb(new Error(res.error))
this.socket.request(request, peer, cb) const pong = decodePeer(res.value)
} if (!pong) return cb(new Error('Invalid pong'))
cb(null, pong)
})
} }
DHT.prototype._onrequest = function (request, peer) { _addNode (id, peer, token) {
if (validateId(request.id)) this._addNode(request.id, peer, request.roundtripToken) if (id.equals(this.id)) return
if (request.roundtripToken) { var node = this.bucket.get(id)
if (!request.roundtripToken.equals(this._token(peer, 0))) { const fresh = !node
if (!request.roundtripToken.equals(this._token(peer, 1))) {
request.roundtripToken = null
}
}
}
if (request.forwardRequest) { if (!node) node = {}
this._forwardRequest(request, peer)
return
}
if (request.forwardResponse) peer = this._forwardResponse(request, peer) node.id = id
node.port = peer.port
node.host = peer.host
if (token) node.roundtripToken = token
node.tick = this._tick
switch (request.command) { if (!fresh) this.nodes.remove(node)
case '_ping': return this._onping(request, peer) this.nodes.add(node)
case '_find_node': return this._onfindnode(request, peer) this.bucket.add(node)
if (fresh) this.emit('add-node', node)
} }
this._onquery(request, peer) _removeNode (node) {
this.nodes.remove(node)
this.bucket.remove(node.id)
this.emit('remove-node')
} }
DHT.prototype._forwardResponse = function (request, peer) { _token (peer, i) {
if (request.command !== '_ping') return // only allow ping for now return blake2b.batch([
this._secrets[i],
try { Buffer.from(peer.host)
var from = peers.decode(request.forwardResponse)[0] ])
if (!from) return
} catch (err) {
return
} }
from.request = true _onnodeping (oldContacts, newContact) {
from.tid = peer.tid // if bootstrapping, we've recently pinged all nodes
if (!this.bootstrapped) return
return from const reping = []
}
DHT.prototype._forwardRequest = function (request, peer) { for (var i = 0; i < oldContacts.length; i++) {
if (request.command !== '_ping') return // only allow ping forwards right now const old = oldContacts[i]
try { // check if we recently talked to this peer ...
var to = peers.decode(request.forwardRequest)[0] if (this._tick === old.tick) {
if (!to) return this.bucket.add(oldContacts[i])
} catch (err) { continue
return
} }
this.emit('holepunch', peer, to) reping.push(old)
request.forwardRequest = null
request.forwardResponse = encodePeer(peer)
this.socket.forwardRequest(request, peer, to)
} }
DHT.prototype._onquery = function (request, peer) { if (reping.length) this._reping(reping, newContact)
if (!validateId(request.target)) return
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
} }
var method = request.roundtripToken ? 'update' : 'query' _check (node) {
const self = this
this.ping(node, function (err) {
if (err) self._removeNode(node)
})
}
if (!this.emit(method + ':' + request.command, query, callback) && !this.emit(method, query, callback)) callback() _reping (oldContacts, newContact) {
const self = this
function callback (err, value) { ping()
if (err) return
var res = { function ping () {
id: self._queryId, const next = oldContacts.shift()
value: value || null, if (!next) return
nodes: nodes.encode(self.bucket.closest(request.target, 20)), self._io.queryImmediately('_ping', null, next.id, next, afterPing)
roundtripToken: self._token(peer, 0)
} }
self.socket.response(res, peer) function afterPing (err, res, node) {
if (!err) return ping()
self._removeNode(node)
self.bucket.add(newContact)
} }
} }
DHT.prototype._onresponse = function (response, peer) { _pingSome () {
if (validateId(response.id)) this._addNode(response.id, peer, response.roundtripToken) var cnt = this.inflightQueries > 2 ? 1 : 3
var oldest = this.nodes.oldest
while (this.socket.inflight < this.concurrency && this._pendingRequests.length) { while (cnt--) {
var next = this._pendingRequests.shift() if (!oldest || this._tick === oldest.tick) continue
this.socket.request(next.request, next.peer, next.callback) this._check(oldest)
oldest = oldest.next
} }
} }
DHT.prototype._onping = function (request, peer) { query (command, target, value, cb) {
var res = { if (typeof value === 'function') return this.query(command, target, null, value)
id: this._queryId, return collect(this.runCommand(command, target, value, { query: true, update: false }), cb)
value: encodePeer(peer),
roundtripToken: this._token(peer, 0)
} }
this.socket.response(res, peer) update (command, target, value, cb) {
if (typeof value === 'function') return this.update(command, target, null, value)
return collect(this.runCommand(command, target, value, { query: false, update: true }), cb)
} }
DHT.prototype._onfindnode = function (request, peer) { queryAndUpdate (command, target, value, cb) {
if (!validateId(request.target)) return if (typeof value === 'function') return this.queryAndUpdate(command, target, null, value)
return collect(this.runCommand(command, target, value, { query: true, update: true }), cb)
}
var res = { runCommand (command, target, value, opts) {
id: this._queryId, return new QueryStream(this, command, target, value, opts)
nodes: nodes.encode(this.bucket.closest(request.target, 20)),
roundtripToken: this._token(peer, 0)
} }
this.socket.response(res, peer) listen (port, addr, cb) {
if (typeof port === 'function') return this.listen(0, null, port)
if (typeof addr === 'function') return this.listen(port, null, addr)
if (cb) this.once('listening', cb)
this.socket.bind(port, addr)
} }
DHT.prototype._onnodeping = function (oldContacts, newContact) { bootstrap (cb) {
if (!this._bootstrapped) return // bootstrapping, we've recently pinged all nodes const self = this
const backgroundCon = Math.min(this.concurrency, Math.max(2, Math.floor(this.concurrency / 8)))
var reping = [] if (!this.bootstrapNodes.length) return process.nextTick(done)
for (var i = 0; i < oldContacts.length; i++) { const qs = this.query('_find_node', this.id)
var old = oldContacts[i]
if (this._tick - old.tick < 3) { // less than 10s since we talked to this peer ... qs.on('data', update)
this.bucket.add(oldContacts[i]) qs.on('error', onerror)
continue qs.on('end', done)
}
reping.push(old) update()
}
if (reping.length) this._reping(reping, newContact) function onerror (err) {
if (cb) cb(err)
} }
DHT.prototype._check = function (node) { function done () {
var self = this if (!self.bootstrapped) {
this._request({command: '_ping', id: this._queryId}, node, false, function (err) { self.bootstrapped = true
if (err) self._removeNode(node) self.emit('ready')
})
} }
if (cb) cb()
DHT.prototype._reping = function (oldContacts, newContact) {
var self = this
var next = null
ping()
function ping () {
next = oldContacts.shift()
if (next) self._request({command: '_ping', id: self._queryId}, next, true, afterPing)
} }
function afterPing (err) { function update () {
if (!err) return ping() qs._concurrency = self.inflightQueries === 1 ? self.concurrency : backgroundCon
self._removeNode(next)
self.bucket.add(newContact)
} }
} }
DHT.prototype._token = function (peer, i) {
return blake2b.batch([this._secrets[i], Buffer.from(peer.host)])
} }
DHT.prototype._addNode = function (id, peer, token) { exports.QUERY = DHT.QUERY = IO.QUERY
if (id.equals(this.id)) return exports.UPDATE = DHT.UPDATE = IO.UPDATE
exports.DHT = DHT
var node = this.bucket.get(id)
var fresh = !node
if (!node) node = {}
node.id = id
node.port = peer.port
node.host = peer.host
node.roundtripToken = token
node.tick = this._tick
if (!fresh) this.nodes.remove(node) function validateId (id) {
this.nodes.add(node) return id && id.length === 32
this.bucket.add(node)
if (fresh) this.emit('add-node', node)
} }
DHT.prototype._removeNode = function (node) { function randomBytes (n) {
this.nodes.remove(node) const buf = Buffer.allocUnsafe(n)
this.bucket.remove(node.id) sodium.randombytes_buf(buf)
this.emit('remove-node', node) return buf
} }
DHT.prototype.listen = function (port, cb) { function decodeHolepunch (buf) {
this.socket.listen(port, cb) try {
return Holepunch.decode(buf)
} catch (err) {
return null
} }
function encodePeer (peer) {
return peer && peers.encode([peer])
} }
function decodePeer (buf) { function decodePeer (buf) {
try { try {
return buf && peers.decode(buf)[0] const p = peers.decode(buf)[0]
if (!p) throw new Error('No peer in buffer')
return p
} catch (err) { } catch (err) {
return null return null
} }
} }
function parseAddr (addr) { function parsePeer (peer) {
if (typeof addr === 'object' && addr) return addr if (typeof peer === 'object' && peer) return peer
if (typeof addr === 'number') return parseAddr(':' + addr) if (typeof peer === 'number') return parsePeer(':' + peer)
if (addr[0] === ':') return parseAddr('127.0.0.1' + addr) if (peer[0] === ':') return parsePeer('127.0.0.1' + peer)
return {port: Number(addr.split(':')[1] || 3282), host: addr.split(':')[0]}
}
function validateId (id) { const parts = peer.split(':')
return id && id.length === 32 return {
host: parts[0],
port: parseInt(parts[1], 10)
}
} }
function arbiter (incumbant, candidate) { function samePeer (a, b) {
return candidate return a.port === b.port && a.host === b.host
} }
function randomBytes (n) { function updateNotSupported (query, cb) {
var buf = Buffer.allocUnsafe(n) cb(new Error('Update not supported'))
sodium.randombytes_buf(buf)
return buf
} }
function noop () {} function queryNotSupported (query, cb) {
cb(null, null)
}

0
blake2b.js → lib/blake2b.js

299
lib/io.js

@ -0,0 +1,299 @@
const { Message, Holepunch, TYPE } = require('./messages')
const blake2b = require('./blake2b')
const peers = require('ipv4-peers')
const sodium = require('sodium-universal')
const QUERY = Symbol('QUERY')
const UPDATE = Symbol('UPDATE')
const ECANCELLED = new Error('Request cancelled')
const ETIMEDOUT = new Error('Request timed out')
ETIMEDOUT.code = 'ETIMEDOUT'
ECANCELLED.code = 'ECANCELLED'
const TRIES = 3
class IO {
constructor (socket, id, ctx) {
this.id = id
this.socket = socket
this.inflight = []
this._ctx = ctx
this._rid = (Math.random() * 65536) | 0
this._requests = new Array(65536)
this._pending = []
this._secrets = [ randomBytes(32), randomBytes(32) ]
this._tickInterval = setInterval(this._ontick.bind(this), 250)
this._rotateInterval = setInterval(this._onrotate.bind(this), 300000)
socket.on('message', this._onmessage.bind(this))
}
_token (peer, i) {
return blake2b.batch([
this._secrets[i],
Buffer.from(peer.host)
])
}
_free () {
const rid = this._rid++
if (this._rid === 65536) this._rid = 0
return rid
}
/*
R
/ \
A B
A sent a message to B that failed
It could be that the message got dropped
or that it needs holepunching
To retry
resend(req, A -> B)
fire_and_forget({ _holepunch, to: B }, A -> R)
R.onholepunch { to: B } => fire_and_forget({ _holepunch, from: A }, R -> B)
B.onholepunch { from: A } => fire_and_forget({ _holepunch }, B -> A)
A and B is now holepunched and the session has been retried as well
*/
_holepunch (req) {
const rid = req.message.command === '_holepunch'
? req.rid
: this._free()
const punch = {
type: req.message.type,
rid,
command: '_holepunch',
value: Holepunch.encode({
to: peers.encode([ req.peer ])
})
}
this.send(Message.encode(punch), req.peer.referrer)
}
_retry (req) {
req.timeout = 4
this.send(req.buffer, req.peer)
// if referrer is avail, try holepunching automatically
if (req.peer.referrer) this._holepunch(req)
}
_onmessage (buf, rinfo) {
const message = decodeMessage(buf)
if (!message) return
const peer = { port: rinfo.port, host: rinfo.address }
switch (message.type) {
case TYPE.RESPONSE:
this._ctx.onresponse(message, peer)
this._finish(message.rid, null, message, peer)
break
case TYPE.QUERY:
this._ctx.onrequest(QUERY, message, peer)
break
case TYPE.UPDATE:
const rt = message.roundtripToken
if (!rt || (!rt.equals(this._token(peer, 0)) && !rt.equals(this._token(peer, 1)))) return
this._ctx.onrequest(UPDATE, message, peer)
break
}
}
_finish (rid, err, val, peer) {
const req = this._requests[rid]
if (!req) return
this._requests[rid] = undefined
const top = this.inflight[this.inflight.length - 1]
this.inflight[top.index = req.index] = top
this.inflight.pop()
const type = req.message.type === TYPE.QUERY
? QUERY
: UPDATE
req.callback(err, val, peer, req.message, req.peer, type)
while (this._pending.length && this.inflight.length < this._ctx.concurrency) {
const { message, peer, callback } = this._pending.shift()
this._requestImmediately(message, peer, callback)
}
}
_request (message, peer, callback) {
// Should we wait to send?
if (this._pending.length || (this.inflight.length >= this._ctx.concurrency)) {
this._pending.push({ message, peer, callback })
} else {
this._requestImmediately(message, peer, callback)
}
}
_requestImmediately (message, peer, callback) {
const rid = message.rid = this._free()
const buffer = Message.encode(message)
const req = {
rid,
index: this.inflight.length,
callback,
message,
buffer,
peer,
timeout: 4,
tries: 0
}
this._requests[rid] = req
this.inflight.push(req)
this.send(buffer, peer)
// if sending a holepunch cmd, forward it right away
if (message.command === '_holepunch') this._holepunch(req)
}
_cancel (rid, err) {
this._finish(rid, err || ECANCELLED, null, null)
}
_onrotate () {
this._secrets[1] = this._secrets[0]
this._secrets[0] = randomBytes(32)
}
_ontick () {
for (var i = this.inflight.length - 1; i >= 0; i--) {
const req = this.inflight[i]
if (req.timeout === 2 && ++req.tries < TRIES) {
this._retry(req)
continue
}
if (--req.timeout) {
continue
}
this._cancel(req.rid, ETIMEDOUT)
}
}
send (buffer, peer) {
this.socket.send(buffer, 0, buffer.length, peer.port, peer.host)
}
destroy () {
clearInterval(this._rotateInterval)
clearInterval(this._tickInterval)
this.socket.close()
const pending = this._pending
this._pending = []
for (const req of pending) req.callback(ECANCELLED)
for (const req of this.inflight) this._cancel(req.rid)
}
response (request, value, closerNodes, peer) {
const message = {
type: TYPE.RESPONSE,
rid: request.rid,
id: this.id,
closerNodes,
roundtripToken: this._token(peer, 0),
value
}
this.send(Message.encode(message), peer)
}
error (request, error, closerNodes, peer) {
const message = {
type: TYPE.RESPONSE,
rid: request.rid,
id: this.id,
closerNodes,
error: error.message
}
this.send(Message.encode(message), peer)
}
query (command, target, value, peer, callback) {
if (!callback) callback = noop
this._request({
type: TYPE.QUERY,
rid: 0,
id: this.id,
target,
command,
value
}, peer, callback)
}
queryImmediately (command, target, value, peer, callback) {
if (!callback) callback = noop
this._requestImmediately({
type: TYPE.QUERY,
rid: 0,
id: this.id,
target,
command,
value
}, peer, callback)
}
update (command, target, value, peer, callback) {
if (!callback) callback = noop
this._request({
type: TYPE.UPDATE,
rid: 0,
id: this.id,
roundtripToken: peer.roundtripToken,
target,
command,
value
}, peer, callback)
}
}
IO.QUERY = QUERY
IO.UPDATE = UPDATE
module.exports = IO
function noop () {}
function decodeMessage (buf) {
try {
return Message.decode(buf)
} catch (err) {
return null
}
}
function randomBytes (n) {
const buf = Buffer.allocUnsafe(32)
sodium.randombytes_buf(buf)
return buf
}

272
messages.js → lib/messages.js

@ -1,4 +1,4 @@
// This file is auto generated by the protocol-buffers cli tool // This file is auto generated by the protocol-buffers compiler
/* eslint-disable quotes */ /* eslint-disable quotes */
/* eslint-disable indent */ /* eslint-disable indent */
@ -10,60 +10,46 @@ var encodings = require('protocol-buffers-encodings')
var varint = encodings.varint var varint = encodings.varint
var skip = encodings.skip var skip = encodings.skip
var Request = exports.Request = { exports.TYPE = {
"QUERY": 1,
"UPDATE": 2,
"RESPONSE": 3
}
var Holepunch = exports.Holepunch = {
buffer: true, buffer: true,
encodingLength: null, encodingLength: null,
encode: null, encode: null,
decode: null decode: null
} }
var Response = exports.Response = { var Message = exports.Message = {
buffer: true, buffer: true,
encodingLength: null, encodingLength: null,
encode: null, encode: null,
decode: null decode: null
} }
defineRequest() defineHolepunch()
defineResponse() defineMessage()
function defineRequest () { function defineHolepunch () {
var enc = [ var enc = [
encodings.string,
encodings.bytes encodings.bytes
] ]
Request.encodingLength = encodingLength Holepunch.encodingLength = encodingLength
Request.encode = encode Holepunch.encode = encode
Request.decode = decode Holepunch.decode = decode
function encodingLength (obj) { function encodingLength (obj) {
var length = 0 var length = 0
if (!defined(obj.command)) throw new Error("command is required") if (defined(obj.from)) {
var len = enc[0].encodingLength(obj.command) var len = enc[0].encodingLength(obj.from)
length += 1 + len
if (defined(obj.id)) {
var len = enc[1].encodingLength(obj.id)
length += 1 + len
}
if (defined(obj.target)) {
var len = enc[1].encodingLength(obj.target)
length += 1 + len
}
if (defined(obj.forwardRequest)) {
var len = enc[1].encodingLength(obj.forwardRequest)
length += 1 + len
}
if (defined(obj.forwardResponse)) {
var len = enc[1].encodingLength(obj.forwardResponse)
length += 1 + len
}
if (defined(obj.roundtripToken)) {
var len = enc[1].encodingLength(obj.roundtripToken)
length += 1 + len length += 1 + len
} }
if (defined(obj.value)) { if (defined(obj.to)) {
var len = enc[1].encodingLength(obj.value) var len = enc[0].encodingLength(obj.to)
length += 1 + len length += 1 + len
} }
return length return length
@ -73,39 +59,15 @@ function defineRequest () {
if (!offset) offset = 0 if (!offset) offset = 0
if (!buf) buf = Buffer.allocUnsafe(encodingLength(obj)) if (!buf) buf = Buffer.allocUnsafe(encodingLength(obj))
var oldOffset = offset var oldOffset = offset
if (!defined(obj.command)) throw new Error("command is required") if (defined(obj.from)) {
buf[offset++] = 10
enc[0].encode(obj.command, buf, offset)
offset += enc[0].encode.bytes
if (defined(obj.id)) {
buf[offset++] = 18 buf[offset++] = 18
enc[1].encode(obj.id, buf, offset) enc[0].encode(obj.from, buf, offset)
offset += enc[1].encode.bytes offset += enc[0].encode.bytes
} }
if (defined(obj.target)) { if (defined(obj.to)) {
buf[offset++] = 26 buf[offset++] = 26
enc[1].encode(obj.target, buf, offset) enc[0].encode(obj.to, buf, offset)
offset += enc[1].encode.bytes offset += enc[0].encode.bytes
}
if (defined(obj.forwardRequest)) {
buf[offset++] = 34
enc[1].encode(obj.forwardRequest, buf, offset)
offset += enc[1].encode.bytes
}
if (defined(obj.forwardResponse)) {
buf[offset++] = 42
enc[1].encode(obj.forwardResponse, buf, offset)
offset += enc[1].encode.bytes
}
if (defined(obj.roundtripToken)) {
buf[offset++] = 50
enc[1].encode(obj.roundtripToken, buf, offset)
offset += enc[1].encode.bytes
}
if (defined(obj.value)) {
buf[offset++] = 58
enc[1].encode(obj.value, buf, offset)
offset += enc[1].encode.bytes
} }
encode.bytes = offset - oldOffset encode.bytes = offset - oldOffset
return buf return buf
@ -117,18 +79,11 @@ function defineRequest () {
if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid")
var oldOffset = offset var oldOffset = offset
var obj = { var obj = {
command: "", from: null,
id: null, to: null
target: null,
forwardRequest: null,
forwardResponse: null,
roundtripToken: null,
value: null
} }
var found0 = false
while (true) { while (true) {
if (end <= offset) { if (end <= offset) {
if (!found0) throw new Error("Decoded message is not valid")
decode.bytes = offset - oldOffset decode.bytes = offset - oldOffset
return obj return obj
} }
@ -136,34 +91,13 @@ function defineRequest () {
offset += varint.decode.bytes offset += varint.decode.bytes
var tag = prefix >> 3 var tag = prefix >> 3
switch (tag) { switch (tag) {
case 1:
obj.command = enc[0].decode(buf, offset)
offset += enc[0].decode.bytes
found0 = true
break
case 2: case 2:
obj.id = enc[1].decode(buf, offset) obj.from = enc[0].decode(buf, offset)
offset += enc[1].decode.bytes offset += enc[0].decode.bytes
break break
case 3: case 3:
obj.target = enc[1].decode(buf, offset) obj.to = enc[0].decode(buf, offset)
offset += enc[1].decode.bytes offset += enc[0].decode.bytes
break
case 4:
obj.forwardRequest = enc[1].decode(buf, offset)
offset += enc[1].decode.bytes
break
case 5:
obj.forwardResponse = enc[1].decode(buf, offset)
offset += enc[1].decode.bytes
break
case 6:
obj.roundtripToken = enc[1].decode(buf, offset)
offset += enc[1].decode.bytes
break
case 7:
obj.value = enc[1].decode(buf, offset)
offset += enc[1].decode.bytes
break break
default: default:
offset = skip(prefix & 7, buf, offset) offset = skip(prefix & 7, buf, offset)
@ -172,31 +106,52 @@ function defineRequest () {
} }
} }
function defineResponse () { function defineMessage () {
var enc = [ var enc = [
encodings.bytes encodings.enum,
encodings.varint,
encodings.bytes,
encodings.string
] ]
Response.encodingLength = encodingLength Message.encodingLength = encodingLength
Response.encode = encode Message.encode = encode
Response.decode = decode Message.decode = decode
function encodingLength (obj) { function encodingLength (obj) {
var length = 0 var length = 0
if (!defined(obj.type)) throw new Error("type is required")
var len = enc[0].encodingLength(obj.type)
length += 1 + len
if (!defined(obj.rid)) throw new Error("rid is required")
var len = enc[1].encodingLength(obj.rid)
length += 1 + len
if (defined(obj.id)) { if (defined(obj.id)) {
var len = enc[0].encodingLength(obj.id) var len = enc[2].encodingLength(obj.id)
length += 1 + len length += 1 + len
} }
if (defined(obj.nodes)) { if (defined(obj.target)) {
var len = enc[0].encodingLength(obj.nodes) var len = enc[2].encodingLength(obj.target)
length += 1 + len length += 1 + len
} }
if (defined(obj.value)) { if (defined(obj.closerNodes)) {
var len = enc[0].encodingLength(obj.value) var len = enc[2].encodingLength(obj.closerNodes)
length += 1 + len length += 1 + len
} }
if (defined(obj.roundtripToken)) { if (defined(obj.roundtripToken)) {
var len = enc[0].encodingLength(obj.roundtripToken) var len = enc[2].encodingLength(obj.roundtripToken)
length += 1 + len
}
if (defined(obj.command)) {
var len = enc[3].encodingLength(obj.command)
length += 1 + len
}
if (defined(obj.error)) {
var len = enc[3].encodingLength(obj.error)
length += 1 + len
}
if (defined(obj.value)) {
var len = enc[2].encodingLength(obj.value)
length += 1 + len length += 1 + len
} }
return length return length
@ -206,25 +161,48 @@ function defineResponse () {
if (!offset) offset = 0 if (!offset) offset = 0
if (!buf) buf = Buffer.allocUnsafe(encodingLength(obj)) if (!buf) buf = Buffer.allocUnsafe(encodingLength(obj))
var oldOffset = offset var oldOffset = offset
if (defined(obj.id)) { if (!defined(obj.type)) throw new Error("type is required")
buf[offset++] = 10 buf[offset++] = 8
enc[0].encode(obj.id, buf, offset) enc[0].encode(obj.type, buf, offset)
offset += enc[0].encode.bytes offset += enc[0].encode.bytes
if (!defined(obj.rid)) throw new Error("rid is required")
buf[offset++] = 16
enc[1].encode(obj.rid, buf, offset)
offset += enc[1].encode.bytes
if (defined(obj.id)) {
buf[offset++] = 26
enc[2].encode(obj.id, buf, offset)
offset += enc[2].encode.bytes
} }
if (defined(obj.nodes)) { if (defined(obj.target)) {
buf[offset++] = 18 buf[offset++] = 34
enc[0].encode(obj.nodes, buf, offset) enc[2].encode(obj.target, buf, offset)
offset += enc[0].encode.bytes offset += enc[2].encode.bytes
} }
if (defined(obj.value)) { if (defined(obj.closerNodes)) {
buf[offset++] = 26 buf[offset++] = 42
enc[0].encode(obj.value, buf, offset) enc[2].encode(obj.closerNodes, buf, offset)
offset += enc[0].encode.bytes offset += enc[2].encode.bytes
} }
if (defined(obj.roundtripToken)) { if (defined(obj.roundtripToken)) {
buf[offset++] = 34 buf[offset++] = 50
enc[0].encode(obj.roundtripToken, buf, offset) enc[2].encode(obj.roundtripToken, buf, offset)
offset += enc[0].encode.bytes offset += enc[2].encode.bytes
}
if (defined(obj.command)) {
buf[offset++] = 58
enc[3].encode(obj.command, buf, offset)
offset += enc[3].encode.bytes
}
if (defined(obj.error)) {
buf[offset++] = 66
enc[3].encode(obj.error, buf, offset)
offset += enc[3].encode.bytes
}
if (defined(obj.value)) {
buf[offset++] = 74
enc[2].encode(obj.value, buf, offset)
offset += enc[2].encode.bytes
} }
encode.bytes = offset - oldOffset encode.bytes = offset - oldOffset
return buf return buf
@ -236,13 +214,21 @@ function defineResponse () {
if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid") if (!(end <= buf.length && offset <= buf.length)) throw new Error("Decoded message is not valid")
var oldOffset = offset var oldOffset = offset
var obj = { var obj = {
type: 1,
rid: 0,
id: null, id: null,
nodes: null, target: null,
value: null, closerNodes: null,
roundtripToken: null roundtripToken: null,
command: "",
error: "",
value: null
} }
var found0 = false
var found1 = false
while (true) { while (true) {
if (end <= offset) { if (end <= offset) {
if (!found0 || !found1) throw new Error("Decoded message is not valid")
decode.bytes = offset - oldOffset decode.bytes = offset - oldOffset
return obj return obj
} }
@ -251,20 +237,42 @@ function defineResponse () {
var tag = prefix >> 3 var tag = prefix >> 3
switch (tag) { switch (tag) {
case 1: case 1:
obj.id = enc[0].decode(buf, offset) obj.type = enc[0].decode(buf, offset)
offset += enc[0].decode.bytes offset += enc[0].decode.bytes
found0 = true
break break
case 2: case 2:
obj.nodes = enc[0].decode(buf, offset) obj.rid = enc[1].decode(buf, offset)
offset += enc[0].decode.bytes offset += enc[1].decode.bytes
found1 = true
break break
case 3: case 3:
obj.value = enc[0].decode(buf, offset) obj.id = enc[2].decode(buf, offset)
offset += enc[0].decode.bytes offset += enc[2].decode.bytes
break break
case 4: case 4:
obj.roundtripToken = enc[0].decode(buf, offset) obj.target = enc[2].decode(buf, offset)
offset += enc[0].decode.bytes offset += enc[2].decode.bytes
break
case 5:
obj.closerNodes = enc[2].decode(buf, offset)
offset += enc[2].decode.bytes
break
case 6:
obj.roundtripToken = enc[2].decode(buf, offset)
offset += enc[2].decode.bytes
break
case 7:
obj.command = enc[3].decode(buf, offset)
offset += enc[3].decode.bytes
break
case 8:
obj.error = enc[3].decode(buf, offset)
offset += enc[3].decode.bytes
break
case 9:
obj.value = enc[2].decode(buf, offset)
offset += enc[2].decode.bytes
break break
default: default:
offset = skip(prefix & 7, buf, offset) offset = skip(prefix & 7, buf, offset)

264
lib/query-stream.js

@ -0,0 +1,264 @@
const { Readable } = require('stream')
const nodes = require('ipv4-peers').idLength(32)
const QueryTable = require('./query-table')
const BOOTSTRAPPING = Symbol('BOOTSTRAPPING')
const MOVING_CLOSER = Symbol('MOVING_CLOSER')
const UPDATING = Symbol('UPDATING')
const FINALIZED = Symbol('FINALIZED')
class QueryStream extends Readable {
constructor (node, command, target, value, opts) {
if (!opts) opts = {}
if (!opts.concurrency) opts.concurrency = opts.highWaterMark || node.concurrency
super({
objectMode: true,
highWaterMark: opts.concurrency
})
const cmd = node._commands.get(command)
this.command = command
this.target = target
this.value = cmd ? cmd.inputEncoding.encode(value) : (value || null)
this.update = !!opts.update
this.query = !!opts.query || !opts.update
this.destroyed = false
this.inflight = 0
this.responses = 0
this.updates = 0
this.table = opts.table || new QueryTable(node.id, target)
node.inflightQueries++
this._status = opts.table ? MOVING_CLOSER : BOOTSTRAPPING
this._node = node
this._concurrency = opts.concurrency
this._callback = this._onresponse.bind(this)
this._map = identity
this._outputEncoding = cmd ? cmd.outputEncoding : null
}
map (fn) {
this._map = fn
return this
}
_onresponse (err, message, peer, request, to, type) {
this.inflight--
if (err && to.id) {
// Request, including retries, failed completely
// Remove the "to" node.
const node = this._node.bucket.get(to.id)
if (node) this._node._removeNode(node)
}
if (this._status === FINALIZED) {
if (!this.inflight) {
if (this.destroyed) this.emit('close')
else this.destroy()
}
return
}
if (err) {
this.emit('warning', err)
this._readMaybe()
return
}
this.responses++
this.table.addVerified(message, peer)
if (this._status === MOVING_CLOSER) {
const candidates = decodeNodes(message.closerNodes)
for (var i = 0; i < candidates.length; i++) {
this.table.addUnverified(candidates[i], peer)
}
} else if (this._status === UPDATING) {
this.updates++
}
if (message.error) {
this.emit('warning', new Error(message.error))
this._readMaybe()
return
}
if (!this.query && this._status === MOVING_CLOSER) {
this._readMaybe()
return
}
const value = this._outputEncoding
? this._decodeOutput(message.value)
: message.value
const data = this._map({
type,
node: {
id: message.id,
port: peer.port,
host: peer.host
},
value
})
if (!data) {
this._readMaybe()
return
}
this.push(data)
}
_decodeOutput (val) {
try {
return val && this._outputEncoding.decode(val)
} catch (err) {
return null
}
}
_bootstrap () {
const table = this.table
const bootstrap = this._node.bucket.closest(table.target, table.k)
var i = 0
for (; i < bootstrap.length; i++) {
const b = bootstrap[i]
const node = { id: b.id, port: b.port, host: b.host }
table.addUnverified(node, null)
}
const bootstrapNodes = this._node.bootstrapNodes
if (bootstrap.length < bootstrapNodes.length) {
for (i = 0; i < bootstrapNodes.length; i++) {
this._send(bootstrapNodes[i], true, false)
}
}
this._status = MOVING_CLOSER
this._moveCloser()
}
_sendAll (nodes, force, sendToken) {
var free = Math.max(0, this._concurrency - this._node._io.inflight.length)
var sent = 0
if (!free && !this.inflight) free = 1
if (!free) return 0
for (var i = 0; i < nodes.length; i++) {
if (this._send(nodes[i], force, sendToken)) {
if (++sent === free) break
}
}
return sent
}
_send (node, force, isUpdate) {
if (!force) {
if (node.queried) return false
node.queried = true
}
this.inflight++
const io = this._node._io
if (isUpdate) {
if (!node.roundtripToken) return this._callback(new Error('Roundtrip token is required'))
io.update(this.command, this.target, this.value, node, this._callback)
} else if (this.query) {
io.query(this.command, this.target, this.value, node, this._callback)
} else {
io.query('_find_node', this.target, null, node, this._callback)
}
return true
}
_sendUpdate () {
const sent = this._sendAll(this.table.closest, false, true)
if (sent || this.inflight) return
this._finalize()
}
_moveCloser () {
const table = this.table
const sent = this._sendAll(table.unverified, false, false)
if (sent || this.inflight) return
if (this.update) {
for (var i = 0; i < table.closest.length; i++) {
table.closest[i].queried = false
}
this._status = UPDATING
this._sendUpdate()
} else {
this._finalize()
}
}
_finalize () {
const status = this._status
if (status === FINALIZED) return
this._status = FINALIZED
this._node.inflightQueries--
if (!this.responses && !this.destroyed) {
this.destroy(new Error('No nodes responded'))
}
if (status === UPDATING && !this.updates && !this.destroyed) {
this.destroy(new Error('No close nodes responded'))
}
this.push(null)
}
_readMaybe () {
if (!this.inflight || this._readableState.flowing === true) this._read()
}
_read () {
if (this._node.destroyed) return
switch (this._status) {
case BOOTSTRAPPING: return this._bootstrap()
case MOVING_CLOSER: return this._moveCloser()
case UPDATING: return this._sendUpdate()
case FINALIZED: return
}
throw new Error('Unknown status: ' + this._status)
}
destroy (err) {
if (this.destroyed) return
this.destroyed = true
if (err) this.emit('error', err)
this._finalize()
if (!this.inflight) this.emit('close')
}
}
module.exports = QueryStream
function decodeNodes (buf) {
if (!buf) return []
try {
return nodes.decode(buf)
} catch (err) {
return []
}
}
function identity (a) {
return a
}

69
lib/query-table.js

@ -0,0 +1,69 @@
const xor = require('xor-distance')
class QueryTable {
constructor (id, target) {
this.k = 20
this.id = id
this.target = target
this.closest = []
this.unverified = []
}
addUnverified (node, referrer) {
if (node.id.equals(this.id)) return
node.distance = xor(this.target, node.id)
node.referrer = referrer
insertSorted(node, this.k, this.unverified)
}
addVerified (message, peer) {
if (!message.id || !message.roundtripToken || message.id.equals(this.id)) {
return
}
var prev = getNode(message.id, this.unverified)
if (!prev) {
prev = {
id: message.id,
host: peer.host,
port: peer.port,
distance: xor(message.id, this.target)
}
}
prev.roundtripToken = message.roundtripToken
insertSorted(prev, this.k, this.closest)
}
}
module.exports = QueryTable
function getNode (id, list) {
// find id in the list.
// technically this would be faster with binary search (against distance)
// but this list is always small, so meh
for (var i = 0; i < list.length; i++) {
if (list[i].id.equals(id)) return list[i]
}
return null
}
function insertSorted (node, max, list) {
if (list.length === max && !xor.lt(node.distance, list[max - 1].distance)) return
if (getNode(node.id, list)) return
if (list.length < max) list.push(node)
else list[max - 1] = node
var pos = list.length - 1
while (pos && xor.gt(list[pos - 1].distance, node.distance)) {
list[pos] = list[pos - 1]
list[pos - 1] = node
pos--
}
}

45
package.json

@ -1,38 +1,35 @@
{ {
"name": "dht-rpc", "name": "dht-rpc",
"version": "3.0.1", "version": "3.0.1",
"description": "Make RPC calls over a Kademlia based DHT.", "description": "Make RPC calls over a Kademlia based DHT",
"main": "index.js", "main": "index.js",
"dependencies": { "scripts": {
"duplexify": "^3.5.0", "test": "standard && tape test.js",
"inherits": "^2.0.3", "protobuf": "protocol-buffers schema.proto -o lib/messages.js"
"ipv4-peers": "^1.1.1",
"k-bucket": "^5.0.0",
"protocol-buffers-encodings": "^1.1.0",
"readable-stream": "^2.1.5",
"sodium-universal": "^2.0.0",
"stream-collector": "^1.0.1",
"time-ordered-set": "^1.0.1",
"udp-request": "^1.3.0",
"xor-distance": "^1.0.0"
},
"devDependencies": {
"protocol-buffers": "^3.2.1",
"standard": "^11.0.1",
"tape": "^4.9.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/mafintosh/dht-rpc.git" "url": "git+https://github.com/mafintosh/dht-rpc.git"
},
"scripts": {
"test": "standard && tape test.js",
"protobuf": "protocol-buffers schema.proto -o messages.js"
}, },
"author": "Mathias Buus (@mafintosh)", "author": "Mathias Buus (@mafintosh)",
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {
"url": "https://github.com/mafintosh/dht-rpc/issues" "url": "https://github.com/mafintosh/dht-rpc/issues"
}, },
"homepage": "https://github.com/mafintosh/dht-rpc" "homepage": "https://github.com/mafintosh/dht-rpc#readme",
"devDependencies": {
"protocol-buffers": "^4.1.0",
"standard": "^12.0.1",
"tape": "^4.9.1"
},
"dependencies": {
"codecs": "^1.2.1",
"ipv4-peers": "^1.1.1",
"k-bucket": "^5.0.0",
"protocol-buffers-encodings": "^1.1.0",
"sodium-universal": "^2.0.0",
"stream-collector": "^1.0.1",
"time-ordered-set": "^1.0.1",
"xor-distance": "^1.0.0"
}
} }

304
query-stream.js

@ -1,304 +0,0 @@
var stream = require('readable-stream')
var inherits = require('inherits')
var nodes = require('ipv4-peers').idLength(32)
var xor = require('xor-distance')
var BLANK = Buffer.from([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
])
module.exports = QueryStream
function QueryStream (dht, query, opts) {
if (!(this instanceof QueryStream)) return new QueryStream(dht, query, opts)
if (!opts) opts = {}
if (!opts.concurrency) opts.concurrency = opts.highWaterMark || dht.concurrency
if (!query.target) throw new Error('query.target is required')
stream.Readable.call(this, {objectMode: true, highWaterMark: opts.concurrency})
var self = this
var nodes = opts.node || opts.nodes
this.query = query
this.query.id = dht._queryId
this.target = query.target
this.token = !!opts.token
this.holepunching = opts.holepunching !== false
this.commits = 0
this.responses = 0
this.errors = 0
this.destroyed = false
this.verbose = !!opts.verbose
dht.inflightQueries++
this._dht = dht
this._committing = false
this._finalized = false
this._closest = opts.closest || []
this._concurrency = opts.concurrency
this._updating = false
this._pending = nodes ? [].concat(nodes).map(copyNode) : []
this._k = nodes ? Infinity : opts.k || 20
this._inflight = 0
this._moveCloser = !nodes
this._map = opts.map || echo
this._bootstrapped = !this._moveCloser
this._onresponse = onresponse
this._onresponseholepunch = onresponseholepunch
function onresponseholepunch (err, res, peer, query) {
if (!err || !peer || !peer.referrer) self._callback(err, res, peer)
else self._holepunch(peer, query)
}
function onresponse (err, res, peer) {
self._callback(err, res, peer)
}
}
inherits(QueryStream, stream.Readable)
QueryStream.prototype.destroy = function (err) {
if (this.destroyed) return
this.destroyed = true
this._finalize()
if (err) this.emit('error', err)
this.emit('close')
}
QueryStream.prototype._finalize = function () {
if (this._finalized) return
this._finalized = true
this._dht.inflightQueries--
if (!this.responses && !this.destroyed) this.destroy(new Error('No nodes responded'))
if (!this.commits && this._committing && !this.destroyed) this.destroy(new Error('No close nodes responded'))
this.push(null)
}
QueryStream.prototype._bootstrap = function () {
this._bootstrapped = true
var bootstrap = this._dht.bucket.closest(this.target, this._k)
var i = 0
for (i = 0; i < bootstrap.length; i++) {
var b = bootstrap[i]
this._addPending({id: b.id, port: b.port, host: b.host}, null)
}
if (bootstrap.length < this._dht._bootstrap.length) {
for (i = 0; i < this._dht._bootstrap.length; i++) {
this._send(this._dht._bootstrap[i], true, false)
}
}
}
QueryStream.prototype._readMaybe = function () {
if (this._readableState.flowing === true) this._read()
}
QueryStream.prototype._sendTokens = function () {
if (this.destroyed) return
var sent = this._sendAll(this._closest, false, true)
if (sent || this._inflight) return
this._finalize()
}
QueryStream.prototype._sendPending = function () {
if (this.destroyed) return
if (!this._bootstrapped) this._bootstrap()
var sent = this._sendAll(this._pending, false, false)
if (sent || this._inflight) return
if (this.token) {
for (var i = 0; i < this._closest.length; i++) this._closest[i].queried = false
this._committing = true
this._sendTokens()
} else {
this._finalize()
}
}
QueryStream.prototype._read = function () {
if (this._committing) this._sendTokens()
else this._sendPending()
}
QueryStream.prototype._holepunch = function (peer, query) {
var self = this
this._dht._holepunch(peer, peer.referrer, function (err) {
if (err) return self._callback(err, null, peer)
self._dht._request(query, peer, false, self._onresponse)
})
}
QueryStream.prototype._callback = function (err, res, peer) {
this._inflight--
if (this.destroyed) return
if (err) {
if (res && res.id) {
var node = this._dht.bucket.get(res.id)
if (node) this._dht._removeNode(node)
}
this.errors++
this.emit('warning', err)
this._readMaybe()
return
}
this.responses++
if (this._committing) this.commits++
this._addClosest(res, peer)
if (this._moveCloser) {
var candidates = decodeNodes(res.nodes)
for (var i = 0; i < candidates.length; i++) this._addPending(candidates[i], peer)
}
if (!validateId(res.id) || (this.token && !this.verbose && !this._committing)) {
this._readMaybe()
return
}
var data = this._map({
node: {
id: res.id,
port: peer.port,
host: peer.host
},
value: res.value
})
if (!data) {
this._readMaybe()
return
}
this.push(data)
}
QueryStream.prototype._sendAll = function (nodes, force, useToken) {
var sent = 0
var free = Math.max(0, this._concurrency - this._dht.socket.inflight)
if (!free && !this._inflight) free = 1
if (!free) return 0
for (var i = 0; i < nodes.length; i++) {
if (this._send(nodes[i], force, useToken)) {
if (++sent === free) break
}
}
return sent
}
QueryStream.prototype._send = function (node, force, useToken) {
if (!force) {
if (node.queried) return false
node.queried = true
}
this._inflight++
var query = this.query
if (useToken && node.roundtripToken) {
query = {
command: this.query.command,
id: this.query.id,
target: this.query.target,
value: this.query.value,
roundtripToken: node.roundtripToken
}
}
this._dht._request(query, node, false, this.holepunching ? this._onresponseholepunch : this._onresponse)
return true
}
QueryStream.prototype._addPending = function (node, ref) {
if (node.id.equals(this._dht.id)) return
node.distance = xor(this.target, node.id)
node.referrer = ref
insertSorted(node, this._k, this._pending)
}
QueryStream.prototype._addClosest = function (res, peer) {
if (!res.id || !res.roundtripToken || res.id.equals(this._dht.id)) return
var prev = getNode(res.id, this._pending)
if (!prev) {
prev = {
id: res.id,
port: peer.port,
host: peer.host,
distance: xor(res.id, this.target)
}
}
prev.roundtripToken = res.roundtripToken
insertSorted(prev, this._k, this._closest)
}
function decodeNodes (buf) {
if (!buf) return []
try {
return nodes.decode(buf)
} catch (err) {
return []
}
}
function getNode (id, list) {
// find id in the list.
// technically this would be faster with binary search (against distance)
// but this list is always small, so meh
for (var i = 0; i < list.length; i++) {
if (list[i].id.equals(id)) return list[i]
}
return null
}
function validateId (id) {
return id && id.length === 32
}
function insertSorted (node, max, list) {
if (list.length === max && !xor.lt(node.distance, list[max - 1].distance)) return
if (getNode(node.id, list)) return
if (list.length < max) list.push(node)
else list[max - 1] = node
var pos = list.length - 1
while (pos && xor.gt(list[pos - 1].distance, node.distance)) {
list[pos] = list[pos - 1]
list[pos - 1] = node
pos--
}
}
function copyNode (node) {
return {
id: node.id || BLANK,
port: node.port,
host: node.host,
roundtripToken: node.roundtripToken,
referrer: node.referrer || node.referer
}
}
function echo (a) {
return a
}

37
schema.proto

@ -1,16 +1,27 @@
message Request { message Holepunch {
required string command = 1; optional bytes from = 2;
optional bytes id = 2; optional bytes to = 3;
optional bytes target = 3; }
optional bytes forwardRequest = 4;
optional bytes forwardResponse = 5; enum TYPE {
optional bytes roundtripToken = 6; QUERY = 1;
optional bytes value = 7; UPDATE = 2;
RESPONSE = 3;
} }
message Response { message Message {
optional bytes id = 1; // request/response type + id
optional bytes nodes = 2; required TYPE type = 1;
optional bytes value = 3; required uint64 rid = 2;
optional bytes roundtripToken = 4;
// kademlia stuff
optional bytes id = 3;
optional bytes target = 4;
optional bytes closerNodes = 5;
optional bytes roundtripToken = 6;
// rpc stuff
optional string command = 7;
optional string error = 8;
optional bytes value = 9;
} }

221
test.js

@ -1,26 +1,25 @@
var tape = require('tape') const tape = require('tape')
var dht = require('./') const dht = require('./')
var blake2b = require('./blake2b') const blake2b = require('./lib/blake2b')
tape('simple update', function (t) { tape('simple update', function (t) {
bootstrap(function (port, node) { bootstrap(function (port, node) {
var a = dht({bootstrap: port}) const a = dht({ bootstrap: port })
var b = dht({bootstrap: port}) const b = dht({ bootstrap: port })
a.on('update:echo', function (data, callback) { a.command('echo', {
t.ok(data.roundtripToken, 'has roundtrip token') query (data, callback) {
t.fail('should not query')
callback(new Error('nope'))
},
update (data, callback) {
t.same(data.value, Buffer.from('Hello, World!'), 'expected data') t.same(data.value, Buffer.from('Hello, World!'), 'expected data')
callback(null, data.value) callback(null, data.value)
}
}) })
a.ready(function () { a.ready(function () {
var data = { b.update('echo', a.id, Buffer.from('Hello, World!'), function (err, responses) {
command: 'echo',
target: a.id,
value: Buffer.from('Hello, World!')
}
b.update(data, function (err, responses) {
a.destroy() a.destroy()
b.destroy() b.destroy()
node.destroy() node.destroy()
@ -36,21 +35,18 @@ tape('simple update', function (t) {
tape('simple query', function (t) { tape('simple query', function (t) {
bootstrap(function (port, node) { bootstrap(function (port, node) {
var a = dht({bootstrap: port}) const a = dht({ bootstrap: port })
var b = dht({bootstrap: port}) const b = dht({ bootstrap: port })
a.on('query:hello', function (data, callback) { a.command('hello', {
query (data, callback) {
t.same(data.value, null, 'expected data') t.same(data.value, null, 'expected data')
callback(null, Buffer.from('world')) callback(null, Buffer.from('world'))
}
}) })
a.ready(function () { a.ready(function () {
var data = { b.query('hello', a.id, function (err, responses) {
command: 'hello',
target: a.id
}
b.query(data, function (err, responses) {
a.destroy() a.destroy()
b.destroy() b.destroy()
node.destroy() node.destroy()
@ -64,114 +60,58 @@ tape('simple query', function (t) {
}) })
}) })
tape('targeted query', function (t) { tape('query and update', function (t) {
bootstrap(function (port, node) { bootstrap(function (port, node) {
var a = dht({bootstrap: port}) const a = dht({ bootstrap: port })
const b = dht({ bootstrap: port })
a.on('query:echo', function (data, cb) {
t.pass('in echo')
cb(null, data.value)
})
var b = dht({bootstrap: port})
b.on('query:echo', function (data, cb) { a.command('hello', {
t.fail('should not hit me') query (data, callback) {
cb() t.same(data.value, null, 'expected query data')
}) callback(null, Buffer.from('world'))
},
a.ready(function () { update (data, callback) {
b.ready(function () { t.same(data.value, null, 'expected update data')
var client = dht({bootstrap: port}) callback(null, Buffer.from('world'))
client.query({
command: 'echo',
value: Buffer.from('hi'),
target: client.id
}, {
node: {
port: a.address().port,
host: '127.0.0.1'
} }
}, function (err, responses) {
client.destroy()
a.destroy()
b.destroy()
node.destroy()
t.error(err, 'no error')
t.same(responses.length, 1, 'one response')
t.same(responses[0].value, Buffer.from('hi'), 'echoed')
t.end()
})
})
})
})
})
tape('targeted update', function (t) {
bootstrap(function (port, node) {
var a = dht({bootstrap: port})
a.on('update:echo', function (data, cb) {
t.pass('in echo')
cb(null, data.value)
})
var b = dht({bootstrap: port})
b.on('update:echo', function (data, cb) {
t.fail('should not hit me')
cb()
}) })
a.ready(function () { a.ready(function () {
b.ready(function () { b.queryAndUpdate('hello', a.id, function (err, responses) {
var client = dht({bootstrap: port})
client.update({
command: 'echo',
value: Buffer.from('hi'),
target: client.id
}, {
node: {
port: a.address().port,
host: '127.0.0.1'
}
}, function (err, responses) {
client.destroy()
a.destroy() a.destroy()
b.destroy() b.destroy()
node.destroy() node.destroy()
t.error(err, 'no error') t.error(err, 'no errors')
t.same(responses.length, 1, 'one response') t.same(responses.length, 2, 'two responses')
t.same(responses[0].value, Buffer.from('hi'), 'echoed') t.same(responses[0].value, Buffer.from('world'), 'responded')
t.same(responses[1].value, Buffer.from('world'), 'responded')
t.ok(responses[0].type !== responses[1].type, 'not the same type')
t.end() t.end()
}) })
}) })
}) })
}) })
})
tape('swarm query', function (t) { tape('swarm query', function (t) {
bootstrap(function (port, node) { bootstrap(function (port, node) {
var swarm = [] const swarm = []
var closest = 0 var closest = 0
loop() loop()
function done () { function done () {
t.pass('created swarm') t.pass('created swarm')
var key = blake2b(Buffer.from('hello'))
var me = dht({bootstrap: port})
me.update({command: 'kv', target: key, value: Buffer.from('hello')}, function (err, responses) { const key = blake2b(Buffer.from('hello'))
const me = dht({ bootstrap: port })
me.update('kv', key, Buffer.from('hello'), function (err, responses) {
t.error(err, 'no error') t.error(err, 'no error')
t.same(closest, 20, '20 closest nodes') t.same(closest, 20, '20 closest nodes')
t.same(responses.length, 20, '20 responses') t.same(responses.length, 20, '20 responses')
var stream = me.query({command: 'kv', target: key}) const stream = me.query('kv', key)
stream.on('data', function (data) { stream.on('data', function (data) {
if (data.value) { if (data.value) {
@ -190,18 +130,20 @@ tape('swarm query', function (t) {
function loop () { function loop () {
if (swarm.length === 256) return done() if (swarm.length === 256) return done()
var node = dht({bootstrap: port}) const node = dht({ bootstrap: port })
swarm.push(node) swarm.push(node)
var value = null var value = null
node.on('update:kv', function (data, cb) { node.command('kv', {
update (data, cb) {
closest++ closest++
value = data.value value = data.value
cb() cb()
}) },
node.on('query:kv', function (data, cb) { query (data, cb) {
cb(null, value) cb(null, value)
}
}) })
node.ready(loop) node.ready(loop)
@ -209,12 +151,73 @@ tape('swarm query', function (t) {
}) })
}) })
tape('holepunch api', function (t) {
bootstrap(function (port, node) {
const a = dht({ bootstrap: port })
const b = dht({ bootstrap: port })
var holepunched = false
a.ready(function () {
b.ready(function () {
node.on('holepunch', function (from, to) {
t.same(from.port, a.address().port)
t.same(to.port, b.address().port)
holepunched = true
})
a.holepunch({
host: '127.0.0.1',
port: b.address().port,
referrer: {
host: '127.0.0.1',
port: node.address().port
}
}, function (err) {
t.error(err, 'no error')
t.ok(holepunched)
t.end()
node.destroy()
a.destroy()
b.destroy()
})
})
})
})
})
tape('timeouts', function (t) {
bootstrap(function (port, node) {
const a = dht({ bootstrap: port, ephemeral: true })
const b = dht({ bootstrap: port })
var tries = 0
b.command('nope', {
update (query, cb) {
tries++
t.pass('ignoring update')
}
})
b.ready(function () {
a.update('nope', Buffer.alloc(32), function (err) {
t.ok(err, 'errored')
t.same(tries, 3)
t.end()
node.destroy()
a.destroy()
b.destroy()
})
})
})
})
function bootstrap (done) { function bootstrap (done) {
var node = dht({ const node = dht({
ephemeral: true ephemeral: true
}) })
node.listen(function () { node.listen(0, function () {
done(node.address().port, node) done(node.address().port, node)
}) })
} }

Loading…
Cancel
Save