fix: multi-repo local dev (file:../ deps, package exports, missing deps)
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2019 Mathias Buus, David Mark Clements & Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+334
@@ -0,0 +1,334 @@
|
||||
# hyperdht
|
||||
|
||||
### [See the full API docs at docs.pears.com](https://docs.pears.com/building-blocks/hyperdht)
|
||||
|
||||
The DHT powering Hyperswarm
|
||||
|
||||
```
|
||||
npm install hyperdht
|
||||
```
|
||||
|
||||
Built on top of [dht-rpc](https://github.com/mafintosh/dht-rpc).
|
||||
|
||||
The Hyperswarm DHT uses a series of holepunching techniques to make sure connectivity works on most networks,
|
||||
and is mainly used to facilitate finding and connecting to peers using end to end encrypted Noise streams.
|
||||
|
||||
## Usage
|
||||
|
||||
To try it out, first instantiate a DHT instance
|
||||
|
||||
```js
|
||||
import DHT from 'hyperdht'
|
||||
|
||||
const node = new DHT()
|
||||
```
|
||||
|
||||
Then on one computer listen for connections
|
||||
|
||||
```js
|
||||
// create a server to listen for secure connections
|
||||
const server = node.createServer()
|
||||
|
||||
server.on('connection', function (socket) {
|
||||
// socket is E2E encrypted between you and the other peer
|
||||
console.log('Remote public key', socket.remotePublicKey)
|
||||
|
||||
// pipe it somewhere like any duplex stream
|
||||
process.stdin.pipe(socket).pipe(process.stdout)
|
||||
})
|
||||
|
||||
// make a ed25519 keypair to listen on
|
||||
const keyPair = DHT.keyPair()
|
||||
|
||||
// this makes the server accept connections on this keypair
|
||||
await server.listen(keyPair)
|
||||
```
|
||||
|
||||
Then on another connect to the computer using the public key of the key-pair it is listening on
|
||||
|
||||
```js
|
||||
// publicKey here is keyPair.publicKey from above
|
||||
const socket = anotherNode.connect(publicKey)
|
||||
|
||||
socket.on('open', function () {
|
||||
// socket fully open with the other peer
|
||||
})
|
||||
|
||||
// pipe it somewhere like any duplex stream
|
||||
process.stdin.pipe(socket).pipe(process.stdout)
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
#### `const node = new DHT([options])`
|
||||
|
||||
Create a new DHT node.
|
||||
|
||||
Options include:
|
||||
|
||||
```js
|
||||
{
|
||||
// Optionally overwrite the default bootstrap servers, just need to be an array of any known dht node(s)
|
||||
// Defaults to Pear.config.dht.bootstrap in a Pear app or ['[email protected]:49737', '[email protected]:49737', '[email protected]:49737'] elsewhere
|
||||
// Supports suggested-IP to avoid DNS calls: [suggested-IP@]<host>:<port>
|
||||
bootstrap: ['host:port'],
|
||||
keyPair, // set the default key pair to use for server.listen and connect
|
||||
connectionKeepAlive, // set a default keep-alive (in ms) on all opened sockets. Defaults to 5000. Set false to turn off (advanced usage).
|
||||
randomPunchInterval: 20000 // set a default time for interval between punches (in ms). Defaults to 20000.
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
See [dht-rpc](https://github.com/mafintosh/dht-rpc) for more options as HyperDHT inherits from that.
|
||||
|
||||
_Note:_ The default bootstrap servers are publicly served on behalf of the commons. To run a fully isolated DHT, start one or more dht nodes with an empty bootstrap array (`new DHT({bootstrap:[]})`) and then use the addresses of those nodes as the `bootstrap` option in all other dht nodes. You'll need at least one persistent node for the network to be completely operational.
|
||||
|
||||
#### `keyPair = DHT.keyPair([seed])`
|
||||
|
||||
Use this method to generate the required keypair for DHT operations.
|
||||
|
||||
Returns an object with `{publicKey, secretKey}`. `publicKey` holds a public key buffer, `secretKey` holds a private key buffer.
|
||||
|
||||
If you pass any options they are forwarded to dht-rpc.
|
||||
|
||||
#### `await node.destroy([options])`
|
||||
|
||||
Fully destroy this DHT node.
|
||||
|
||||
This will also unannounce any running servers.
|
||||
If you want to force close the node without waiting for the servers to unannounce pass `{ force: true }`.
|
||||
|
||||
#### `node = DHT.bootstrapper(port, host, [options])`
|
||||
|
||||
If you want to run your own Hyperswarm network use this method to easily create a bootstrap node.
|
||||
|
||||
## Creating P2P servers
|
||||
|
||||
#### `const server = node.createServer([options], [onconnection])`
|
||||
|
||||
Create a new server for accepting incoming encrypted P2P connections.
|
||||
|
||||
Options include:
|
||||
|
||||
```js
|
||||
{
|
||||
firewall (remotePublicKey, remoteHandshakePayload) {
|
||||
// validate if you want a connection from remotePublicKey
|
||||
// if you do return false, else return true
|
||||
// remoteHandshakePayload contains their ip and some more info
|
||||
return true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can run servers on normal home computers, as the DHT will UDP holepunch connections for you.
|
||||
|
||||
#### `await server.listen(keyPair)`
|
||||
|
||||
Make the server listen on a keyPair.
|
||||
To connect to this server use keyPair.publicKey as the connect address.
|
||||
|
||||
#### `server.refresh()`
|
||||
|
||||
Refresh the server, causing it to reannounce its address. This is automatically called on network changes.
|
||||
|
||||
#### `server.on('connection', socket)`
|
||||
|
||||
Emitted when a new encrypted connection has passed the firewall check.
|
||||
|
||||
`socket` is a [NoiseSecretStream](https://github.com/holepunchto/hyperswarm-secret-stream) instance.
|
||||
|
||||
You can check who you are connected to using `socket.remotePublicKey` and `socket.handshakeHash` contains a unique hash representing this crypto session (same on both sides).
|
||||
|
||||
#### `server.on('listening')`
|
||||
|
||||
Emitted when the server is fully listening on a keyPair.
|
||||
|
||||
#### `server.address()`
|
||||
|
||||
Returns an object containing the address of the server:
|
||||
|
||||
```js
|
||||
{
|
||||
;(host, // external IP of the server,
|
||||
port, // external port of the server if predictable,
|
||||
publicKey) // public key of the server
|
||||
}
|
||||
```
|
||||
|
||||
You can also get this info from `node.remoteAddress()` minus the public key.
|
||||
|
||||
#### `await server.close()`
|
||||
|
||||
Stop listening.
|
||||
|
||||
#### `server.on('close')`
|
||||
|
||||
Emitted when the server is fully closed.
|
||||
|
||||
## Connecting to P2P servers
|
||||
|
||||
#### `const socket = node.connect(remotePublicKey, [options])`
|
||||
|
||||
Connect to a remote server. Similar to `createServer` this performs UDP holepunching for P2P connectivity.
|
||||
|
||||
The remote public key can be encoded as either a buffer, a hex string or a z-base32 string.
|
||||
|
||||
Options include:
|
||||
|
||||
```js
|
||||
{
|
||||
nodes: [...], // optional array of close dht nodes to speed up connecting
|
||||
keyPair // optional key pair to use when connection (defaults to node.defaultKeyPair)
|
||||
}
|
||||
```
|
||||
|
||||
#### `socket.on('open')`
|
||||
|
||||
Emitted when the encrypted connection has been fully established with the server.
|
||||
|
||||
#### `socket.remotePublicKey`
|
||||
|
||||
The public key of the remote peer.
|
||||
|
||||
#### `socket.publicKey`
|
||||
|
||||
The public key of the local socket.
|
||||
|
||||
## Additional peer discovery
|
||||
|
||||
#### `const stream = node.lookup(topic, [options])`
|
||||
|
||||
Look for peers in the DHT on the given topic. Topic should be a 32 byte buffer (normally a hash of something).
|
||||
|
||||
The returned stream looks like this
|
||||
|
||||
```js
|
||||
{
|
||||
// Who sent the response?
|
||||
from: { id, host, port },
|
||||
// What address they responded to (i.e. your address)
|
||||
to: { host, port },
|
||||
// List of peers announcing under this topic
|
||||
peers: [ { publicKey, nodes: [{ host, port }, ...] } ]
|
||||
}
|
||||
```
|
||||
|
||||
To connect to the peers you should afterwards call `connect` with those public keys.
|
||||
|
||||
If you pass any options they are forwarded to dht-rpc.
|
||||
|
||||
#### `const stream = node.announce(topic, keyPair, [relayAddresses], [options])`
|
||||
|
||||
Announce that you are listening on a key-pair to the DHT under a specific topic.
|
||||
|
||||
When announcing you'll send a signed proof to peers that you own the key-pair and wish to announce under the specific topic. Optionally you can provide up to 3 nodes, indicating which DHT nodes can relay messages to you - this speeds up connects later on for other users.
|
||||
|
||||
An announce does a parallel lookup so the stream returned looks like the lookup stream.
|
||||
|
||||
Creating a server using `dht.createServer` automatically announces itself periodically on the key-pair it is listening on. When announcing the server under a specific topic, you can access the nodes it is close to using `server.nodes`.
|
||||
|
||||
If you pass any options they are forwarded to dht-rpc.
|
||||
|
||||
#### `await node.unannounce(topic, keyPair, [options])`
|
||||
|
||||
Unannounce a key-pair.
|
||||
|
||||
If you pass any options they are forwarded to dht-rpc.
|
||||
|
||||
## Mutable/immutable records
|
||||
|
||||
#### `const { hash, closestNodes } = await node.immutablePut(value, [options])`
|
||||
|
||||
Store an immutable value in the DHT. When successful, the hash of the value is returned.
|
||||
|
||||
If you pass any options they are forwarded to dht-rpc.
|
||||
|
||||
#### `const { value, from } = await node.immutableGet(hash, [options])`
|
||||
|
||||
Fetch an immutable value from the DHT. When successful, it returns the value corresponding to the hash.
|
||||
|
||||
If you pass any options they are forwarded to dht-rpc.
|
||||
|
||||
#### `const { publicKey, closestNodes, seq, signature } = await node.mutablePut(keyPair, value, [options])`
|
||||
|
||||
Store a mutable value in the DHT.
|
||||
|
||||
If you pass any options they are forwarded to dht-rpc.
|
||||
|
||||
#### `const { value, from, seq, signature } = await node.mutableGet(publicKey, [options])`
|
||||
|
||||
Fetch a mutable value from the DHT.
|
||||
|
||||
Options:
|
||||
|
||||
- `seq` - OPTIONAL, default `0`, a number which will only return values with corresponding `seq` values that are greater than or equal to the supplied `seq` option.
|
||||
- `latest` - OPTIONAL - default `false`, a boolean indicating whether the query should try to find the highest seq before returning, or just the first verified value larger than `options.seq` it sees.
|
||||
|
||||
Any additional options you pass are forwarded to dht-rpc.
|
||||
|
||||
## Additional API
|
||||
|
||||
See [dht-rpc](https://github.com/mafintosh/dht-rpc) for the additional APIs the DHT exposes.
|
||||
|
||||
## CLI
|
||||
|
||||
You can start a DHT node in the command line:
|
||||
|
||||
```sh
|
||||
npm install -g hyperdht
|
||||
```
|
||||
|
||||
Run a DHT node:
|
||||
|
||||
```sh
|
||||
hyperdht # [--port 0] [--host 0.0.0.0] [--bootstrap <comma separated list of ip:port>]
|
||||
```
|
||||
|
||||
Or run multiple nodes:
|
||||
|
||||
```sh
|
||||
hyperdht --nodes 5 # [--host 0.0.0.0] [--bootstrap <list>]
|
||||
```
|
||||
|
||||
Note: by default it uses the [mainnet bootstrap nodes](lib/constants.js).
|
||||
|
||||
#### Isolated DHT network
|
||||
|
||||
To create your own DHT network is as follows:
|
||||
|
||||
1. Run your first bootstrap node:
|
||||
|
||||
```sh
|
||||
hyperdht --bootstrap --host (server-ip) # [--port 49737]
|
||||
```
|
||||
|
||||
Important: it requires the port to be open.
|
||||
|
||||
Now your bootstrap node is ready to use at `(server-ip):49737`, for example:
|
||||
|
||||
```js
|
||||
const dht = new DHT({ bootstrap: ['(server-ip):49737'] })
|
||||
```
|
||||
|
||||
Note: You could configure some DNS for the bootstrap IP addresses.
|
||||
|
||||
For the network to be fully operational it needs at least one persistent node.
|
||||
|
||||
2. Provide the first node by using your own bootstrap values:
|
||||
|
||||
```sh
|
||||
hyperdht --port 49738 --bootstrap (server-ip):49737
|
||||
```
|
||||
|
||||
Important: it requires the port to be open too.
|
||||
|
||||
You need to wait ~30 mins for the node to become persistent.
|
||||
|
||||
Having persistent nodes in different places makes the network more decentralized and resilient!
|
||||
|
||||
For more information: [`examples/isolated-dht.mjs`](examples/isolated-dht.mjs)
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const HyperDHT = require('./')
|
||||
|
||||
const bootstrap = arg('bootstrap')
|
||||
const nodes = arg('node') ? '' : arg('nodes')
|
||||
|
||||
const isBootstrap = bootstrap === '' || (bootstrap !== null && bootstrap.startsWith('--'))
|
||||
|
||||
if (isBootstrap) {
|
||||
const port = Number(arg('port') || '0') || 49737
|
||||
const host = arg('host')
|
||||
if (!host) throw new Error('You need to specify --host <node ip>')
|
||||
startBootstrapNode(port, host)
|
||||
} else {
|
||||
startNodes(Number(nodes) || 1, bootstrap ? bootstrap.split(',') : undefined)
|
||||
}
|
||||
|
||||
function arg(name) {
|
||||
const i = process.argv.indexOf('--' + name)
|
||||
if (i === -1) return null
|
||||
return i < process.argv.length - 1 ? process.argv[i + 1] : ''
|
||||
}
|
||||
|
||||
async function startBootstrapNode(port, host) {
|
||||
console.log('Starting DHT bootstrap node...')
|
||||
|
||||
const node = HyperDHT.bootstrapper(port, host)
|
||||
await node.ready()
|
||||
|
||||
node.on('close', function () {
|
||||
console.log('Bootstrap node closed')
|
||||
})
|
||||
|
||||
console.log('Bootstrap node bound to', node.address())
|
||||
console.log('Fully started Hyperswarm DHT bootstrap node')
|
||||
|
||||
process.once('SIGINT', function () {
|
||||
node.destroy()
|
||||
})
|
||||
}
|
||||
|
||||
async function startNodes(cnt, bootstrap) {
|
||||
console.log('Booting DHT nodes...')
|
||||
|
||||
const port = Number(arg('port') || '0') || 0
|
||||
const host = arg('host') || undefined
|
||||
const all = []
|
||||
|
||||
if (port && cnt !== 1) throw new Error('--port is only valid when running a single node')
|
||||
|
||||
while (all.length < cnt) {
|
||||
const node = new HyperDHT({ host, port, anyPort: !port, bootstrap })
|
||||
await node.ready()
|
||||
|
||||
all.push(node)
|
||||
|
||||
const id = all.push(node) - 1
|
||||
console.log('Node #' + id + ' bound to', node.address())
|
||||
|
||||
node.on('ephemeral', function () {
|
||||
console.log('Node #' + id + ' is ephemeral', node.address())
|
||||
})
|
||||
|
||||
node.on('persistent', function () {
|
||||
console.log('Node #' + id + ' is persistent, joining remote routing tables', node.address())
|
||||
})
|
||||
|
||||
node.on('close', function () {
|
||||
console.log('Node #' + id + ' closed')
|
||||
})
|
||||
}
|
||||
|
||||
console.log('Fully started ' + cnt + ' Hyperswarm DHT node' + (cnt === 1 ? '' : 's'))
|
||||
|
||||
process.once('SIGINT', function () {
|
||||
console.log('Shutting down nodes...')
|
||||
|
||||
for (const node of all) {
|
||||
node.destroy()
|
||||
}
|
||||
})
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
const { hash, createKeyPair } = require('./lib/crypto')
|
||||
|
||||
module.exports = class Stub {
|
||||
constructor() {
|
||||
throw new Error('hyperdht is not supported in browsers')
|
||||
}
|
||||
|
||||
static keyPair(seed) {
|
||||
return createKeyPair(seed)
|
||||
}
|
||||
|
||||
static hash(data) {
|
||||
return hash(data)
|
||||
}
|
||||
}
|
||||
+631
@@ -0,0 +1,631 @@
|
||||
const DHT = require('dht-rpc')
|
||||
const sodium = require('sodium-universal')
|
||||
const c = require('compact-encoding')
|
||||
const b4a = require('b4a')
|
||||
const safetyCatch = require('safety-catch')
|
||||
const m = require('./lib/messages')
|
||||
const SocketPool = require('./lib/socket-pool')
|
||||
const Persistent = require('./lib/persistent')
|
||||
const Router = require('./lib/router')
|
||||
const Cache = require('xache')
|
||||
const Server = require('./lib/server')
|
||||
const connect = require('./lib/connect')
|
||||
const { FIREWALL, BOOTSTRAP_NODES, KNOWN_NODES, COMMANDS } = require('./lib/constants')
|
||||
const { hash, createKeyPair } = require('./lib/crypto')
|
||||
const RawStreamSet = require('./lib/raw-stream-set')
|
||||
const ConnectionPool = require('./lib/connection-pool')
|
||||
const { STREAM_NOT_CONNECTED } = require('./lib/errors')
|
||||
|
||||
const DEFAULTS = {
|
||||
...DHT.DEFAULTS,
|
||||
connectionKeepAlive: 5000,
|
||||
randomPunchInterval: 20000
|
||||
}
|
||||
|
||||
class HyperDHT extends DHT {
|
||||
constructor(opts = {}) {
|
||||
const port = opts.port || 49737
|
||||
const bootstrap = opts.bootstrap || BOOTSTRAP_NODES
|
||||
const nodes = opts.nodes || KNOWN_NODES
|
||||
|
||||
super({ ...opts, port, bootstrap, nodes, filterNode })
|
||||
|
||||
const { router, relayAddresses, persistent } = defaultCacheOpts(opts)
|
||||
|
||||
this.defaultKeyPair = opts.keyPair || createKeyPair(opts.seed)
|
||||
this.listening = new Set()
|
||||
this.connectionKeepAlive =
|
||||
opts.connectionKeepAlive === false
|
||||
? 0
|
||||
: opts.connectionKeepAlive || DEFAULTS.connectionKeepAlive
|
||||
|
||||
// stats is inherited from dht-rpc so fwd the ones from there
|
||||
this.stats = {
|
||||
punches: { consistent: 0, random: 0, open: 0 },
|
||||
relaying: { attempts: 0, successes: 0, aborts: 0 },
|
||||
...this.stats
|
||||
}
|
||||
this.rawStreams = new RawStreamSet(this)
|
||||
this.plugins = new Map()
|
||||
|
||||
this._router = new Router(this, router)
|
||||
this._socketPool = new SocketPool(this, opts.host || '0.0.0.0')
|
||||
this._persistent = null
|
||||
this._validatedLocalAddresses = new Map()
|
||||
this._relayAddressesCache = new Cache(relayAddresses)
|
||||
|
||||
this._deferRandomPunch = !!opts.deferRandomPunch
|
||||
this._lastRandomPunch = this._deferRandomPunch ? Date.now() : 0
|
||||
this._connectable = true
|
||||
this._randomPunchInterval = opts.randomPunchInterval || DEFAULTS.randomPunchInterval // min 20s between random punches...
|
||||
this._randomPunches = 0
|
||||
this._randomPunchLimit = 1 // set to one for extra safety for now
|
||||
|
||||
this.once('persistent', () => {
|
||||
this._persistent = new Persistent(this, persistent)
|
||||
for (const plugin of this.plugins.values()) plugin.onpersistent()
|
||||
})
|
||||
|
||||
this.on('network-change', () => {
|
||||
for (const server of this.listening) server.refresh()
|
||||
})
|
||||
|
||||
this.on('network-update', () => {
|
||||
if (!this.online) return
|
||||
for (const server of this.listening) server.notifyOnline()
|
||||
})
|
||||
}
|
||||
|
||||
static DEFAULTS = DEFAULTS
|
||||
|
||||
connect(remotePublicKey, opts) {
|
||||
return connect(this, remotePublicKey, opts)
|
||||
}
|
||||
|
||||
createServer(opts, onconnection) {
|
||||
if (typeof opts === 'function') return this.createServer({}, opts)
|
||||
if (opts && opts.onconnection) onconnection = opts.onconnection
|
||||
const s = new Server(this, opts)
|
||||
if (onconnection) s.on('connection', onconnection)
|
||||
return s
|
||||
}
|
||||
|
||||
pool() {
|
||||
return new ConnectionPool(this)
|
||||
}
|
||||
|
||||
async resume({ log = noop } = {}) {
|
||||
if (this._deferRandomPunch) this._lastRandomPunch = Date.now()
|
||||
await super.resume({ log })
|
||||
const resuming = []
|
||||
for (const server of this.listening) resuming.push(server.resume())
|
||||
log('Resuming hyperdht servers')
|
||||
await Promise.allSettled(resuming)
|
||||
log('Done, hyperdht fully resumed')
|
||||
}
|
||||
|
||||
async suspend({ log = noop } = {}) {
|
||||
this._connectable = false // just so nothing gets connected during suspension
|
||||
const suspending = []
|
||||
for (const server of this.listening) suspending.push(server.suspend())
|
||||
log('Suspending all hyperdht servers')
|
||||
await Promise.allSettled(suspending)
|
||||
log('Done, clearing all raw streams')
|
||||
await this.rawStreams.clear()
|
||||
log('Done, suspending dht-rpc')
|
||||
await super.suspend({ log })
|
||||
log('Done, clearing raw streams again')
|
||||
await this.rawStreams.clear()
|
||||
log('Done, hyperdht fully suspended')
|
||||
this._connectable = true
|
||||
}
|
||||
|
||||
async destroy({ force = false } = {}) {
|
||||
if (!force) {
|
||||
const closing = []
|
||||
for (const server of this.listening) closing.push(server.close())
|
||||
await Promise.allSettled(closing)
|
||||
}
|
||||
this._router.destroy()
|
||||
if (this._persistent) this._persistent.destroy()
|
||||
for (const plugin of this.plugins.values()) plugin.destroy()
|
||||
await this.rawStreams.clear()
|
||||
await this._socketPool.destroy()
|
||||
await super.destroy()
|
||||
}
|
||||
|
||||
async validateLocalAddresses(addresses) {
|
||||
const list = []
|
||||
const socks = []
|
||||
const waiting = []
|
||||
|
||||
for (const addr of addresses) {
|
||||
const { host } = addr
|
||||
|
||||
if (this._validatedLocalAddresses.has(host)) {
|
||||
if (await this._validatedLocalAddresses.get(host)) {
|
||||
list.push(addr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const sock = this.udx.createSocket()
|
||||
try {
|
||||
sock.bind(0, host)
|
||||
} catch {
|
||||
this._validatedLocalAddresses.set(host, Promise.resolve(false))
|
||||
continue
|
||||
}
|
||||
|
||||
socks.push(sock)
|
||||
|
||||
// semi terrible heuristic until we proper fix local connections by racing them to the remote...
|
||||
const promise = new Promise((resolve) => {
|
||||
sock.on('message', () => resolve(true))
|
||||
setTimeout(() => resolve(false), 500)
|
||||
sock.trySend(b4a.alloc(1), sock.address().port, addr.host)
|
||||
})
|
||||
|
||||
this._validatedLocalAddresses.set(host, promise)
|
||||
waiting.push(addr)
|
||||
}
|
||||
|
||||
for (const addr of waiting) {
|
||||
const { host } = addr
|
||||
if (this._validatedLocalAddresses.has(host)) {
|
||||
if (await this._validatedLocalAddresses.get(host)) {
|
||||
list.push(addr)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
for (const sock of socks) await sock.close()
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
findPeer(publicKey, opts = {}) {
|
||||
const target = opts.hash === false ? publicKey : hash(publicKey)
|
||||
opts = { ...opts, map: mapFindPeer }
|
||||
return this.query({ target, command: COMMANDS.FIND_PEER, value: null }, opts)
|
||||
}
|
||||
|
||||
lookup(target, opts = {}) {
|
||||
opts = { ...opts, map: mapLookup }
|
||||
return this.query({ target, command: COMMANDS.LOOKUP, value: null }, opts)
|
||||
}
|
||||
|
||||
lookupAndUnannounce(target, keyPair, opts = {}) {
|
||||
const unannounces = []
|
||||
const dht = this
|
||||
const userCommit = opts.commit || noop
|
||||
const signUnannounce = opts.signUnannounce || Persistent.signUnannounce
|
||||
|
||||
if (this._persistent !== null) {
|
||||
// unlink self
|
||||
this._persistent.unannounce(target, keyPair.publicKey)
|
||||
}
|
||||
|
||||
opts = { ...opts, map, commit }
|
||||
return this.query({ target, command: COMMANDS.LOOKUP, value: null }, opts)
|
||||
|
||||
async function commit(reply, dht, query) {
|
||||
await Promise.all(unannounces) // can never fail, caught below
|
||||
return userCommit(reply, dht, query)
|
||||
}
|
||||
|
||||
function map(reply) {
|
||||
const data = mapLookup(reply)
|
||||
|
||||
if (!data || !data.token) return data
|
||||
|
||||
let found = data.peers.length >= 20
|
||||
for (let i = 0; !found && i < data.peers.length; i++) {
|
||||
found = b4a.equals(data.peers[i].publicKey, keyPair.publicKey)
|
||||
}
|
||||
|
||||
if (!found) return data
|
||||
|
||||
if (!data.from.id) return data
|
||||
|
||||
unannounces.push(
|
||||
dht
|
||||
._requestUnannounce(keyPair, dht, target, data.token, data.from, signUnannounce)
|
||||
.catch(safetyCatch)
|
||||
)
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
unannounce(target, keyPair, opts = {}) {
|
||||
return this.lookupAndUnannounce(target, keyPair, opts).finished()
|
||||
}
|
||||
|
||||
announce(target, keyPair, relayAddresses, opts = {}) {
|
||||
const signAnnounce = opts.signAnnounce || Persistent.signAnnounce
|
||||
const bump = opts.bump || 0
|
||||
|
||||
opts = { ...opts, commit }
|
||||
|
||||
return opts.clear ? this.lookupAndUnannounce(target, keyPair, opts) : this.lookup(target, opts)
|
||||
|
||||
function commit(reply, dht) {
|
||||
return dht._requestAnnounce(
|
||||
keyPair,
|
||||
dht,
|
||||
target,
|
||||
reply.token,
|
||||
reply.from,
|
||||
relayAddresses,
|
||||
signAnnounce,
|
||||
bump
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async immutableGet(target, opts = {}) {
|
||||
opts = { ...opts, map: mapImmutable }
|
||||
|
||||
const query = this.query({ target, command: COMMANDS.IMMUTABLE_GET, value: null }, opts)
|
||||
const check = b4a.allocUnsafe(32)
|
||||
|
||||
for await (const node of query) {
|
||||
const { value } = node
|
||||
sodium.crypto_generichash(check, value)
|
||||
if (b4a.equals(check, target)) return node
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async immutablePut(value, opts = {}) {
|
||||
const target = b4a.allocUnsafe(32)
|
||||
sodium.crypto_generichash(target, value)
|
||||
|
||||
opts = {
|
||||
...opts,
|
||||
map: mapImmutable,
|
||||
commit(reply, dht) {
|
||||
return dht.request(
|
||||
{ token: reply.token, target, command: COMMANDS.IMMUTABLE_PUT, value },
|
||||
reply.from
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const query = this.query({ target, command: COMMANDS.IMMUTABLE_GET, value: null }, opts)
|
||||
await query.finished()
|
||||
|
||||
return { hash: target, closestNodes: query.closestNodes }
|
||||
}
|
||||
|
||||
async mutableGet(publicKey, opts = {}) {
|
||||
let refresh = opts.refresh || null
|
||||
let signed = null
|
||||
let result = null
|
||||
|
||||
opts = { ...opts, map: mapMutable, commit: refresh ? commit : null }
|
||||
|
||||
const target = b4a.allocUnsafe(32)
|
||||
sodium.crypto_generichash(target, publicKey)
|
||||
|
||||
const userSeq = opts.seq || 0
|
||||
const query = this.query(
|
||||
{ target, command: COMMANDS.MUTABLE_GET, value: c.encode(c.uint, userSeq) },
|
||||
opts
|
||||
)
|
||||
const latest = opts.latest !== false
|
||||
|
||||
for await (const node of query) {
|
||||
if (result && node.seq <= result.seq) continue
|
||||
if (
|
||||
node.seq < userSeq ||
|
||||
!Persistent.verifyMutable(node.signature, node.seq, node.value, publicKey)
|
||||
)
|
||||
continue
|
||||
if (!latest) return node
|
||||
if (!result || node.seq > result.seq) result = node
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
function commit(reply, dht) {
|
||||
if (!signed && result && refresh) {
|
||||
if (refresh(result)) {
|
||||
signed = c.encode(m.mutablePutRequest, {
|
||||
publicKey,
|
||||
seq: result.seq,
|
||||
value: result.value,
|
||||
signature: result.signature
|
||||
})
|
||||
} else {
|
||||
refresh = null
|
||||
}
|
||||
}
|
||||
|
||||
return signed
|
||||
? dht.request(
|
||||
{ token: reply.token, target, command: COMMANDS.MUTABLE_PUT, value: signed },
|
||||
reply.from
|
||||
)
|
||||
: Promise.resolve(null)
|
||||
}
|
||||
}
|
||||
|
||||
async mutablePut(keyPair, value, opts = {}) {
|
||||
const signMutable = opts.signMutable || Persistent.signMutable
|
||||
|
||||
const target = b4a.allocUnsafe(32)
|
||||
sodium.crypto_generichash(target, keyPair.publicKey)
|
||||
|
||||
const seq = opts.seq || 0
|
||||
const signature = await signMutable(seq, value, keyPair)
|
||||
|
||||
const signed = c.encode(m.mutablePutRequest, {
|
||||
publicKey: keyPair.publicKey,
|
||||
seq,
|
||||
value,
|
||||
signature
|
||||
})
|
||||
|
||||
opts = {
|
||||
...opts,
|
||||
map: mapMutable,
|
||||
commit(reply, dht) {
|
||||
return dht.request(
|
||||
{ token: reply.token, target, command: COMMANDS.MUTABLE_PUT, value: signed },
|
||||
reply.from
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// use seq = 0, for the query part here, as we don't care about the actual values
|
||||
const query = this.query(
|
||||
{ target, command: COMMANDS.MUTABLE_GET, value: c.encode(c.uint, 0) },
|
||||
opts
|
||||
)
|
||||
await query.finished()
|
||||
|
||||
return { publicKey: keyPair.publicKey, closestNodes: query.closestNodes, seq, signature }
|
||||
}
|
||||
|
||||
onrequest(req) {
|
||||
switch (req.command) {
|
||||
case COMMANDS.PEER_HANDSHAKE: {
|
||||
this._router.onpeerhandshake(req)
|
||||
return true
|
||||
}
|
||||
case COMMANDS.PEER_HOLEPUNCH: {
|
||||
this._router.onpeerholepunch(req)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (this._persistent === null || this.id === null) return false
|
||||
|
||||
switch (req.command) {
|
||||
case COMMANDS.FIND_PEER: {
|
||||
this._persistent.onfindpeer(req)
|
||||
return true
|
||||
}
|
||||
case COMMANDS.LOOKUP: {
|
||||
this._persistent.onlookup(req)
|
||||
return true
|
||||
}
|
||||
case COMMANDS.ANNOUNCE: {
|
||||
this._persistent.onannounce(req)
|
||||
return true
|
||||
}
|
||||
case COMMANDS.UNANNOUNCE: {
|
||||
this._persistent.onunannounce(req)
|
||||
return true
|
||||
}
|
||||
case COMMANDS.MUTABLE_PUT: {
|
||||
this._persistent.onmutableput(req)
|
||||
return true
|
||||
}
|
||||
case COMMANDS.MUTABLE_GET: {
|
||||
this._persistent.onmutableget(req)
|
||||
return true
|
||||
}
|
||||
case COMMANDS.IMMUTABLE_PUT: {
|
||||
this._persistent.onimmutableput(req)
|
||||
return true
|
||||
}
|
||||
case COMMANDS.IMMUTABLE_GET: {
|
||||
this._persistent.onimmutableget(req)
|
||||
return true
|
||||
}
|
||||
case COMMANDS.PLUGIN: {
|
||||
this._persistent.onplugin(req)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
static keyPair(seed) {
|
||||
return createKeyPair(seed)
|
||||
}
|
||||
|
||||
static hash(data) {
|
||||
return hash(data)
|
||||
}
|
||||
|
||||
static connectRawStream(encryptedStream, rawStream, remoteId) {
|
||||
const stream = encryptedStream.rawStream
|
||||
|
||||
if (!stream.connected) throw STREAM_NOT_CONNECTED()
|
||||
|
||||
rawStream.connect(stream.socket, remoteId, stream.remotePort, stream.remoteHost)
|
||||
}
|
||||
|
||||
createRawStream(opts) {
|
||||
return this.rawStreams.add(opts)
|
||||
}
|
||||
|
||||
async _requestAnnounce(keyPair, dht, target, token, from, relayAddresses, sign, bump) {
|
||||
const ann = {
|
||||
peer: {
|
||||
publicKey: keyPair.publicKey,
|
||||
relayAddresses: relayAddresses || []
|
||||
},
|
||||
refresh: null,
|
||||
signature: null,
|
||||
bump
|
||||
}
|
||||
|
||||
ann.signature = await sign(target, token, from.id, ann, keyPair)
|
||||
|
||||
const value = c.encode(m.announce, ann)
|
||||
|
||||
return dht.request(
|
||||
{
|
||||
token,
|
||||
target,
|
||||
command: COMMANDS.ANNOUNCE,
|
||||
value
|
||||
},
|
||||
from
|
||||
)
|
||||
}
|
||||
|
||||
async _requestUnannounce(keyPair, dht, target, token, from, sign) {
|
||||
const unann = {
|
||||
peer: {
|
||||
publicKey: keyPair.publicKey,
|
||||
relayAddresses: []
|
||||
},
|
||||
signature: null
|
||||
}
|
||||
|
||||
unann.signature = await sign(target, token, from.id, unann, keyPair)
|
||||
|
||||
const value = c.encode(m.announce, unann)
|
||||
|
||||
return dht.request(
|
||||
{
|
||||
token,
|
||||
target,
|
||||
command: COMMANDS.UNANNOUNCE,
|
||||
value
|
||||
},
|
||||
from
|
||||
)
|
||||
}
|
||||
|
||||
register(name, plugin) {
|
||||
this.plugins.set(name, plugin)
|
||||
plugin.onregister(this)
|
||||
}
|
||||
}
|
||||
|
||||
HyperDHT.BOOTSTRAP = BOOTSTRAP_NODES
|
||||
HyperDHT.FIREWALL = FIREWALL
|
||||
|
||||
module.exports = HyperDHT
|
||||
|
||||
function mapLookup(node) {
|
||||
if (!node.value) return null
|
||||
|
||||
try {
|
||||
const l = c.decode(m.lookupRawReply, node.value)
|
||||
|
||||
return {
|
||||
token: node.token,
|
||||
from: node.from,
|
||||
to: node.to,
|
||||
peers: l.peers,
|
||||
bump: l.bump
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function mapFindPeer(node) {
|
||||
if (!node.value) return null
|
||||
|
||||
try {
|
||||
return {
|
||||
token: node.token,
|
||||
from: node.from,
|
||||
to: node.to,
|
||||
peer: c.decode(m.peer, node.value)
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function mapImmutable(node) {
|
||||
if (!node.value) return null
|
||||
|
||||
return {
|
||||
token: node.token,
|
||||
from: node.from,
|
||||
to: node.to,
|
||||
value: node.value
|
||||
}
|
||||
}
|
||||
|
||||
function mapMutable(node) {
|
||||
if (!node.value) return null
|
||||
|
||||
try {
|
||||
const { seq, value, signature } = c.decode(m.mutableGetResponse, node.value)
|
||||
|
||||
return {
|
||||
token: node.token,
|
||||
from: node.from,
|
||||
to: node.to,
|
||||
seq,
|
||||
value,
|
||||
signature
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function noop() {}
|
||||
|
||||
function filterNode(node) {
|
||||
// always skip these testnet nodes that got mixed in by accident, until they get updated
|
||||
return (
|
||||
!(node.port === 49738 && (node.host === '134.209.28.98' || node.host === '167.99.142.185')) &&
|
||||
!(node.port === 9400 && node.host === '35.233.47.252') &&
|
||||
!(node.host === '150.136.142.116')
|
||||
)
|
||||
}
|
||||
|
||||
const defaultMaxSize = 65536
|
||||
const defaultMaxAge = 20 * 60 * 1000 // 20 minutes
|
||||
|
||||
function defaultCacheOpts(opts) {
|
||||
const maxSize = opts.maxSize || defaultMaxSize
|
||||
const maxAge = opts.maxAge || defaultMaxAge
|
||||
|
||||
return {
|
||||
router: {
|
||||
forwards: { maxSize, maxAge }
|
||||
},
|
||||
relayAddresses: { maxSize: Math.min(maxSize, 512), maxAge: 0 },
|
||||
persistent: {
|
||||
records: { maxSize, maxAge },
|
||||
refreshes: { maxSize, maxAge },
|
||||
mutables: {
|
||||
maxSize: (maxSize / 2) | 0,
|
||||
maxAge: opts.maxAge || 48 * 60 * 60 * 1000 // 48 hours
|
||||
},
|
||||
immutables: {
|
||||
maxSize: (maxSize / 2) | 0,
|
||||
maxAge: opts.maxAge || 48 * 60 * 60 * 1000 // 48 hours
|
||||
},
|
||||
bumps: { maxSize, maxAge }
|
||||
}
|
||||
}
|
||||
}
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
const safetyCatch = require('safety-catch')
|
||||
const c = require('compact-encoding')
|
||||
const Signal = require('signal-promise')
|
||||
const { encodeUnslab } = require('./encode')
|
||||
const Sleeper = require('./sleeper')
|
||||
const m = require('./messages')
|
||||
const Persistent = require('./persistent')
|
||||
const { COMMANDS } = require('./constants')
|
||||
|
||||
const MIN_ACTIVE = 3
|
||||
|
||||
module.exports = class Announcer {
|
||||
constructor(dht, keyPair, target, opts = {}) {
|
||||
this.dht = dht
|
||||
this.keyPair = keyPair
|
||||
this.target = target
|
||||
this.relays = []
|
||||
this.relayAddresses = []
|
||||
this.stopped = false
|
||||
this.suspended = false
|
||||
this.record = encodeUnslab(m.peer, { publicKey: keyPair.publicKey, relayAddresses: [] })
|
||||
this.online = new Signal()
|
||||
|
||||
this._refreshing = false
|
||||
this._closestNodes = null
|
||||
this._active = null
|
||||
this._sleeper = new Sleeper()
|
||||
this._resumed = new Signal()
|
||||
this._signAnnounce = opts.signAnnounce || Persistent.signAnnounce
|
||||
this._signUnannounce = opts.signUnannounce || Persistent.signUnannounce
|
||||
this._updating = null
|
||||
this._activeQuery = null
|
||||
this._unannouncing = null
|
||||
|
||||
this._serverRelays = [new Map(), new Map(), new Map()]
|
||||
}
|
||||
|
||||
isRelay(addr) {
|
||||
const id = addr.host + ':' + addr.port
|
||||
const [a, b, c] = this._serverRelays
|
||||
return a.has(id) || b.has(id) || c.has(id)
|
||||
}
|
||||
|
||||
async suspend({ log = noop } = {}) {
|
||||
if (this.suspended) return
|
||||
this.suspended = true
|
||||
|
||||
log('Suspending announcer')
|
||||
|
||||
// Suspend has its own sleep logic
|
||||
// so we don't want to hang on this one
|
||||
this.online.notify()
|
||||
|
||||
if (this._activeQuery) this._activeQuery.destroy()
|
||||
|
||||
this._sleeper.resume()
|
||||
if (this._updating) await this._updating
|
||||
log('Suspending announcer (post update)')
|
||||
|
||||
if (this.suspended === false || this.stopped) return
|
||||
|
||||
log('Suspending announcer (pre unannounce)')
|
||||
await this._unannounceCurrent()
|
||||
log('Suspending announcer (post unannounce)')
|
||||
}
|
||||
|
||||
resume() {
|
||||
if (!this.suspended) return
|
||||
this.suspended = false
|
||||
|
||||
this.refresh()
|
||||
this._sleeper.resume()
|
||||
this._resumed.notify()
|
||||
}
|
||||
|
||||
refresh() {
|
||||
if (this.stopped) return
|
||||
this._refreshing = true
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.stopped) return
|
||||
this._active = this._runUpdate()
|
||||
await this._active
|
||||
if (this.stopped) return
|
||||
this._active = this._background()
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.stopped = true
|
||||
this.online.notify() // Break out of the _background loop if we're offline
|
||||
this._sleeper.resume()
|
||||
this._resumed.notify()
|
||||
await this._active
|
||||
await this._unannounceCurrent()
|
||||
}
|
||||
|
||||
async _unannounceCurrent() {
|
||||
while (this._unannouncing !== null) await this._unannouncing
|
||||
const un = (this._unannouncing = this._unannounceAll(this._serverRelays[2].values()))
|
||||
await this._unannouncing
|
||||
if (un === this._unannouncing) this._unannouncing = null
|
||||
}
|
||||
|
||||
async _background() {
|
||||
while (!this.dht.destroyed && !this.stopped) {
|
||||
try {
|
||||
this._refreshing = false
|
||||
|
||||
// ~5min +-
|
||||
for (let i = 0; i < 100 && !this.stopped && !this._refreshing && !this.suspended; i++) {
|
||||
const pings = []
|
||||
|
||||
for (const node of this._serverRelays[2].values()) {
|
||||
pings.push(this.dht.ping(node))
|
||||
}
|
||||
|
||||
const active = await resolved(pings)
|
||||
if (active < Math.min(pings.length, MIN_ACTIVE)) {
|
||||
this.refresh() // we lost too many relay nodes, retry all
|
||||
}
|
||||
|
||||
if (this.stopped) return
|
||||
|
||||
if (!this.suspended && !this._refreshing) await this._sleeper.pause(3000)
|
||||
}
|
||||
|
||||
while (!this.stopped && this.suspended) await this._resumed.wait()
|
||||
|
||||
if (!this.stopped) await this._runUpdate()
|
||||
|
||||
while (!this.dht.online && !this.stopped && !this.suspended) {
|
||||
// Being offline can make _background repeat very quickly
|
||||
// So wait until we're back online
|
||||
await this.online.wait()
|
||||
}
|
||||
} catch (err) {
|
||||
safetyCatch(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _runUpdate() {
|
||||
this._updating = this._update()
|
||||
await this._updating
|
||||
this._updating = null
|
||||
}
|
||||
|
||||
async _update() {
|
||||
while (this._unannouncing) await this._unannouncing
|
||||
|
||||
this._cycle()
|
||||
|
||||
const q = (this._activeQuery = this.dht.findPeer(this.target, {
|
||||
hash: false,
|
||||
nodes: this._closestNodes
|
||||
}))
|
||||
|
||||
try {
|
||||
await q.finished()
|
||||
} catch {
|
||||
// ignore failures...
|
||||
}
|
||||
|
||||
this._activeQuery = null
|
||||
|
||||
if (this.stopped || this.suspended) return
|
||||
|
||||
const ann = []
|
||||
const replies = pickBest(q.closestReplies)
|
||||
|
||||
const relays = []
|
||||
const relayAddresses = []
|
||||
|
||||
if (!this.dht.firewalled) {
|
||||
const addr = this.dht.remoteAddress()
|
||||
if (addr) relayAddresses.push(addr)
|
||||
}
|
||||
|
||||
for (const msg of replies) {
|
||||
ann.push(this._commit(msg, relays, relayAddresses))
|
||||
}
|
||||
|
||||
await Promise.allSettled(ann)
|
||||
if (this.stopped || this.suspended) return
|
||||
|
||||
this._closestNodes = q.closestNodes
|
||||
this.relays = relays
|
||||
this.relayAddresses = relayAddresses
|
||||
|
||||
const removed = []
|
||||
for (const [key, value] of this._serverRelays[1]) {
|
||||
if (!this._serverRelays[2].has(key)) removed.push(value)
|
||||
}
|
||||
|
||||
await this._unannounceAll(removed)
|
||||
}
|
||||
|
||||
_unannounceAll(relays) {
|
||||
const unann = []
|
||||
for (const r of relays) unann.push(this._unannounce(r))
|
||||
return Promise.allSettled(unann)
|
||||
}
|
||||
|
||||
async _unannounce(to) {
|
||||
const unann = {
|
||||
peer: {
|
||||
publicKey: this.keyPair.publicKey,
|
||||
relayAddresses: []
|
||||
},
|
||||
refresh: null,
|
||||
signature: null
|
||||
}
|
||||
|
||||
const { from, token, value } = await this.dht.request(
|
||||
{
|
||||
token: null,
|
||||
command: COMMANDS.FIND_PEER,
|
||||
target: this.target,
|
||||
value: null
|
||||
},
|
||||
to
|
||||
)
|
||||
|
||||
if (!token || !from.id || !value) return
|
||||
|
||||
unann.signature = await this._signUnannounce(this.target, token, from.id, unann, this.keyPair)
|
||||
|
||||
await this.dht.request(
|
||||
{
|
||||
token,
|
||||
command: COMMANDS.UNANNOUNCE,
|
||||
target: this.target,
|
||||
value: c.encode(m.announce, unann)
|
||||
},
|
||||
to
|
||||
)
|
||||
}
|
||||
|
||||
async _commit(msg, relays, relayAddresses) {
|
||||
const ann = {
|
||||
peer: {
|
||||
publicKey: this.keyPair.publicKey,
|
||||
relayAddresses: []
|
||||
},
|
||||
refresh: null,
|
||||
signature: null
|
||||
}
|
||||
|
||||
ann.signature = await this._signAnnounce(this.target, msg.token, msg.from.id, ann, this.keyPair)
|
||||
|
||||
const res = await this.dht.request(
|
||||
{
|
||||
token: msg.token,
|
||||
command: COMMANDS.ANNOUNCE,
|
||||
target: this.target,
|
||||
value: c.encode(m.announce, ann)
|
||||
},
|
||||
msg.from
|
||||
)
|
||||
|
||||
if (res.error !== 0) return
|
||||
|
||||
if (relayAddresses.length < 3) relayAddresses.push({ host: msg.from.host, port: msg.from.port })
|
||||
relays.push({ relayAddress: msg.from, peerAddress: msg.to })
|
||||
|
||||
this._serverRelays[2].set(msg.from.host + ':' + msg.from.port, msg.from)
|
||||
}
|
||||
|
||||
_cycle() {
|
||||
const tmp = this._serverRelays[0]
|
||||
this._serverRelays[0] = this._serverRelays[1]
|
||||
this._serverRelays[1] = this._serverRelays[2]
|
||||
this._serverRelays[2] = tmp
|
||||
tmp.clear()
|
||||
}
|
||||
}
|
||||
|
||||
function resolved(ps) {
|
||||
let replied = 0
|
||||
let ticks = ps.length + 1
|
||||
|
||||
return new Promise((resolve) => {
|
||||
for (const p of ps) p.then(push, tick)
|
||||
tick()
|
||||
|
||||
function push(v) {
|
||||
replied++
|
||||
tick()
|
||||
}
|
||||
|
||||
function tick() {
|
||||
if (--ticks === 0) resolve(replied)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function pickBest(replies) {
|
||||
// TODO: pick the ones closest to us RTT wise
|
||||
return replies.slice(0, 3)
|
||||
}
|
||||
|
||||
function noop() {}
|
||||
+889
@@ -0,0 +1,889 @@
|
||||
const NoiseSecretStream = require('@hyperswarm/secret-stream')
|
||||
const b4a = require('b4a')
|
||||
const relay = require('blind-relay')
|
||||
const { isReserved, isBogon } = require('bogon')
|
||||
const safetyCatch = require('safety-catch')
|
||||
const unslab = require('unslab')
|
||||
const Semaphore = require('./semaphore')
|
||||
const NoiseWrap = require('./noise-wrap')
|
||||
const SecurePayload = require('./secure-payload')
|
||||
const Holepuncher = require('./holepuncher')
|
||||
const Sleeper = require('./sleeper')
|
||||
const { FIREWALL, ERROR } = require('./constants')
|
||||
const { unslabbedHash } = require('./crypto')
|
||||
const {
|
||||
CANNOT_HOLEPUNCH,
|
||||
HANDSHAKE_INVALID,
|
||||
HOLEPUNCH_ABORTED,
|
||||
HOLEPUNCH_INVALID,
|
||||
HOLEPUNCH_PROBE_TIMEOUT,
|
||||
HOLEPUNCH_DOUBLE_RANDOMIZED_NATS,
|
||||
PEER_CONNECTION_FAILED,
|
||||
PEER_NOT_FOUND,
|
||||
REMOTE_ABORTED,
|
||||
REMOTE_NOT_HOLEPUNCHABLE,
|
||||
REMOTE_NOT_HOLEPUNCHING,
|
||||
SERVER_ERROR,
|
||||
SERVER_INCOMPATIBLE,
|
||||
RELAY_ABORTED,
|
||||
SUSPENDED
|
||||
} = require('./errors')
|
||||
const { decode } = require('hypercore-id-encoding')
|
||||
const HyperDHTAddress = require('hyperdht-address')
|
||||
|
||||
module.exports = function connect(dht, publicKey, opts = {}) {
|
||||
const pool = opts.pool || null
|
||||
|
||||
const { key, nodes: providedNodes } = HyperDHTAddress.decode(
|
||||
b4a.isBuffer(publicKey) ? publicKey : decode(publicKey)
|
||||
)
|
||||
publicKey = key
|
||||
|
||||
if (pool && pool.has(publicKey)) return pool.get(publicKey)
|
||||
|
||||
publicKey = unslab(publicKey)
|
||||
|
||||
opts.relayAddresses = opts.relayAddresses || providedNodes || []
|
||||
const keyPair = opts.keyPair || dht.defaultKeyPair
|
||||
const relayThrough = selectRelay(opts.relayThrough || null)
|
||||
const encryptedSocket = (opts.createSecretStream || defaultCreateSecretStream)(true, null, {
|
||||
publicKey: keyPair.publicKey,
|
||||
remotePublicKey: publicKey,
|
||||
autoStart: false,
|
||||
keepAlive: dht.connectionKeepAlive
|
||||
})
|
||||
|
||||
// in case a socket is made during suspended state, destroy it immediately
|
||||
if (dht.suspended || !dht._connectable) {
|
||||
encryptedSocket.destroy(SUSPENDED())
|
||||
return encryptedSocket
|
||||
}
|
||||
|
||||
if (pool) pool._attachStream(encryptedSocket, false)
|
||||
|
||||
const id = b4a.toString(publicKey, 'hex')
|
||||
const c = {
|
||||
id,
|
||||
dht,
|
||||
session: dht.session(),
|
||||
relayAddresses: opts.relayAddresses,
|
||||
remoteRelayAddresses: [],
|
||||
pool,
|
||||
round: 0,
|
||||
target: unslabbedHash(publicKey),
|
||||
remotePublicKey: publicKey,
|
||||
reusableSocket: !!opts.reusableSocket,
|
||||
handshake: (opts.createHandshake || defaultCreateHandshake)(keyPair, publicKey),
|
||||
request: null,
|
||||
requesting: false,
|
||||
lan: opts.localConnection !== false,
|
||||
firewall: FIREWALL.UNKNOWN,
|
||||
rawStream: dht.createRawStream({ framed: true, firewall }),
|
||||
connect: null,
|
||||
query: null,
|
||||
puncher: null,
|
||||
payload: null,
|
||||
passiveConnectTimeout: null,
|
||||
serverSocket: null,
|
||||
serverAddress: null,
|
||||
onsocket: null,
|
||||
sleeper: new Sleeper(),
|
||||
encryptedSocket,
|
||||
|
||||
// Relay state
|
||||
relayTimeout: null,
|
||||
relayThrough,
|
||||
relayToken: relayThrough ? relay.token() : null,
|
||||
relaySocket: null,
|
||||
relayClient: null,
|
||||
relayPaired: false,
|
||||
relayKeepAlive: opts.relayKeepAlive || 5000
|
||||
}
|
||||
|
||||
// If the raw stream receives an error signal pre connect (ie from the firewall hook), make sure
|
||||
// to forward that to the encrypted socket for proper teardown
|
||||
c.rawStream.on('error', autoDestroy)
|
||||
c.rawStream.once('connect', () => {
|
||||
c.rawStream.removeListener('error', autoDestroy)
|
||||
})
|
||||
|
||||
encryptedSocket.on('close', function () {
|
||||
if (c.passiveConnectTimeout) clearPassiveConnectTimeout(c)
|
||||
if (c.query) c.query.destroy()
|
||||
if (c.puncher) c.puncher.destroy()
|
||||
if (c.rawStream) c.rawStream.destroy()
|
||||
c.session.destroy()
|
||||
c.sleeper.resume()
|
||||
})
|
||||
|
||||
// Safe to run in the background - never throws
|
||||
if (dht.suspended) encryptedSocket.destroy(SUSPENDED())
|
||||
else connectAndHolepunch(c, opts)
|
||||
|
||||
return encryptedSocket
|
||||
|
||||
function autoDestroy(err) {
|
||||
maybeDestroyEncryptedSocket(c, err)
|
||||
}
|
||||
|
||||
function firewall(socket, port, host) {
|
||||
// Check if the traffic originated from the socket on which we're expecting relay traffic. If so,
|
||||
// we haven't hole punched yet and the other side is just sending us traffic through the relay.
|
||||
if (c.relaySocket && isRelay(c.relaySocket, socket, port, host)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (c.onsocket) {
|
||||
c.onsocket(socket, port, host)
|
||||
} else {
|
||||
c.serverSocket = socket
|
||||
c.serverAddress = { port, host }
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function isDone(c) {
|
||||
// we are destroying or the puncher is connected - done
|
||||
if (c.encryptedSocket.destroying || !!(c.puncher && c.puncher.connected)) {
|
||||
return true
|
||||
}
|
||||
// not destroying, but no raw stream - def not done
|
||||
if (c.encryptedSocket.rawStream === null) {
|
||||
return false
|
||||
}
|
||||
// we are relayed, but the puncher is not done yet
|
||||
if (c.relaySocket && !!(c.puncher && !c.puncher.connected && !c.puncher.destroyed)) {
|
||||
return false
|
||||
}
|
||||
// we are done
|
||||
return true
|
||||
}
|
||||
|
||||
async function retryRoute(c, route) {
|
||||
const ref = c.dht._socketPool.lookup(route.socket)
|
||||
|
||||
if (!ref) {
|
||||
if (route.socket === c.dht.socket) {
|
||||
await connectThroughNode(c, route.address, c.dht.socket)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ref.active()
|
||||
|
||||
try {
|
||||
await connectThroughNode(c, route.address, route.socket)
|
||||
} catch {
|
||||
// if error, just ignore, and continue through the existing strat
|
||||
}
|
||||
|
||||
ref.inactive()
|
||||
}
|
||||
|
||||
async function connectAndHolepunch(c, opts) {
|
||||
const route = c.reusableSocket ? c.dht._socketPool.routes.get(c.remotePublicKey) : null
|
||||
|
||||
if (route) {
|
||||
await retryRoute(c, route)
|
||||
if (isDone(c)) return
|
||||
}
|
||||
|
||||
await findAndConnect(c, opts)
|
||||
if (isDone(c)) return
|
||||
|
||||
if (!c.connect) {
|
||||
// TODO: just a quick fix for now, should retry prob
|
||||
maybeDestroyEncryptedSocket(c, HANDSHAKE_INVALID())
|
||||
return
|
||||
}
|
||||
|
||||
await holepunch(c, opts)
|
||||
}
|
||||
|
||||
function getFirstRemoteAddress(addrs, serverAddress) {
|
||||
for (const addr of addrs) {
|
||||
if (isBogon(addr.host)) continue
|
||||
return addr
|
||||
}
|
||||
|
||||
return serverAddress
|
||||
}
|
||||
|
||||
async function holepunch(c, opts) {
|
||||
let { relayAddress, serverAddress, clientAddress, payload } = c.connect
|
||||
|
||||
const remoteHolepunchable = !!(payload.holepunch && payload.holepunch.relays.length)
|
||||
|
||||
const relayed = diffAddress(serverAddress, relayAddress)
|
||||
|
||||
if (payload.firewall === FIREWALL.OPEN || (relayed && !remoteHolepunchable)) {
|
||||
const addr = getFirstRemoteAddress(payload.addresses4, serverAddress)
|
||||
if (addr) {
|
||||
const socket = c.dht.socket
|
||||
c.dht.stats.punches.open++
|
||||
c.onsocket(socket, addr.port, addr.host)
|
||||
return
|
||||
}
|
||||
// TODO: check all addresses also obvs
|
||||
}
|
||||
|
||||
const onabort = () => {
|
||||
c.session.destroy()
|
||||
maybeDestroyEncryptedSocket(c, HOLEPUNCH_ABORTED())
|
||||
}
|
||||
|
||||
if (c.firewall === FIREWALL.OPEN) {
|
||||
c.passiveConnectTimeout = setTimeout(onabort, 10000)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: would be better to just try local addrs in the background whilst continuing with other strategies...
|
||||
if (c.lan && relayed && clientAddress.host === serverAddress.host) {
|
||||
const serverAddresses = payload.addresses4.filter(onlyNonReserved)
|
||||
|
||||
if (serverAddresses.length > 0) {
|
||||
const myAddresses = Holepuncher.localAddresses(c.dht.io.serverSocket)
|
||||
const addr = Holepuncher.matchAddress(myAddresses, serverAddresses) || serverAddresses[0]
|
||||
|
||||
const socket = c.dht.io.serverSocket
|
||||
try {
|
||||
await c.dht.ping(addr)
|
||||
} catch {
|
||||
maybeDestroyEncryptedSocket(c, HOLEPUNCH_ABORTED())
|
||||
return
|
||||
}
|
||||
c.onsocket(socket, addr.port, addr.host)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!remoteHolepunchable) {
|
||||
maybeDestroyEncryptedSocket(c, CANNOT_HOLEPUNCH())
|
||||
return
|
||||
}
|
||||
|
||||
c.puncher = new Holepuncher(c.dht, c.session, true, payload.firewall)
|
||||
|
||||
c.puncher.onconnect = c.onsocket
|
||||
c.puncher.onabort = onabort
|
||||
|
||||
const serverRelay = pickServerRelay(payload.holepunch.relays, relayAddress)
|
||||
|
||||
// Begin holepunching!
|
||||
|
||||
let probe
|
||||
try {
|
||||
probe = await probeRound(c, opts.fastOpen === false ? null : serverAddress, serverRelay, true)
|
||||
} catch (err) {
|
||||
destroyPuncher(c)
|
||||
// TODO: we should retry here with some of the other relays, bail for now
|
||||
maybeDestroyEncryptedSocket(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
if (isDone(c) || !probe) return
|
||||
const { token, peerAddress } = probe
|
||||
|
||||
// If the relay the server picked is the same as the relay the client picked,
|
||||
// then we can use the peerAddress that round one indicates the server wants to use.
|
||||
// This shaves off a roundtrip if the server chose to reroll its socket due to some NAT
|
||||
// issue with the first one it picked (ie mobile nat inconsistencies...).
|
||||
// If the relays were different, then the server would not have a UDP session open on this address
|
||||
// to the client relay, which round2 uses.
|
||||
if (
|
||||
!diffAddress(serverRelay.relayAddress, relayAddress) &&
|
||||
diffAddress(serverAddress, peerAddress)
|
||||
) {
|
||||
serverAddress = peerAddress
|
||||
await c.puncher.openSession(serverAddress)
|
||||
if (isDone(c)) return
|
||||
}
|
||||
|
||||
// TODO: still continue here if a local connection might work, but then do not holepunch...
|
||||
if (
|
||||
opts.holepunch &&
|
||||
!opts.holepunch(
|
||||
c.puncher.remoteFirewall,
|
||||
c.puncher.nat.firewall,
|
||||
c.puncher.remoteAddresses,
|
||||
c.puncher.nat.addresses
|
||||
)
|
||||
) {
|
||||
await abort(c, serverRelay, HOLEPUNCH_ABORTED('Client aborted holepunch'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await roundPunch(c, serverAddress, token, relayAddress, serverRelay, false)
|
||||
} catch (err) {
|
||||
destroyPuncher(c)
|
||||
// TODO: retry with another relay?
|
||||
maybeDestroyEncryptedSocket(c, err)
|
||||
}
|
||||
}
|
||||
|
||||
async function connectThroughNodes(c, addresses, socket) {
|
||||
for (const address of addresses) {
|
||||
if (isDone(c) || c.connect) return
|
||||
|
||||
c.remoteRelayAddresses.push(address)
|
||||
await connectThroughNode(c, address, socket)
|
||||
}
|
||||
}
|
||||
|
||||
async function findAndConnect(c, opts) {
|
||||
let attempts = 0
|
||||
let relayAddresses =
|
||||
opts.relayAddresses && opts.relayAddresses.length ? opts.relayAddresses : null
|
||||
|
||||
if (!relayAddresses) {
|
||||
const cachedRelayAddresses = c.dht._relayAddressesCache.get(c.id)
|
||||
if (cachedRelayAddresses) relayAddresses = cachedRelayAddresses
|
||||
}
|
||||
|
||||
if (c.dht._persistent) {
|
||||
// check if we know the route ourself...
|
||||
const route = c.dht._router.get(c.target)
|
||||
if (route && route.relay !== null) {
|
||||
relayAddresses = [{ host: route.relay.host, port: route.relay.port }]
|
||||
}
|
||||
}
|
||||
|
||||
// 2 is how many parallel connect attempts we want to do, we can make this configurable
|
||||
const preConnect = relayAddresses !== null && relayAddresses.length > 0
|
||||
const sem = new Semaphore(preConnect ? 3 : 2)
|
||||
const signal = sem.signal.bind(sem)
|
||||
const tries = relayAddresses !== null ? 2 : 1
|
||||
|
||||
if (preConnect) {
|
||||
await sem.wait()
|
||||
connectThroughNodes(c, relayAddresses, null).then(signal, signal)
|
||||
}
|
||||
|
||||
try {
|
||||
for (let i = 0; i < tries && !isDone(c) && !c.connect; i++) {
|
||||
c.query = c.dht.findPeer(c.target, {
|
||||
hash: false,
|
||||
session: c.session,
|
||||
nodes: relayAddresses,
|
||||
retries: 3
|
||||
})
|
||||
|
||||
for await (const data of c.query) {
|
||||
await sem.wait()
|
||||
if (isDone(c)) return
|
||||
|
||||
if (c.connect) {
|
||||
sem.signal()
|
||||
break
|
||||
}
|
||||
|
||||
// Skip node already run via preConnect
|
||||
if (preConnect && relayAddresses && isRelayAddress(relayAddresses, data)) {
|
||||
sem.signal()
|
||||
continue
|
||||
}
|
||||
|
||||
c.remoteRelayAddresses.push(data.from)
|
||||
attempts++
|
||||
connectThroughNode(c, data.from, null).then(signal, signal)
|
||||
}
|
||||
|
||||
relayAddresses = null
|
||||
|
||||
if (attempts > 0) await sem.flush()
|
||||
}
|
||||
|
||||
c.query = null
|
||||
if (isDone(c)) return
|
||||
|
||||
// flush the semaphore
|
||||
await sem.flush()
|
||||
if (isDone(c)) return
|
||||
} catch (err) {
|
||||
c.query = null
|
||||
maybeDestroyEncryptedSocket(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
if (!c.connect) {
|
||||
maybeDestroyEncryptedSocket(c, attempts ? PEER_CONNECTION_FAILED() : PEER_NOT_FOUND())
|
||||
}
|
||||
}
|
||||
|
||||
async function connectThroughNode(c, address, socket) {
|
||||
if (!c.requesting) {
|
||||
// If we have a stable server address, send it over now
|
||||
const addr = c.dht.remoteAddress()
|
||||
const localAddrs = c.lan ? Holepuncher.localAddresses(c.dht.io.serverSocket) : null
|
||||
const addresses4 = []
|
||||
|
||||
if (addr) addresses4.push(addr)
|
||||
if (localAddrs) addresses4.push(...localAddrs)
|
||||
|
||||
c.firewall = addr ? FIREWALL.OPEN : FIREWALL.UNKNOWN
|
||||
c.requesting = true
|
||||
c.request = await c.handshake.send({
|
||||
error: ERROR.NONE,
|
||||
firewall: c.firewall,
|
||||
holepunch: null,
|
||||
addresses4,
|
||||
addresses6: [],
|
||||
udx: {
|
||||
reusableSocket: c.reusableSocket,
|
||||
id: c.rawStream.id,
|
||||
seq: 0
|
||||
},
|
||||
secretStream: {},
|
||||
relayThrough: c.relayThrough ? { publicKey: c.relayThrough, token: c.relayToken } : null
|
||||
})
|
||||
if (isDone(c)) return
|
||||
}
|
||||
|
||||
const { serverAddress, clientAddress, relayed, noise } = await c.dht._router.peerHandshake(
|
||||
c.target,
|
||||
{ noise: c.request, socket, session: c.session },
|
||||
address
|
||||
)
|
||||
if (isDone(c) || c.connect) return
|
||||
|
||||
const payload = await c.handshake.recv(noise)
|
||||
if (isDone(c) || !payload) return
|
||||
|
||||
if (payload.version !== 1) {
|
||||
maybeDestroyEncryptedSocket(c, SERVER_INCOMPATIBLE())
|
||||
return
|
||||
}
|
||||
if (payload.error !== ERROR.NONE) {
|
||||
maybeDestroyEncryptedSocket(c, SERVER_ERROR())
|
||||
return
|
||||
}
|
||||
if (!payload.udx) {
|
||||
maybeDestroyEncryptedSocket(c, SERVER_ERROR('Server did not send UDX data'))
|
||||
return
|
||||
}
|
||||
|
||||
const hs = c.handshake.final()
|
||||
|
||||
c.handshake = null
|
||||
c.request = null
|
||||
c.requesting = false
|
||||
c.connect = {
|
||||
relayed,
|
||||
relayAddress: address,
|
||||
clientAddress,
|
||||
serverAddress,
|
||||
payload
|
||||
}
|
||||
|
||||
c.payload = new SecurePayload(hs.holepunchSecret)
|
||||
|
||||
c.onsocket = function (socket, port, host) {
|
||||
if (c.rawStream === null) return // Already hole punched
|
||||
|
||||
if (c.rawStream.connected) {
|
||||
const remoteChanging = c.rawStream.changeRemote(socket, c.connect.payload.udx.id, port, host)
|
||||
|
||||
if (remoteChanging) remoteChanging.catch(safetyCatch)
|
||||
} else {
|
||||
// cache the relay addrs for a future reconnect, we prefer the remote one so they
|
||||
// can give us the correct ones from their pov
|
||||
if (payload.relayAddresses && payload.relayAddresses.length) {
|
||||
c.dht._relayAddressesCache.set(c.id, payload.relayAddresses)
|
||||
} else if (c.remoteRelayAddresses.length) {
|
||||
c.dht._relayAddressesCache.set(c.id, c.remoteRelayAddresses)
|
||||
}
|
||||
|
||||
c.rawStream.connect(socket, c.connect.payload.udx.id, port, host)
|
||||
c.encryptedSocket.start(c.rawStream, { handshake: hs })
|
||||
}
|
||||
|
||||
if (c.reusableSocket && payload.udx.reusableSocket) {
|
||||
c.dht._socketPool.routes.add(c.remotePublicKey, c.rawStream)
|
||||
}
|
||||
|
||||
if (c.puncher) {
|
||||
c.puncher.onabort = noop
|
||||
c.puncher.destroy()
|
||||
}
|
||||
|
||||
if (c.passiveConnectTimeout) {
|
||||
clearPassiveConnectTimeout(c)
|
||||
}
|
||||
|
||||
c.rawStream = null
|
||||
}
|
||||
|
||||
if (payload.relayThrough || c.relayThrough) {
|
||||
relayConnection(c, c.relayThrough, payload, hs)
|
||||
}
|
||||
|
||||
if (c.serverSocket) {
|
||||
c.onsocket(c.serverSocket, c.serverAddress.port, c.serverAddress.host)
|
||||
return
|
||||
}
|
||||
|
||||
if (!relayed) {
|
||||
c.onsocket(socket || c.dht.socket, address.port, address.host)
|
||||
}
|
||||
|
||||
c.session.destroy()
|
||||
}
|
||||
|
||||
async function updateHolepunch(c, peerAddress, relayAddr, payload) {
|
||||
const holepunch = await c.dht._router.peerHolepunch(
|
||||
c.target,
|
||||
{
|
||||
id: c.connect.payload.holepunch.id,
|
||||
payload: c.payload.encrypt(payload),
|
||||
peerAddress,
|
||||
socket: c.puncher.socket,
|
||||
session: c.session
|
||||
},
|
||||
relayAddr
|
||||
)
|
||||
|
||||
if (isDone(c)) return null
|
||||
|
||||
const remotePayload = c.payload.decrypt(holepunch.payload)
|
||||
if (!remotePayload) {
|
||||
throw HOLEPUNCH_INVALID()
|
||||
}
|
||||
|
||||
const { error, firewall, punching, addresses, remoteToken } = remotePayload
|
||||
|
||||
if (error === ERROR.TRY_LATER && c.relayToken && payload.punching) {
|
||||
return {
|
||||
tryLater: true,
|
||||
...holepunch,
|
||||
payload: remotePayload
|
||||
}
|
||||
}
|
||||
|
||||
if (error !== ERROR.NONE) {
|
||||
throw REMOTE_ABORTED('Remote aborted with error code ' + error)
|
||||
}
|
||||
|
||||
const echoed = !!(remoteToken && payload.token && b4a.equals(remoteToken, payload.token))
|
||||
|
||||
c.puncher.updateRemote({
|
||||
punching,
|
||||
firewall,
|
||||
addresses,
|
||||
verified: echoed ? peerAddress.host : null
|
||||
})
|
||||
|
||||
return {
|
||||
tryLater: false,
|
||||
...holepunch,
|
||||
payload: remotePayload
|
||||
}
|
||||
}
|
||||
|
||||
async function probeRound(c, serverAddress, serverRelay, retry) {
|
||||
// Open a quick low ttl session against what we think is the server
|
||||
if (serverAddress) await c.puncher.openSession(serverAddress)
|
||||
|
||||
if (isDone(c)) return null
|
||||
|
||||
const reply = await updateHolepunch(c, serverRelay.peerAddress, serverRelay.relayAddress, {
|
||||
error: ERROR.NONE,
|
||||
firewall: c.puncher.nat.firewall,
|
||||
round: c.round++,
|
||||
connected: false,
|
||||
punching: false,
|
||||
addresses: c.puncher.nat.addresses,
|
||||
remoteAddress: serverAddress,
|
||||
token: null,
|
||||
remoteToken: null
|
||||
})
|
||||
|
||||
if (isDone(c) || !reply) return null
|
||||
|
||||
const { peerAddress } = reply
|
||||
const { address, token } = reply.payload
|
||||
|
||||
c.puncher.nat.add(reply.to, reply.from)
|
||||
|
||||
// Open another quick low ttl session against what the server says their address is,
|
||||
// if they haven't said they are random yet
|
||||
if (
|
||||
c.puncher.remoteFirewall < FIREWALL.RANDOM &&
|
||||
address &&
|
||||
address.host &&
|
||||
address.port &&
|
||||
diffAddress(address, serverAddress)
|
||||
) {
|
||||
await c.puncher.openSession(address)
|
||||
if (isDone(c)) return null
|
||||
}
|
||||
|
||||
// If the remote told us they didn't know their nat firewall yet, give them a chance to figure it out
|
||||
// They might say this to see if the "fast mode" punch comes through first.
|
||||
if (c.puncher.remoteFirewall === FIREWALL.UNKNOWN) {
|
||||
await c.sleeper.pause(1000)
|
||||
if (isDone(c)) return null
|
||||
}
|
||||
|
||||
let stable = await c.puncher.analyze(false)
|
||||
if (isDone(c)) return null
|
||||
|
||||
// If the socket seems unstable, try to make it stable by setting the "allowReopen" flag
|
||||
// Mostly relevant for mobile networks
|
||||
if (!stable) {
|
||||
stable = await c.puncher.analyze(true)
|
||||
if (isDone(c)) return null
|
||||
if (stable) return probeRound(c, serverAddress, serverRelay, false)
|
||||
}
|
||||
|
||||
if ((c.puncher.remoteFirewall === FIREWALL.UNKNOWN || !token) && retry) {
|
||||
return probeRound(c, serverAddress, serverRelay, false)
|
||||
}
|
||||
|
||||
if (
|
||||
c.puncher.remoteFirewall === FIREWALL.UNKNOWN ||
|
||||
c.puncher.nat.firewall === FIREWALL.UNKNOWN
|
||||
) {
|
||||
await abort(c, serverRelay, HOLEPUNCH_PROBE_TIMEOUT())
|
||||
return null
|
||||
}
|
||||
|
||||
if (c.puncher.remoteFirewall >= FIREWALL.RANDOM && c.puncher.nat.firewall >= FIREWALL.RANDOM) {
|
||||
await abort(c, serverRelay, HOLEPUNCH_DOUBLE_RANDOMIZED_NATS())
|
||||
return null
|
||||
}
|
||||
|
||||
return { token, peerAddress }
|
||||
}
|
||||
|
||||
async function roundPunch(c, serverAddress, remoteToken, clientRelay, serverRelay, delayed) {
|
||||
// We are gossiping our final NAT status to the other peer now
|
||||
// so make sure we don't update our local view for now as that can make things weird
|
||||
c.puncher.nat.freeze()
|
||||
|
||||
const isRandom =
|
||||
c.puncher.remoteFirewall >= FIREWALL.RANDOM || c.puncher.nat.firewall >= FIREWALL.RANDOM
|
||||
if (isRandom) {
|
||||
while (
|
||||
c.dht._randomPunches >= c.dht._randomPunchLimit ||
|
||||
Date.now() - c.dht._lastRandomPunch < c.dht._randomPunchInterval
|
||||
) {
|
||||
// if no relay can help, bail
|
||||
if (!c.relayToken) throw HOLEPUNCH_ABORTED()
|
||||
|
||||
if (!delayed) {
|
||||
delayed = true
|
||||
await updateHolepunch(c, serverAddress, clientRelay, {
|
||||
error: ERROR.NONE,
|
||||
firewall: c.puncher.nat.firewall,
|
||||
round: c.round++,
|
||||
connected: false,
|
||||
punching: false,
|
||||
addresses: c.puncher.nat.addresses,
|
||||
remoteAddress: null,
|
||||
token: c.payload.token(serverAddress),
|
||||
remoteToken
|
||||
})
|
||||
if (isDone(c)) return
|
||||
}
|
||||
|
||||
await tryLater(c)
|
||||
if (isDone(c)) return
|
||||
}
|
||||
}
|
||||
|
||||
// increment now, so we can commit to punching
|
||||
if (isRandom) c.dht._randomPunches++
|
||||
|
||||
let reply
|
||||
|
||||
try {
|
||||
// if delayed switch to the servers chosen relay - we validated anyway
|
||||
reply = await updateHolepunch(
|
||||
c,
|
||||
delayed ? serverRelay.peerAddress : serverAddress,
|
||||
delayed ? serverRelay.relayAddress : clientRelay,
|
||||
{
|
||||
error: ERROR.NONE,
|
||||
firewall: c.puncher.nat.firewall,
|
||||
round: c.round++,
|
||||
connected: false,
|
||||
punching: true,
|
||||
addresses: c.puncher.nat.addresses,
|
||||
remoteAddress: null,
|
||||
token: delayed ? null : c.payload.token(serverAddress),
|
||||
remoteToken
|
||||
}
|
||||
)
|
||||
} finally {
|
||||
// decrement as punch increments for us
|
||||
if (isRandom) c.dht._randomPunches--
|
||||
}
|
||||
|
||||
if (isDone(c)) return
|
||||
if (!reply) return
|
||||
|
||||
if (reply.tryLater) {
|
||||
await tryLater(c)
|
||||
if (isDone(c)) return
|
||||
return roundPunch(c, serverAddress, remoteToken, clientRelay, serverRelay, true)
|
||||
}
|
||||
|
||||
if (!c.puncher.remoteHolepunching) {
|
||||
throw REMOTE_NOT_HOLEPUNCHING()
|
||||
}
|
||||
|
||||
if (!(await c.puncher.punch())) {
|
||||
throw REMOTE_NOT_HOLEPUNCHABLE()
|
||||
}
|
||||
}
|
||||
|
||||
async function tryLater(c) {
|
||||
if (!c.relayToken) throw HOLEPUNCH_ABORTED()
|
||||
await c.sleeper.pause(10000 + Math.round(Math.random() * 10000))
|
||||
}
|
||||
|
||||
function maybeDestroyEncryptedSocket(c, err) {
|
||||
if (isDone(c)) return
|
||||
if (c.encryptedSocket.rawStream) return
|
||||
if (c.relaySocket) return // waiting for the relay
|
||||
if (c.puncher && !c.puncher.destroyed) return // waiting for the puncher
|
||||
c.session.destroy()
|
||||
c.encryptedSocket.destroy(err)
|
||||
}
|
||||
|
||||
async function abort(c, { peerAddress, relayAddress }, err) {
|
||||
try {
|
||||
await updateHolepunch(c, peerAddress, relayAddress, {
|
||||
error: ERROR.ABORTED,
|
||||
firewall: FIREWALL.UNKNOWN,
|
||||
round: c.round++,
|
||||
connected: false,
|
||||
punching: false,
|
||||
addresses: null,
|
||||
remoteAddress: null,
|
||||
token: null,
|
||||
remoteToken: null
|
||||
})
|
||||
} catch (err) {
|
||||
safetyCatch(err)
|
||||
}
|
||||
|
||||
destroyPuncher(c)
|
||||
maybeDestroyEncryptedSocket(c, err)
|
||||
}
|
||||
|
||||
function relayConnection(c, relayThrough, payload, hs) {
|
||||
let isInitiator
|
||||
let publicKey
|
||||
let token
|
||||
|
||||
if (payload.relayThrough) {
|
||||
isInitiator = false
|
||||
publicKey = payload.relayThrough.publicKey
|
||||
token = payload.relayThrough.token
|
||||
} else {
|
||||
isInitiator = true
|
||||
publicKey = relayThrough
|
||||
token = c.relayToken
|
||||
}
|
||||
|
||||
c.relayToken = token
|
||||
c.relaySocket = c.dht.connect(publicKey)
|
||||
c.relaySocket.setKeepAlive(c.relayKeepAlive)
|
||||
c.relayClient = relay.Client.from(c.relaySocket, { id: c.relaySocket.publicKey })
|
||||
c.relayTimeout = setTimeout(onabort, 15000, null)
|
||||
|
||||
c.relayClient.pair(isInitiator, token, c.rawStream).on('error', onabort).on('data', ondata)
|
||||
|
||||
function ondata(remoteId) {
|
||||
if (c.relayTimeout) clearRelayTimeout(c)
|
||||
if (c.rawStream === null) {
|
||||
onabort(null)
|
||||
return
|
||||
}
|
||||
|
||||
c.relayPaired = true
|
||||
|
||||
const { remotePort, remoteHost, socket } = c.relaySocket.rawStream
|
||||
|
||||
c.rawStream
|
||||
.on('close', () => c.relaySocket.destroy())
|
||||
.connect(socket, remoteId, remotePort, remoteHost)
|
||||
|
||||
c.encryptedSocket.start(c.rawStream, { handshake: hs })
|
||||
}
|
||||
|
||||
function onabort(err) {
|
||||
if (c.relayTimeout) clearRelayTimeout(c)
|
||||
const socket = c.relaySocket
|
||||
c.relayToken = null
|
||||
c.relaySocket = null
|
||||
if (socket) socket.destroy()
|
||||
maybeDestroyEncryptedSocket(c, err || RELAY_ABORTED())
|
||||
}
|
||||
}
|
||||
|
||||
function clearPassiveConnectTimeout(c) {
|
||||
clearTimeout(c.passiveConnectTimeout)
|
||||
c.passiveConnectTimeout = null
|
||||
}
|
||||
|
||||
function clearRelayTimeout(c) {
|
||||
clearTimeout(c.relayTimeout)
|
||||
c.relayTimeout = null
|
||||
}
|
||||
|
||||
function destroyPuncher(c) {
|
||||
if (c.puncher) c.puncher.destroy()
|
||||
c.session.destroy()
|
||||
}
|
||||
|
||||
function pickServerRelay(relays, clientRelay) {
|
||||
for (const r of relays) {
|
||||
if (!diffAddress(r.relayAddress, clientRelay)) return r
|
||||
}
|
||||
return relays[0]
|
||||
}
|
||||
|
||||
function diffAddress(a, b) {
|
||||
return a.host !== b.host || a.port !== b.port
|
||||
}
|
||||
|
||||
function defaultCreateHandshake(keyPair, remotePublicKey) {
|
||||
return new NoiseWrap(keyPair, remotePublicKey)
|
||||
}
|
||||
|
||||
function defaultCreateSecretStream(isInitiator, rawStream, opts) {
|
||||
return new NoiseSecretStream(isInitiator, rawStream, opts)
|
||||
}
|
||||
|
||||
function onlyNonReserved(addr) {
|
||||
return !isReserved(addr.host)
|
||||
}
|
||||
|
||||
function isRelay(relaySocket, socket, port, host) {
|
||||
const stream = relaySocket.rawStream
|
||||
if (!stream) return false
|
||||
if (stream.socket !== socket) return false
|
||||
return port === stream.remotePort && host === stream.remoteHost
|
||||
}
|
||||
|
||||
function selectRelay(relayThrough) {
|
||||
if (typeof relayThrough === 'function') relayThrough = relayThrough()
|
||||
if (relayThrough === null) return null
|
||||
if (Array.isArray(relayThrough))
|
||||
return relayThrough[Math.floor(Math.random() * relayThrough.length)]
|
||||
return relayThrough
|
||||
}
|
||||
|
||||
function isRelayAddress(relayAddresses, data) {
|
||||
for (const node of relayAddresses) {
|
||||
if (node.host === data.from.host && node.port === data.from.port) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function noop() {}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
const EventEmitter = require('events')
|
||||
const b4a = require('b4a')
|
||||
const errors = require('./errors')
|
||||
|
||||
module.exports = class ConnectionPool extends EventEmitter {
|
||||
constructor(dht) {
|
||||
super()
|
||||
|
||||
this._dht = dht
|
||||
this._servers = new Map()
|
||||
this._connecting = new Map()
|
||||
this._connections = new Map()
|
||||
}
|
||||
|
||||
_attachServer(server) {
|
||||
const keyString = b4a.toString(server.publicKey, 'hex')
|
||||
|
||||
this._servers.set(keyString, server)
|
||||
|
||||
server
|
||||
.on('close', () => {
|
||||
this._servers.delete(keyString)
|
||||
})
|
||||
.on('connection', (socket) => {
|
||||
this._attachStream(socket, true)
|
||||
})
|
||||
}
|
||||
|
||||
_attachStream(stream, opened) {
|
||||
const existing = this.get(stream.remotePublicKey)
|
||||
|
||||
if (existing) {
|
||||
const keepNew =
|
||||
stream.isInitiator === existing.isInitiator ||
|
||||
b4a.compare(stream.publicKey, stream.remotePublicKey) > 0
|
||||
|
||||
if (keepNew) {
|
||||
let closed = false
|
||||
|
||||
const onclose = () => {
|
||||
closed = true
|
||||
}
|
||||
|
||||
existing
|
||||
.on('error', noop)
|
||||
.on('close', () => {
|
||||
if (closed) return
|
||||
|
||||
stream.off('error', noop).off('close', onclose)
|
||||
|
||||
this._attachStream(stream, opened)
|
||||
})
|
||||
.destroy(errors.DUPLICATE_CONNECTION())
|
||||
|
||||
stream.on('error', noop).on('close', onclose)
|
||||
} else {
|
||||
stream.on('error', noop).destroy(errors.DUPLICATE_CONNECTION())
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const session = new ConnectionRef(this, stream)
|
||||
|
||||
const keyString = b4a.toString(stream.remotePublicKey, 'hex')
|
||||
|
||||
if (opened) {
|
||||
this._connections.set(keyString, session)
|
||||
|
||||
stream.on('close', () => {
|
||||
this._connections.delete(keyString)
|
||||
})
|
||||
|
||||
this.emit('connection', stream, session)
|
||||
} else {
|
||||
this._connecting.set(keyString, session)
|
||||
|
||||
stream
|
||||
.on('error', noop)
|
||||
.on('close', () => {
|
||||
if (opened) this._connections.delete(keyString)
|
||||
else this._connecting.delete(keyString)
|
||||
})
|
||||
.on('open', () => {
|
||||
opened = true
|
||||
|
||||
this._connecting.delete(keyString)
|
||||
this._connections.set(keyString, session)
|
||||
|
||||
stream.off('error', noop)
|
||||
|
||||
this.emit('connection', stream, session)
|
||||
})
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
get connecting() {
|
||||
return this._connecting.size
|
||||
}
|
||||
|
||||
get connections() {
|
||||
return this._connections.values()
|
||||
}
|
||||
|
||||
has(publicKey) {
|
||||
const keyString = b4a.toString(publicKey, 'hex')
|
||||
|
||||
return this._connections.has(keyString) || this._connecting.has(keyString)
|
||||
}
|
||||
|
||||
get(publicKey) {
|
||||
const keyString = b4a.toString(publicKey, 'hex')
|
||||
|
||||
const existing = this._connections.get(keyString) || this._connecting.get(keyString)
|
||||
|
||||
return existing?._stream || null
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectionRef {
|
||||
constructor(pool, stream) {
|
||||
this._pool = pool
|
||||
this._stream = stream
|
||||
this._refs = 0
|
||||
}
|
||||
|
||||
active() {
|
||||
this._refs++
|
||||
}
|
||||
|
||||
inactive() {
|
||||
this._refs--
|
||||
}
|
||||
|
||||
release() {
|
||||
this._stream.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
function noop() {}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
const crypto = require('hypercore-crypto')
|
||||
|
||||
const COMMANDS = (exports.COMMANDS = {
|
||||
PEER_HANDSHAKE: 0,
|
||||
PEER_HOLEPUNCH: 1,
|
||||
FIND_PEER: 2,
|
||||
LOOKUP: 3,
|
||||
ANNOUNCE: 4,
|
||||
UNANNOUNCE: 5,
|
||||
MUTABLE_PUT: 6,
|
||||
MUTABLE_GET: 7,
|
||||
IMMUTABLE_PUT: 8,
|
||||
IMMUTABLE_GET: 9,
|
||||
PLUGIN: 10
|
||||
})
|
||||
|
||||
exports.BOOTSTRAP_NODES = global.Pear?.config.dht?.bootstrap || [
|
||||
'[email protected]:49737',
|
||||
'[email protected]:49737',
|
||||
'[email protected]:49737'
|
||||
]
|
||||
|
||||
exports.KNOWN_NODES = global.Pear?.config.dht?.nodes || []
|
||||
|
||||
exports.FIREWALL = {
|
||||
UNKNOWN: 0,
|
||||
OPEN: 1,
|
||||
CONSISTENT: 2,
|
||||
RANDOM: 3
|
||||
}
|
||||
|
||||
exports.ERROR = {
|
||||
// noise / connection related
|
||||
NONE: 0,
|
||||
ABORTED: 1,
|
||||
VERSION_MISMATCH: 2,
|
||||
TRY_LATER: 3,
|
||||
// dht related
|
||||
SEQ_REUSED: 16,
|
||||
SEQ_TOO_LOW: 17
|
||||
}
|
||||
|
||||
const [
|
||||
NS_ANNOUNCE,
|
||||
NS_UNANNOUNCE,
|
||||
NS_MUTABLE_PUT,
|
||||
NS_PEER_HANDSHAKE,
|
||||
NS_PEER_HOLEPUNCH,
|
||||
NS_PLUGIN
|
||||
] = crypto.namespace('hyperswarm/dht', [
|
||||
COMMANDS.ANNOUNCE,
|
||||
COMMANDS.UNANNOUNCE,
|
||||
COMMANDS.MUTABLE_PUT,
|
||||
COMMANDS.PEER_HANDSHAKE,
|
||||
COMMANDS.PEER_HOLEPUNCH,
|
||||
COMMANDS.PLUGIN
|
||||
])
|
||||
|
||||
exports.NS = {
|
||||
ANNOUNCE: NS_ANNOUNCE,
|
||||
UNANNOUNCE: NS_UNANNOUNCE,
|
||||
MUTABLE_PUT: NS_MUTABLE_PUT,
|
||||
PEER_HANDSHAKE: NS_PEER_HANDSHAKE,
|
||||
PEER_HOLEPUNCH: NS_PEER_HOLEPUNCH,
|
||||
PLUGIN: NS_PLUGIN
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
const sodium = require('sodium-universal')
|
||||
const b4a = require('b4a')
|
||||
|
||||
function hash(data) {
|
||||
const out = b4a.allocUnsafe(32)
|
||||
sodium.crypto_generichash(out, data)
|
||||
return out
|
||||
}
|
||||
|
||||
function unslabbedHash(data) {
|
||||
const out = b4a.allocUnsafeSlow(32)
|
||||
sodium.crypto_generichash(out, data)
|
||||
return out
|
||||
}
|
||||
|
||||
function createKeyPair(seed) {
|
||||
const publicKey = b4a.alloc(32)
|
||||
const secretKey = b4a.alloc(64)
|
||||
if (seed) sodium.crypto_sign_seed_keypair(publicKey, secretKey, seed)
|
||||
else sodium.crypto_sign_keypair(publicKey, secretKey)
|
||||
return { publicKey, secretKey }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
hash,
|
||||
unslabbedHash,
|
||||
createKeyPair
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
const b4a = require('b4a')
|
||||
const cenc = require('compact-encoding')
|
||||
|
||||
function encodeUnslab(enc, m) {
|
||||
// Faster than unslab(c.encode(enc, data)) because it avoids the mem copy.
|
||||
// Makes sense to put in compact-encoding when we need it in other modules too
|
||||
const state = cenc.state()
|
||||
enc.preencode(state, m)
|
||||
state.buffer = b4a.allocUnsafeSlow(state.end)
|
||||
enc.encode(state, m)
|
||||
return state.buffer
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
encodeUnslab
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
module.exports = class DHTError extends Error {
|
||||
constructor(msg, code, fn = DHTError) {
|
||||
super(`${code}: ${msg}`)
|
||||
this.code = code
|
||||
|
||||
if (Error.captureStackTrace) {
|
||||
Error.captureStackTrace(this, fn)
|
||||
}
|
||||
}
|
||||
|
||||
get name() {
|
||||
return 'DHTError'
|
||||
}
|
||||
|
||||
static BAD_HANDSHAKE_REPLY(msg = 'Bad handshake reply') {
|
||||
return new DHTError(msg, 'BAD_HANDSHAKE_REPLY', DHTError.BAD_HANDSHAKE_REPLY)
|
||||
}
|
||||
|
||||
static BAD_HOLEPUNCH_REPLY(msg = 'Bad holepunch reply') {
|
||||
return new DHTError(msg, 'BAD_HOLEPUNCH_REPLY', DHTError.BAD_HOLEPUNCH_REPLY)
|
||||
}
|
||||
|
||||
static HOLEPUNCH_ABORTED(msg = 'Holepunch aborted') {
|
||||
return new DHTError(msg, 'HOLEPUNCH_ABORTED', DHTError.HOLEPUNCH_ABORTED)
|
||||
}
|
||||
|
||||
static HOLEPUNCH_INVALID(msg = 'Invalid holepunch payload') {
|
||||
return new DHTError(msg, 'HOLEPUNCH_INVALID', DHTError.HOLEPUNCH_INVALID)
|
||||
}
|
||||
|
||||
static HOLEPUNCH_PROBE_TIMEOUT(msg = 'Holepunching probe did not finish in time') {
|
||||
return new DHTError(msg, 'HOLEPUNCH_PROBE_TIMEOUT', DHTError.HOLEPUNCH_PROBE_TIMEOUT)
|
||||
}
|
||||
|
||||
static HOLEPUNCH_DOUBLE_RANDOMIZED_NATS(msg = 'Both remote and local NATs are randomized') {
|
||||
return new DHTError(
|
||||
msg,
|
||||
'HOLEPUNCH_DOUBLE_RANDOMIZED_NATS',
|
||||
DHTError.HOLEPUNCH_DOUBLE_RANDOMIZED_NATS
|
||||
)
|
||||
}
|
||||
|
||||
static CANNOT_HOLEPUNCH(msg = 'Cannot holepunch to remote') {
|
||||
return new DHTError(msg, 'CANNOT_HOLEPUNCH', DHTError.CANNOT_HOLEPUNCH)
|
||||
}
|
||||
|
||||
static REMOTE_NOT_HOLEPUNCHING(msg = 'Remote is not holepunching') {
|
||||
return new DHTError(msg, 'REMOTE_NOT_HOLEPUNCHING', DHTError.REMOTE_NOT_HOLEPUNCHING)
|
||||
}
|
||||
|
||||
static REMOTE_NOT_HOLEPUNCHABLE(msg = 'Remote is not holepunchable') {
|
||||
return new DHTError(msg, 'REMOTE_NOT_HOLEPUNCHABLE', DHTError.REMOTE_NOT_HOLEPUNCHABLE)
|
||||
}
|
||||
|
||||
static REMOTE_ABORTED(msg = 'Remote aborted') {
|
||||
return new DHTError(msg, 'REMOTE_ABORTED', DHTError.REMOTE_ABORTED)
|
||||
}
|
||||
|
||||
static HANDSHAKE_UNFINISHED(msg = 'Handshake did not finish') {
|
||||
return new DHTError(msg, 'HANDSHAKE_UNFINISHED', DHTError.HANDSHAKE_UNFINISHED)
|
||||
}
|
||||
|
||||
static HANDSHAKE_INVALID(msg = 'Received invalid handshake') {
|
||||
return new DHTError(msg, 'HANDSHAKE_INVALID', DHTError.HANDSHAKE_INVALID)
|
||||
}
|
||||
|
||||
static ALREADY_LISTENING(msg = 'Already listening') {
|
||||
return new DHTError(msg, 'ALREADY_LISTENING', DHTError.ALREADY_LISTENING)
|
||||
}
|
||||
|
||||
static KEYPAIR_ALREADY_USED(msg = 'Keypair already used') {
|
||||
return new DHTError(msg, 'KEYPAIR_ALREADY_USED', DHTError.KEYPAIR_ALREADY_USED)
|
||||
}
|
||||
|
||||
static NODE_DESTROYED(msg = 'Node destroyed') {
|
||||
return new DHTError(msg, 'NODE_DESTROYED', DHTError.NODE_DESTROYED)
|
||||
}
|
||||
|
||||
static PEER_CONNECTION_FAILED(msg = 'Could not connect to peer') {
|
||||
return new DHTError(msg, 'PEER_CONNECTION_FAILED', DHTError.PEER_CONNECTION_FAILED)
|
||||
}
|
||||
|
||||
static PEER_NOT_FOUND(msg = 'Peer not found') {
|
||||
return new DHTError(msg, 'PEER_NOT_FOUND', DHTError.PEER_NOT_FOUND)
|
||||
}
|
||||
|
||||
static STREAM_NOT_CONNECTED(msg = 'Stream is not connected') {
|
||||
return new DHTError(msg, 'STREAM_NOT_CONNECTED', DHTError.STREAM_DISCONNECTED)
|
||||
}
|
||||
|
||||
static SERVER_INCOMPATIBLE(msg = 'Server is using an incompatible version') {
|
||||
return new DHTError(msg, 'SERVER_INCOMPATIBLE', DHTError.SERVER_INCOMPATIBLE)
|
||||
}
|
||||
|
||||
static SERVER_ERROR(msg = 'Server returned an error') {
|
||||
return new DHTError(msg, 'SERVER_ERROR', DHTError.SERVER_ERROR)
|
||||
}
|
||||
|
||||
static DUPLICATE_CONNECTION(msg = 'Duplicate connection') {
|
||||
return new DHTError(msg, 'DUPLICATE_CONNECTION', DHTError.DUPLICATE_CONNECTION)
|
||||
}
|
||||
|
||||
static RELAY_ABORTED(msg = 'Relay aborted') {
|
||||
return new DHTError(msg, 'RELAY_ABORTED', DHTError.RELAY_ABORTED)
|
||||
}
|
||||
|
||||
static SUSPENDED(msg = 'Suspended') {
|
||||
return new DHTError(msg, 'SUSPENDED', DHTError.SUSPENDED)
|
||||
}
|
||||
}
|
||||
+388
@@ -0,0 +1,388 @@
|
||||
const b4a = require('b4a')
|
||||
const Nat = require('./nat')
|
||||
const Sleeper = require('./sleeper')
|
||||
const { FIREWALL } = require('./constants')
|
||||
|
||||
const BIRTHDAY_SOCKETS = 256
|
||||
const HOLEPUNCH = b4a.from([0])
|
||||
const HOLEPUNCH_TTL = 5
|
||||
const DEFAULT_TTL = 64
|
||||
const MAX_REOPENS = 3
|
||||
|
||||
module.exports = class Holepuncher {
|
||||
constructor(dht, session, isInitiator, remoteFirewall = FIREWALL.UNKNOWN) {
|
||||
const holder = dht._socketPool.acquire()
|
||||
|
||||
this.dht = dht
|
||||
this.session = session
|
||||
|
||||
this.nat = new Nat(dht, session, holder.socket)
|
||||
this.nat.autoSample()
|
||||
|
||||
this.isInitiator = isInitiator
|
||||
|
||||
// events
|
||||
this.onconnect = noop
|
||||
this.onabort = noop
|
||||
|
||||
this.punching = false
|
||||
this.connected = false
|
||||
this.destroyed = false
|
||||
this.randomized = false
|
||||
|
||||
// track remote state
|
||||
this.remoteFirewall = remoteFirewall
|
||||
this.remoteAddresses = []
|
||||
this.remoteHolepunching = false
|
||||
|
||||
this._sleeper = new Sleeper()
|
||||
this._reopening = null
|
||||
this._timeout = null
|
||||
this._punching = null
|
||||
this._allHolders = []
|
||||
this._holder = this._addRef(holder)
|
||||
}
|
||||
|
||||
get socket() {
|
||||
return this._holder.socket
|
||||
}
|
||||
|
||||
updateRemote({ punching, firewall, addresses, verified }) {
|
||||
const remoteAddresses = []
|
||||
|
||||
if (addresses) {
|
||||
for (const addr of addresses) {
|
||||
remoteAddresses.push({
|
||||
host: addr.host,
|
||||
port: addr.port,
|
||||
verified: verified === addr.host || this._isVerified(addr.host)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.remoteFirewall = firewall
|
||||
this.remoteAddresses = remoteAddresses
|
||||
this.remoteHolepunching = punching
|
||||
}
|
||||
|
||||
_isVerified(host) {
|
||||
for (const addr of this.remoteAddresses) {
|
||||
if (addr.verified && addr.host === host) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
ping(addr, socket = this._holder.socket) {
|
||||
return holepunch(socket, addr, false)
|
||||
}
|
||||
|
||||
openSession(addr, socket = this._holder.socket) {
|
||||
return holepunch(socket, addr, true)
|
||||
}
|
||||
|
||||
async analyze(allowReopen) {
|
||||
await this.nat.analyzing
|
||||
if (this._unstable()) {
|
||||
if (!allowReopen) return false
|
||||
if (!this._reopening) this._reopening = this._reopen()
|
||||
return this._reopening
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
_unstable() {
|
||||
// TODO!!: We need an additional heuristic here... If we were NOT random in the past we should also do this.
|
||||
const firewall = this.nat.firewall
|
||||
return (
|
||||
(this.remoteFirewall >= FIREWALL.RANDOM && firewall >= FIREWALL.RANDOM) ||
|
||||
firewall === FIREWALL.UNKNOWN
|
||||
)
|
||||
}
|
||||
|
||||
_reset() {
|
||||
const prev = this._holder
|
||||
|
||||
this._allHolders.pop()
|
||||
this._holder = this._addRef(this.dht._socketPool.acquire())
|
||||
|
||||
prev.release()
|
||||
this.nat.destroy()
|
||||
|
||||
this.nat = new Nat(this.dht, this.session, this._holder.socket)
|
||||
// TODO: maybe make auto sampling configurable somehow?
|
||||
this.nat.autoSample()
|
||||
}
|
||||
|
||||
_addRef(ref) {
|
||||
this._allHolders.push(ref)
|
||||
ref.onholepunchmessage = (msg, rinfo) => this._onholepunchmessage(msg, rinfo, ref)
|
||||
return ref
|
||||
}
|
||||
|
||||
_onholepunchmessage(_, addr, ref) {
|
||||
if (!this.isInitiator) {
|
||||
// TODO: we don't need this if we had a way to connect a socket to many hosts
|
||||
holepunch(ref.socket, addr, false) // never fails
|
||||
return
|
||||
}
|
||||
|
||||
if (this.connected) return
|
||||
|
||||
this.connected = true
|
||||
this.punching = false
|
||||
|
||||
for (const r of this._allHolders) {
|
||||
if (r === ref) continue
|
||||
r.release()
|
||||
}
|
||||
|
||||
this._allHolders[0] = ref
|
||||
while (this._allHolders.length > 1) this._allHolders.pop()
|
||||
|
||||
this._decrementRandomized()
|
||||
this.onconnect(ref.socket, addr.port, addr.host)
|
||||
}
|
||||
|
||||
_done() {
|
||||
return this.destroyed || this.connected
|
||||
}
|
||||
|
||||
async _reopen() {
|
||||
for (let i = 0; this._unstable() && i < MAX_REOPENS && !this._done() && !this.punching; i++) {
|
||||
this._reset()
|
||||
await this.nat.analyzing
|
||||
}
|
||||
|
||||
return coerceFirewall(this.nat.firewall) === FIREWALL.CONSISTENT
|
||||
}
|
||||
|
||||
punch() {
|
||||
if (!this._punching) this._punching = this._punch()
|
||||
return this._punching
|
||||
}
|
||||
|
||||
async _punch() {
|
||||
if (this._done() || !this.remoteAddresses.length) return false
|
||||
|
||||
this.punching = true
|
||||
|
||||
// Coerce into consistency for now. Obvs we could make this this more efficient if we use that info
|
||||
// but that's seldomly used since those will just use tcp most of the time.
|
||||
|
||||
const local = coerceFirewall(this.nat.firewall)
|
||||
const remote = coerceFirewall(this.remoteFirewall)
|
||||
|
||||
// Note that most of these async functions are meant to run in the background
|
||||
// which is why we don't await them here and why they are not allowed to throw
|
||||
|
||||
let remoteVerifiedAddress = null
|
||||
for (const addr of this.remoteAddresses) {
|
||||
if (addr.verified) {
|
||||
remoteVerifiedAddress = addr
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (local === FIREWALL.CONSISTENT && remote === FIREWALL.CONSISTENT) {
|
||||
this.dht.stats.punches.consistent++
|
||||
this._consistentProbe()
|
||||
return true
|
||||
}
|
||||
|
||||
if (!remoteVerifiedAddress) return false
|
||||
|
||||
if (local === FIREWALL.CONSISTENT && remote >= FIREWALL.RANDOM) {
|
||||
this.dht.stats.punches.random++
|
||||
this._incrementRandomized()
|
||||
this._randomProbes(remoteVerifiedAddress)
|
||||
return true
|
||||
}
|
||||
|
||||
if (local >= FIREWALL.RANDOM && remote === FIREWALL.CONSISTENT) {
|
||||
this.dht.stats.punches.random++
|
||||
this._incrementRandomized()
|
||||
await this._openBirthdaySockets(remoteVerifiedAddress)
|
||||
if (this.punching) this._keepAliveRandomNat(remoteVerifiedAddress)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Note that this never throws so it is safe to run in the background
|
||||
async _consistentProbe() {
|
||||
// Here we do the sleep first because the "fast open" mode in the server just fired a ping
|
||||
if (!this.isInitiator) await this._sleeper.pause(1000)
|
||||
|
||||
let tries = 0
|
||||
|
||||
while (this.punching && tries++ < 10) {
|
||||
for (const addr of this.remoteAddresses) {
|
||||
// only try unverified addresses every 4 ticks
|
||||
if (!addr.verified && (tries & 3) !== 0) continue
|
||||
await holepunch(this._holder.socket, addr, false)
|
||||
}
|
||||
if (this.punching) await this._sleeper.pause(1000)
|
||||
}
|
||||
|
||||
this._autoDestroy()
|
||||
}
|
||||
|
||||
// Note that this never throws so it is safe to run in the background
|
||||
async _randomProbes(remoteAddr) {
|
||||
let tries = 1750 // ~35s
|
||||
|
||||
while (this.punching && tries-- > 0) {
|
||||
const addr = { host: remoteAddr.host, port: randomPort() }
|
||||
await holepunch(this._holder.socket, addr, false)
|
||||
if (this.punching) await this._sleeper.pause(20)
|
||||
}
|
||||
|
||||
this._autoDestroy()
|
||||
}
|
||||
|
||||
// Note that this never throws so it is safe to run in the background
|
||||
async _keepAliveRandomNat(remoteAddr) {
|
||||
let i = 0
|
||||
let lowTTLRounds = 1
|
||||
|
||||
// TODO: experiment with this here. We just bursted all the messages in
|
||||
// openOtherSockets to ensure the sockets are open, so it's potentially
|
||||
// a good idea to slow down for a bit.
|
||||
await this._sleeper.pause(100)
|
||||
|
||||
let tries = 1750 // ~35s
|
||||
|
||||
while (this.punching && tries-- > 0) {
|
||||
if (i === this._allHolders.length) {
|
||||
i = 0
|
||||
if (lowTTLRounds > 0) lowTTLRounds--
|
||||
}
|
||||
|
||||
await holepunch(this._allHolders[i++].socket, remoteAddr, lowTTLRounds > 0)
|
||||
if (this.punching) await this._sleeper.pause(20)
|
||||
}
|
||||
|
||||
this._autoDestroy()
|
||||
}
|
||||
|
||||
async _openBirthdaySockets(remoteAddr) {
|
||||
while (this.punching && this._allHolders.length < BIRTHDAY_SOCKETS) {
|
||||
const ref = this._addRef(this.dht._socketPool.acquire())
|
||||
await holepunch(ref.socket, remoteAddr, HOLEPUNCH_TTL)
|
||||
}
|
||||
}
|
||||
|
||||
_autoDestroy() {
|
||||
if (!this.connected) this.destroy()
|
||||
}
|
||||
|
||||
_incrementRandomized() {
|
||||
if (!this.randomized) {
|
||||
this.randomized = true
|
||||
this.dht._randomPunches++
|
||||
}
|
||||
}
|
||||
|
||||
_decrementRandomized() {
|
||||
if (this.randomized) {
|
||||
this.dht._lastRandomPunch = Date.now()
|
||||
this.randomized = false
|
||||
this.dht._randomPunches--
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.destroyed) return
|
||||
this.destroyed = true
|
||||
this.punching = false
|
||||
|
||||
for (const ref of this._allHolders) ref.release()
|
||||
this._allHolders = []
|
||||
this.nat.destroy()
|
||||
|
||||
if (!this.connected) {
|
||||
this._decrementRandomized()
|
||||
this.onabort()
|
||||
}
|
||||
}
|
||||
|
||||
static ping(socket, addr) {
|
||||
return holepunch(socket, addr, false)
|
||||
}
|
||||
|
||||
static localAddresses(socket) {
|
||||
return localAddresses(socket)
|
||||
}
|
||||
|
||||
static matchAddress(myAddresses, externalAddresses) {
|
||||
return matchAddress(myAddresses, externalAddresses)
|
||||
}
|
||||
}
|
||||
|
||||
function holepunch(socket, addr, lowTTL) {
|
||||
return socket.send(HOLEPUNCH, addr.port, addr.host, lowTTL ? HOLEPUNCH_TTL : DEFAULT_TTL)
|
||||
}
|
||||
|
||||
function randomPort() {
|
||||
return (1000 + Math.random() * 64536) | 0
|
||||
}
|
||||
|
||||
function coerceFirewall(fw) {
|
||||
return fw === FIREWALL.OPEN ? FIREWALL.CONSISTENT : fw
|
||||
}
|
||||
|
||||
function localAddresses(socket) {
|
||||
const addrs = []
|
||||
const { host, port } = socket.address()
|
||||
|
||||
if (host === '127.0.0.1') return [{ host, port }]
|
||||
|
||||
for (const n of socket.udx.networkInterfaces()) {
|
||||
if (n.family !== 4 || n.internal) continue
|
||||
|
||||
addrs.push({ host: n.host, port })
|
||||
}
|
||||
|
||||
if (addrs.length === 0) {
|
||||
addrs.push({ host: '127.0.0.1', port })
|
||||
}
|
||||
|
||||
return addrs
|
||||
}
|
||||
|
||||
function matchAddress(localAddresses, remoteLocalAddresses) {
|
||||
if (remoteLocalAddresses.length === 0) return null
|
||||
|
||||
let best = { segment: 1, addr: null }
|
||||
|
||||
for (const localAddress of localAddresses) {
|
||||
// => 192.168.122.238
|
||||
const a = localAddress.host.split('.')
|
||||
|
||||
for (const remoteAddress of remoteLocalAddresses) {
|
||||
// => 192.168.0.23
|
||||
// => 192.168.122.1
|
||||
const b = remoteAddress.host.split('.')
|
||||
|
||||
// Matches 192.*.*.*
|
||||
if (a[0] === b[0]) {
|
||||
if (best.segment === 1) best = { segment: 2, addr: remoteAddress }
|
||||
|
||||
// Matches 192.168.*.*
|
||||
if (a[1] === b[1]) {
|
||||
if (best.segment === 2) best = { segment: 3, addr: remoteAddress }
|
||||
|
||||
// Matches 192.168.122.*
|
||||
if (a[2] === b[2]) return remoteAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return best.addr
|
||||
}
|
||||
|
||||
function noop() {}
|
||||
+455
@@ -0,0 +1,455 @@
|
||||
const c = require('compact-encoding')
|
||||
|
||||
const ipv4 = {
|
||||
...c.ipv4Address,
|
||||
decode(state) {
|
||||
const ip = c.ipv4Address.decode(state)
|
||||
return {
|
||||
host: ip.host,
|
||||
port: ip.port
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ipv4Array = c.array(ipv4)
|
||||
|
||||
const ipv6 = {
|
||||
...c.ipv6Address,
|
||||
decode(state) {
|
||||
const ip = c.ipv6Address.decode(state)
|
||||
return {
|
||||
host: ip.host,
|
||||
port: ip.port
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ipv6Array = c.array(ipv6)
|
||||
|
||||
exports.handshake = {
|
||||
preencode(state, m) {
|
||||
state.end += 1 + 1 + (m.peerAddress ? 6 : 0) + (m.relayAddress ? 6 : 0)
|
||||
c.buffer.preencode(state, m.noise)
|
||||
},
|
||||
encode(state, m) {
|
||||
const flags = (m.peerAddress ? 1 : 0) | (m.relayAddress ? 2 : 0)
|
||||
|
||||
c.uint.encode(state, flags)
|
||||
c.uint.encode(state, m.mode)
|
||||
c.buffer.encode(state, m.noise)
|
||||
|
||||
if (m.peerAddress) ipv4.encode(state, m.peerAddress)
|
||||
if (m.relayAddress) ipv4.encode(state, m.relayAddress)
|
||||
},
|
||||
decode(state) {
|
||||
const flags = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
mode: c.uint.decode(state),
|
||||
noise: c.buffer.decode(state),
|
||||
peerAddress: flags & 1 ? ipv4.decode(state) : null,
|
||||
relayAddress: flags & 2 ? ipv4.decode(state) : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const relayInfo = {
|
||||
preencode(state, m) {
|
||||
state.end += 12
|
||||
},
|
||||
encode(state, m) {
|
||||
ipv4.encode(state, m.relayAddress)
|
||||
ipv4.encode(state, m.peerAddress)
|
||||
},
|
||||
decode(state) {
|
||||
return {
|
||||
relayAddress: ipv4.decode(state),
|
||||
peerAddress: ipv4.decode(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const relayInfoArray = c.array(relayInfo)
|
||||
|
||||
const holepunchInfo = {
|
||||
preencode(state, m) {
|
||||
c.uint.preencode(state, m.id)
|
||||
relayInfoArray.preencode(state, m.relays)
|
||||
},
|
||||
encode(state, m) {
|
||||
c.uint.encode(state, m.id)
|
||||
relayInfoArray.encode(state, m.relays)
|
||||
},
|
||||
decode(state) {
|
||||
return {
|
||||
id: c.uint.decode(state),
|
||||
relays: relayInfoArray.decode(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const udxInfo = {
|
||||
preencode(state, m) {
|
||||
state.end += 2 // version + features
|
||||
c.uint.preencode(state, m.id)
|
||||
c.uint.preencode(state, m.seq)
|
||||
},
|
||||
encode(state, m) {
|
||||
c.uint.encode(state, 1)
|
||||
c.uint.encode(state, m.reusableSocket ? 1 : 0)
|
||||
c.uint.encode(state, m.id)
|
||||
c.uint.encode(state, m.seq)
|
||||
},
|
||||
decode(state) {
|
||||
const version = c.uint.decode(state)
|
||||
const features = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
version,
|
||||
reusableSocket: (features & 1) !== 0,
|
||||
id: c.uint.decode(state),
|
||||
seq: c.uint.decode(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const secretStreamInfo = {
|
||||
preencode(state, m) {
|
||||
c.uint.preencode(state, 1)
|
||||
},
|
||||
encode(state, m) {
|
||||
c.uint.encode(state, 1)
|
||||
},
|
||||
decode(state) {
|
||||
return {
|
||||
version: c.uint.decode(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const relayThroughInfo = {
|
||||
preencode(state, m) {
|
||||
c.uint.preencode(state, 1) // version
|
||||
c.uint.preencode(state, 0) // flags
|
||||
c.fixed32.preencode(state, m.publicKey)
|
||||
c.fixed32.preencode(state, m.token)
|
||||
},
|
||||
encode(state, m) {
|
||||
c.uint.encode(state, 1)
|
||||
c.uint.encode(state, 0)
|
||||
c.fixed32.encode(state, m.publicKey)
|
||||
c.fixed32.encode(state, m.token)
|
||||
},
|
||||
decode(state) {
|
||||
const version = c.uint.decode(state)
|
||||
c.uint.decode(state)
|
||||
|
||||
return {
|
||||
version,
|
||||
publicKey: c.fixed32.decode(state),
|
||||
token: c.fixed32.decode(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.noisePayload = {
|
||||
preencode(state, m) {
|
||||
state.end += 4 // version + flags + error + firewall
|
||||
if (m.holepunch) holepunchInfo.preencode(state, m.holepunch)
|
||||
if (m.addresses4 && m.addresses4.length) ipv4Array.preencode(state, m.addresses4)
|
||||
if (m.addresses6 && m.addresses6.length) ipv6Array.preencode(state, m.addresses6)
|
||||
if (m.udx) udxInfo.preencode(state, m.udx)
|
||||
if (m.secretStream) secretStreamInfo.preencode(state, m.secretStream)
|
||||
if (m.relayThrough) relayThroughInfo.preencode(state, m.relayThrough)
|
||||
if (m.relayAddresses) ipv4Array.preencode(state, m.relayAddresses)
|
||||
},
|
||||
encode(state, m) {
|
||||
let flags = 0
|
||||
|
||||
if (m.holepunch) flags |= 1
|
||||
if (m.addresses4 && m.addresses4.length) flags |= 2
|
||||
if (m.addresses6 && m.addresses6.length) flags |= 4
|
||||
if (m.udx) flags |= 8
|
||||
if (m.secretStream) flags |= 16
|
||||
if (m.relayThrough) flags |= 32
|
||||
if (m.relayAddresses) flags |= 64
|
||||
|
||||
c.uint.encode(state, 1) // version
|
||||
c.uint.encode(state, flags)
|
||||
c.uint.encode(state, m.error)
|
||||
c.uint.encode(state, m.firewall)
|
||||
|
||||
if (m.holepunch) holepunchInfo.encode(state, m.holepunch)
|
||||
if (m.addresses4 && m.addresses4.length) ipv4Array.encode(state, m.addresses4)
|
||||
if (m.addresses6 && m.addresses6.length) ipv6Array.encode(state, m.addresses6)
|
||||
if (m.udx) udxInfo.encode(state, m.udx)
|
||||
if (m.secretStream) secretStreamInfo.encode(state, m.secretStream)
|
||||
if (m.relayThrough) relayThroughInfo.encode(state, m.relayThrough)
|
||||
if (m.relayAddresses) ipv4Array.encode(state, m.relayAddresses)
|
||||
},
|
||||
decode(state) {
|
||||
const version = c.uint.decode(state)
|
||||
|
||||
if (version !== 1) {
|
||||
// Do not attempt to decode but return this back to the user so they can
|
||||
// actually handle it
|
||||
return {
|
||||
version,
|
||||
error: 0,
|
||||
firewall: 0,
|
||||
holepunch: null,
|
||||
addresses4: [],
|
||||
addresses6: [],
|
||||
udx: null,
|
||||
secretStream: null,
|
||||
relayThrough: null,
|
||||
relayAddresses: null
|
||||
}
|
||||
}
|
||||
|
||||
const flags = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
version,
|
||||
error: c.uint.decode(state),
|
||||
firewall: c.uint.decode(state),
|
||||
holepunch: (flags & 1) !== 0 ? holepunchInfo.decode(state) : null,
|
||||
addresses4: (flags & 2) !== 0 ? ipv4Array.decode(state) : [],
|
||||
addresses6: (flags & 4) !== 0 ? ipv6Array.decode(state) : [],
|
||||
udx: (flags & 8) !== 0 ? udxInfo.decode(state) : null,
|
||||
secretStream: (flags & 16) !== 0 ? secretStreamInfo.decode(state) : null,
|
||||
relayThrough: (flags & 32) !== 0 ? relayThroughInfo.decode(state) : null,
|
||||
relayAddresses: (flags & 64) !== 0 ? ipv4Array.decode(state) : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.holepunch = {
|
||||
preencode(state, m) {
|
||||
state.end += 2
|
||||
c.uint.preencode(state, m.id)
|
||||
c.buffer.preencode(state, m.payload)
|
||||
if (m.peerAddress) ipv4.preencode(state, m.peerAddress)
|
||||
},
|
||||
encode(state, m) {
|
||||
const flags = m.peerAddress ? 1 : 0
|
||||
c.uint.encode(state, flags)
|
||||
c.uint.encode(state, m.mode)
|
||||
c.uint.encode(state, m.id)
|
||||
c.buffer.encode(state, m.payload)
|
||||
if (m.peerAddress) ipv4.encode(state, m.peerAddress)
|
||||
},
|
||||
decode(state) {
|
||||
const flags = c.uint.decode(state)
|
||||
return {
|
||||
mode: c.uint.decode(state),
|
||||
id: c.uint.decode(state),
|
||||
payload: c.buffer.decode(state),
|
||||
peerAddress: flags & 1 ? ipv4.decode(state) : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.holepunchPayload = {
|
||||
preencode(state, m) {
|
||||
state.end += 4 // flags + error + firewall + round
|
||||
if (m.addresses) ipv4Array.preencode(state, m.addresses)
|
||||
if (m.remoteAddress) state.end += 6
|
||||
if (m.token) state.end += 32
|
||||
if (m.remoteToken) state.end += 32
|
||||
},
|
||||
encode(state, m) {
|
||||
const flags =
|
||||
(m.connected ? 1 : 0) |
|
||||
(m.punching ? 2 : 0) |
|
||||
(m.addresses ? 4 : 0) |
|
||||
(m.remoteAddress ? 8 : 0) |
|
||||
(m.token ? 16 : 0) |
|
||||
(m.remoteToken ? 32 : 0)
|
||||
|
||||
c.uint.encode(state, flags)
|
||||
c.uint.encode(state, m.error)
|
||||
c.uint.encode(state, m.firewall)
|
||||
c.uint.encode(state, m.round)
|
||||
|
||||
if (m.addresses) ipv4Array.encode(state, m.addresses)
|
||||
if (m.remoteAddress) ipv4.encode(state, m.remoteAddress)
|
||||
if (m.token) c.fixed32.encode(state, m.token)
|
||||
if (m.remoteToken) c.fixed32.encode(state, m.remoteToken)
|
||||
},
|
||||
decode(state) {
|
||||
const flags = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
error: c.uint.decode(state),
|
||||
firewall: c.uint.decode(state),
|
||||
round: c.uint.decode(state),
|
||||
connected: (flags & 1) !== 0,
|
||||
punching: (flags & 2) !== 0,
|
||||
addresses: (flags & 4) !== 0 ? ipv4Array.decode(state) : null,
|
||||
remoteAddress: (flags & 8) !== 0 ? ipv4.decode(state) : null,
|
||||
token: (flags & 16) !== 0 ? c.fixed32.decode(state) : null,
|
||||
remoteToken: (flags & 32) !== 0 ? c.fixed32.decode(state) : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const peer = (exports.peer = {
|
||||
preencode(state, m) {
|
||||
state.end += 32
|
||||
ipv4Array.preencode(state, m.relayAddresses)
|
||||
},
|
||||
encode(state, m) {
|
||||
c.fixed32.encode(state, m.publicKey)
|
||||
ipv4Array.encode(state, m.relayAddresses)
|
||||
},
|
||||
decode(state) {
|
||||
return {
|
||||
publicKey: c.fixed32.decode(state),
|
||||
relayAddresses: ipv4Array.decode(state)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const peers = (exports.peers = c.array(peer))
|
||||
|
||||
const rawPeers = c.array(c.raw)
|
||||
|
||||
exports.lookupRawReply = {
|
||||
preencode(state, m) {
|
||||
rawPeers.preencode(state, m.peers)
|
||||
c.uint.preencode(state, m.bump)
|
||||
},
|
||||
encode(state, m) {
|
||||
rawPeers.encode(state, m.peers)
|
||||
c.uint.encode(state, m.bump)
|
||||
},
|
||||
decode(state) {
|
||||
return {
|
||||
peers: peers.decode(state),
|
||||
bump: state.start < state.end ? c.uint.decode(state) : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.announce = {
|
||||
preencode(state, m) {
|
||||
state.end++ // flags
|
||||
if (m.peer) peer.preencode(state, m.peer)
|
||||
if (m.refresh) state.end += 32
|
||||
if (m.signature) state.end += 64
|
||||
if (m.bump) c.uint.preencode(state, m.bump)
|
||||
},
|
||||
encode(state, m) {
|
||||
const flags = (m.peer ? 1 : 0) | (m.refresh ? 2 : 0) | (m.signature ? 4 : 0) | (m.bump ? 8 : 0)
|
||||
c.uint.encode(state, flags)
|
||||
if (m.peer) peer.encode(state, m.peer)
|
||||
if (m.refresh) c.fixed32.encode(state, m.refresh)
|
||||
if (m.signature) c.fixed64.encode(state, m.signature)
|
||||
if (m.bump) c.uint.encode(state, m.bump)
|
||||
},
|
||||
decode(state) {
|
||||
const flags = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
peer: (flags & 1) !== 0 ? peer.decode(state) : null,
|
||||
refresh: (flags & 2) !== 0 ? c.fixed32.decode(state) : null,
|
||||
signature: (flags & 4) !== 0 ? c.fixed64.decode(state) : null,
|
||||
bump: (flags & 8) !== 0 ? c.uint.decode(state) : 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.mutableSignable = {
|
||||
preencode(state, m) {
|
||||
c.uint.preencode(state, m.seq)
|
||||
c.buffer.preencode(state, m.value)
|
||||
},
|
||||
encode(state, m) {
|
||||
c.uint.encode(state, m.seq)
|
||||
c.buffer.encode(state, m.value)
|
||||
},
|
||||
decode(state) {
|
||||
return {
|
||||
seq: c.uint.decode(state),
|
||||
value: c.buffer.decode(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.mutablePutRequest = {
|
||||
preencode(state, m) {
|
||||
c.fixed32.preencode(state, m.publicKey)
|
||||
c.uint.preencode(state, m.seq)
|
||||
c.buffer.preencode(state, m.value)
|
||||
c.fixed64.preencode(state, m.signature)
|
||||
},
|
||||
encode(state, m) {
|
||||
c.fixed32.encode(state, m.publicKey)
|
||||
c.uint.encode(state, m.seq)
|
||||
c.buffer.encode(state, m.value)
|
||||
c.fixed64.encode(state, m.signature)
|
||||
},
|
||||
decode(state) {
|
||||
return {
|
||||
publicKey: c.fixed32.decode(state),
|
||||
seq: c.uint.decode(state),
|
||||
value: c.buffer.decode(state),
|
||||
signature: c.fixed64.decode(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.mutableGetResponse = {
|
||||
preencode(state, m) {
|
||||
c.uint.preencode(state, m.seq)
|
||||
c.buffer.preencode(state, m.value)
|
||||
c.fixed64.preencode(state, m.signature)
|
||||
},
|
||||
encode(state, m) {
|
||||
c.uint.encode(state, m.seq)
|
||||
c.buffer.encode(state, m.value)
|
||||
c.fixed64.encode(state, m.signature)
|
||||
},
|
||||
decode(state) {
|
||||
return {
|
||||
seq: c.uint.decode(state),
|
||||
value: c.buffer.decode(state),
|
||||
signature: c.fixed64.decode(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.pluginRequest = {
|
||||
preencode(state, m) {
|
||||
c.string.preencode(state, m.plugin)
|
||||
c.uint.preencode(state, m.version)
|
||||
c.uint.preencode(state, m.command)
|
||||
state.end++ // max flag is 1 so always one byte
|
||||
|
||||
if (m.value) c.buffer.preencode(state, m.value)
|
||||
},
|
||||
encode(state, m) {
|
||||
const flags = m.value ? 1 : 0
|
||||
|
||||
c.string.encode(state, m.plugin)
|
||||
c.uint.encode(state, m.version)
|
||||
c.uint.encode(state, m.command)
|
||||
c.uint.encode(state, flags)
|
||||
|
||||
if (m.value) c.buffer.encode(state, m.value)
|
||||
},
|
||||
decode(state) {
|
||||
const r0 = c.string.decode(state)
|
||||
const r1 = c.uint.decode(state)
|
||||
const r2 = c.uint.decode(state)
|
||||
const flags = c.uint.decode(state)
|
||||
|
||||
return {
|
||||
plugin: r0,
|
||||
version: r1,
|
||||
command: r2,
|
||||
value: (flags & 1) !== 0 ? c.buffer.decode(state) : null
|
||||
}
|
||||
}
|
||||
}
|
||||
+207
@@ -0,0 +1,207 @@
|
||||
const { FIREWALL } = require('../lib/constants')
|
||||
|
||||
module.exports = class Nat {
|
||||
constructor(dht, session, socket) {
|
||||
this._samplesHost = []
|
||||
this._samplesFull = []
|
||||
this._visited = new Map()
|
||||
this._resolve = null
|
||||
this._minSamples = 4
|
||||
this._autoSampling = false
|
||||
|
||||
this.dht = dht
|
||||
this.session = session
|
||||
this.socket = socket
|
||||
|
||||
this.sampled = 0
|
||||
this.firewall = dht.firewalled ? FIREWALL.UNKNOWN : FIREWALL.OPEN
|
||||
this.addresses = null
|
||||
|
||||
this.analyzing = new Promise((resolve) => {
|
||||
this._resolve = resolve
|
||||
})
|
||||
}
|
||||
|
||||
autoSample(retry = true) {
|
||||
if (this._autoSampling) return
|
||||
this._autoSampling = true
|
||||
|
||||
const self = this
|
||||
const socket = this.socket
|
||||
const maxPings = this._minSamples
|
||||
|
||||
let skip = this.dht.nodes.length >= 8 ? 5 : 0
|
||||
let pending = 0
|
||||
|
||||
// TODO: it would be best to pick the nodes to help us based on latency to us
|
||||
// That should reduce connect latency in general. We should investigate tracking that later on.
|
||||
|
||||
// TODO 2: try to pick nodes with different IPs as well, as that'll help multi IP cell connections...
|
||||
// If we expose this from the nat sampler then the DHT should be able to help us filter out scams as well...
|
||||
|
||||
for (
|
||||
let node = this.dht.nodes.latest;
|
||||
node && this.sampled + pending < maxPings;
|
||||
node = node.prev
|
||||
) {
|
||||
if (skip > 0) {
|
||||
skip--
|
||||
continue
|
||||
}
|
||||
|
||||
const ref = node.host + ':' + node.port
|
||||
|
||||
if (this._visited.has(ref)) continue
|
||||
this._visited.set(ref, 1)
|
||||
|
||||
pending++
|
||||
this.session.ping(node, { socket, retry: false }).then(onpong, onskip)
|
||||
}
|
||||
|
||||
pending++
|
||||
onskip()
|
||||
|
||||
function onpong(res) {
|
||||
self.add(res.to, res.from)
|
||||
onskip()
|
||||
}
|
||||
|
||||
function onskip() {
|
||||
if (--pending === 0 && self.sampled < self._minSamples) {
|
||||
if (retry) {
|
||||
self._autoSampling = false
|
||||
self.autoSample(false)
|
||||
return
|
||||
}
|
||||
self._resolve()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._autoSampling = true
|
||||
this._minSamples = 0
|
||||
this._resolve()
|
||||
}
|
||||
|
||||
unfreeze() {
|
||||
this.frozen = false
|
||||
this._updateFirewall()
|
||||
this._updateAddresses()
|
||||
}
|
||||
|
||||
freeze() {
|
||||
this.frozen = true
|
||||
}
|
||||
|
||||
_updateFirewall() {
|
||||
if (!this.dht.firewalled) {
|
||||
this.firewall = FIREWALL.OPEN
|
||||
return
|
||||
}
|
||||
|
||||
if (this.sampled < 3) return
|
||||
|
||||
const max = this._samplesFull[0].hits
|
||||
|
||||
if (max >= 3) {
|
||||
this.firewall = FIREWALL.CONSISTENT
|
||||
return
|
||||
}
|
||||
|
||||
if (max === 1) {
|
||||
this.firewall = FIREWALL.RANDOM
|
||||
return
|
||||
}
|
||||
|
||||
// else max === 2
|
||||
|
||||
// 1 host, >= 4 total samples ie, 2 bad ones -> random
|
||||
if (this._samplesHost.length === 1 && this.sampled > 3) {
|
||||
this.firewall = FIREWALL.RANDOM
|
||||
return
|
||||
}
|
||||
|
||||
// double hit on two different ips -> assume consistent
|
||||
if (this._samplesHost.length > 1 && this._samplesFull[1].hits > 1) {
|
||||
this.firewall = FIREWALL.CONSISTENT
|
||||
return
|
||||
}
|
||||
|
||||
// (4 is just means - all the samples we expect) - no decision - assume random
|
||||
if (this.sampled > 4) {
|
||||
this.firewall = FIREWALL.RANDOM
|
||||
}
|
||||
}
|
||||
|
||||
_updateAddresses() {
|
||||
if (this.firewall === FIREWALL.UNKNOWN) {
|
||||
this.addresses = null
|
||||
return
|
||||
}
|
||||
|
||||
if (this.firewall === FIREWALL.RANDOM) {
|
||||
this.addresses = [this._samplesHost[0]]
|
||||
return
|
||||
}
|
||||
|
||||
if (this.firewall === FIREWALL.CONSISTENT) {
|
||||
this.addresses = []
|
||||
for (const addr of this._samplesFull) {
|
||||
if (addr.hits >= 2 || this.addresses.length < 2) this.addresses.push(addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this.dht.firewalled && this.firewall === FIREWALL.OPEN) {
|
||||
this.firewall = FIREWALL.UNKNOWN
|
||||
}
|
||||
this._updateFirewall()
|
||||
this._updateAddresses()
|
||||
}
|
||||
|
||||
add(addr, from) {
|
||||
const ref = from.host + ':' + from.port
|
||||
|
||||
if (this._visited.get(ref) === 2) return
|
||||
this._visited.set(ref, 2)
|
||||
|
||||
addSample(this._samplesHost, addr.host, 0)
|
||||
addSample(this._samplesFull, addr.host, addr.port)
|
||||
|
||||
if ((++this.sampled >= 3 || !this.dht.firewalled) && !this.frozen) {
|
||||
this.update()
|
||||
}
|
||||
|
||||
if (this.firewall === FIREWALL.CONSISTENT || this.firewall === FIREWALL.OPEN) {
|
||||
this._resolve()
|
||||
} else if (this.sampled >= this._minSamples) {
|
||||
this._resolve()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addSample(samples, host, port) {
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
const s = samples[i]
|
||||
|
||||
if (s.port !== port || s.host !== host) continue
|
||||
s.hits++
|
||||
|
||||
for (; i > 0; i--) {
|
||||
const prev = samples[i - 1]
|
||||
if (prev.hits >= s.hits) return
|
||||
samples[i - 1] = s
|
||||
samples[i] = prev
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
samples.push({
|
||||
host,
|
||||
port,
|
||||
hits: 1
|
||||
})
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
const NoiseSecretStream = require('@hyperswarm/secret-stream')
|
||||
const NoiseHandshake = require('noise-handshake')
|
||||
const curve = require('noise-curve-ed')
|
||||
const c = require('compact-encoding')
|
||||
const b4a = require('b4a')
|
||||
const sodium = require('sodium-universal')
|
||||
const m = require('./messages')
|
||||
const { NS } = require('./constants')
|
||||
const { HANDSHAKE_UNFINISHED } = require('./errors')
|
||||
|
||||
const NOISE_PROLOUGE = NS.PEER_HANDSHAKE
|
||||
|
||||
module.exports = class NoiseWrap {
|
||||
constructor(keyPair, remotePublicKey) {
|
||||
this.isInitiator = !!remotePublicKey
|
||||
this.remotePublicKey = remotePublicKey
|
||||
this.keyPair = keyPair
|
||||
this.handshake = new NoiseHandshake('IK', this.isInitiator, keyPair, { curve })
|
||||
this.handshake.initialise(NOISE_PROLOUGE, remotePublicKey)
|
||||
}
|
||||
|
||||
send(payload) {
|
||||
const buf = c.encode(m.noisePayload, payload)
|
||||
return this.handshake.send(buf)
|
||||
}
|
||||
|
||||
recv(buf) {
|
||||
const payload = c.decode(m.noisePayload, this.handshake.recv(buf))
|
||||
this.remotePublicKey = b4a.toBuffer(this.handshake.rs)
|
||||
return payload
|
||||
}
|
||||
|
||||
final() {
|
||||
if (!this.handshake.complete) throw HANDSHAKE_UNFINISHED()
|
||||
|
||||
const holepunchSecret = b4a.allocUnsafe(32)
|
||||
|
||||
sodium.crypto_generichash(holepunchSecret, NS.PEER_HOLEPUNCH, this.handshake.hash)
|
||||
|
||||
return {
|
||||
isInitiator: this.isInitiator,
|
||||
publicKey: this.keyPair.publicKey,
|
||||
streamId: this.streamId,
|
||||
remotePublicKey: this.remotePublicKey,
|
||||
remoteId: NoiseSecretStream.id(this.handshake.hash, !this.isInitiator),
|
||||
holepunchSecret,
|
||||
hash: b4a.toBuffer(this.handshake.hash),
|
||||
rx: b4a.toBuffer(this.handshake.rx),
|
||||
tx: b4a.toBuffer(this.handshake.tx)
|
||||
}
|
||||
}
|
||||
}
|
||||
+315
@@ -0,0 +1,315 @@
|
||||
const c = require('compact-encoding')
|
||||
const sodium = require('sodium-universal')
|
||||
const RecordCache = require('record-cache')
|
||||
const Cache = require('xache')
|
||||
const b4a = require('b4a')
|
||||
const unslab = require('unslab')
|
||||
|
||||
const { encodeUnslab } = require('./encode')
|
||||
const m = require('./messages')
|
||||
const { NS, ERROR } = require('./constants')
|
||||
|
||||
const EMPTY = b4a.alloc(0)
|
||||
const TMP = b4a.allocUnsafe(32)
|
||||
const MAX_BUMP_DRIFT = 60_000
|
||||
|
||||
module.exports = class Persistent {
|
||||
constructor(dht, opts) {
|
||||
this.dht = dht
|
||||
this.records = new RecordCache(opts.records)
|
||||
this.bumps = new Cache(opts.bumps)
|
||||
this.refreshes = new Cache(opts.refreshes)
|
||||
this.mutables = new Cache(opts.mutables)
|
||||
this.immutables = new Cache(opts.immutables)
|
||||
}
|
||||
|
||||
onlookup(req) {
|
||||
if (!req.target) return
|
||||
|
||||
const k = b4a.toString(req.target, 'hex')
|
||||
const records = this.records.get(k, 20)
|
||||
const bump = this.bumps.get(k) || 0
|
||||
const fwd = this.dht._router.get(k)
|
||||
|
||||
if (fwd && records.length < 20) records.push(fwd.record)
|
||||
|
||||
req.reply(records.length ? c.encode(m.lookupRawReply, { peers: records, bump }) : null)
|
||||
}
|
||||
|
||||
onfindpeer(req) {
|
||||
if (!req.target) return
|
||||
const fwd = this.dht._router.get(req.target)
|
||||
req.reply(fwd ? fwd.record : null)
|
||||
}
|
||||
|
||||
unannounce(target, publicKey) {
|
||||
const k = b4a.toString(target, 'hex')
|
||||
sodium.crypto_generichash(TMP, publicKey)
|
||||
|
||||
if (b4a.equals(TMP, target)) this.dht._router.delete(k)
|
||||
this.records.remove(k, publicKey)
|
||||
}
|
||||
|
||||
onunannounce(req) {
|
||||
if (!req.target || !req.token) return
|
||||
|
||||
const unann = decode(m.announce, req.value)
|
||||
if (unann === null) return
|
||||
|
||||
const { peer, signature } = unann
|
||||
if (!peer || !signature) return
|
||||
|
||||
const signable = annSignable(req.target, req.token, this.dht.id, unann, NS.UNANNOUNCE)
|
||||
|
||||
if (!sodium.crypto_sign_verify_detached(signature, signable, peer.publicKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
this.unannounce(req.target, peer.publicKey)
|
||||
req.reply(null, { token: false, closerNodes: false })
|
||||
}
|
||||
|
||||
_onrefresh(token, req) {
|
||||
sodium.crypto_generichash(TMP, token)
|
||||
const activeRefresh = b4a.toString(TMP, 'hex')
|
||||
|
||||
const r = this.refreshes.get(activeRefresh)
|
||||
if (!r) return
|
||||
|
||||
const { announceSelf, k, record } = r
|
||||
const publicKey = record.subarray(0, 32)
|
||||
|
||||
if (announceSelf) {
|
||||
this.dht._router.set(k, {
|
||||
relay: req.from,
|
||||
record,
|
||||
onconnect: null,
|
||||
onholepunch: null
|
||||
})
|
||||
this.records.remove(k, publicKey)
|
||||
} else {
|
||||
this.records.add(k, publicKey, record)
|
||||
}
|
||||
|
||||
this.refreshes.delete(activeRefresh)
|
||||
this.refreshes.set(b4a.toString(token, 'hex'), r)
|
||||
|
||||
req.reply(null, { token: false, closerNodes: false })
|
||||
}
|
||||
|
||||
onannounce(req) {
|
||||
if (!req.target || !req.token || !this.dht.id) return
|
||||
|
||||
const ann = decode(m.announce, req.value)
|
||||
if (ann === null) return
|
||||
|
||||
const { peer, refresh, signature, bump } = ann
|
||||
|
||||
if (!peer) {
|
||||
if (!refresh) return
|
||||
this._onrefresh(refresh, req)
|
||||
return
|
||||
}
|
||||
|
||||
const signable = annSignable(req.target, req.token, this.dht.id, ann, NS.ANNOUNCE)
|
||||
|
||||
if (!signature || !sodium.crypto_sign_verify_detached(signature, signable, peer.publicKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: it would be potentially be more optimal to allow more than 3 addresses here for a findPeer response
|
||||
// and only use max 3 for a lookup reply
|
||||
if (peer.relayAddresses.length > 3) {
|
||||
peer.relayAddresses = peer.relayAddresses.slice(0, 3)
|
||||
}
|
||||
|
||||
sodium.crypto_generichash(TMP, peer.publicKey)
|
||||
|
||||
const k = b4a.toString(req.target, 'hex')
|
||||
const announceSelf = b4a.equals(TMP, req.target)
|
||||
const record = encodeUnslab(m.peer, peer)
|
||||
|
||||
if (announceSelf) {
|
||||
this.dht._router.set(k, {
|
||||
relay: req.from,
|
||||
record,
|
||||
onconnect: null,
|
||||
onholepunch: null
|
||||
})
|
||||
this.records.remove(k, peer.publicKey)
|
||||
} else {
|
||||
const currentBump = this.bumps.get(k) || 0
|
||||
if (bump > currentBump && bump <= Date.now() + MAX_BUMP_DRIFT) this.bumps.set(k, bump)
|
||||
this.records.add(k, peer.publicKey, record)
|
||||
}
|
||||
|
||||
if (refresh) {
|
||||
this.refreshes.set(b4a.toString(refresh, 'hex'), { k, record, announceSelf })
|
||||
}
|
||||
|
||||
req.reply(null, { token: false, closerNodes: false })
|
||||
}
|
||||
|
||||
onmutableget(req) {
|
||||
if (!req.target || !req.value) return
|
||||
|
||||
let seq = 0
|
||||
try {
|
||||
seq = c.decode(c.uint, req.value)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const k = b4a.toString(req.target, 'hex')
|
||||
const value = this.mutables.get(k)
|
||||
|
||||
if (!value) {
|
||||
req.reply(null)
|
||||
return
|
||||
}
|
||||
|
||||
const localSeq = c.decode(c.uint, value)
|
||||
req.reply(localSeq < seq ? null : value)
|
||||
}
|
||||
|
||||
onmutableput(req) {
|
||||
if (!req.target || !req.token || !req.value) return
|
||||
|
||||
const p = decode(m.mutablePutRequest, req.value)
|
||||
if (!p) return
|
||||
|
||||
const { publicKey, seq, value, signature } = p
|
||||
|
||||
const hash = b4a.allocUnsafe(32)
|
||||
sodium.crypto_generichash(hash, publicKey)
|
||||
if (!b4a.equals(hash, req.target)) return
|
||||
|
||||
if (!value || !verifyMutable(signature, seq, value, publicKey)) return
|
||||
|
||||
const k = b4a.toString(hash, 'hex')
|
||||
const local = this.mutables.get(k)
|
||||
|
||||
if (local) {
|
||||
const existing = c.decode(m.mutableGetResponse, local)
|
||||
if (existing.value && existing.seq === seq && b4a.compare(value, existing.value) !== 0) {
|
||||
req.error(ERROR.SEQ_REUSED)
|
||||
return
|
||||
}
|
||||
if (seq < existing.seq) {
|
||||
req.error(ERROR.SEQ_TOO_LOW)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.mutables.set(k, encodeUnslab(m.mutableGetResponse, { seq, value, signature }))
|
||||
req.reply(null)
|
||||
}
|
||||
|
||||
onimmutableget(req) {
|
||||
if (!req.target) return
|
||||
|
||||
const k = b4a.toString(req.target, 'hex')
|
||||
const value = this.immutables.get(k)
|
||||
|
||||
req.reply(value || null)
|
||||
}
|
||||
|
||||
onimmutableput(req) {
|
||||
if (!req.target || !req.token || !req.value) return
|
||||
|
||||
const hash = b4a.alloc(32)
|
||||
sodium.crypto_generichash(hash, req.value)
|
||||
if (!b4a.equals(hash, req.target)) return
|
||||
|
||||
const k = b4a.toString(hash, 'hex')
|
||||
this.immutables.set(k, unslab(req.value))
|
||||
|
||||
req.reply(null)
|
||||
}
|
||||
|
||||
onplugin(req) {
|
||||
if (!req.value) return
|
||||
|
||||
const plugreq = decode(m.pluginRequest, req.value)
|
||||
if (plugreq === null) return
|
||||
|
||||
const p = this.dht.plugins.get(plugreq.plugin)
|
||||
if (!p || p.version !== plugreq.version) return
|
||||
|
||||
p.onrequest(plugreq, req)
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.records.destroy()
|
||||
this.refreshes.destroy()
|
||||
this.mutables.destroy()
|
||||
this.immutables.destroy()
|
||||
}
|
||||
|
||||
static signMutable(seq, value, keyPair) {
|
||||
const signable = b4a.allocUnsafe(32 + 32)
|
||||
const hash = signable.subarray(32)
|
||||
|
||||
signable.set(NS.MUTABLE_PUT, 0)
|
||||
|
||||
sodium.crypto_generichash(hash, c.encode(m.mutableSignable, { seq, value }))
|
||||
return sign(signable, keyPair)
|
||||
}
|
||||
|
||||
static verifyMutable(signature, seq, value, publicKey) {
|
||||
return verifyMutable(signature, seq, value, publicKey)
|
||||
}
|
||||
|
||||
static signAnnounce(target, token, id, ann, keyPair) {
|
||||
return sign(annSignable(target, token, id, ann, NS.ANNOUNCE), keyPair)
|
||||
}
|
||||
|
||||
static signUnannounce(target, token, id, ann, keyPair) {
|
||||
return sign(annSignable(target, token, id, ann, NS.UNANNOUNCE), keyPair)
|
||||
}
|
||||
}
|
||||
|
||||
function verifyMutable(signature, seq, value, publicKey) {
|
||||
const signable = b4a.allocUnsafe(32 + 32)
|
||||
const hash = signable.subarray(32)
|
||||
|
||||
signable.set(NS.MUTABLE_PUT, 0)
|
||||
|
||||
sodium.crypto_generichash(hash, c.encode(m.mutableSignable, { seq, value }))
|
||||
return sodium.crypto_sign_verify_detached(signature, signable, publicKey)
|
||||
}
|
||||
|
||||
function annSignable(target, token, id, ann, ns) {
|
||||
const signable = b4a.allocUnsafe(32 + 32)
|
||||
const hash = signable.subarray(32)
|
||||
|
||||
signable.set(ns, 0)
|
||||
|
||||
sodium.crypto_generichash_batch(hash, [
|
||||
target,
|
||||
id,
|
||||
token,
|
||||
c.encode(m.peer, ann.peer), // note that this is the partial encoding of the announce message so we could just use that for perf
|
||||
ann.refresh || EMPTY
|
||||
])
|
||||
|
||||
return signable
|
||||
}
|
||||
|
||||
function sign(signable, keyPair) {
|
||||
if (keyPair.sign) {
|
||||
return keyPair.sign(signable)
|
||||
}
|
||||
const secretKey = keyPair.secretKey ? keyPair.secretKey : keyPair
|
||||
const signature = b4a.allocUnsafe(64)
|
||||
sodium.crypto_sign_detached(signature, signable, secretKey)
|
||||
return signature
|
||||
}
|
||||
|
||||
function decode(enc, val) {
|
||||
try {
|
||||
return val && c.decode(enc, val)
|
||||
} catch (err) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
const c = require('compact-encoding')
|
||||
const m = require('./messages')
|
||||
const { COMMANDS: HYPERDHT_COMMANDS } = require('./constants')
|
||||
|
||||
module.exports = class Plugin {
|
||||
constructor(name, version) {
|
||||
this.name = name
|
||||
this.version = version
|
||||
this.dht = null
|
||||
}
|
||||
|
||||
onregister(dht) {
|
||||
this.dht = dht
|
||||
}
|
||||
|
||||
onrequest(req, outerReq) {
|
||||
throw new Error('onrequest() must be implemented')
|
||||
}
|
||||
|
||||
onpersistent() {
|
||||
throw new Error('onpersistent() must be implemented')
|
||||
}
|
||||
|
||||
destroy() {
|
||||
throw new Error('destroy() must be implemented')
|
||||
}
|
||||
|
||||
request({ token = null, command, target = null, value = null }, to, opts) {
|
||||
const req = c.encode(m.pluginRequest, {
|
||||
plugin: this.name,
|
||||
version: this.version,
|
||||
command,
|
||||
value
|
||||
})
|
||||
|
||||
return this.dht.request(
|
||||
{
|
||||
token,
|
||||
target,
|
||||
command: HYPERDHT_COMMANDS.PLUGIN,
|
||||
value: req
|
||||
},
|
||||
to,
|
||||
opts
|
||||
)
|
||||
}
|
||||
|
||||
query({ command, target = null, value = null }, opts) {
|
||||
const req = c.encode(m.pluginRequest, {
|
||||
plugin: this.name,
|
||||
version: this.version,
|
||||
command,
|
||||
value
|
||||
})
|
||||
|
||||
return this.dht.query(
|
||||
{
|
||||
target,
|
||||
command: HYPERDHT_COMMANDS.PLUGIN,
|
||||
value: req
|
||||
},
|
||||
opts
|
||||
)
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
module.exports = class RawStreamSet {
|
||||
constructor(dht) {
|
||||
this._dht = dht
|
||||
|
||||
this._prefix = 16 - 1 // 16 is the default stream-set side in udx
|
||||
this._streams = new Map()
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this._streams.size
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this._streams.values()
|
||||
}
|
||||
|
||||
add(opts) {
|
||||
const self = this
|
||||
|
||||
// TODO: we should prob have a udx helper for id generation, given the slight complexity
|
||||
// of the below. requires a PRNG in udx tho.
|
||||
|
||||
let id = 0
|
||||
|
||||
while (true) {
|
||||
id = (Math.random() * 0x100000000) >>> 0
|
||||
|
||||
if (this._streams.has(id & this._prefix)) continue
|
||||
break
|
||||
}
|
||||
|
||||
// always have ~50% change of rolling a free one
|
||||
if (2 * this._streams.size >= this._prefix) {
|
||||
// ie 0b11111 = 0b1111 + 1 + 0b1111
|
||||
this._prefix = 2 * this._prefix + 1
|
||||
|
||||
// move the prefixes over
|
||||
const next = new Map()
|
||||
for (const stream of this._streams.values()) {
|
||||
next.set(stream.id & this._prefix, stream)
|
||||
}
|
||||
this._streams = next
|
||||
}
|
||||
|
||||
const stream = this._dht.udx.createStream(id, opts)
|
||||
this._streams.set(id & this._prefix, stream)
|
||||
|
||||
stream.on('close', onclose)
|
||||
|
||||
return stream
|
||||
|
||||
function onclose() {
|
||||
self._streams.delete(id & self._prefix)
|
||||
}
|
||||
}
|
||||
|
||||
async clear() {
|
||||
const destroying = []
|
||||
|
||||
for (const stream of this._streams.values()) {
|
||||
destroying.push(new Promise((resolve) => stream.once('close', resolve).destroy()))
|
||||
}
|
||||
|
||||
await Promise.allSettled(destroying)
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
const sodium = require('sodium-universal')
|
||||
const b4a = require('b4a')
|
||||
|
||||
module.exports = function createRefreshChain(cnt) {
|
||||
const blocks = new Array(cnt)
|
||||
if (!blocks.length) return blocks
|
||||
|
||||
const all = b4a.allocUnsafe(cnt * 32 + 32)
|
||||
|
||||
let prev = all.subarray(all.byteLength - 32)
|
||||
sodium.randombytes_buf(prev)
|
||||
|
||||
for (let i = cnt - 1; i >= 0; i--) {
|
||||
blocks[i] = all.subarray(32 * i, 32 * i + 32)
|
||||
sodium.crypto_generichash(blocks[i], prev)
|
||||
prev = blocks[i]
|
||||
}
|
||||
|
||||
return blocks
|
||||
}
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
const c = require('compact-encoding')
|
||||
const Cache = require('xache')
|
||||
const safetyCatch = require('safety-catch')
|
||||
const b4a = require('b4a')
|
||||
const { handshake, holepunch } = require('./messages')
|
||||
const { COMMANDS } = require('./constants')
|
||||
const { BAD_HANDSHAKE_REPLY, BAD_HOLEPUNCH_REPLY } = require('./errors')
|
||||
|
||||
const FROM_CLIENT = 0
|
||||
const FROM_SERVER = 1
|
||||
const FROM_RELAY = 2
|
||||
const FROM_SECOND_RELAY = 3
|
||||
const REPLY = 4
|
||||
|
||||
// TODO: While the current design is very trustless in regards to clients/servers trusting the DHT,
|
||||
// we should add a bunch of rate limits everywhere, especially including here to avoid bad users
|
||||
// using a DHT node to relay traffic indiscriminately using the connect/holepunch messages.
|
||||
// That's mostly from an abuse POV as none of the messsages do amplication.
|
||||
|
||||
module.exports = class Router {
|
||||
constructor(dht, opts) {
|
||||
this.dht = dht
|
||||
this.forwards = new Cache(opts.forwards)
|
||||
}
|
||||
|
||||
set(target, state) {
|
||||
if (state.onpeerhandshake) {
|
||||
this.forwards.retain(toString(target), state)
|
||||
} else {
|
||||
this.forwards.set(toString(target), state)
|
||||
}
|
||||
}
|
||||
|
||||
get(target) {
|
||||
return this.forwards.get(toString(target))
|
||||
}
|
||||
|
||||
delete(target) {
|
||||
this.forwards.delete(toString(target))
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.forwards.destroy()
|
||||
}
|
||||
|
||||
async peerHandshake(target, { noise, peerAddress, relayAddress, socket, session }, to) {
|
||||
const dht = this.dht
|
||||
|
||||
const requestValue = c.encode(handshake, {
|
||||
mode: FROM_CLIENT,
|
||||
noise,
|
||||
peerAddress,
|
||||
relayAddress
|
||||
})
|
||||
|
||||
const res = await dht.request(
|
||||
{ command: COMMANDS.PEER_HANDSHAKE, target, value: requestValue },
|
||||
to,
|
||||
{ socket, session }
|
||||
)
|
||||
|
||||
const hs = decode(handshake, res.value)
|
||||
if (
|
||||
!hs ||
|
||||
hs.mode !== REPLY ||
|
||||
to.host !== res.from.host ||
|
||||
to.port !== res.from.port ||
|
||||
!hs.noise
|
||||
) {
|
||||
throw BAD_HANDSHAKE_REPLY()
|
||||
}
|
||||
|
||||
return {
|
||||
noise: hs.noise,
|
||||
relayed: !!hs.peerAddress,
|
||||
serverAddress: hs.peerAddress || to,
|
||||
clientAddress: res.to
|
||||
}
|
||||
}
|
||||
|
||||
async onpeerhandshake(req) {
|
||||
const hs = req.value && decode(handshake, req.value)
|
||||
if (!hs) return
|
||||
|
||||
const { mode, noise, peerAddress, relayAddress } = hs
|
||||
|
||||
const state = req.target && this.get(req.target)
|
||||
const isServer = !!(state && state.onpeerhandshake)
|
||||
const relay = state && state.relay
|
||||
|
||||
if (isServer) {
|
||||
let reply = null
|
||||
try {
|
||||
reply = noise && (await state.onpeerhandshake({ noise, peerAddress }, req))
|
||||
} catch (e) {
|
||||
safetyCatch(e)
|
||||
return
|
||||
}
|
||||
if (!reply || !reply.noise) return
|
||||
const opts = { socket: reply.socket, closerNodes: false, token: false }
|
||||
|
||||
switch (mode) {
|
||||
case FROM_CLIENT: {
|
||||
req.reply(
|
||||
c.encode(handshake, { mode: REPLY, noise: reply.noise, peerAddress: null }),
|
||||
opts
|
||||
)
|
||||
return
|
||||
}
|
||||
case FROM_RELAY: {
|
||||
req.relay(
|
||||
c.encode(handshake, { mode: FROM_SERVER, noise: reply.noise, peerAddress }),
|
||||
req.from,
|
||||
opts
|
||||
)
|
||||
return
|
||||
}
|
||||
case FROM_SECOND_RELAY: {
|
||||
if (!relayAddress) return
|
||||
req.relay(
|
||||
c.encode(handshake, { mode: FROM_SERVER, noise: reply.noise, peerAddress }),
|
||||
relayAddress,
|
||||
opts
|
||||
)
|
||||
return // eslint-disable-line
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch (mode) {
|
||||
case FROM_CLIENT: {
|
||||
// TODO: if no relay is known route closer to the target instead of timing out
|
||||
if (!noise) return
|
||||
if (!relay && !relayAddress) {
|
||||
// help the user route
|
||||
req.reply(null, { token: false, closerNodes: true })
|
||||
return
|
||||
}
|
||||
req.relay(
|
||||
c.encode(handshake, {
|
||||
mode: FROM_RELAY,
|
||||
noise,
|
||||
peerAddress: req.from,
|
||||
relayAddress: null
|
||||
}),
|
||||
relayAddress || relay
|
||||
)
|
||||
return
|
||||
}
|
||||
case FROM_RELAY: {
|
||||
if (!relay || !noise) return
|
||||
req.relay(
|
||||
c.encode(handshake, {
|
||||
mode: FROM_SECOND_RELAY,
|
||||
noise,
|
||||
peerAddress,
|
||||
relayAddress: req.from
|
||||
}),
|
||||
relay
|
||||
)
|
||||
return
|
||||
}
|
||||
case FROM_SERVER: {
|
||||
if (!peerAddress || !noise) return
|
||||
req.reply(
|
||||
c.encode(handshake, { mode: REPLY, noise, peerAddress: req.from, relayAddress: null }),
|
||||
{ to: peerAddress, closerNodes: false, token: false }
|
||||
)
|
||||
return // eslint-disable-line
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async peerHolepunch(target, { id, payload, peerAddress, socket, session }, to) {
|
||||
const dht = this.dht
|
||||
const requestValue = c.encode(holepunch, {
|
||||
mode: FROM_CLIENT,
|
||||
id,
|
||||
payload,
|
||||
peerAddress
|
||||
})
|
||||
|
||||
const res = await dht.request(
|
||||
{ command: COMMANDS.PEER_HOLEPUNCH, target, value: requestValue },
|
||||
to,
|
||||
{ socket, session }
|
||||
)
|
||||
|
||||
const hp = decode(holepunch, res.value)
|
||||
if (!hp || hp.mode !== REPLY || to.host !== res.from.host || to.port !== res.from.port) {
|
||||
throw BAD_HOLEPUNCH_REPLY()
|
||||
}
|
||||
|
||||
return {
|
||||
from: res.from,
|
||||
to: res.to,
|
||||
payload: hp.payload,
|
||||
peerAddress: hp.peerAddress || to
|
||||
}
|
||||
}
|
||||
|
||||
async onpeerholepunch(req) {
|
||||
const hp = req.value && decode(holepunch, req.value)
|
||||
if (!hp) return
|
||||
|
||||
const { mode, id, payload, peerAddress } = hp
|
||||
|
||||
const state = req.target && this.get(req.target)
|
||||
const isServer = !!(state && state.onpeerholepunch)
|
||||
const relay = state && state.relay
|
||||
|
||||
switch (mode) {
|
||||
case FROM_CLIENT: {
|
||||
if (!peerAddress && !relay) return
|
||||
req.relay(
|
||||
c.encode(holepunch, { mode: FROM_RELAY, id, payload, peerAddress: req.from }),
|
||||
peerAddress || relay
|
||||
)
|
||||
return
|
||||
}
|
||||
case FROM_RELAY: {
|
||||
if (!isServer || !peerAddress) return
|
||||
let reply = null
|
||||
try {
|
||||
reply = await state.onpeerholepunch({ id, payload, peerAddress }, req)
|
||||
} catch (e) {
|
||||
safetyCatch(e)
|
||||
return
|
||||
}
|
||||
if (!reply) return
|
||||
const opts = { socket: reply.socket, closerNodes: false, token: false }
|
||||
req.relay(
|
||||
c.encode(holepunch, { mode: FROM_SERVER, id: 0, payload: reply.payload, peerAddress }),
|
||||
req.from,
|
||||
opts
|
||||
)
|
||||
return
|
||||
}
|
||||
case FROM_SERVER: {
|
||||
req.reply(c.encode(holepunch, { mode: REPLY, id, payload, peerAddress: req.from }), {
|
||||
to: peerAddress,
|
||||
closerNodes: false,
|
||||
token: false
|
||||
})
|
||||
return // eslint-disable-line
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function decode(enc, val) {
|
||||
try {
|
||||
return c.decode(enc, val)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function toString(t) {
|
||||
return typeof t === 'string' ? t : b4a.toString(t, 'hex')
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
const sodium = require('sodium-universal')
|
||||
const b4a = require('b4a')
|
||||
const { holepunchPayload } = require('./messages')
|
||||
|
||||
module.exports = class HolepunchPayload {
|
||||
constructor(holepunchSecret) {
|
||||
this._sharedSecret = holepunchSecret
|
||||
this._localSecret = b4a.allocUnsafe(32)
|
||||
|
||||
sodium.randombytes_buf(this._localSecret)
|
||||
}
|
||||
|
||||
decrypt(buffer) {
|
||||
const state = { start: 24, end: buffer.byteLength - 16, buffer }
|
||||
|
||||
if (state.end <= state.start) return null
|
||||
|
||||
const nonce = buffer.subarray(0, 24)
|
||||
const msg = state.buffer.subarray(state.start, state.end)
|
||||
const cipher = state.buffer.subarray(state.start)
|
||||
|
||||
if (!sodium.crypto_secretbox_open_easy(msg, cipher, nonce, this._sharedSecret)) return null
|
||||
|
||||
try {
|
||||
return holepunchPayload.decode(state)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
encrypt(payload) {
|
||||
const state = { start: 24, end: 24, buffer: null }
|
||||
holepunchPayload.preencode(state, payload)
|
||||
state.buffer = b4a.allocUnsafe(state.end + 16)
|
||||
|
||||
const nonce = state.buffer.subarray(0, 24)
|
||||
const msg = state.buffer.subarray(state.start, state.end)
|
||||
const cipher = state.buffer.subarray(state.start)
|
||||
|
||||
holepunchPayload.encode(state, payload)
|
||||
sodium.randombytes_buf(nonce)
|
||||
sodium.crypto_secretbox_easy(cipher, msg, nonce, this._sharedSecret)
|
||||
|
||||
return state.buffer
|
||||
}
|
||||
|
||||
token(addr) {
|
||||
const out = b4a.allocUnsafe(32)
|
||||
sodium.crypto_generichash(out, b4a.from(addr.host), this._localSecret)
|
||||
return out
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
const DONE = Promise.resolve(true)
|
||||
const DESTROYED = Promise.resolve(false)
|
||||
|
||||
module.exports = class Semaphore {
|
||||
constructor(limit = 1) {
|
||||
this.limit = limit
|
||||
this.active = 0
|
||||
this.waiting = []
|
||||
|
||||
this.flushedPromise = null
|
||||
this.flushedResolve = null
|
||||
|
||||
this.destroyed = false
|
||||
|
||||
this._onwait = this._queueWaiting.bind(this)
|
||||
this._onflush = this._queueFlushed.bind(this)
|
||||
}
|
||||
|
||||
_queueWaiting(resolve) {
|
||||
this.waiting.push(resolve)
|
||||
}
|
||||
|
||||
_queueFlushed(resolve) {
|
||||
this.flushedResolve = resolve
|
||||
}
|
||||
|
||||
wait() {
|
||||
if (this.destroyed === true) return DESTROYED
|
||||
|
||||
if (this.active < this.limit && this.waiting.length === 0) {
|
||||
this.active++
|
||||
return DONE
|
||||
}
|
||||
|
||||
return new Promise(this._onwait)
|
||||
}
|
||||
|
||||
signal() {
|
||||
if (this.destroyed === true) return
|
||||
|
||||
this.active--
|
||||
while (this.active < this.limit && this.waiting.length > 0 && this.destroyed === false) {
|
||||
this.active++
|
||||
this.waiting.shift()(true)
|
||||
}
|
||||
|
||||
if (this.active === 0 && this.flushedResolve) {
|
||||
const resolve = this.flushedResolve
|
||||
this.flushedResolve = null
|
||||
this.flushedPromise = null
|
||||
resolve(true)
|
||||
}
|
||||
}
|
||||
|
||||
async flush() {
|
||||
if (this.destroyed === true) return
|
||||
if (this.active === 0) return
|
||||
if (this.flushedPromise) return this.flushedPromise
|
||||
this.flushedPromise = new Promise(this._onflush)
|
||||
return this.flushedPromise
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.destroyed = true
|
||||
this.active = 0
|
||||
while (this.waiting.length) this.waiting.pop()(false)
|
||||
if (this.flushedResolve) this.flushedResolve(false)
|
||||
}
|
||||
}
|
||||
+738
@@ -0,0 +1,738 @@
|
||||
const { EventEmitter } = require('events')
|
||||
const safetyCatch = require('safety-catch')
|
||||
const NoiseSecretStream = require('@hyperswarm/secret-stream')
|
||||
const b4a = require('b4a')
|
||||
const relay = require('blind-relay')
|
||||
const NoiseWrap = require('./noise-wrap')
|
||||
const Announcer = require('./announcer')
|
||||
const { FIREWALL, ERROR } = require('./constants')
|
||||
const { unslabbedHash } = require('./crypto')
|
||||
const SecurePayload = require('./secure-payload')
|
||||
const Holepuncher = require('./holepuncher')
|
||||
const { isPrivate } = require('bogon')
|
||||
const { ALREADY_LISTENING, NODE_DESTROYED, KEYPAIR_ALREADY_USED } = require('./errors')
|
||||
|
||||
const HANDSHAKE_CLEAR_WAIT = 10000
|
||||
const HANDSHAKE_INITIAL_TIMEOUT = 10000
|
||||
|
||||
module.exports = class Server extends EventEmitter {
|
||||
constructor(dht, opts = {}) {
|
||||
super()
|
||||
|
||||
this.dht = dht
|
||||
this.target = null
|
||||
|
||||
this.closed = false
|
||||
this.firewall = opts.firewall || (() => false)
|
||||
this.holepunch = opts.holepunch || (() => true)
|
||||
this.relayThrough = opts.relayThrough || null
|
||||
this.relayKeepAlive = opts.relayKeepAlive || 5000
|
||||
this.pool = opts.pool || null
|
||||
this.createHandshake = opts.createHandshake || defaultCreateHandshake
|
||||
this.createSecretStream = opts.createSecretStream || defaultCreateSecretStream
|
||||
this.suspended = false
|
||||
this.handshakeClearWait = opts.handshakeClearWait || HANDSHAKE_CLEAR_WAIT
|
||||
|
||||
this._shareLocalAddress = opts.shareLocalAddress !== false
|
||||
this._reusableSocket = !!opts.reusableSocket
|
||||
this._neverPunch = opts.holepunch === false // useful for fully disabling punching
|
||||
this._keyPair = null
|
||||
this._announcer = null
|
||||
this._connects = new Map()
|
||||
this._holepunches = []
|
||||
this._listening = null
|
||||
this._closing = null
|
||||
}
|
||||
|
||||
get listening() {
|
||||
return this._listening !== null
|
||||
}
|
||||
|
||||
get publicKey() {
|
||||
return this._keyPair && this._keyPair.publicKey
|
||||
}
|
||||
|
||||
get relayAddresses() {
|
||||
return this._announcer ? this._announcer.relayAddresses : []
|
||||
}
|
||||
|
||||
onconnection(encryptedSocket) {
|
||||
this.emit('connection', encryptedSocket)
|
||||
}
|
||||
|
||||
async suspend({ log = noop } = {}) {
|
||||
log('Suspending hyperdht server')
|
||||
if (this._listening !== null) await this._listening
|
||||
log('Suspending hyperdht server (post listening)')
|
||||
this.suspended = true
|
||||
this._clearAll()
|
||||
return this._announcer ? this._announcer.suspend({ log }) : Promise.resolve()
|
||||
}
|
||||
|
||||
async resume() {
|
||||
if (this._listening !== null) await this._listening
|
||||
this.suspended = false
|
||||
return this._announcer ? this._announcer.resume() : Promise.resolve()
|
||||
}
|
||||
|
||||
address() {
|
||||
if (!this._keyPair) return null
|
||||
|
||||
return {
|
||||
publicKey: this._keyPair.publicKey,
|
||||
host: this.dht.host,
|
||||
port: this.dht.port
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this._closing) return this._closing
|
||||
this._closing = this._close()
|
||||
return this._closing
|
||||
}
|
||||
|
||||
_gc() {
|
||||
this.dht.listening.delete(this)
|
||||
if (this.target) this.dht._router.delete(this.target)
|
||||
}
|
||||
|
||||
async _stopListening() {
|
||||
try {
|
||||
if (this._announcer) await this._announcer.stop()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
this._announcer = null
|
||||
this._listening = null
|
||||
this._keyPair = null
|
||||
}
|
||||
|
||||
async _close() {
|
||||
if (this._listening === null) {
|
||||
this.closed = true
|
||||
this.emit('close')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this._listening
|
||||
} catch (err) {
|
||||
safetyCatch(err)
|
||||
}
|
||||
|
||||
this._gc()
|
||||
this._clearAll()
|
||||
|
||||
await this._stopListening()
|
||||
|
||||
this.closed = true
|
||||
this.emit('close')
|
||||
}
|
||||
|
||||
_clearAll() {
|
||||
while (this._holepunches.length > 0) {
|
||||
const h = this._holepunches.pop()
|
||||
if (h && h.puncher) h.puncher.destroy()
|
||||
if (h && h.clearing) clearTimeout(h.clearing)
|
||||
if (h && h.prepunching) clearTimeout(h.prepunching)
|
||||
if (h && h.rawStream) h.rawStream.destroy()
|
||||
}
|
||||
|
||||
this._connects.clear()
|
||||
}
|
||||
|
||||
async listen(keyPair = this.dht.defaultKeyPair, opts = {}) {
|
||||
if (this._listening !== null) throw ALREADY_LISTENING()
|
||||
if (this.dht.destroyed) throw NODE_DESTROYED()
|
||||
|
||||
this._listening = this._listen(keyPair, opts)
|
||||
await this._listening
|
||||
return this
|
||||
}
|
||||
|
||||
async _listen(keyPair, opts) {
|
||||
// From now on, the DHT object which created me is responsible for closing me
|
||||
this.dht.listening.add(this)
|
||||
|
||||
try {
|
||||
await this.dht.bind()
|
||||
if (this._closing) return
|
||||
|
||||
for (const s of this.dht.listening) {
|
||||
if (s._keyPair && b4a.equals(s._keyPair.publicKey, keyPair.publicKey)) {
|
||||
throw KEYPAIR_ALREADY_USED()
|
||||
}
|
||||
}
|
||||
|
||||
this.target = unslabbedHash(keyPair.publicKey)
|
||||
this._keyPair = keyPair
|
||||
this._announcer = new Announcer(this.dht, keyPair, this.target, opts)
|
||||
|
||||
this.dht._router.set(this.target, {
|
||||
relay: null,
|
||||
record: this._announcer.record,
|
||||
onpeerhandshake: this._onpeerhandshake.bind(this),
|
||||
onpeerholepunch: this._onpeerholepunch.bind(this)
|
||||
})
|
||||
|
||||
// warm it up for now
|
||||
this._localAddresses().catch(safetyCatch)
|
||||
|
||||
await this._announcer.start()
|
||||
} catch (err) {
|
||||
await this._stopListening()
|
||||
this._gc()
|
||||
throw err
|
||||
}
|
||||
|
||||
if (this._closing) return
|
||||
if (this.suspended) await this._announcer.suspend()
|
||||
|
||||
if (this._closing) return
|
||||
if (this.dht.destroyed) throw NODE_DESTROYED()
|
||||
|
||||
if (this.pool) this.pool._attachServer(this)
|
||||
|
||||
this.emit('listening')
|
||||
}
|
||||
|
||||
refresh() {
|
||||
if (this._announcer && !this.suspended) this._announcer.refresh()
|
||||
}
|
||||
|
||||
notifyOnline() {
|
||||
if (this._announcer) this._announcer.online.notify()
|
||||
}
|
||||
|
||||
_localAddresses() {
|
||||
return this.dht.validateLocalAddresses(Holepuncher.localAddresses(this.dht.io.serverSocket))
|
||||
}
|
||||
|
||||
async _addHandshake(k, noise, clientAddress, { from, to: serverAddress, socket }, direct) {
|
||||
let id = this._holepunches.indexOf(null)
|
||||
if (id === -1) id = this._holepunches.push(null) - 1
|
||||
|
||||
const hs = {
|
||||
round: 0,
|
||||
reply: null,
|
||||
puncher: null,
|
||||
payload: null,
|
||||
rawStream: null,
|
||||
encryptedSocket: null,
|
||||
prepunching: null,
|
||||
firewalled: true,
|
||||
clearing: null,
|
||||
onsocket: null,
|
||||
aborted: false,
|
||||
|
||||
// Relay state
|
||||
relayTimeout: null,
|
||||
relayToken: null,
|
||||
relaySocket: null,
|
||||
relayClient: null,
|
||||
relayPaired: false
|
||||
}
|
||||
|
||||
this._holepunches[id] = hs
|
||||
|
||||
const handshake = this.createHandshake(this._keyPair, null)
|
||||
|
||||
let remotePayload
|
||||
try {
|
||||
remotePayload = await handshake.recv(noise)
|
||||
} catch (err) {
|
||||
safetyCatch(err)
|
||||
this._clearLater(hs, id, k)
|
||||
return null
|
||||
}
|
||||
|
||||
if (this._closing || this.suspended) return null
|
||||
|
||||
try {
|
||||
hs.firewalled = await this.firewall(handshake.remotePublicKey, remotePayload, clientAddress)
|
||||
} catch (err) {
|
||||
safetyCatch(err)
|
||||
}
|
||||
|
||||
if (this._closing || this.suspended) return null
|
||||
|
||||
if (hs.firewalled) {
|
||||
this._clearLater(hs, id, k)
|
||||
return null
|
||||
}
|
||||
|
||||
const error =
|
||||
remotePayload.version === 1
|
||||
? remotePayload.udx
|
||||
? ERROR.NONE
|
||||
: ERROR.ABORTED
|
||||
: ERROR.VERSION_MISMATCH
|
||||
|
||||
const addresses = []
|
||||
const ourRemoteAddr = this.dht.remoteAddress()
|
||||
const ourLocalAddrs = this._shareLocalAddress ? await this._localAddresses() : null
|
||||
|
||||
if (this._closing || this.suspended) return null
|
||||
|
||||
if (ourRemoteAddr) addresses.push(ourRemoteAddr)
|
||||
if (ourLocalAddrs) addresses.push(...ourLocalAddrs)
|
||||
|
||||
if (error === ERROR.NONE) {
|
||||
hs.rawStream = this.dht.createRawStream({
|
||||
framed: true,
|
||||
firewall(socket, port, host) {
|
||||
if (!(port > 0 && port < 65536)) return true
|
||||
|
||||
// Check if the traffic originated from the socket on which we're expecting relay traffic. If so,
|
||||
// we haven't hole punched yet and the other side is just sending us traffic through the relay.
|
||||
if (hs.relaySocket && isRelay(hs.relaySocket, socket, port, host)) {
|
||||
return false
|
||||
}
|
||||
|
||||
hs.onsocket(socket, port, host)
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
hs.rawStream.on('error', autoDestroy)
|
||||
|
||||
// Handles the case where onsocket is never called, but the stream got setup
|
||||
// This can happen on a relayed connection which never connects directly
|
||||
// (onsocket is called there only when the direct connection is established)
|
||||
const onrawstreamclose = () => {
|
||||
if (this._closing) return
|
||||
this._clearLater(hs, id, k)
|
||||
}
|
||||
hs.rawStream.on('close', onrawstreamclose)
|
||||
|
||||
hs.onsocket = (socket, port, host) => {
|
||||
if (hs.rawStream === null) return // Already hole punched
|
||||
|
||||
this._clearLater(hs, id, k)
|
||||
|
||||
if (hs.prepunching) {
|
||||
clearTimeout(hs.prepunching)
|
||||
hs.prepunching = null
|
||||
}
|
||||
|
||||
if (this._reusableSocket && remotePayload.udx.reusableSocket) {
|
||||
this.dht._socketPool.routes.add(handshake.remotePublicKey, hs.rawStream)
|
||||
}
|
||||
|
||||
hs.rawStream.removeListener('error', autoDestroy)
|
||||
hs.rawStream.removeListener('close', onrawstreamclose)
|
||||
|
||||
if (hs.rawStream.connected) {
|
||||
const remoteChanging = hs.rawStream.changeRemote(socket, remotePayload.udx.id, port, host)
|
||||
|
||||
if (remoteChanging) remoteChanging.catch(safetyCatch)
|
||||
} else {
|
||||
hs.rawStream.connect(socket, remotePayload.udx.id, port, host)
|
||||
hs.encryptedSocket = this.createSecretStream(false, hs.rawStream, {
|
||||
handshake: h,
|
||||
keepAlive: this.dht.connectionKeepAlive
|
||||
})
|
||||
|
||||
this.onconnection(hs.encryptedSocket)
|
||||
}
|
||||
|
||||
if (hs.puncher) {
|
||||
hs.puncher.onabort = noop
|
||||
hs.puncher.destroy()
|
||||
}
|
||||
|
||||
hs.rawStream = null
|
||||
}
|
||||
|
||||
function autoDestroy() {
|
||||
if (hs.puncher) hs.puncher.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
const relayAddresses = this.relayAddresses
|
||||
const relayThrough = selectRelay(this.relayThrough)
|
||||
|
||||
if (relayThrough) hs.relayToken = relay.token()
|
||||
|
||||
try {
|
||||
hs.reply = await handshake.send({
|
||||
error,
|
||||
firewall: ourRemoteAddr ? FIREWALL.OPEN : FIREWALL.UNKNOWN,
|
||||
holepunch: ourRemoteAddr ? null : { id, relays: this._announcer.relays },
|
||||
addresses4: addresses,
|
||||
addresses6: null,
|
||||
udx: {
|
||||
reusableSocket: this._reusableSocket,
|
||||
id: hs.rawStream ? hs.rawStream.id : 0,
|
||||
seq: 0
|
||||
},
|
||||
secretStream: {},
|
||||
relayThrough: relayThrough ? { publicKey: relayThrough, token: hs.relayToken } : null,
|
||||
relayAddresses: relayAddresses.length ? relayAddresses : null
|
||||
})
|
||||
} catch (err) {
|
||||
safetyCatch(err)
|
||||
if (hs.rawStream) hs.rawStream.destroy()
|
||||
this._clearLater(hs, id, k)
|
||||
return null
|
||||
}
|
||||
|
||||
if (this._closing || this.suspended) {
|
||||
if (hs.rawStream) hs.rawStream.destroy()
|
||||
return null
|
||||
}
|
||||
|
||||
const h = handshake.final()
|
||||
|
||||
if (error !== ERROR.NONE) {
|
||||
if (hs.rawStream) hs.rawStream.destroy()
|
||||
this._clearLater(hs, id, k)
|
||||
return hs
|
||||
}
|
||||
|
||||
if (remotePayload.firewall === FIREWALL.OPEN || direct) {
|
||||
const sock = direct ? socket : this.dht.socket
|
||||
this.dht.stats.punches.open++
|
||||
hs.onsocket(sock, clientAddress.port, clientAddress.host)
|
||||
return hs
|
||||
}
|
||||
|
||||
if (relayThrough || remotePayload.relayThrough) {
|
||||
this._relayConnection(hs, relayThrough, remotePayload, h)
|
||||
}
|
||||
|
||||
const onabort = () => {
|
||||
hs.aborted = true
|
||||
if (hs.prepunching) clearTimeout(hs.prepunching)
|
||||
hs.prepunching = null
|
||||
if (hs.rawStream.destroyed) {
|
||||
this._clearLater(hs, id, k)
|
||||
return
|
||||
}
|
||||
|
||||
hs.rawStream.on('close', () => this._clearLater(hs, id, k))
|
||||
if (hs.relayToken === null) hs.rawStream.destroy()
|
||||
}
|
||||
|
||||
if (!direct && clientAddress.host === serverAddress.host) {
|
||||
const clientAddresses = remotePayload.addresses4.filter(onlyPrivateHosts)
|
||||
|
||||
if (clientAddresses.length > 0 && this._shareLocalAddress) {
|
||||
const myAddresses = await this._localAddresses()
|
||||
const addr = Holepuncher.matchAddress(myAddresses, clientAddresses)
|
||||
|
||||
if (addr) {
|
||||
hs.prepunching = setTimeout(onabort, HANDSHAKE_INITIAL_TIMEOUT)
|
||||
return hs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this._closing || this.suspended) return null
|
||||
|
||||
if (ourRemoteAddr || this._neverPunch) {
|
||||
hs.prepunching = setTimeout(onabort, HANDSHAKE_INITIAL_TIMEOUT)
|
||||
return hs
|
||||
}
|
||||
|
||||
hs.payload = new SecurePayload(h.holepunchSecret)
|
||||
hs.puncher = new Holepuncher(this.dht, this.dht.session(), false, remotePayload.firewall)
|
||||
|
||||
hs.puncher.onconnect = hs.onsocket
|
||||
hs.puncher.onabort = onabort
|
||||
hs.prepunching = setTimeout(hs.puncher.destroy.bind(hs.puncher), HANDSHAKE_INITIAL_TIMEOUT)
|
||||
|
||||
return hs
|
||||
}
|
||||
|
||||
_clearLater(hs, id, k) {
|
||||
if (hs.clearing) return
|
||||
hs.clearing = setTimeout(() => this._clear(hs, id, k), this.handshakeClearWait)
|
||||
}
|
||||
|
||||
_clear(hs, id, k) {
|
||||
if (id >= this._holepunches.length || this._holepunches[id] !== hs) return
|
||||
if (hs.clearing) clearTimeout(hs.clearing)
|
||||
|
||||
this._holepunches[id] = null
|
||||
while (
|
||||
this._holepunches.length > 0 &&
|
||||
this._holepunches[this._holepunches.length - 1] === null
|
||||
) {
|
||||
this._holepunches.pop()
|
||||
}
|
||||
this._connects.delete(k)
|
||||
}
|
||||
|
||||
async _onpeerhandshake({ noise, peerAddress }, req) {
|
||||
const k = b4a.toString(noise, 'hex')
|
||||
|
||||
// The next couple of statements MUST run within the same tick to prevent
|
||||
// a malicious peer from flooding us with handshakes.
|
||||
let p = this._connects.get(k)
|
||||
if (!p) {
|
||||
p = this._addHandshake(k, noise, peerAddress || req.from, req, !peerAddress)
|
||||
this._connects.set(k, p)
|
||||
}
|
||||
|
||||
const h = await p
|
||||
if (!h) return null
|
||||
|
||||
if (this._closing !== null || this.suspended) return null
|
||||
|
||||
return { socket: h.puncher && h.puncher.socket, noise: h.reply }
|
||||
}
|
||||
|
||||
async _onpeerholepunch({ id, peerAddress, payload }, req) {
|
||||
const h = id < this._holepunches.length ? this._holepunches[id] : null
|
||||
if (!h) return null
|
||||
|
||||
if (!peerAddress || this._closing !== null || this.suspended) return null
|
||||
|
||||
const p = h.puncher
|
||||
if (!p || !p.socket) return this._abort(h) // not opened
|
||||
|
||||
const remotePayload = h.payload.decrypt(payload)
|
||||
if (!remotePayload) return null
|
||||
|
||||
const isServerRelay = this._announcer.isRelay(req.from)
|
||||
const { error, firewall, round, punching, addresses, remoteAddress, remoteToken } =
|
||||
remotePayload
|
||||
|
||||
if (error !== ERROR.NONE) {
|
||||
// We actually do not need to set the round here, but just do it for consistency.
|
||||
if (round >= h.round) h.round = round
|
||||
return this._abort(h)
|
||||
}
|
||||
|
||||
const token = h.payload.token(peerAddress)
|
||||
const echoed = isServerRelay && !!remoteToken && b4a.equals(token, remoteToken)
|
||||
|
||||
// Update our heuristics here
|
||||
if (req.socket === p.socket) {
|
||||
p.nat.add(req.to, req.from)
|
||||
}
|
||||
|
||||
if (round >= h.round) {
|
||||
h.round = round
|
||||
p.updateRemote({ punching, firewall, addresses, verified: echoed ? peerAddress.host : null })
|
||||
}
|
||||
|
||||
// Wait for the analyzer to reach a conclusion...
|
||||
let stable = await p.analyze(false)
|
||||
if (p.destroyed) return null
|
||||
|
||||
if (!p.remoteHolepunching && !stable) {
|
||||
stable = await p.analyze(true)
|
||||
if (p.destroyed) return null
|
||||
if (!stable) return this._abort(h)
|
||||
}
|
||||
|
||||
// Fast mode! If we are consistent and the remote has opened a session to us (remoteAddress)
|
||||
// then fire a quick punch back. Note the await here just waits for the udp socket to flush.
|
||||
if (
|
||||
isConsistent(p.nat.firewall) &&
|
||||
remoteAddress &&
|
||||
hasSameAddr(p.nat.addresses, remoteAddress)
|
||||
) {
|
||||
await p.ping(peerAddress)
|
||||
if (p.destroyed) return null
|
||||
}
|
||||
|
||||
// Remote said they are punching (or willing to), so we will punch as well.
|
||||
// Note that this returns when the punching has STARTED, so no guarantee
|
||||
// we will have a connection after this promise etc.
|
||||
if (p.remoteHolepunching) {
|
||||
// TODO: still continue here if a local connection might work, but then do not holepunch...
|
||||
if (!this.holepunch(p.remoteFirewall, p.nat.firewall, p.remoteAddresses, p.nat.addresses)) {
|
||||
return p.destroyed ? null : this._abort(h)
|
||||
}
|
||||
|
||||
if (h.prepunching) {
|
||||
clearTimeout(h.prepunching)
|
||||
h.prepunching = null
|
||||
}
|
||||
|
||||
if (p.remoteFirewall >= FIREWALL.RANDOM || p.nat.firewall >= FIREWALL.RANDOM) {
|
||||
if (
|
||||
this.dht._randomPunches >= this.dht._randomPunchLimit ||
|
||||
Date.now() - this.dht._lastRandomPunch < this.dht._randomPunchInterval
|
||||
) {
|
||||
if (!h.relayToken) return this._abort(h, ERROR.TRY_LATER)
|
||||
return {
|
||||
socket: p.socket,
|
||||
payload: h.payload.encrypt({
|
||||
error: ERROR.TRY_LATER,
|
||||
firewall: p.nat.firewall,
|
||||
round: h.round,
|
||||
connected: p.connected,
|
||||
punching: p.punching,
|
||||
addresses: p.nat.addresses,
|
||||
remoteAddress: null,
|
||||
token: isServerRelay ? token : null,
|
||||
remoteToken: remotePayload.token
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const punching = await p.punch()
|
||||
if (p.destroyed) return null
|
||||
if (!punching) return this._abort(h)
|
||||
}
|
||||
|
||||
// Freeze that analysis as soon as we have a result we are giving to the other peer
|
||||
if (p.nat.firewall !== FIREWALL.UNKNOWN) {
|
||||
p.nat.freeze()
|
||||
}
|
||||
|
||||
return {
|
||||
socket: p.socket,
|
||||
payload: h.payload.encrypt({
|
||||
error: ERROR.NONE,
|
||||
firewall: p.nat.firewall,
|
||||
round: h.round,
|
||||
connected: p.connected,
|
||||
punching: p.punching,
|
||||
addresses: p.nat.addresses,
|
||||
remoteAddress: null,
|
||||
token: isServerRelay ? token : null,
|
||||
remoteToken: remotePayload.token
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_abort(h, error = ERROR.ABORTED) {
|
||||
if (!h.payload) {
|
||||
if (h.puncher) h.puncher.destroy()
|
||||
return null
|
||||
}
|
||||
|
||||
const payload = h.payload.encrypt({
|
||||
error,
|
||||
firewall: FIREWALL.UNKNOWN,
|
||||
round: h.round,
|
||||
connected: false,
|
||||
punching: false,
|
||||
addresses: null,
|
||||
remoteAddress: null,
|
||||
token: null,
|
||||
remoteToken: null
|
||||
})
|
||||
|
||||
h.puncher.destroy()
|
||||
|
||||
return { socket: this.dht.socket, payload }
|
||||
}
|
||||
|
||||
_relayConnection(hs, relayThrough, remotePayload, h) {
|
||||
this.dht.stats.relaying.attempts++
|
||||
|
||||
let isInitiator
|
||||
let publicKey
|
||||
let token
|
||||
|
||||
if (relayThrough) {
|
||||
isInitiator = true
|
||||
publicKey = relayThrough
|
||||
token = hs.relayToken
|
||||
} else {
|
||||
isInitiator = false
|
||||
publicKey = remotePayload.relayThrough.publicKey
|
||||
token = remotePayload.relayThrough.token
|
||||
}
|
||||
|
||||
hs.relayToken = token
|
||||
hs.relaySocket = this.dht.connect(publicKey)
|
||||
hs.relaySocket.setKeepAlive(this.relayKeepAlive)
|
||||
hs.relayClient = relay.Client.from(hs.relaySocket, { id: hs.relaySocket.publicKey })
|
||||
hs.relayTimeout = setTimeout(onabort, 15000)
|
||||
|
||||
hs.relayClient
|
||||
.pair(isInitiator, token, hs.rawStream)
|
||||
.on('error', onabort)
|
||||
.on('data', (remoteId) => {
|
||||
if (hs.relayTimeout) clearRelayTimeout(hs)
|
||||
if (hs.rawStream === null) {
|
||||
onabort(null)
|
||||
return
|
||||
}
|
||||
|
||||
hs.relayPaired = true
|
||||
this.dht.stats.relaying.successes++
|
||||
|
||||
if (hs.prepunching) clearTimeout(hs.prepunching)
|
||||
hs.prepunching = null
|
||||
|
||||
const { remotePort, remoteHost, socket } = hs.relaySocket.rawStream
|
||||
|
||||
hs.rawStream
|
||||
.on('close', () => hs.relaySocket.destroy())
|
||||
.connect(socket, remoteId, remotePort, remoteHost)
|
||||
|
||||
hs.encryptedSocket = this.createSecretStream(false, hs.rawStream, { handshake: h })
|
||||
|
||||
this.onconnection(hs.encryptedSocket)
|
||||
})
|
||||
|
||||
const dht = this.dht
|
||||
function onabort() {
|
||||
if (!hs.relayPaired) dht.stats.relaying.aborts++
|
||||
if (hs.relayTimeout) clearRelayTimeout(hs)
|
||||
const socket = hs.relaySocket
|
||||
hs.relayToken = null
|
||||
hs.relaySocket = null
|
||||
if (socket) socket.destroy()
|
||||
if (hs.aborted && hs.rawStream) hs.rawStream.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearRelayTimeout(hs) {
|
||||
clearTimeout(hs.relayTimeout)
|
||||
hs.relayTimeout = null
|
||||
}
|
||||
|
||||
function isConsistent(fw) {
|
||||
return fw === FIREWALL.OPEN || fw === FIREWALL.CONSISTENT
|
||||
}
|
||||
|
||||
function hasSameAddr(addrs, other) {
|
||||
if (addrs === null) return false
|
||||
|
||||
for (const addr of addrs) {
|
||||
if (addr.port === other.port && addr.host === other.host) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function defaultCreateHandshake(keyPair, remotePublicKey) {
|
||||
return new NoiseWrap(keyPair, remotePublicKey)
|
||||
}
|
||||
|
||||
function defaultCreateSecretStream(isInitiator, rawStream, opts) {
|
||||
return new NoiseSecretStream(isInitiator, rawStream, opts)
|
||||
}
|
||||
|
||||
function onlyPrivateHosts(addr) {
|
||||
return isPrivate(addr.host)
|
||||
}
|
||||
|
||||
function isRelay(relaySocket, socket, port, host) {
|
||||
const stream = relaySocket.rawStream
|
||||
if (!stream) return false
|
||||
if (stream.socket !== socket) return false
|
||||
return port === stream.remotePort && host === stream.remoteHost
|
||||
}
|
||||
|
||||
function selectRelay(relayThrough) {
|
||||
if (typeof relayThrough === 'function') relayThrough = relayThrough()
|
||||
if (relayThrough === null) return null
|
||||
if (Array.isArray(relayThrough)) {
|
||||
return relayThrough[Math.floor(Math.random() * relayThrough.length)]
|
||||
}
|
||||
return relayThrough
|
||||
}
|
||||
|
||||
function noop() {}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
module.exports = class Sleeper {
|
||||
constructor() {
|
||||
this._timeout = null
|
||||
this._resolve = null
|
||||
|
||||
this._start = (resolve) => {
|
||||
this._resolve = resolve
|
||||
}
|
||||
|
||||
this._trigger = () => {
|
||||
if (this._resolve === null) return
|
||||
const resolve = this._resolve
|
||||
this._timeout = null
|
||||
this._resolve = null
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
|
||||
pause(ms) {
|
||||
const p = new Promise(this._start)
|
||||
if (this._timeout !== null) {
|
||||
clearTimeout(this._timeout)
|
||||
this._trigger()
|
||||
}
|
||||
this._timeout = setTimeout(this._trigger, ms)
|
||||
return p
|
||||
}
|
||||
|
||||
resume() {
|
||||
if (this._timeout !== null) {
|
||||
clearTimeout(this._timeout)
|
||||
this._trigger()
|
||||
}
|
||||
}
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
const b4a = require('b4a')
|
||||
|
||||
const LINGER_TIME = 3000
|
||||
|
||||
module.exports = class SocketPool {
|
||||
constructor(dht, host) {
|
||||
this._dht = dht
|
||||
this._sockets = new Map()
|
||||
this._lingering = new Set() // updated by the ref
|
||||
this._host = host
|
||||
|
||||
this.routes = new SocketRoutes(this)
|
||||
}
|
||||
|
||||
_onmessage(ref, data, address) {
|
||||
this._dht.onmessage(ref.socket, data, address)
|
||||
}
|
||||
|
||||
_add(ref) {
|
||||
this._sockets.set(ref.socket, ref)
|
||||
}
|
||||
|
||||
_remove(ref) {
|
||||
this._sockets.delete(ref.socket)
|
||||
this._lingering.delete(ref)
|
||||
}
|
||||
|
||||
lookup(socket) {
|
||||
return this._sockets.get(socket) || null
|
||||
}
|
||||
|
||||
setReusable(socket, bool) {
|
||||
const ref = this.lookup(socket)
|
||||
if (ref) ref.reusable = bool
|
||||
}
|
||||
|
||||
acquire() {
|
||||
// TODO: Enable socket reuse
|
||||
return new SocketRef(this)
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
const closing = []
|
||||
|
||||
for (const ref of this._sockets.values()) {
|
||||
ref._unlinger()
|
||||
closing.push(ref.socket.close())
|
||||
}
|
||||
|
||||
await Promise.allSettled(closing)
|
||||
}
|
||||
}
|
||||
|
||||
class SocketRoutes {
|
||||
constructor(pool) {
|
||||
this._pool = pool
|
||||
this._routes = new Map()
|
||||
}
|
||||
|
||||
add(publicKey, rawStream) {
|
||||
if (rawStream.socket) this._onconnect(publicKey, rawStream)
|
||||
else rawStream.on('connect', this._onconnect.bind(this, publicKey, rawStream))
|
||||
}
|
||||
|
||||
get(publicKey) {
|
||||
const id = b4a.toString(publicKey, 'hex')
|
||||
const route = this._routes.get(id)
|
||||
if (!route) return null
|
||||
return route
|
||||
}
|
||||
|
||||
_onconnect(publicKey, rawStream) {
|
||||
const id = b4a.toString(publicKey, 'hex')
|
||||
const socket = rawStream.socket
|
||||
|
||||
let route = this._routes.get(id)
|
||||
|
||||
if (!route) {
|
||||
const gc = () => {
|
||||
if (this._routes.get(id) === route) this._routes.delete(id)
|
||||
socket.removeListener('close', gc)
|
||||
}
|
||||
|
||||
route = {
|
||||
socket,
|
||||
address: { host: rawStream.remoteHost, port: rawStream.remotePort },
|
||||
gc
|
||||
}
|
||||
|
||||
this._routes.set(id, route)
|
||||
socket.on('close', gc)
|
||||
}
|
||||
|
||||
this._pool.setReusable(socket, true)
|
||||
|
||||
rawStream.on('error', () => {
|
||||
this._pool.setReusable(socket, false)
|
||||
if (!route) route = this._routes.get(id)
|
||||
if (route && route.socket === socket) route.gc()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: we should just make some "user data" object on udx to allow to attach this info
|
||||
class SocketRef {
|
||||
constructor(pool) {
|
||||
this._pool = pool
|
||||
|
||||
// Events
|
||||
this.onholepunchmessage = noop
|
||||
|
||||
// Whether it should teardown immediately or wait a bit
|
||||
this.reusable = false
|
||||
|
||||
this.socket = pool._dht.udx.createSocket()
|
||||
this.socket
|
||||
.on('close', this._onclose.bind(this))
|
||||
.on('message', this._onmessage.bind(this))
|
||||
.on('idle', this._onidle.bind(this))
|
||||
.on('busy', this._onbusy.bind(this))
|
||||
.bind(0, this._pool._host)
|
||||
|
||||
this._refs = 1
|
||||
this._released = false
|
||||
this._closed = false
|
||||
|
||||
this._timeout = null
|
||||
this._wasBusy = false
|
||||
|
||||
this._pool._add(this)
|
||||
}
|
||||
|
||||
_onclose() {
|
||||
this._pool._remove(this)
|
||||
}
|
||||
|
||||
_onmessage(data, address) {
|
||||
if (data.byteLength > 1) {
|
||||
this._pool._onmessage(this, data, address)
|
||||
} else {
|
||||
this.onholepunchmessage(data, address, this)
|
||||
}
|
||||
}
|
||||
|
||||
_onidle() {
|
||||
this._closeMaybe()
|
||||
}
|
||||
|
||||
_onbusy() {
|
||||
this._wasBusy = true
|
||||
this._unlinger()
|
||||
}
|
||||
|
||||
_reset() {
|
||||
this.onholepunchmessage = noop
|
||||
}
|
||||
|
||||
_closeMaybe() {
|
||||
if (this._refs === 0 && this.socket.idle && !this._timeout) this._close()
|
||||
}
|
||||
|
||||
_lingeringClose() {
|
||||
this._pool._lingering.delete(this)
|
||||
this._timeout = null
|
||||
this._closeMaybe()
|
||||
}
|
||||
|
||||
_close() {
|
||||
this._unlinger()
|
||||
|
||||
if (this.reusable && this._wasBusy) {
|
||||
this._wasBusy = false
|
||||
this._pool._lingering.add(this)
|
||||
this._timeout = setTimeout(this._lingeringClose.bind(this), LINGER_TIME)
|
||||
return
|
||||
}
|
||||
|
||||
this._closed = true
|
||||
this.socket.close()
|
||||
}
|
||||
|
||||
_unlinger() {
|
||||
if (this._timeout !== null) {
|
||||
clearTimeout(this._timeout)
|
||||
this._pool._lingering.delete(this)
|
||||
this._timeout = null
|
||||
}
|
||||
}
|
||||
|
||||
get free() {
|
||||
return this._refs === 0
|
||||
}
|
||||
|
||||
active() {
|
||||
this._refs++
|
||||
this._unlinger()
|
||||
}
|
||||
|
||||
inactive() {
|
||||
this._refs--
|
||||
this._closeMaybe()
|
||||
}
|
||||
|
||||
address() {
|
||||
return this.socket.address()
|
||||
}
|
||||
|
||||
release() {
|
||||
if (this._released) return
|
||||
|
||||
this._released = true
|
||||
this._reset()
|
||||
|
||||
this._refs--
|
||||
this._closeMaybe()
|
||||
}
|
||||
}
|
||||
|
||||
function noop() {}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
# compact-encoding
|
||||
|
||||
A series of compact encoding schemes for building small and fast parsers and serializers
|
||||
|
||||
```
|
||||
npm install compact-encoding
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
const cenc = require('compact-encoding')
|
||||
|
||||
const state = cenc.state()
|
||||
|
||||
// use preencode to figure out how big a buffer is needed
|
||||
cenc.uint.preencode(state, 42)
|
||||
cenc.string.preencode(state, 'hi')
|
||||
|
||||
console.log(state) // { start: 0, end: 4, buffer: null }
|
||||
|
||||
state.buffer = Buffer.allocUnsafe(state.end)
|
||||
|
||||
// then use encode to actually encode it to the buffer
|
||||
cenc.uint.encode(state, 42)
|
||||
cenc.string.encode(state, 'hi')
|
||||
|
||||
// to decode it simply use decode instead
|
||||
|
||||
state.start = 0
|
||||
cenc.uint.decode(state) // 42
|
||||
cenc.string.decode(state) // 'hi'
|
||||
```
|
||||
|
||||
## Encoder API
|
||||
|
||||
#### `state`
|
||||
|
||||
Should be an object that looks like this `{ start, end, buffer }`.
|
||||
|
||||
You can also get a blank state object using `cenc.state()`.
|
||||
|
||||
- `start` is the byte offset to start encoding/decoding at.
|
||||
- `end` is the byte offset indicating the end of the buffer.
|
||||
- `buffer` should be either a Node.js Buffer or Uint8Array.
|
||||
|
||||
#### `enc.preencode(state, val)`
|
||||
|
||||
Does a fast preencode dry-run that only sets state.end.
|
||||
Use this to figure out how big of a buffer you need.
|
||||
|
||||
#### `enc.encode(state, val)`
|
||||
|
||||
Encodes `val` into `state.buffer` at position `state.start`.
|
||||
Updates `state.start` to point after the encoded value when done.
|
||||
|
||||
#### `val = enc.decode(state)`
|
||||
|
||||
Decodes a value from `state.buffer` as position `state.start`.
|
||||
Updates `state.start` to point after the decoded value when done in the buffer.
|
||||
|
||||
## Helpers
|
||||
|
||||
If you are just encoding to a buffer or decoding from one you can use the `encode` and `decode` helpers
|
||||
to reduce your boilerplate
|
||||
|
||||
```js
|
||||
const buf = cenc.encode(cenc.bool, true)
|
||||
const bool = cenc.decode(cenc.bool, buf)
|
||||
```
|
||||
|
||||
## Bundled encodings
|
||||
|
||||
The following encodings are bundled as they are primitives that can be used
|
||||
to build others on top. Feel free to PR more that are missing.
|
||||
|
||||
- `cenc.raw` - Pass through encodes a buffer, i.e. a basic copy.
|
||||
- `cenc.uint` - Encodes a uint using the smallest fixed size encoding with a prefix to signal which one. Useful for uints that can be a wide range of values.
|
||||
- `cenc.uint8` - Encodes a fixed size uint8.
|
||||
- `cenc.uint16` - Encodes a fixed size uint16. Useful for things like ports.
|
||||
- `cenc.uint24` - Encodes a fixed size uint24. Useful for message framing.
|
||||
- `cenc.uint32` - Encodes a fixed size uint32. Useful for very large message framing.
|
||||
- `cenc.uint40` - Encodes a fixed size uint40.
|
||||
- `cenc.uint48` - Encodes a fixed size uint48.
|
||||
- `cenc.uint56` - Encodes a fixed size uint56.
|
||||
- `cenc.uint64` - Encodes a fixed size uint64.
|
||||
- `cenc.int` - Encodes an int using `cenc.uint` with ZigZag encoding.
|
||||
- `cenc.int8` - Encodes a fixed size int8 using `cenc.uint8` with ZigZag encoding.
|
||||
- `cenc.int16` - Encodes a fixed size int16 using `cenc.uint16` with ZigZag encoding.
|
||||
- `cenc.int24` - Encodes a fixed size int24 using `cenc.uint24` with ZigZag encoding.
|
||||
- `cenc.int32` - Encodes a fixed size int32 using `cenc.uint32` with ZigZag encoding.
|
||||
- `cenc.int40` - Encodes a fixed size int40 using `cenc.uint40` with ZigZag encoding.
|
||||
- `cenc.int48` - Encodes a fixed size int48 using `cenc.uint48` with ZigZag encoding.
|
||||
- `cenc.int56` - Encodes a fixed size int56 using `cenc.uint56` with ZigZag encoding.
|
||||
- `cenc.int64` - Encodes a fixed size int64 using `cenc.uint64` with ZigZag encoding.
|
||||
- `cenc.biguint64` - Encodes a fixed size biguint64.
|
||||
- `cenc.bigint64` - Encodes a fixed size bigint64 using `cenc.biguint64` with ZigZag encoding.
|
||||
- `cenc.biguint` - Encodes a biguint with its word count uint prefixed.
|
||||
- `cenc.bigint` - Encodes a bigint using `cenc.biguint` with ZigZag encoding.
|
||||
- `cenc.float32` - Encodes a fixed size float32.
|
||||
- `cenc.float64` - Encodes a fixed size float64.
|
||||
- `cenc.buffer` - Encodes a buffer with its length uint prefixed. When decoding an empty buffer, `null` is returned.
|
||||
- `cenc.raw.buffer` - Encodes a buffer without a length prefixed.
|
||||
- `cenc.arraybuffer` - Encodes an arraybuffer with its length uint prefixed.
|
||||
- `cenc.raw.arraybuffer` - Encodes an arraybuffer without a length prefixed.
|
||||
- `cenc.uint8array` - Encodes a uint8array with its element length uint prefixed.
|
||||
- `cenc.raw.uint8array` - Encodes a uint8array without a length prefixed.
|
||||
- `cenc.uint16array` - Encodes a uint16array with its element length uint prefixed.
|
||||
- `cenc.raw.uint16array` - Encodes a uint16array without a length prefixed.
|
||||
- `cenc.uint32array` - Encodes a uint32array with its element length uint prefixed.
|
||||
- `cenc.raw.uint32array` - Encodes a uint32array without a length prefixed.
|
||||
- `cenc.int8array` - Encodes a int8array with its element length uint prefixed.
|
||||
- `cenc.raw.int8array` - Encodes a int8array without a length prefixed.
|
||||
- `cenc.int16array` - Encodes a int16array with its element length uint prefixed.
|
||||
- `cenc.raw.int16array` - Encodes a int16array without a length prefixed.
|
||||
- `cenc.int32array` - Encodes a int32array with its element length uint prefixed.
|
||||
- `cenc.raw.int32array` - Encodes a int32array without a length prefixed.
|
||||
- `cenc.biguint64array` - Encodes a biguint64array with its element length uint prefixed.
|
||||
- `cenc.raw.biguint64array` - Encodes a biguint64array without a length prefixed.
|
||||
- `cenc.bigint64array` - Encodes a bigint64array with its element length uint prefixed.
|
||||
- `cenc.raw.bigint64array` - Encodes a bigint64array without a length prefixed.
|
||||
- `cenc.float32array` - Encodes a float32array with its element length uint prefixed.
|
||||
- `cenc.raw.float32array` - Encodes a float32array without a length prefixed.
|
||||
- `cenc.float64array` - Encodes a float64array with its element length uint prefixed.
|
||||
- `cenc.raw.float64array` - Encodes a float64array without a length prefixed.
|
||||
- `cenc.bool` - Encodes a boolean as 1 or 0.
|
||||
- `cenc.string`, `cenc.utf8` - Encodes a utf-8 string, similar to buffer.
|
||||
- `cenc.raw.string`, `cenc.raw.utf8` - Encodes a utf-8 string without a length prefixed.
|
||||
- `cenc.string.fixed(n)`, `cenc.utf8.fixed(n)` - Encodes a fixed sized utf-8 string.
|
||||
- `cenc.ascii` - Encodes an ascii string.
|
||||
- `cenc.raw.ascii` - Encodes an ascii string without a length prefixed.
|
||||
- `cenc.ascii.fixed(n)` - Encodes a fixed size ascii string.
|
||||
- `cenc.hex` - Encodes a hex string.
|
||||
- `cenc.raw.hex` - Encodes a hex string without a length prefixed.
|
||||
- `cenc.hex.fixed(n)` - Encodes a fixed size hex string.
|
||||
- `cenc.base64` - Encodes a base64 string.
|
||||
- `cenc.raw.base64` - Encodes a base64 string without a length prefixed.
|
||||
- `cenc.base64.fixed(n)` - Encodes a fixed size base64 string.
|
||||
- `cenc.utf16le`, `cenc.ucs2` - Encodes a utf16le string.
|
||||
- `cenc.raw.utf16le`, `cenc.raw.ucs2` - Encodes a utf16le string without a length prefixed.
|
||||
- `cenc.utf16le.fixed(n)`, `cenc.ucs2.fixed(n)` - Encodes a fixed size utf16le string.
|
||||
- `cenc.fixed32` - Encodes a fixed 32 byte buffer.
|
||||
- `cenc.fixed64` - Encodes a fixed 64 byte buffer.
|
||||
- `cenc.fixed(n)` - Makes a fixed sized encoder.
|
||||
- `cenc.date(d)` - Encodes a date object.
|
||||
- `cenc.array(enc)` - Makes an array encoder from another encoder. Arrays are uint prefixed with their length.
|
||||
- `cenc.raw.array(enc)` - Makes an array encoder from another encoder, without a length prefixed.
|
||||
- `cenc.json` - Encodes a JSON value as utf-8.
|
||||
- `cenc.raw.json` - Encodes a JSON value as utf-8 without a length prefixed.
|
||||
- `cenc.ndjson` - Encodes a JSON value as newline delimited utf-8.
|
||||
- `cenc.raw.ndjson` - Encodes a JSON value as newline delimited utf-8 without a length prefixed.
|
||||
- `cenc.any` - Encodes any JSON representable value into a self described buffer. Like JSON + buffer, but using compact types. Useful for schemaless codecs.
|
||||
- `cenc.port` - Encodes a port number for network addresses.
|
||||
- `cenc.ipv4` - Encodes an IPv4 network address.
|
||||
- `cenc.ipv4Address` Encodes an IPv4 network address and a port number.
|
||||
- `cenc.ipv6` - Encodes an IPv6 network address.
|
||||
- `cenc.ipv6Address` Encodes an IPv6 network address and a port number.
|
||||
- `cenc.ip` - Encodes a dual IPv4/6 network address.
|
||||
- `cenc.ipAddress` Encodes a dual IPv4/6 network address and a port number.
|
||||
- `cenc.from(enc)` - Makes a compact encoder from a [codec](https://github.com/mafintosh/codecs) or [abstract-encoding](https://github.com/mafintosh/abstract-encoding).
|
||||
- `cenc.none` - Helper for when you want to just express nothing
|
||||
|
||||
## License
|
||||
|
||||
Apache 2.0
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
const LE = (exports.LE =
|
||||
new Uint8Array(new Uint16Array([0xff]).buffer)[0] === 0xff)
|
||||
|
||||
exports.BE = !LE
|
||||
+1091
File diff suppressed because it is too large
Load Diff
+117
@@ -0,0 +1,117 @@
|
||||
module.exports = {
|
||||
preencode,
|
||||
encode,
|
||||
decode
|
||||
}
|
||||
|
||||
function preencode(state, num) {
|
||||
if (num < 251) {
|
||||
state.end++
|
||||
} else if (num < 256) {
|
||||
state.end += 2
|
||||
} else if (num < 0x10000) {
|
||||
state.end += 3
|
||||
} else if (num < 0x1000000) {
|
||||
state.end += 4
|
||||
} else if (num < 0x100000000) {
|
||||
state.end += 5
|
||||
} else {
|
||||
state.end++
|
||||
const exp = Math.floor(Math.log(num) / Math.log(2)) - 32
|
||||
preencode(state, exp)
|
||||
state.end += 6
|
||||
}
|
||||
}
|
||||
|
||||
function encode(state, num) {
|
||||
const max = 251
|
||||
const x = num - max
|
||||
|
||||
if (num < max) {
|
||||
state.buffer[state.start++] = num
|
||||
} else if (num < 256) {
|
||||
state.buffer[state.start++] = max
|
||||
state.buffer[state.start++] = x
|
||||
} else if (num < 0x10000) {
|
||||
state.buffer[state.start++] = max + 1
|
||||
state.buffer[state.start++] = (x >> 8) & 0xff
|
||||
state.buffer[state.start++] = x & 0xff
|
||||
} else if (num < 0x1000000) {
|
||||
state.buffer[state.start++] = max + 2
|
||||
state.buffer[state.start++] = x >> 16
|
||||
state.buffer[state.start++] = (x >> 8) & 0xff
|
||||
state.buffer[state.start++] = x & 0xff
|
||||
} else if (num < 0x100000000) {
|
||||
state.buffer[state.start++] = max + 3
|
||||
state.buffer[state.start++] = x >> 24
|
||||
state.buffer[state.start++] = (x >> 16) & 0xff
|
||||
state.buffer[state.start++] = (x >> 8) & 0xff
|
||||
state.buffer[state.start++] = x & 0xff
|
||||
} else {
|
||||
// need to use Math here as bitwise ops are 32 bit
|
||||
const exp = Math.floor(Math.log(x) / Math.log(2)) - 32
|
||||
state.buffer[state.start++] = 0xff
|
||||
|
||||
encode(state, exp)
|
||||
const rem = x / Math.pow(2, exp - 11)
|
||||
|
||||
for (let i = 5; i >= 0; i--) {
|
||||
state.buffer[state.start++] = (rem / Math.pow(2, 8 * i)) & 0xff
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function decode(state) {
|
||||
const max = 251
|
||||
|
||||
if (state.end - state.start < 1) throw new Error('Out of bounds')
|
||||
|
||||
const flag = state.buffer[state.start++]
|
||||
|
||||
if (flag < max) return flag
|
||||
|
||||
if (state.end - state.start < flag - max + 1) {
|
||||
throw new Error('Out of bounds.')
|
||||
}
|
||||
|
||||
if (flag < 252) {
|
||||
return state.buffer[state.start++] + max
|
||||
}
|
||||
|
||||
if (flag < 253) {
|
||||
return (
|
||||
(state.buffer[state.start++] << 8) + state.buffer[state.start++] + max
|
||||
)
|
||||
}
|
||||
|
||||
if (flag < 254) {
|
||||
return (
|
||||
(state.buffer[state.start++] << 16) +
|
||||
(state.buffer[state.start++] << 8) +
|
||||
state.buffer[state.start++] +
|
||||
max
|
||||
)
|
||||
}
|
||||
|
||||
// << 24 result may be interpreted as negative
|
||||
if (flag < 255) {
|
||||
return (
|
||||
state.buffer[state.start++] * 0x1000000 +
|
||||
(state.buffer[state.start++] << 16) +
|
||||
(state.buffer[state.start++] << 8) +
|
||||
state.buffer[state.start++] +
|
||||
max
|
||||
)
|
||||
}
|
||||
|
||||
const exp = decode(state)
|
||||
|
||||
if (state.end - state.start < 6) throw new Error('Out of bounds')
|
||||
|
||||
let rem = 0
|
||||
for (let i = 5; i >= 0; i--) {
|
||||
rem += state.buffer[state.start++] * Math.pow(2, 8 * i)
|
||||
}
|
||||
|
||||
return rem * Math.pow(2, exp - 11) + max
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "compact-encoding",
|
||||
"version": "3.1.0",
|
||||
"description": "A series of compact encoding schemes for building small and fast parsers and serializers",
|
||||
"main": "index.js",
|
||||
"files": [
|
||||
"endian.js",
|
||||
"index.js",
|
||||
"lexint.js",
|
||||
"raw.js"
|
||||
],
|
||||
"dependencies": {
|
||||
"b4a": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"brittle": "^3.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-config-holepunch": "^1.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"format": "prettier . --write",
|
||||
"test": "prettier . --check && brittle test.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/holepunchto/compact-encoding.git"
|
||||
},
|
||||
"author": "Mathias Buus (@mafintosh)",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/holepunchto/compact-encoding/issues"
|
||||
},
|
||||
"homepage": "https://github.com/holepunchto/compact-encoding"
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
const b4a = require('b4a')
|
||||
|
||||
const { BE } = require('./endian')
|
||||
|
||||
exports = module.exports = {
|
||||
preencode(state, b) {
|
||||
state.end += b.byteLength
|
||||
},
|
||||
encode(state, b) {
|
||||
state.buffer.set(b, state.start)
|
||||
state.start += b.byteLength
|
||||
},
|
||||
decode(state) {
|
||||
const b = state.buffer.subarray(state.start, state.end)
|
||||
state.start = state.end
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
const buffer = (exports.buffer = {
|
||||
preencode(state, b) {
|
||||
uint8array.preencode(state, b)
|
||||
},
|
||||
encode(state, b) {
|
||||
uint8array.encode(state, b)
|
||||
},
|
||||
decode(state) {
|
||||
const b = state.buffer.subarray(state.start)
|
||||
state.start = state.end
|
||||
return b
|
||||
}
|
||||
})
|
||||
|
||||
exports.binary = {
|
||||
...buffer,
|
||||
preencode(state, b) {
|
||||
if (typeof b === 'string') utf8.preencode(state, b)
|
||||
else buffer.preencode(state, b)
|
||||
},
|
||||
encode(state, b) {
|
||||
if (typeof b === 'string') utf8.encode(state, b)
|
||||
else buffer.encode(state, b)
|
||||
}
|
||||
}
|
||||
|
||||
exports.arraybuffer = {
|
||||
preencode(state, b) {
|
||||
state.end += b.byteLength
|
||||
},
|
||||
encode(state, b) {
|
||||
const view = new Uint8Array(b)
|
||||
|
||||
state.buffer.set(view, state.start)
|
||||
state.start += b.byteLength
|
||||
},
|
||||
decode(state) {
|
||||
const b = new ArrayBuffer(state.end - state.start)
|
||||
const view = new Uint8Array(b)
|
||||
|
||||
view.set(state.buffer.subarray(state.start))
|
||||
|
||||
state.start = state.end
|
||||
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
function typedarray(TypedArray, swap) {
|
||||
const n = TypedArray.BYTES_PER_ELEMENT
|
||||
|
||||
return {
|
||||
preencode(state, b) {
|
||||
state.end += b.byteLength
|
||||
},
|
||||
encode(state, b) {
|
||||
const view = new Uint8Array(b.buffer, b.byteOffset, b.byteLength)
|
||||
|
||||
if (BE && swap) swap(view)
|
||||
|
||||
state.buffer.set(view, state.start)
|
||||
state.start += b.byteLength
|
||||
},
|
||||
decode(state) {
|
||||
let b = state.buffer.subarray(state.start)
|
||||
if (b.byteOffset % n !== 0) b = new Uint8Array(b)
|
||||
|
||||
if (BE && swap) swap(b)
|
||||
|
||||
state.start = state.end
|
||||
|
||||
return new TypedArray(b.buffer, b.byteOffset, b.byteLength / n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const uint8array = (exports.uint8array = typedarray(Uint8Array))
|
||||
exports.uint16array = typedarray(Uint16Array, b4a.swap16)
|
||||
exports.uint32array = typedarray(Uint32Array, b4a.swap32)
|
||||
|
||||
exports.int8array = typedarray(Int8Array)
|
||||
exports.int16array = typedarray(Int16Array, b4a.swap16)
|
||||
exports.int32array = typedarray(Int32Array, b4a.swap32)
|
||||
|
||||
exports.biguint64array = typedarray(BigUint64Array, b4a.swap64)
|
||||
exports.bigint64array = typedarray(BigInt64Array, b4a.swap64)
|
||||
|
||||
exports.float32array = typedarray(Float32Array, b4a.swap32)
|
||||
exports.float64array = typedarray(Float64Array, b4a.swap64)
|
||||
|
||||
function string(encoding) {
|
||||
return {
|
||||
preencode(state, s) {
|
||||
state.end += b4a.byteLength(s, encoding)
|
||||
},
|
||||
encode(state, s) {
|
||||
state.start += b4a.write(state.buffer, s, state.start, encoding)
|
||||
},
|
||||
decode(state) {
|
||||
const s = b4a.toString(state.buffer, encoding, state.start)
|
||||
state.start = state.end
|
||||
return s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const utf8 = (exports.string = exports.utf8 = string('utf-8'))
|
||||
exports.ascii = string('ascii')
|
||||
exports.hex = string('hex')
|
||||
exports.base64 = string('base64')
|
||||
exports.ucs2 = exports.utf16le = string('utf16le')
|
||||
|
||||
exports.array = function array(enc) {
|
||||
return {
|
||||
preencode(state, list) {
|
||||
for (const value of list) enc.preencode(state, value)
|
||||
},
|
||||
encode(state, list) {
|
||||
for (const value of list) enc.encode(state, value)
|
||||
},
|
||||
decode(state) {
|
||||
const arr = []
|
||||
while (state.start < state.end) arr.push(enc.decode(state))
|
||||
return arr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.json = {
|
||||
preencode(state, v) {
|
||||
utf8.preencode(state, JSON.stringify(v))
|
||||
},
|
||||
encode(state, v) {
|
||||
utf8.encode(state, JSON.stringify(v))
|
||||
},
|
||||
decode(state) {
|
||||
return JSON.parse(utf8.decode(state))
|
||||
}
|
||||
}
|
||||
|
||||
exports.ndjson = {
|
||||
preencode(state, v) {
|
||||
utf8.preencode(state, JSON.stringify(v) + '\n')
|
||||
},
|
||||
encode(state, v) {
|
||||
utf8.encode(state, JSON.stringify(v) + '\n')
|
||||
},
|
||||
decode(state) {
|
||||
return JSON.parse(utf8.decode(state))
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"name": "hyperdht",
|
||||
"version": "6.32.0",
|
||||
"description": "The DHT powering Hyperswarm",
|
||||
"main": "index.js",
|
||||
"browser": "browser.js",
|
||||
"bin": {
|
||||
"hyperdht": "./bin.js"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"browser.js",
|
||||
"testnet.js",
|
||||
"bin.js",
|
||||
"lib/**.js"
|
||||
],
|
||||
"imports": {
|
||||
"events": {
|
||||
"bare": "bare-events",
|
||||
"default": "events"
|
||||
},
|
||||
"child_process": {
|
||||
"bare": "bare-node-child-process",
|
||||
"default": "child_process"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@hyperswarm/secret-stream": "^6.6.2",
|
||||
"b4a": "^1.3.1",
|
||||
"bare-events": "^2.2.0",
|
||||
"blind-relay": "^1.3.0",
|
||||
"bogon": "^1.0.0",
|
||||
"compact-encoding": "^3.0.0",
|
||||
"dht-rpc": "^6.15.1",
|
||||
"hypercore-crypto": "^3.3.0",
|
||||
"hypercore-id-encoding": "^1.2.0",
|
||||
"hyperdht-address": "^1.0.1",
|
||||
"noise-curve-ed": "^2.0.0",
|
||||
"noise-handshake": "^4.0.0",
|
||||
"record-cache": "^1.1.1",
|
||||
"safety-catch": "^1.0.1",
|
||||
"signal-promise": "^1.0.3",
|
||||
"sodium-universal": "^5.0.1",
|
||||
"streamx": "^2.16.1",
|
||||
"unslab": "^1.3.0",
|
||||
"xache": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"bare-node-child-process": "^1.0.1",
|
||||
"brittle": "^3.0.0",
|
||||
"graceful-goodbye": "^1.3.0",
|
||||
"newline-decoder": "^1.0.2",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-config-holepunch": "^2.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"test": "prettier --check . && node test/all.js",
|
||||
"test:bare": "bare test/all.js",
|
||||
"test:generate": "brittle -r test/all.js test/*.js",
|
||||
"integration": "brittle test/integration/*.js",
|
||||
"end-to-end": "brittle test/end-to-end/*.js"
|
||||
},
|
||||
"author": "Mathias Buus (@mafintosh)",
|
||||
"license": "MIT",
|
||||
"directories": {
|
||||
"lib": "lib",
|
||||
"test": "test"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/holepunchto/hyperdht.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"bugs": {
|
||||
"url": "https://github.com/holepunchto/hyperdht/issues"
|
||||
},
|
||||
"homepage": "https://github.com/holepunchto/hyperdht#readme"
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
const DHT = require('.')
|
||||
|
||||
module.exports = async function createTestnet(size = 10, opts = {}) {
|
||||
const swarm = []
|
||||
const teardown =
|
||||
typeof opts === 'function' ? opts : opts.teardown ? opts.teardown.bind(opts) : noop
|
||||
const host = opts.host || '127.0.0.1'
|
||||
const port = opts.port || 0
|
||||
const bootstrap = opts.bootstrap ? [...opts.bootstrap] : []
|
||||
const bindHost = host === '127.0.0.1' ? '127.0.0.1' : '0.0.0.0'
|
||||
|
||||
if (size === 0) return new Testnet(swarm)
|
||||
|
||||
const first = new DHT({
|
||||
ephemeral: false,
|
||||
firewalled: false,
|
||||
bootstrap,
|
||||
port,
|
||||
host: bindHost
|
||||
})
|
||||
|
||||
await first.fullyBootstrapped()
|
||||
|
||||
if (bootstrap.length === 0) bootstrap.push({ host, port: first.address().port })
|
||||
|
||||
swarm.push(first)
|
||||
|
||||
while (swarm.length < size) {
|
||||
const node = new DHT({
|
||||
ephemeral: false,
|
||||
firewalled: false,
|
||||
bootstrap,
|
||||
host: bindHost
|
||||
})
|
||||
|
||||
await node.fullyBootstrapped()
|
||||
swarm.push(node)
|
||||
}
|
||||
|
||||
const testnet = new Testnet(swarm, bootstrap)
|
||||
|
||||
teardown(() => testnet.destroy(), { order: Infinity })
|
||||
|
||||
return testnet
|
||||
}
|
||||
|
||||
class Testnet {
|
||||
constructor(nodes, bootstrap = []) {
|
||||
this.nodes = nodes
|
||||
this.bootstrap = bootstrap
|
||||
}
|
||||
|
||||
createNode(opts = {}) {
|
||||
const node = new DHT({
|
||||
ephemeral: true,
|
||||
bootstrap: this.bootstrap,
|
||||
host: '127.0.0.1',
|
||||
...opts
|
||||
})
|
||||
|
||||
this.nodes.push(node)
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
async destroy() {
|
||||
for (const node of this.nodes) {
|
||||
for (const server of node.listening) await server.close()
|
||||
}
|
||||
|
||||
for (let i = this.nodes.length - 1; i >= 0; i--) {
|
||||
await this.nodes[i].destroy()
|
||||
}
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this.nodes[Symbol.iterator]()
|
||||
}
|
||||
}
|
||||
|
||||
function noop() {}
|
||||
Reference in New Issue
Block a user