UNPKG

holesail-server

Version:

Proxy any server peer-to-peer using Holepunching.

258 lines (243 loc) 8.21 kB
// Importing required modules const HyperDHT = require('hyperdht') // HyperDHT module for DHT functionality 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.refreshInterval = 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) this.refreshInterval = 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.pipeTcpServer( c, { port: args.port, host: args.host }, { isServer: true, compress: false, logger: this.logger }, this.stats ) this.logger.log({ type: 0, msg: 'TCP connection piped' }) } // Handle UDP connections (updated to use framed reliable tunneling) 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.pipeUdpFramedServer( c, { port: args.port, host: args.host }, this.logger, this.stats ) this.logger.log({ type: 0, msg: 'UDP connection framed and 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.refreshInterval) { clearInterval(this.refreshInterval) this.refreshInterval = null this.logger.log({ type: 1, msg: 'Cleared DHT refresh interval' }) } 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 = {}) { if (data == null) { throw new Error('data cannot be undefined') } this.logger.log({ type: 0, msg: `Putting DHT record: ${data}` }) this.logger.log({ type: 0, msg: `Incoming data type: ${typeof data}, value: ${data}` }) data = b4a.isBuffer(data) ? data : Buffer.from(data) this.logger.log({ type: 0, msg: 'Checking for existing DHT record' }) const oldRecord = await this.get({ latest: true }) const putOpts = { ...opts } if (oldRecord) { if (oldRecord.value == null) { this.logger.log({ type: 0, msg: 'oldRecord.value is null or undefined' }) putOpts.seq = oldRecord.seq + 1 } else { const same = b4a.equals(b4a.from(oldRecord.value), data) putOpts.seq = same ? oldRecord.seq : oldRecord.seq + 1 this.logger.log({ type: 0, msg: `Existing record found, putting with seq: ${putOpts.seq} (same: ${same})` }) } } else { this.logger.log({ type: 0, msg: 'No existing DHT record found, creating new' }) } const { seq } = await this.dht.mutablePut(this.keyPair, data, putOpts) this.logger.log({ type: 0, msg: `DHT put completed with seq: ${seq}` }) return 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: 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