/** * Proxy startup and persisted tunnel restore. * Starts the HTTPS and CONNECT proxies after loading persisted settings, * then asynchronously restores all tunnels from state.json. */ const { log, debugLog } = require('./logger.js'); const PROXY_PORT = 8443; const CONNECT_PROXY_PORT = 8442; let proxiesReadyPromise = null; let tunnelsRestoredPromise = null; /** * Re-start all tunnels that were persisted in state.json. * Runs after the proxies are ready so tunnels have a working backend to connect through. * Logs per-type success/failure counts but never throws — failures are non-fatal. * @param {object} holesailManager - The holesail manager module. * @param {object} restored - The persisted state object returned by `holesailManager.restorePersistedState()`. * @returns {Promise} */ async function restorePersistedTunnels(holesailManager, restored) { try { debugLog('restorePersistedTunnels: starting'); const { servers: savedServers, virtualHosts: savedVhosts, serviceTunnels: savedServiceTunnels } = restored; log('Restore: loaded', savedServers.length, 'servers,', savedVhosts.length, 'virtual hosts,', savedServiceTunnels.length, 'service tunnels'); let serverOk = 0, serverFail = 0; for (const s of savedServers) { const result = await holesailManager.startServer({ serverId: s.id, port: s.port, host: s.host || '127.0.0.1', secure: s.secure !== false, udp: s.udp === true, label: s.label || '' }); if (result.ok) serverOk++; else serverFail++; } let vhostOk = 0, vhostFail = 0; for (const v of savedVhosts) { const result = await holesailManager.setVirtualHost({ hostname: v.hostname, hsUrl: v.hsUrl, useTls: v.useTls === true }); if (result.ok) vhostOk++; else vhostFail++; } let svcOk = 0, svcFail = 0; for (const t of savedServiceTunnels) { const result = await holesailManager.startServiceTunnel({ tunnelId: t.id, label: t.label, hsUrl: t.hsUrl, localPort: t.localPort }); if (result.ok) svcOk++; else svcFail++; } if (savedServers.length || savedVhosts.length || savedServiceTunnels.length) { log('Restored on startup: servers', serverOk + '/' + savedServers.length, 'virtual hosts', vhostOk + '/' + savedVhosts.length, 'service tunnels', svcOk + '/' + savedServiceTunnels.length); if (serverFail || vhostFail || svcFail) log('Restore failures: servers', serverFail, 'virtual hosts', vhostFail, 'service tunnels', svcFail); } } catch (e) { log('Restore persisted tunnels failed:', e.message); } } /** * Kick off the async startup sequence: restore settings, start the CA, start both * proxies, then begin restoring persisted tunnels in the background. * Sets `proxiesReadyPromise` which resolves once the proxies are up (or have failed). * Sets `tunnelsRestoredPromise` which resolves once all tunnels have been re-started. * @param {object} holesailManager * @param {object} certificateAuthority * @param {object} httpsProxy * @param {object} connectProxy */ function initStartup(holesailManager, certificateAuthority, httpsProxy, connectProxy) { if (typeof setImmediate !== 'function') return; proxiesReadyPromise = new Promise((resolve) => { setImmediate(async () => { try { const restored = holesailManager.restorePersistedState(); const savedProxyPort = (restored.settings && restored.settings.proxyPort) || PROXY_PORT; const savedConnectPort = (restored.settings && restored.settings.connectProxyPort) || CONNECT_PROXY_PORT; try { await certificateAuthority.caReady; } catch (e) { log('CA generation failed:', e.message, '— HTTPS proxy may not work correctly'); } try { await new Promise((res, rej) => { httpsProxy.start(savedProxyPort, certificateAuthority, (err) => { if (err) return rej(err); res(); }); }); log('HTTPS proxy ready on port', savedProxyPort); } catch (e) { log('HTTPS proxy startup failed:', e.message); } try { await new Promise((res, rej) => { const timeout = setTimeout(() => { rej(new Error('CONNECT proxy listen timed out after 5s')); }, 5000); connectProxy.start(savedConnectPort, savedProxyPort, (err) => { clearTimeout(timeout); if (err) return rej(err); res(); }); }); log('CONNECT proxy ready on port', savedConnectPort); } catch (e) { log('CONNECT proxy startup failed:', e.message); } log('Proxies startup complete: HTTPS', savedProxyPort, 'CONNECT', connectProxy.getPort() ?? 'FAILED'); // Assign tunnelsRestoredPromise BEFORE resolving proxiesReadyPromise so // that any getState call awaiting proxiesReadyPromise immediately sees the // non-null promise rather than the one-microtask window where it is null. tunnelsRestoredPromise = restorePersistedTunnels(holesailManager, restored).catch((e) => log('Restore tunnels failed:', e.message)); } catch (e) { log('Unexpected startup error:', e.message); } finally { resolve(); } }); }); } /** @returns {Promise|null} Resolves when both proxies have started (or failed). */ function getProxiesReadyPromise() { return proxiesReadyPromise; } /** @returns {Promise|null} Resolves when all persisted tunnels have been re-started. */ function getTunnelsRestoredPromise() { return tunnelsRestoredPromise; } /** * Override the tunnels-restored promise (used by message-router to assign it * before proxiesReadyPromise resolves, avoiding a one-microtask null window). * @param {Promise} p */ function setTunnelsRestoredPromise(p) { tunnelsRestoredPromise = p; } module.exports = { initStartup, restorePersistedTunnels, getProxiesReadyPromise, getTunnelsRestoredPromise, setTunnelsRestoredPromise };