UNPKG

holesail-server

Version:

Proxy any server peer-to-peer using Holepunching.

245 lines (230 loc) 8.3 kB
// Importing required modules const HyperDHT = require('hyperdht') // HyperDHT module for DHT functionality const net = require('net') // Node.js net module for creating network clients and servers const libNet = require('@holesail/hyper-cmd-lib-net') // Custom network library const libKeys = require('hyper-cmd-lib-keys') // To generate a random preSeed for server seed. const b4a = require('b4a') const z32 = require('z32') class HolesailServer { constructor (opts = {}) { this.logger = opts.logger || { log: () => {} } this.dht = new HyperDHT() this.stats = {} this.server = null this.keyPair = null this.seed = null this.state = null this.connection = null this.activeConnections = new Map() } generateKeyPair (seed) { // Allows us to keep the same keyPair everytime. if (!seed) { seed = libKeys.randomBytes(32).toString('hex') } // generate a seed from the buffer key this.seed = Buffer.from(seed, 'hex') // generate a keypair from the seed this.keyPair = HyperDHT.keyPair(this.seed) this.logger.log({ type: 0, msg: `Generated key pair from seed: ${seed}` }) return this.keyPair } // start the client on port and the address specified async start (args, callback) { this.logger.log({ type: 1, msg: 'Starting server' }) this.args = args this.secure = args.secure === true // generate the keypair this.generateKeyPair(args.seed) // this is needed for the secure mode to work and is implemented by HyperDHT let privateFirewall = false if (this.secure) { privateFirewall = (remotePublicKey) => { return !b4a.equals(remotePublicKey, this.keyPair.publicKey) } this.logger.log({ type: 1, msg: 'Using Private Mode' }) } else { this.logger.log({ type: 1, msg: 'Using Public Mode' }) } this.server = this.dht.createServer( { firewall: privateFirewall, reusableSocket: true }, (c) => { const encodedKey = z32.encode(c.remotePublicKey) this.logger.log({ type: 0, msg: `Incoming connection received from ${encodedKey}` }) const count = this.activeConnections.get(encodedKey) || 0 this.activeConnections.set(encodedKey, count + 1) if (!args.udp) { this.handleTCP(c, args) } else { this.handleUDP(c, args) } } ) this.logger.log({ type: 0, msg: 'Server created, awaiting listen' }) // start listening on the keyPair this.server.listen(this.keyPair).then(() => { this.state = 'listening' this.logger.log({ type: 1, msg: `Server listening on key: ${this.key}` }) if (typeof callback === 'function') { callback() // Invoke the callback after the server has started } }) const interval = 50 * 60 * 1000 // put host information on the dht const data = JSON.stringify({ host: this.args.host, udp: this.args.udp, port: this.args.port }) this.logger.log({ type: 0, msg: `Initializing DHT with host info: ${data}` }) await this.put(data) setInterval(async () => { this.logger.log({ type: 0, msg: `Refreshing DHT record: ${data}` }) await this.put(data) }, interval) } // Handle TCP connections handleTCP (c, args) { this.logger.log({ type: 0, msg: 'Handling TCP connection' }) const encodedKey = z32.encode(c.remotePublicKey) c.on('close', () => { let count = this.activeConnections.get(encodedKey) || 1 count-- if (count <= 0) { this.logger.log({ type: 0, msg: `Disconnected from ${encodedKey}` }) this.activeConnections.delete(encodedKey) } else { this.activeConnections.set(encodedKey, count) } }) // Connection handling using custom connection piper function this.connection = libNet.connPiper( c, () => { this.logger.log({ type: 0, msg: `Connecting to local TCP: ${args.host}:${args.port}` }) return net.connect({ port: +args.port, host: args.host, allowHalfOpen: true }) }, { isServer: true, compress: false, logger: this.logger }, this.stats ) this.logger.log({ type: 0, msg: 'TCP connection piped' }) } // Handle UDP connections handleUDP (c, args) { this.logger.log({ type: 0, msg: 'Handling UDP connection' }) const encodedKey = z32.encode(c.remotePublicKey) c.on('close', () => { let count = this.activeConnections.get(encodedKey) || 1 count-- if (count <= 0) { this.logger.log({ type: 0, msg: `Disconnected from ${encodedKey}` }) this.activeConnections.delete(encodedKey) } else { this.activeConnections.set(encodedKey, count) } }) this.connection = libNet.udpPiper( c, () => { this.logger.log({ type: 0, msg: `Connecting to local UDP: ${args.host}:${args.port}` }) return libNet.udpConnect({ port: +args.port, host: args.host }) }, { isServer: true, compress: false, logger: this.logger }, this.stats ) this.logger.log({ type: 0, msg: 'UDP connection piped' }) } // Return the public/connection key get key () { if (this.secure) { return z32.encode(this.seed) } else { return z32.encode(this.keyPair.publicKey) } } // resume functionality async resume () { this.logger.log({ type: 1, msg: 'Resuming server' }) await this.dht.resume() this.state = 'listening' this.logger.log({ type: 1, msg: 'Server resumed' }) } async pause () { this.logger.log({ type: 1, msg: 'Pausing server' }) await this.dht.suspend() this.state = 'paused' this.logger.log({ type: 1, msg: 'Server paused' }) } // destroy the dht instance and free up resources async destroy () { this.logger.log({ type: 1, msg: 'Destroying server' }) if (this.dht) await this.dht.destroy() this.dht = null if (this.server) this.server = null if (this.connection) this.connection = null this.state = 'destroyed' this.logger.log({ type: 1, msg: 'Server destroyed' }) } // put a mutable record on the dht, can be retrieved by any client using the keypair, max limit is 1KB async put (data, opts = {}) { this.logger.log({ type: 0, msg: `Putting DHT record: ${data}` }) data = b4a.isBuffer(data) ? data : Buffer.from(data) if (opts.seq) { await this.dht.mutablePut(this.keyPair, data, opts) this.logger.log({ type: 0, msg: `DHT put completed with seq: ${opts.seq}` }) return opts.seq } this.logger.log({ type: 0, msg: 'Checking for existing DHT record' }) const oldRecord = await this.get({ latest: true }) if (!oldRecord) { this.logger.log({ type: 0, msg: 'No existing DHT record found, creating new' }) const { seq } = await this.dht.mutablePut(this.keyPair, data, opts) this.logger.log({ type: 0, msg: `DHT put (new) completed with seq: ${seq}` }) return seq } else if (b4a.toString(oldRecord.value) === b4a.toString(data)) { this.logger.log({ type: 0, msg: `DHT put skipped (unchanged), seq: ${oldRecord.seq}` }) return oldRecord.seq } else { this.logger.log({ type: 0, msg: 'Existing DHT record found, updating' }) opts.seq = oldRecord.seq + 1 await this.dht.mutablePut(this.keyPair, data, opts) this.logger.log({ type: 0, msg: `DHT put (updated) completed with seq: ${opts.seq}` }) return opts.seq } } // get mutable record from dht async get (opts = {}) { const record = await this.dht.mutableGet(this.keyPair.publicKey, opts) if (record) { const value = b4a.toString(record.value) this.logger.log({ type: 0, msg: `Existing DHT record found: seq=${record.seq}, value=${value}` }) return { seq: record.seq, value } } return null } // return information about the server get info () { return { type: 'server', state: this.state, secure: this.secure, port: this.args.port, host: this.args.host, protocol: this.args.udp ? 'udp' : 'tcp', seed: this.args.seed, key: this.key, publicKey: z32.encode(this.keyPair.publicKey) } } } module.exports = HolesailServer