CI / Build & Test (push) Successful in 2m51s
Serve the dashboard over a local HTTPS virtual host (my.dash.board) so Chrome treats it as a secure origin and shows the PWA install prompt. - Add native-host/dashboard-server.js: bare-http1 static file server backed by bare-bundle assets in distribution mode, disk fallback in dev - Add setLocalVirtualHost() to virtual-hosts.js; type:local entries are protected from removal and filtered out of saveState persistence - Export setLocalVirtualHost from holesail-manager/index.js - Start dashboard server in startup.js after proxies are ready and register my.dash.board as a local virtual host - Stop dashboard server in message-router.js cleanup() - Update manifest.webmanifest: start_url, id, scope → https://my.dash.board/ - Embed all extension/dashboard/ files as bare-bundle assets in build-distributable.js patchBundle() — no installer copy step needed - Hide checkbox, hs:// URL, Reconnect, and Remove controls for built-in my.dash.board row in the virtual hosts table UI
356 lines
14 KiB
JavaScript
356 lines
14 KiB
JavaScript
/**
|
|
* 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 };
|