/** * PAC (Proxy Auto-Config) proxy management. * Builds and applies a PAC script that routes virtual host traffic through the * CONNECT proxy (127.0.0.1:8442). Re-applies automatically if another extension * or system setting overrides the proxy configuration. * Depends on: state.js (extensionState, pacConfirmedActive, DEFAULT_CONNECT_PROXY_PORT, DEFAULT_PROXY_PORT) * Depends on: logs.js (log, debugLog) * * In Firefox, proxy.settings requires "Allow in Private Windows" for the extension. * When that permission is missing we skip PAC set/clear and notify once. */ let pacFirefoxPrivateBrowsingBlocked = false; let pacFirefoxPrivateBrowsingNotified = false; /** True when running in Firefox (uses proxyType/autoConfigUrl; Chrome uses mode/pacScript). */ const isFirefox = typeof (browser.runtime && browser.runtime.getBrowserInfo) === 'function'; /** Extract unique two-label base domains from virtualHosts array, always including hole.sail */ function getActiveTlds(virtualHosts) { const seen = new Set(['hole.sail']); for (const v of (virtualHosts || [])) { if (v && v.hostname) { const parts = v.hostname.split('.'); if (parts.length >= 3) seen.add(parts.slice(-2).join('.')); } } return Array.from(seen).map(b => '.' + b); } /** * Build and apply a PAC script routing all virtual host TLDs through the CONNECT proxy. * If `tlds` is omitted, derives the active TLD list from `extensionState.virtualHosts`. * @param {string[]} [tlds] - Array of TLD suffixes (e.g. `['.hole.sail', '.hs']`). */ function applyPAC(tlds) { if (!browser.proxy || !browser.proxy.settings) { log('applyPAC: SKIPPED - no browser.proxy.settings API'); return; } if (pacFirefoxPrivateBrowsingBlocked) { debugLog('applyPAC: SKIPPED - Firefox private browsing permission required (user must enable in about:addons)'); return; } const proxyPort = extensionState.connectProxyPort ?? DEFAULT_CONNECT_PROXY_PORT ?? extensionState.proxyPort ?? DEFAULT_PROXY_PORT; const activeTlds = tlds || getActiveTlds(extensionState.virtualHosts); log('applyPAC: setting PAC port=', proxyPort, 'tlds=', activeTlds); const clauses = activeTlds.map(t => ` if (dnsDomainIs(host, "${t}")) return "PROXY 127.0.0.1:${proxyPort}";` ).join('\n'); const pacData = `function FindProxyForURL(url, host) {\n${clauses}\n return "DIRECT";\n}`; const payload = isFirefox ? { value: { proxyType: 'autoConfig', autoConfigUrl: 'data:application/javascript;base64,' + btoa(pacData) }, scope: 'regular' } : { value: { mode: 'pac_script', pacScript: { data: pacData, mandatory: false } }, scope: 'regular' }; browser.proxy.settings.set( payload, () => { if (browser.runtime.lastError) { const msg = browser.runtime.lastError.message || ''; if (msg.includes('private browsing') || msg.includes('private window')) { pacFirefoxPrivateBrowsingBlocked = true; pacConfirmedActive = false; if (!pacFirefoxPrivateBrowsingNotified) { pacFirefoxPrivateBrowsingNotified = true; log('applyPAC: Firefox requires "Allow in Private Windows" for proxy. Enable it in about:addons for this extension.'); if (browser.notifications && browser.notifications.create) { browser.notifications.create('holesail-pac-private-browsing', { type: 'basic', title: 'Holesail Browser', message: 'To use the proxy in Firefox, enable "Allow in Private Windows" for this extension in about:addons.', iconUrl: browser.runtime.getURL ? browser.runtime.getURL('icons/48.png') : undefined, }).catch(() => {}); } } return; } log('applyPAC: ERROR:', msg); pacConfirmedActive = false; return; } browser.proxy.settings.get({}, (details) => { if (browser.runtime.lastError) { log('applyPAC: get ERROR:', browser.runtime.lastError.message); return; } const val = details && details.value; const mode = val && val.mode; const proxyType = val && val.proxyType; const loc = details && details.levelOfControl; log('applyPAC: mode=', mode, 'proxyType=', proxyType, 'levelOfControl=', loc); const isPacActive = (mode === 'pac_script') || (proxyType === 'autoConfig'); if (loc === 'controlled_by_this_extension' && isPacActive) { pacConfirmedActive = true; log('applyPAC: ACTIVE'); } else { pacConfirmedActive = false; log('applyPAC: WARNING not active - mode=', mode, 'proxyType=', proxyType, 'loc=', loc); } }); } ); } // Re-apply PAC whenever proxy settings change (e.g. another extension or system override) if (browser.proxy && browser.proxy.settings && browser.proxy.settings.onChange) { browser.proxy.settings.onChange.addListener((details) => { const loc = details && details.levelOfControl; const val = details && details.value; const mode = val && val.mode; const proxyType = val && val.proxyType; log('proxy.settings.onChange: levelOfControl=', loc, 'mode=', mode, 'proxyType=', proxyType); if (loc === 'controlled_by_this_extension') { const isPacActive = (mode === 'pac_script') || (proxyType === 'autoConfig'); if (isPacActive) pacConfirmedActive = true; return; } pacConfirmedActive = false; log('proxy settings changed externally - reapplying PAC'); applyPAC(); }); } /** * Clear the PAC proxy setting, reverting to direct connections. * Called when the native host disconnects. */ function clearProxy() { debugLog('clearProxy'); if (pacFirefoxPrivateBrowsingBlocked) return; if (!browser.proxy || !browser.proxy.settings) return; const value = isFirefox ? { proxyType: 'none' } : { mode: 'direct' }; browser.proxy.settings.set({ value, scope: 'regular' }).catch(() => {}); } // Log proxy errors for *.hole.sail to help diagnose issues if (browser.proxy && browser.proxy.onError) { browser.proxy.onError.addListener((details) => { log('Proxy error:', details.error, 'url=', details.url); }); }