/** * Central chrome.runtime.onMessage handler for the background service worker. * Routes messages from content scripts and the dashboard to local handlers or * the native host. Handled actions: * - send Forward a typed payload to the native host (async). * - registerSwarm Track a swarm ID for a tab (local, not forwarded). * - subscribe Subscribe a tab to tunnel lifecycle events. * - unsubscribe Unsubscribe a tab from tunnel lifecycle events. * - registerDashboard Add a tab to the dashboard broadcast set. * - unregisterDashboard Remove a tab from the dashboard broadcast set. * - getState Fetch full extension + native host state (async). * Depends on: state.js, logs.js, proxy.js (getActiveTlds, applyPAC, pacConfirmedActive), * native-messaging.js (send, port, subscribedTabs, dashboardTabs) */ browser.runtime.onMessage.addListener((message, sender, sendResponse) => { let responded = false; function reply(value) { if (responded) return; responded = true; try { sendResponse(value); } catch (_) {} } if (message.target !== 'holesail-native') { reply({ error: 'Unknown target' }); return false; } // Handle registerSwarm locally (legacy) - don't forward to native host if (message.action === 'send' && message.payload?.type === 'registerSwarm') { const swarmId = message.payload?.payload?.swarmId; if (swarmId && sender.tab?.id != null) { if (!tabSwarms.has(sender.tab.id)) { tabSwarms.set(sender.tab.id, new Set()); } tabSwarms.get(sender.tab.id).add(swarmId); const count = swarmRefCount.get(swarmId) || 0; swarmRefCount.set(swarmId, count + 1); log('Registered swarm', swarmId, 'for tab', sender.tab.id, '- refcount:', count + 1); } reply({ ok: true }); return false; } if (message.action === 'send') { send(message.payload) .then((r) => { // After setVirtualHost or removeVirtualHost, refresh virtualHosts and re-apply PAC if (r && r.ok && ( message.payload?.type === 'setVirtualHost' || message.payload?.type === 'removeVirtualHost' )) { send({ type: 'getState', payload: {} }).then((state) => { if (state && state.ok && Array.isArray(state.virtualHosts)) { extensionState.virtualHosts = state.virtualHosts; const newTlds = getActiveTlds(extensionState.virtualHosts); applyPAC(newTlds); if (message.payload?.type === 'setVirtualHost' && message.payload?.payload?.hostname) { const hostname = message.payload.payload.hostname; const parts = hostname.split('.'); if (parts.length >= 3) { const baseDomain = parts.slice(-2).join('.'); if (baseDomain !== 'hole.sail') { const origin = '*://*.' + baseDomain + '/*'; browser.permissions.request({ origins: [origin] }, (granted) => { log('permissions.request for', origin, ':', granted ? 'granted' : 'denied'); }); } } } } }).catch(() => {}); } reply(r); }) .catch((err) => reply({ error: err.message })); return true; // async response } if (message.action === 'subscribe' && sender.tab && sender.tab.id != null) { subscribedTabs.add(sender.tab.id); reply({ ok: true }); return false; } if (message.action === 'unsubscribe' && sender.tab && sender.tab.id != null) { subscribedTabs.delete(sender.tab.id); reply({ ok: true }); return false; } if (message.action === 'registerDashboard' && sender.tab && sender.tab.id != null) { dashboardTabs.add(sender.tab.id); log('Dashboard registered for tab', sender.tab.id); reply({ ok: true, logs: logs.slice(0) }); return false; } if (message.action === 'unregisterDashboard' && sender.tab && sender.tab.id != null) { dashboardTabs.delete(sender.tab.id); reply({ ok: true }); return false; } if (message.action === 'getState') { debugLog('getState action: port=', !!port); updateExtensionState(); if (port) { send({ type: 'getState', payload: {} }) .then((payload) => { debugLog('getState response: ok=', payload && payload.ok, 'servers=', payload && payload.servers && payload.servers.length, 'virtualHosts=', payload && payload.virtualHosts && payload.virtualHosts.length, 'proxyPort=', payload && payload.proxyPort, 'connectProxyPort=', payload && payload.connectProxyPort); if (payload && payload.ok === true && (payload.servers || payload.virtualHosts || payload.proxyPort != null || payload.connectProxyPort != null)) { extensionState.proxyPort = payload.proxyPort ?? null; extensionState.connectProxyPort = payload.connectProxyPort ?? null; extensionState.virtualHosts = payload.virtualHosts || []; if (payload.settings && typeof payload.settings.notifyOnDisconnect === 'boolean') { notifyOnDisconnect = payload.settings.notifyOnDisconnect; } if (payload.settings && typeof payload.settings.notifyOnTunnelError === 'boolean') { notifyOnTunnelError = payload.settings.notifyOnTunnelError; } if (!pacConfirmedActive) { log('getState: PAC not confirmed active, re-applying'); applyPAC(getActiveTlds(extensionState.virtualHosts)); } reply({ ok: true, state: { hostConnected: true, servers: payload.servers || [], virtualHosts: payload.virtualHosts || [], serviceTunnels: payload.serviceTunnels || [], proxyPort: payload.proxyPort ?? null, connectProxyPort: payload.connectProxyPort ?? null, caInstalled: payload.caInstalled ?? false, settings: payload.settings || {}, sshConnections: payload.sshConnections || [], rdpConnections: payload.rdpConnections || [], trafficStats: payload.trafficStats || null, stats: { ...extensionState.stats, ...payload.stats } } }); } else { reply({ ok: true, state: { hostConnected: true, servers: extensionState.servers || [], virtualHosts: extensionState.virtualHosts || [], serviceTunnels: [], sshConnections: [], rdpConnections: [], proxyPort: null, connectProxyPort: null, caInstalled: false, stats: extensionState.stats } }); } }) .catch((err) => { debugLog('getState send failed:', err && err.message); reply({ ok: true, state: { hostConnected: extensionState.hostConnected, servers: extensionState.servers || [], virtualHosts: extensionState.virtualHosts || [], serviceTunnels: [], sshConnections: [], rdpConnections: [], proxyPort: null, connectProxyPort: null, caInstalled: false, stats: extensionState.stats } }); }); } else { debugLog('getState: no port, replying hostConnected=false'); reply({ ok: true, state: { hostConnected: false, servers: [], virtualHosts: [], serviceTunnels: [], sshConnections: [], rdpConnections: [], proxyPort: null, connectProxyPort: null, caInstalled: false, stats: extensionState.stats } }); } return true; } reply({ error: 'Unknown action' }); return false; });