/** * Holesail server tunnel management. * Exposes a local TCP/UDP port to the Holesail P2P network (server mode). */ let Holesail = null; try { Holesail = require('holesail'); } catch (e) { if (process.stderr) process.stderr.write('[holesail-manager] Holesail not installed: ' + e.message + '\n'); } const DEBUG = process.env.HOLESAIL_DEBUG === '1' || process.env.HOLESAIL_DEBUG === 'true'; function debugLog(...args) { if (!DEBUG) return; const msg = '[holesail-manager:debug] ' + args.map(a => typeof a === 'object' ? JSON.stringify(a) : String(a)).join(' '); if (process.stderr) process.stderr.write(msg + '\n'); } const servers = new Map(); // serverId -> { holesail, port, host, secure, udp, label, url, createdAt } let nextServerId = 0; let _saveState = null; let _getReadyTimeoutMs = null; function init(saveStateFn, getReadyTimeoutMsFn) { _saveState = saveStateFn; _getReadyTimeoutMs = getReadyTimeoutMsFn; } function applyLoaded(loadedNextServerId) { if (loadedNextServerId > nextServerId) nextServerId = loadedNextServerId; } function parseServerIdNum(serverId) { if (!serverId || typeof serverId !== 'string') return 0; const m = serverId.match(/^server_(\d+)$/); return m ? Math.max(0, parseInt(m[1], 10)) : 0; } function generateServerId() { nextServerId += 1; return 'server_' + nextServerId; } function useServerId(givenId) { if (givenId && typeof givenId === 'string' && givenId.startsWith('server_')) { const n = parseServerIdNum(givenId); if (n >= nextServerId) nextServerId = n + 1; return givenId; } return generateServerId(); } function readyWithTimeout(hs, label) { const ms = _getReadyTimeoutMs ? _getReadyTimeoutMs() : 0; if (!ms || ms <= 0) return hs.ready(); return new Promise((resolve, reject) => { const t = setTimeout(() => reject(new Error('Tunnel ready timeout after ' + ms + 'ms for ' + label)), ms); hs.ready().then(() => { clearTimeout(t); resolve(); }, (e) => { clearTimeout(t); reject(e); }); }); } async function startServer(payload) { if (!Holesail) return { ok: false, error: 'Holesail module not installed' }; const port = payload.port || 3000; const host = payload.host || '127.0.0.1'; const secure = payload.secure !== false; const udp = payload.udp === true; const label = (payload.label || '').trim(); const serverId = useServerId(payload.serverId); debugLog('startServer: serverId=', serverId, 'port=', port, 'host=', host, 'secure=', secure, 'udp=', udp, 'label=', label); if (servers.has(serverId)) { debugLog('startServer: serverId already in use'); return { ok: false, error: 'Server id already in use' }; } try { const hs = new Holesail({ server: true, port, host, secure, udp }); await readyWithTimeout(hs, 'server:' + serverId); const url = hs.info.url; servers.set(serverId, { holesail: hs, port: hs.info.port, host: hs.info.host, secure, udp, label, url, createdAt: Date.now() }); if (_saveState) _saveState(); debugLog('startServer: ok serverId=', serverId, 'url=', url); return { ok: true, serverId, url, port: hs.info.port, host: hs.info.host, secure, udp, label }; } catch (e) { debugLog('startServer: error', e.message); return { ok: false, error: e.message }; } } async function stopServer(payload) { const serverId = payload.serverId; debugLog('stopServer: serverId=', serverId); const entry = servers.get(serverId); if (!entry) { debugLog('stopServer: not found'); return { ok: false, error: 'Server not found' }; } try { await entry.holesail.close(); } catch (_) {} servers.delete(serverId); if (_saveState) _saveState(); return { ok: true }; } function getServers() { const list = []; for (const [id, s] of servers) { list.push({ id, serverId: id, port: s.port, host: s.host, url: s.url, secure: s.secure, udp: s.udp || false, label: s.label || '', createdAt: s.createdAt }); } return list; } function getNextServerId() { return nextServerId; } async function cleanupServers() { for (const [, s] of servers) { try { await s.holesail.close(); } catch (_) {} } servers.clear(); } module.exports = { init, applyLoaded, startServer, stopServer, getServers, getNextServerId, cleanupServers };