/** * Central message dispatcher for the native host. * Receives all messages from the browser extension and routes them to the * appropriate manager. Also wires up all managers and starts the proxy/tunnel * restore sequence on first require. */ const { STORAGE_PATH } = require('./paths.js'); const { log, debugLog } = require('./logger.js'); const { initStartup, getProxiesReadyPromise, getTunnelsRestoredPromise, setTunnelsRestoredPromise, restorePersistedTunnels } = require('./startup.js'); const holesailManager = require('../holesail-manager.js'); holesailManager.setStoragePath(STORAGE_PATH); const certificateAuthority = require('../certificate-authority.js'); const httpsProxy = require('../https-proxy.js'); const connectProxy = require('../connect-proxy.js'); const sshManager = require('../ssh-manager.js'); const dashboardServer = require('../dashboard-server.js'); const backupManager = require('../backup-manager.js'); backupManager.setStoragePath(STORAGE_PATH); backupManager.setCertsPath(certificateAuthority.getCertsDir()); const rdpManager = require('../rdp-manager.js'); httpsProxy.setHostnameResolver((hostname) => holesailManager.getLocalBackend(hostname)); initStartup(holesailManager, certificateAuthority, httpsProxy, connectProxy); // Scheduled auto-backup: fires every backupIntervalHours when > 0. let _scheduledBackupTimer = null; function scheduleNextAutoBackup() { if (_scheduledBackupTimer) { clearTimeout(_scheduledBackupTimer); _scheduledBackupTimer = null; } const settings = holesailManager.getSettings(); const hours = typeof settings.backupIntervalHours === 'number' ? settings.backupIntervalHours : 0; if (hours <= 0) return; const ms = hours * 60 * 60 * 1000; _scheduledBackupTimer = setTimeout(async () => { _scheduledBackupTimer = null; log('Scheduled auto-backup starting (interval=' + hours + 'h)'); const result = await backupManager.createBackup().catch((e) => ({ ok: false, error: e.message })); if (result.ok) { const retention = (holesailManager.getSettings().backupRetention) || backupManager.DEFAULT_RETENTION; backupManager.pruneOldBackups(retention); log('Scheduled auto-backup complete:', result.filename); } else { log('Scheduled auto-backup failed:', result.error); } scheduleNextAutoBackup(); }, ms); } // Start the auto-backup schedule once proxies are ready. const _proxiesReady = getProxiesReadyPromise(); if (_proxiesReady) { _proxiesReady.then(() => scheduleNextAutoBackup()).catch(() => {}); } else { scheduleNextAutoBackup(); } // Module-level send reference — updated once on first message so the event emitter // always routes to the current active port without being re-set on every message. let _send = null; holesailManager.setEventEmitter((event, eventPayload) => { if (!_send) return; debugLog('emit event:', event, eventPayload && Object.keys(eventPayload)); _send({ type: 'event', event, payload: eventPayload }); }); /** * @param {Function} send - messenger.send(msg) * @param {object} msg - { id, type, payload } */ async function handleMessageAsync(send, msg) { _send = send; const { id, type, payload = {} } = msg; function reply(result) { send({ id, type: 'response', payload: result }); } try { debugLog('handleMessage: id=', id, 'type=', type, 'payloadKeys=', payload && Object.keys(payload)); const proxiesReadyPromise = getProxiesReadyPromise(); const tunnelsRestoredPromise = getTunnelsRestoredPromise(); switch (type) { case 'getState': { if (proxiesReadyPromise) await proxiesReadyPromise; if (tunnelsRestoredPromise) { let _fallbackTimer; await Promise.race([ tunnelsRestoredPromise.finally(() => clearTimeout(_fallbackTimer)), new Promise((r) => { _fallbackTimer = setTimeout(r, 15000); }) ]); } const servers = holesailManager.getServers(); const virtualHosts = holesailManager.getVirtualHosts(); const serviceTunnels = holesailManager.getServiceTunnels(); const proxyPort = holesailManager.getProxyPort(); const connectProxyPort = connectProxy.getPort(); const settings = holesailManager.getSettings(); const sshConnections = holesailManager.getSshConnections(); const rdpConnections = holesailManager.getRdpConnections(); debugLog('getState: servers=', servers.length, 'virtualHosts=', virtualHosts.length, 'serviceTunnels=', serviceTunnels.length, 'proxyPort=', proxyPort, 'connectProxyPort=', connectProxyPort); const caInstalled = await new Promise((resolve) => { certificateAuthority.isRootCAInstalled((installed) => resolve(!!installed)); }); reply({ ok: true, servers, virtualHosts, serviceTunnels, proxyPort, connectProxyPort: connectProxyPort ?? null, caInstalled, settings, sshConnections, rdpConnections, trafficStats: httpsProxy.getTrafficStats() }); break; } case 'getSettings': { reply({ ok: true, settings: holesailManager.getSettings() }); break; } case 'updateSettings': { const { requiresRestart } = holesailManager.updateSettings(payload); debugLog('updateSettings: applied', JSON.stringify(payload), 'requiresRestart=', requiresRestart); scheduleNextAutoBackup(); reply({ ok: true, settings: holesailManager.getSettings(), requiresRestart }); break; } case 'getSshConnections': { reply({ ok: true, sshConnections: holesailManager.getSshConnections() }); break; } case 'setSshConnections': { const list = Array.isArray(payload.connections) ? payload.connections : []; holesailManager.setSshConnections(list); debugLog('setSshConnections: count=', list.length); reply({ ok: true }); break; } case 'getRdpConnections': { reply({ ok: true, rdpConnections: holesailManager.getRdpConnections() }); break; } case 'setRdpConnections': { const list = Array.isArray(payload.connections) ? payload.connections : []; holesailManager.setRdpConnections(list); debugLog('setRdpConnections: count=', list.length); reply({ ok: true }); break; } case 'startServer': { debugLog('startServer: payload=', JSON.stringify(payload)); const result = await holesailManager.startServer(payload); debugLog('startServer: result=', JSON.stringify(result)); reply(result); break; } case 'stopServer': { debugLog('stopServer: payload=', JSON.stringify(payload)); const result = await holesailManager.stopServer(payload); debugLog('stopServer: result=', JSON.stringify(result)); reply(result); break; } case 'startServiceTunnel': { debugLog('startServiceTunnel: payload=', JSON.stringify(payload)); const result = await holesailManager.startServiceTunnel(payload); debugLog('startServiceTunnel: result=', JSON.stringify(result)); reply(result); break; } case 'updateServiceTunnel': { debugLog('updateServiceTunnel: payload=', JSON.stringify(payload)); await holesailManager.stopServiceTunnel({ tunnelId: payload.tunnelId }).catch(() => {}); const result = await holesailManager.startServiceTunnel(payload); debugLog('updateServiceTunnel: result=', JSON.stringify(result)); reply(result); break; } case 'stopServiceTunnel': { debugLog('stopServiceTunnel: payload=', JSON.stringify(payload)); const result = await holesailManager.stopServiceTunnel(payload); debugLog('stopServiceTunnel: result=', JSON.stringify(result)); reply(result); break; } case 'getServiceTunnels': { reply({ ok: true, tunnels: holesailManager.getServiceTunnels() }); break; } case 'getVirtualHosts': { const hosts = holesailManager.getVirtualHosts(); debugLog('getVirtualHosts: count=', hosts.length); reply({ ok: true, hosts }); break; } case 'setVirtualHost': { debugLog('setVirtualHost: payload=', JSON.stringify(payload)); const result = await holesailManager.setVirtualHost(payload); debugLog('setVirtualHost: result=', JSON.stringify(result)); reply(result); break; } case 'removeVirtualHost': { debugLog('removeVirtualHost: payload=', JSON.stringify(payload)); const result = await holesailManager.removeVirtualHost(payload); debugLog('removeVirtualHost: result=', JSON.stringify(result)); reply(result); break; } case 'getProxyPort': { const port = holesailManager.getProxyPort(); debugLog('getProxyPort: port=', port); reply({ ok: true, port }); break; } case 'lookup': { debugLog('lookup: payload=', JSON.stringify(payload)); const result = await holesailManager.lookup(payload); debugLog('lookup: result=', JSON.stringify(result)); reply(result); break; } case 'installRootCA': { certificateAuthority.installRootCA((err) => { if (err) reply({ ok: false, error: err.message }); else reply({ ok: true }); }); break; } case 'startSshSession': { debugLog('startSshSession: payload=', JSON.stringify({ ...payload, hsUrl: payload.hsUrl ? payload.hsUrl.slice(0, 20) + '...' : null })); const result = await sshManager.startSession(payload); debugLog('startSshSession: result=', JSON.stringify({ ok: result.ok, sessionId: result.sessionId, wsPort: result.wsPort })); reply(result); break; } case 'stopSshSession': { debugLog('stopSshSession: payload=', JSON.stringify(payload)); const result = await sshManager.stopSession(payload); debugLog('stopSshSession: result=', JSON.stringify(result)); reply(result); break; } case 'resizeSshSession': { sshManager.resizeSession(payload); reply({ ok: true }); break; } case 'getSshSessions': { reply({ ok: true, sessions: sshManager.getSessions() }); break; } case 'startRdpSession': { debugLog('startRdpSession: type=', payload.type, 'label=', payload.label); const result = await rdpManager.startSession(payload); debugLog('startRdpSession: result=', JSON.stringify({ ok: result.ok, sessionId: result.sessionId, wsPort: result.wsPort })); reply(result); break; } case 'stopRdpSession': { debugLog('stopRdpSession: sessionId=', payload.sessionId); const result = await rdpManager.stopSession(payload); reply(result); break; } case 'getRdpSessions': { reply({ ok: true, sessions: rdpManager.getSessions() }); break; } case 'createBackup': { const backupResult = await backupManager.createBackup(); if (backupResult.ok) { const retention = (holesailManager.getSettings().backupRetention) || backupManager.DEFAULT_RETENTION; backupManager.pruneOldBackups(retention); } reply(backupResult); break; } case 'listBackups': { reply(backupManager.listBackups()); break; } case 'restoreBackup': { const restoreResult = await backupManager.restoreBackup(payload.filename); if (restoreResult.ok) { await holesailManager.cleanup().catch(() => {}); const restored = holesailManager.restorePersistedState(); setTunnelsRestoredPromise( restorePersistedTunnels(holesailManager, restored).catch((e) => log('Post-restore tunnel start failed:', e.message)) ); } reply(restoreResult); break; } case 'deleteBackup': { reply(backupManager.deleteBackup(payload.filename)); break; } case 'pingTunnel': { // Measure TCP connect latency to a local tunnel port. // payload: { host, port } const pingHost = payload.host || '127.0.0.1'; const pingPort = typeof payload.port === 'number' ? payload.port : null; if (!pingPort) { reply({ ok: false, error: 'port required' }); break; } const tcp = require('bare-tcp'); const t0 = Date.now(); const sock = tcp.connect(pingPort, pingHost); const pingTimeout = setTimeout(() => { try { sock.destroy(); } catch (_) {} reply({ ok: false, error: 'timeout' }); }, 3000); sock.on('connect', () => { clearTimeout(pingTimeout); const latencyMs = Date.now() - t0; try { sock.destroy(); } catch (_) {} reply({ ok: true, latencyMs }); }); sock.on('error', (err) => { clearTimeout(pingTimeout); try { sock.destroy(); } catch (_) {} reply({ ok: false, error: err.message }); }); break; } default: reply({ ok: false, error: `Unknown command: ${type}` }); } } catch (err) { reply({ ok: false, error: err.message }); if (process.stderr) { process.stderr.write(`[holesail-browser-host] ${err.stack}\n`); } } } function cleanup() { if (_scheduledBackupTimer) { clearTimeout(_scheduledBackupTimer); _scheduledBackupTimer = null; } dashboardServer.stop(); httpsProxy.stop(() => {}); connectProxy.stop(() => {}); sshManager.cleanup(); rdpManager.cleanup().catch(() => {}); holesailManager.cleanup().catch(() => {}); } module.exports = { handleMessage: handleMessageAsync, cleanup };