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)
202 lines
7.9 KiB
JavaScript
202 lines
7.9 KiB
JavaScript
/**
|
|
* 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;
|
|
});
|