dht-rpc 5.0.0-rc.6 → 5.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/test-node.yml +3 -4
- package/README.md +96 -67
- package/examples/bootstrap.js +2 -4
- package/examples/find.js +1 -1
- package/examples/insert.js +5 -1
- package/examples/network.js +1 -1
- package/index.js +462 -449
- package/lib/commands.js +4 -0
- package/lib/errors.js +8 -0
- package/lib/io.js +424 -0
- package/lib/peer.js +27 -0
- package/lib/query.js +141 -54
- package/package.json +7 -4
- package/test.js +165 -109
- package/lib/id.js +0 -25
- package/lib/messages.js +0 -107
- package/lib/nat-analyzer.js +0 -117
- package/lib/race.js +0 -16
- package/lib/rpc.js +0 -255
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
name: Build Status
|
|
2
|
-
|
|
3
2
|
on:
|
|
4
3
|
push:
|
|
5
4
|
branches:
|
|
@@ -11,13 +10,13 @@ jobs:
|
|
|
11
10
|
build:
|
|
12
11
|
strategy:
|
|
13
12
|
matrix:
|
|
14
|
-
node-version: [
|
|
15
|
-
os: [ubuntu-
|
|
13
|
+
node-version: [lts/*]
|
|
14
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
16
15
|
runs-on: ${{ matrix.os }}
|
|
17
16
|
steps:
|
|
18
17
|
- uses: actions/checkout@v2
|
|
19
18
|
- name: Use Node.js ${{ matrix.node-version }}
|
|
20
|
-
uses: actions/setup-node@
|
|
19
|
+
uses: actions/setup-node@v2
|
|
21
20
|
with:
|
|
22
21
|
node-version: ${{ matrix.node-version }}
|
|
23
22
|
- run: npm install
|
package/README.md
CHANGED
|
@@ -3,17 +3,16 @@
|
|
|
3
3
|
Make RPC calls over a [Kademlia](https://pdos.csail.mit.edu/~petar/papers/maymounkov-kademlia-lncs.pdf) based DHT.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
npm install dht-rpc
|
|
6
|
+
npm install dht-rpc
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
## NOTE: v5
|
|
9
|
+
## NOTE: v5
|
|
10
10
|
|
|
11
|
-
Note that
|
|
12
|
-
To see the v4 documentation/code go to https://github.com/mafintosh/dht-rpc/tree/v4
|
|
11
|
+
Note that the latest release is v5. To see the v4 documentation/code go to https://github.com/mafintosh/dht-rpc/tree/v4
|
|
13
12
|
|
|
14
13
|
## Key Features
|
|
15
14
|
|
|
16
|
-
*
|
|
15
|
+
* Remote IP / firewall detection
|
|
17
16
|
* Easily add any command to your DHT
|
|
18
17
|
* Streaming queries and updates
|
|
19
18
|
|
|
@@ -27,19 +26,18 @@ Here is an example implementing a simple key value store
|
|
|
27
26
|
First spin up a bootstrap node. You can make multiple if you want for redundancy.
|
|
28
27
|
|
|
29
28
|
``` js
|
|
30
|
-
|
|
29
|
+
import DHT from 'dht-rpc'
|
|
31
30
|
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
bootstrap.bind(10001)
|
|
31
|
+
// If the bootstrap node doesn't implement the same commands as your other nodes
|
|
32
|
+
// remember to set ephemeral: true so it isn't added to the routing table.
|
|
33
|
+
const bootstrap = DHT.bootstrapper(10001, { ephemeral: true })
|
|
36
34
|
```
|
|
37
35
|
|
|
38
36
|
Now lets make some dht nodes that can store values in our key value store.
|
|
39
37
|
|
|
40
38
|
``` js
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
import DHT from 'dht-rpc'
|
|
40
|
+
import crypto from 'crypto'
|
|
43
41
|
|
|
44
42
|
// Let's create 100 dht nodes for our example.
|
|
45
43
|
for (var i = 0; i < 100; i++) createNode()
|
|
@@ -52,11 +50,12 @@ function createNode () {
|
|
|
52
50
|
})
|
|
53
51
|
|
|
54
52
|
const values = new Map()
|
|
53
|
+
const VALUES = 0 // define a command enum
|
|
55
54
|
|
|
56
55
|
node.on('request', function (req) {
|
|
57
|
-
if (req.command ===
|
|
58
|
-
if (req.
|
|
59
|
-
const key =
|
|
56
|
+
if (req.command === VALUES) {
|
|
57
|
+
if (req.token) { // if we are the closest node store the value (ie the node sent a valid roundtrip token)
|
|
58
|
+
const key = hash(req.value).toString('hex')
|
|
60
59
|
values.set(key, req.value)
|
|
61
60
|
console.log('Storing', key, '-->', req.value.toString())
|
|
62
61
|
return req.reply(null)
|
|
@@ -68,8 +67,8 @@ function createNode () {
|
|
|
68
67
|
})
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
function
|
|
72
|
-
return crypto.createHash('sha256').update(
|
|
70
|
+
function hash (value) {
|
|
71
|
+
return crypto.createHash('sha256').update(value).digest()
|
|
73
72
|
}
|
|
74
73
|
```
|
|
75
74
|
|
|
@@ -78,14 +77,25 @@ To insert a value into this dht make another script that does this following
|
|
|
78
77
|
``` js
|
|
79
78
|
const node = new DHT()
|
|
80
79
|
|
|
81
|
-
|
|
80
|
+
const q = node.query({
|
|
81
|
+
target: hash(val),
|
|
82
|
+
command: VALUES,
|
|
83
|
+
value
|
|
84
|
+
}, {
|
|
85
|
+
// commit true will make the query re-reuqest the 20 closest
|
|
86
|
+
// nodes with a valid round trip token to update the values
|
|
87
|
+
commit: true
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
await q.finished()
|
|
82
91
|
```
|
|
83
92
|
|
|
84
93
|
Then after inserting run this script to query for a value
|
|
85
94
|
|
|
86
95
|
``` js
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
const target = Buffer.from(hexFromAbove, 'hex')
|
|
97
|
+
for await (const data of node.query({ target, command: VALUES })) {
|
|
98
|
+
if (data.value && hash(data.value).toString('hex') === hexFromAbove) {
|
|
89
99
|
// We found the value! Destroy the query stream as there is no need to continue.
|
|
90
100
|
console.log(val, '-->', data.value.toString())
|
|
91
101
|
break
|
|
@@ -104,11 +114,6 @@ Options include:
|
|
|
104
114
|
|
|
105
115
|
``` js
|
|
106
116
|
{
|
|
107
|
-
// Whether or not this node is ephemeral or should join the routing table
|
|
108
|
-
ephemeral: false,
|
|
109
|
-
// If you don't explicitly specific the ephemerality, the node will automatically
|
|
110
|
-
// figure it out in adaptive mode, based on your NAT settings, uptime and some other heuristics
|
|
111
|
-
adaptive: true,
|
|
112
117
|
// A list of bootstrap nodes
|
|
113
118
|
bootstrap: [ 'bootstrap-node.com:24242', ... ],
|
|
114
119
|
// Optionally pass in your own UDP socket to use.
|
|
@@ -116,24 +121,36 @@ Options include:
|
|
|
116
121
|
// Optionally pass in array of { host, port } to add to the routing table if you know any peers
|
|
117
122
|
nodes: [{ host, port }, ...],
|
|
118
123
|
// Optionally pass a port you prefer to bind to instead of a random one
|
|
119
|
-
bind: 0
|
|
124
|
+
bind: 0,
|
|
125
|
+
// dht-rpc will automatically detect if you are firewalled. If you know that you are not set this to false
|
|
126
|
+
firewalled: true
|
|
120
127
|
}
|
|
121
128
|
```
|
|
122
129
|
|
|
123
|
-
|
|
124
|
-
|
|
130
|
+
Nodes per default use something called adaptive mode to decide whether or not they want to join other nodes' routing table.
|
|
131
|
+
This includes things like node uptime, if the node is firewalled etc. Adaptive mode is conservative, so it might take ~20-30 mins for the node to turn persistent. If you are making a test case with your own bootstrap network you'd usually want to turn this off to make sure your test finishes in a timely maner. You can do this by passing `ephemeral: false` in the constructor.
|
|
132
|
+
For the vast majority of use-cases you should always use adaptive mode to ensure good DHT health, ie the defaults.
|
|
125
133
|
|
|
126
134
|
Your DHT routing id is `hash(publicIp + publicPort)` and will be autoconfigured internally.
|
|
127
135
|
|
|
136
|
+
#### `const node = DHT.boostrapper(bind, [options])`
|
|
137
|
+
|
|
138
|
+
Sugar for the options needed to run a bootstrap node, ie
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
{
|
|
142
|
+
firewalled: false, // a bootstrapper can never be firewalled
|
|
143
|
+
bootstrap: [] // force set no other bootstrappers.
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Additionally since you'll want a known port for a bootstrap node it adds the bind option as a primary argument.
|
|
148
|
+
|
|
128
149
|
#### `await node.ready()`
|
|
129
150
|
|
|
130
151
|
Wait for the node to be fully bootstrapped etc.
|
|
131
152
|
You don't have to wait for this method, but can be useful during testing.
|
|
132
153
|
|
|
133
|
-
#### `await node.bind([preferredPort])`
|
|
134
|
-
|
|
135
|
-
Wait for the underlying socket to bind. If you prefer a specific port you can specify it here.
|
|
136
|
-
|
|
137
154
|
#### `node.id`
|
|
138
155
|
|
|
139
156
|
Get your own routing ID. Only available when the node is not ephemeral.
|
|
@@ -167,60 +184,65 @@ it will switch from persistent mode to ephemeral again.
|
|
|
167
184
|
Refresh the routing table by looking up a random node in the background.
|
|
168
185
|
This is called internally periodically, but exposed in-case you want to force a refresh.
|
|
169
186
|
|
|
170
|
-
#### `
|
|
187
|
+
#### `node.host`
|
|
171
188
|
|
|
172
|
-
Get your node's public ip,
|
|
173
|
-
|
|
189
|
+
Get your node's public ip, inferred from other nodes in the DHT.
|
|
190
|
+
If the ip cannot be determined, this is set to `null`.
|
|
174
191
|
|
|
175
|
-
|
|
176
|
-
peers behind the DHT later on.
|
|
192
|
+
#### `node.port`
|
|
177
193
|
|
|
178
|
-
|
|
194
|
+
Get your node's public port, inferred from other nodes in the DHT.
|
|
195
|
+
If your node does not have a consistent port, this is set to 0.
|
|
179
196
|
|
|
180
|
-
|
|
181
|
-
* `DHT.NAT_OPEN` - fully open nat (ie a server) - a requirement for adaptive nodes to go persistent.
|
|
182
|
-
* `DHT.NAT_PORT_CONSISTENT` - NAT sessions appear consistent across multiple peers.
|
|
183
|
-
* `DHT.NAT_PORT_INCREMENTING` - NAT sessions appear to have an incremeting port across sessions.
|
|
184
|
-
* `DHT.NAT_PORT_RANDOMIZED` - NAT sessions appear randomized across sessions.
|
|
197
|
+
#### `node.firewalled`
|
|
185
198
|
|
|
186
|
-
|
|
199
|
+
Boolean indicated if your node is behind a firewall.
|
|
187
200
|
|
|
188
|
-
|
|
189
|
-
|
|
201
|
+
This is auto detected by having other node's trying to do a PING to you
|
|
202
|
+
without you contacting them first.
|
|
203
|
+
|
|
204
|
+
#### `const udpAddr = node.address()`
|
|
205
|
+
|
|
206
|
+
Get the local address of the UDP socket bound.
|
|
207
|
+
|
|
208
|
+
Note that if you are in ephemeral mode, this will return a different
|
|
209
|
+
port than the one you provided in the constructor (under bind), as ephemeral
|
|
210
|
+
mode always uses a random port.
|
|
190
211
|
|
|
191
212
|
#### `node.on('request', req)`
|
|
192
213
|
|
|
193
214
|
Emitted when an incoming DHT request is received. This is where you can add your own RPC methods.
|
|
194
215
|
|
|
195
216
|
* `req.target` - the dht target the peer is looking (routing is handled behind the scene)
|
|
196
|
-
* `req.command` - the RPC command
|
|
217
|
+
* `req.command` - the RPC command enum
|
|
197
218
|
* `req.value` - the RPC value buffer
|
|
198
219
|
* `req.token` - If the remote peer echoed back a valid roundtrip token, proving their "from address" this is set
|
|
199
|
-
* `req.commit` - Boolean set as a convenience if a valid token was provided
|
|
200
220
|
* `req.from` - who sent this request (host, port)
|
|
201
221
|
|
|
202
222
|
To reply to a request use the `req.reply(value)` method and to reply with an error code use `req.error(errorCode)`.
|
|
203
|
-
Error codes are up to the user to define. `dht-rpc` defines `0` as OK (ie no error), `1` as `UNKNOWN_COMMAND`,
|
|
204
|
-
both available as `DHT.OK` and `DHT.UNKNOWN_COMMAND`.
|
|
205
223
|
|
|
206
|
-
|
|
207
|
-
|
|
224
|
+
In general error codes are up to the user to define, with the general suggestion to start application specific errors
|
|
225
|
+
from error code `16` and up, to avoid future clashes with `dht-rpc` internals.
|
|
208
226
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
227
|
+
Currently dht-rpc defines the following errors
|
|
228
|
+
|
|
229
|
+
``` js
|
|
230
|
+
DHT.OK = 0 // ie no error
|
|
231
|
+
DHT.ERROR_UNKNOWN_COMMAND = 1 // the command requested does not exist
|
|
232
|
+
DHT.ERROR_INVALID_TOKEN = 2 // the round trip token sent is invalid
|
|
233
|
+
```
|
|
212
234
|
|
|
213
|
-
#### `reply = await node.request(target, command, value, to, [options])`
|
|
235
|
+
#### `reply = await node.request({ token, target, command, value }, to, [options])`
|
|
214
236
|
|
|
215
237
|
Send a request to a specific node specified by the to address (`{ host, port }`).
|
|
238
|
+
See the query API for more info on the arguments.
|
|
216
239
|
|
|
217
240
|
Options include:
|
|
218
241
|
|
|
219
242
|
```js
|
|
220
243
|
{
|
|
221
|
-
token: roundtripTokenFromAReply,
|
|
222
244
|
retry: true, // whether the request should retry on timeout
|
|
223
|
-
|
|
245
|
+
socket: udpSocket // request on this specific socket
|
|
224
246
|
}
|
|
225
247
|
```
|
|
226
248
|
|
|
@@ -228,17 +250,14 @@ Normally you'd set the token when commiting to the dht in the query's commit hoo
|
|
|
228
250
|
|
|
229
251
|
#### `reply = await node.ping(to)`
|
|
230
252
|
|
|
231
|
-
Sugar for `dht.request(
|
|
253
|
+
Sugar for `dht.request({ command: 'ping' }, to)`
|
|
232
254
|
|
|
233
|
-
#### `
|
|
234
|
-
|
|
235
|
-
Conveinience method for requesting many nodes at once.
|
|
236
|
-
|
|
237
|
-
#### `stream = node.query(target, command, [value], [options])`
|
|
255
|
+
#### `stream = node.query({ target, command, value }, [options])`
|
|
238
256
|
|
|
239
257
|
Query the DHT. Will move as close as possible to the `target` provided, which should be a 32-byte uniformly distributed buffer (ie a hash).
|
|
240
258
|
|
|
241
|
-
* `
|
|
259
|
+
* `target` - find nodes close to this (should be a 32 byte buffer like a hash)
|
|
260
|
+
* `command` - an enum (uint) indicating the method you want to invoke
|
|
242
261
|
* `value` - optional binary payload to send with it
|
|
243
262
|
|
|
244
263
|
If you want to modify state stored in the dht, you can use the commit flag to signal the closest
|
|
@@ -257,10 +276,10 @@ that is called for each close reply.
|
|
|
257
276
|
|
|
258
277
|
``` js
|
|
259
278
|
{
|
|
260
|
-
async commit (
|
|
279
|
+
async commit (reply, dht, query) {
|
|
261
280
|
// normally you'd send back the roundtrip token here, to prove to the remote that you own
|
|
262
281
|
// your ip/port
|
|
263
|
-
await dht.request(
|
|
282
|
+
await dht.request({ token: reply.token, target, command, value }, reply.from)
|
|
264
283
|
}
|
|
265
284
|
}
|
|
266
285
|
```
|
|
@@ -273,6 +292,10 @@ Other options include:
|
|
|
273
292
|
// start the query by querying these nodes
|
|
274
293
|
// useful if you are re-doing a query from a set of closest nodes.
|
|
275
294
|
],
|
|
295
|
+
replies: [
|
|
296
|
+
// similar to nodes, but if you useful if you have an array of closest replies instead
|
|
297
|
+
// from a previous query.
|
|
298
|
+
],
|
|
276
299
|
map (reply) {
|
|
277
300
|
// map the reply into what you want returned on the stram
|
|
278
301
|
return { onlyValue: reply.value }
|
|
@@ -284,10 +307,16 @@ The query method returns a stream encapsulating the query, that is also an async
|
|
|
284
307
|
If you just want to wait for the query to finish, you can use the `await stream.finished()` helper. After completion the closest
|
|
285
308
|
nodes are stored in `stream.closestNodes` array.
|
|
286
309
|
|
|
310
|
+
If you want to access the closest replies to your provided target you can see those at `stream.closestReplies`.
|
|
311
|
+
|
|
287
312
|
#### `node.destroy()`
|
|
288
313
|
|
|
289
314
|
Shutdown the DHT node.
|
|
290
315
|
|
|
316
|
+
#### `node.destroyed`
|
|
317
|
+
|
|
318
|
+
Boolean indicating if this has been destroyed.
|
|
319
|
+
|
|
291
320
|
#### `node.toArray()`
|
|
292
321
|
|
|
293
322
|
Get the routing table peers out as an array of `{ host, port}`
|
package/examples/bootstrap.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
const DHT = require('../')
|
|
2
2
|
|
|
3
|
-
// Set ephemeral: true
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
bootstrap.bind(10001)
|
|
3
|
+
// Set ephemeral: true since this does not implement any APIs
|
|
4
|
+
DHT.bootstrapper(10001, { ephemeral: true })
|
package/examples/find.js
CHANGED
|
@@ -7,7 +7,7 @@ const node = new DHT({ ephemeral: true, bootstrap: ['localhost:10001'] })
|
|
|
7
7
|
run()
|
|
8
8
|
|
|
9
9
|
async function run () {
|
|
10
|
-
const q = node.query(Buffer.from(hex, 'hex'), 'values')
|
|
10
|
+
const q = node.query({ target: Buffer.from(hex, 'hex'), command: 'values' })
|
|
11
11
|
|
|
12
12
|
for await (const data of q) {
|
|
13
13
|
if (data.value && sha256(data.value).toString('hex') === hex) {
|
package/examples/insert.js
CHANGED
|
@@ -8,10 +8,14 @@ const val = Buffer.from(process.argv[2])
|
|
|
8
8
|
run()
|
|
9
9
|
|
|
10
10
|
async function run () {
|
|
11
|
-
const q = node.query(sha256(val), 'values')
|
|
11
|
+
const q = node.query({ target: sha256(val), command: 'values', commit })
|
|
12
12
|
await q.finished()
|
|
13
13
|
await q.commit('values', val)
|
|
14
14
|
console.log('Inserted', sha256(val).toString('hex'))
|
|
15
|
+
|
|
16
|
+
async function commit (reply) {
|
|
17
|
+
await node.request({ token: reply.token, target: sha256(val), command: 'values', value: val }, reply.from)
|
|
18
|
+
}
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
function sha256 (val) {
|
package/examples/network.js
CHANGED
|
@@ -17,7 +17,7 @@ function createNode () {
|
|
|
17
17
|
|
|
18
18
|
node.on('request', function (req) {
|
|
19
19
|
if (req.command === 'values') {
|
|
20
|
-
if (req.
|
|
20
|
+
if (req.token) {
|
|
21
21
|
const key = sha256(req.value).toString('hex')
|
|
22
22
|
values.set(key, req.value)
|
|
23
23
|
console.log('Storing', key, '-->', req.value.toString())
|