133 lines
3.4 KiB
JavaScript
133 lines
3.4 KiB
JavaScript
/**
|
|
* Holesail-style HyperDHT TCP tunnel for the local REST API.
|
|
*
|
|
* Enable: PEARDATA_REST_TUNNEL=1
|
|
* Optional: PEARDATA_REST_TUNNEL_SEED=<64 hex> for a stable public key
|
|
*
|
|
* Peers connect with HyperDHT to the tunnel public key; each connection is
|
|
* proxied to PEARDATA_REST_HOST:PEARDATA_REST_PORT (default 127.0.0.1:18888).
|
|
*
|
|
* Uses existing `hyperdht` — no holesail package dependency (Bare-friendly).
|
|
*/
|
|
import net from 'net'
|
|
import DHT from 'hyperdht'
|
|
import b4a from 'b4a'
|
|
import logger from '../utils/logger.js'
|
|
|
|
const log = logger.child('rest-tunnel')
|
|
|
|
/** @type {import('hyperdht')|null} */
|
|
let dht = null
|
|
/** @type {any} */
|
|
let server = null
|
|
/** @type {string|null} */
|
|
let publicKeyHex = null
|
|
|
|
export function isRestTunnelEnabled() {
|
|
const v = process.env.PEARDATA_REST_TUNNEL
|
|
return v === '1' || v === 'on' || v === 'true'
|
|
}
|
|
|
|
function tunnelKeyPair() {
|
|
const seedHex = process.env.PEARDATA_REST_TUNNEL_SEED
|
|
if (seedHex && /^[0-9a-fA-F]{64}$/.test(seedHex)) {
|
|
return DHT.keyPair(b4a.from(seedHex, 'hex'))
|
|
}
|
|
return DHT.keyPair()
|
|
}
|
|
|
|
/**
|
|
* @returns {Promise<{ publicKeyHex: string, target: string }|null>}
|
|
*/
|
|
export async function startRestTunnel() {
|
|
if (!isRestTunnelEnabled()) {
|
|
log.info('REST tunnel disabled (set PEARDATA_REST_TUNNEL=1)')
|
|
return null
|
|
}
|
|
if (server) {
|
|
return {
|
|
publicKeyHex,
|
|
target: `${process.env.PEARDATA_REST_HOST || '127.0.0.1'}:${Number(process.env.PEARDATA_REST_PORT) || 18888}`,
|
|
}
|
|
}
|
|
|
|
const host = process.env.PEARDATA_REST_HOST || '127.0.0.1'
|
|
const port = Number(process.env.PEARDATA_REST_PORT) || 18888
|
|
const keyPair = tunnelKeyPair()
|
|
publicKeyHex = b4a.toString(keyPair.publicKey, 'hex')
|
|
|
|
dht = new DHT()
|
|
server = dht.createServer()
|
|
|
|
server.on('connection', (socket) => {
|
|
const remote = socket.remotePublicKey
|
|
? b4a.toString(socket.remotePublicKey, 'hex').slice(0, 12)
|
|
: '?'
|
|
log.info('Tunnel peer connected', { remote })
|
|
const local = net.connect({ host, port })
|
|
const cleanup = () => {
|
|
try {
|
|
socket.destroy()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
try {
|
|
local.destroy()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
socket.on('error', cleanup)
|
|
local.on('error', cleanup)
|
|
socket.on('close', cleanup)
|
|
local.on('close', cleanup)
|
|
socket.pipe(local).pipe(socket)
|
|
})
|
|
|
|
await server.listen(keyPair)
|
|
log.info('REST tunnel listening on HyperDHT', {
|
|
publicKeyHex,
|
|
target: `${host}:${port}`,
|
|
tip: `Dial HyperDHT ${publicKeyHex} → localhost REST`,
|
|
})
|
|
return { publicKeyHex, target: `${host}:${port}` }
|
|
}
|
|
|
|
export function getRestTunnelInfo() {
|
|
if (!publicKeyHex) return { enabled: isRestTunnelEnabled(), active: false }
|
|
return {
|
|
enabled: true,
|
|
active: Boolean(server),
|
|
publicKeyHex,
|
|
target: `${process.env.PEARDATA_REST_HOST || '127.0.0.1'}:${Number(process.env.PEARDATA_REST_PORT) || 18888}`,
|
|
seedConfigured: Boolean(process.env.PEARDATA_REST_TUNNEL_SEED),
|
|
}
|
|
}
|
|
|
|
export async function stopRestTunnel() {
|
|
if (server) {
|
|
try {
|
|
await server.close()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
server = null
|
|
}
|
|
if (dht) {
|
|
try {
|
|
await dht.destroy()
|
|
} catch {
|
|
// ignore
|
|
}
|
|
dht = null
|
|
}
|
|
publicKeyHex = null
|
|
log.info('REST tunnel stopped')
|
|
}
|
|
|
|
/** Exported for tests — stable key from seed */
|
|
export function publicKeyFromTunnelSeed(seedHex) {
|
|
const kp = DHT.keyPair(b4a.from(seedHex, 'hex'))
|
|
return b4a.toString(kp.publicKey, 'hex')
|
|
}
|