CI / Build & Test (push) Successful in 2m54s
Add docs/CONTRIBUTING.md covering the build system, dev workflow, all npm scripts, how to add new native host message types, code style, and debugging guidance. Add CHANGELOG.md at the project root documenting all features and fixes across the 1.0.0 release. Add JSDoc (@param, @returns) to all previously undocumented exported functions across 35 JS files: - native-host/holesail-manager/ (index, virtual-hosts, service-tunnels, servers, port-allocator) - native-host top-level managers (startup, connect-proxy, https-proxy, certificate-authority, ssh-manager, rdp-manager) - extension/background/ (logs, native-messaging, proxy, message-router) - extension/dashboard/core/ (utils, navigation, init) - extension/dashboard/ui/ (modal, toast, state-tag) - extension/dashboard/pages/ (all 10 page files) - extension/dashboard/refresh.js, events.js - extension/dashboard/data/hostname-validator.js - scripts/ (build-host, run-install)
161 lines
6.2 KiB
JavaScript
161 lines
6.2 KiB
JavaScript
/**
|
|
* 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;
|
|
|
|
/**
|
|
* Inject shared dependencies from the parent holesail-manager.
|
|
* Must be called once before any other function in this module.
|
|
* @param {Function} saveStateFn - Callback that persists the current state to disk.
|
|
* @param {Function} getReadyTimeoutMsFn - Returns the configured ready-timeout in ms (0 = no timeout).
|
|
*/
|
|
function init(saveStateFn, getReadyTimeoutMsFn) {
|
|
_saveState = saveStateFn;
|
|
_getReadyTimeoutMs = getReadyTimeoutMsFn;
|
|
}
|
|
|
|
/**
|
|
* Advance the server ID counter to be at least as high as the persisted value,
|
|
* ensuring IDs generated after a restart do not collide with existing ones.
|
|
* @param {number} loadedNextServerId - The `nextServerId` value read from state.json.
|
|
*/
|
|
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); });
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Start a Holesail server tunnel, exposing a local port to the P2P network.
|
|
* @param {object} payload
|
|
* @param {number} [payload.port=3000] - Local TCP port to expose.
|
|
* @param {string} [payload.host='127.0.0.1'] - Local host to bind.
|
|
* @param {boolean} [payload.secure=true] - Whether to use an encrypted (private) tunnel.
|
|
* @param {boolean} [payload.udp=false] - Whether to use UDP mode.
|
|
* @param {string} [payload.label=''] - Human-readable label for the UI.
|
|
* @param {string} [payload.serverId] - Optional explicit ID; auto-generated if omitted.
|
|
* @returns {Promise<{ok: boolean, serverId?: string, url?: string, port?: number, host?: string, secure?: boolean, udp?: boolean, label?: string, error?: string}>}
|
|
*/
|
|
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 });
|
|
if (typeof hs.on === 'function') {
|
|
hs.on('error', (err) => {
|
|
if (process.stderr) process.stderr.write('[holesail-manager] server tunnel error ' + serverId + ': ' + (err && err.message) + '\n');
|
|
});
|
|
}
|
|
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 };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop a running server tunnel and remove it from state.
|
|
* @param {object} payload
|
|
* @param {string} payload.serverId - ID of the server tunnel to stop.
|
|
* @returns {Promise<{ok: boolean, error?: string}>}
|
|
*/
|
|
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 };
|
|
}
|
|
|
|
/**
|
|
* Return a snapshot of all active server tunnels.
|
|
* @returns {Array<{id: string, serverId: string, port: number, host: string, url: string, secure: boolean, udp: boolean, label: string, createdAt: number}>}
|
|
*/
|
|
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; }
|
|
|
|
/**
|
|
* Close all active server tunnels. Called during native host shutdown.
|
|
* @returns {Promise<void>}
|
|
*/
|
|
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 };
|