'use strict'; // ----- global //const FF = typeof browser !== 'undefined'; // for later let storageArea; // keeping track of sync let bgDisable = false; // Start in disabled mode because it's going to take time to load setings from storage let activeSettings = {mode: 'disabled'}; // ----------------- logger -------------------------------- let logger; function getLog() { return logger; } class Logger { constructor(size = 100, active = false) { this.size = size; this.matchedList = []; this.unmatchedList = []; this.active = active; } clear() { this.matchedList = []; this.unmatchedList = []; } addMatched(item) { this.matchedList.push(item); this.matchedList = this.matchedList.slice(-this.size); // slice to the ending size entries } addUnmatched(item) { this.unmatchedList.push(item); this.unmatchedList = this.unmatchedList.slice(-this.size); // slice to the ending size entries } updateStorage() { this.matchedList = this.matchedList.slice(-this.size); // slice to the ending size entries this.unmatchedList = this.unmatchedList.slice(-this.size); // slice to the ending size entries storageArea.set({logging: {size: this.size, active: this.active} }); } } // ----------------- /logger ------------------------------- // --- registering persistent listener // https://bugzilla.mozilla.org/show_bug.cgi?id=1359693 ...Resolution: --- ? WONTFIX chrome.webRequest.onAuthRequired.addListener(sendAuth, {urls: ['*://*/*']}, ['blocking']); chrome.webRequest.onCompleted.addListener(clearPending, {urls: ['*://*/*']}); chrome.webRequest.onErrorOccurred.addListener(clearPending, {urls: ['*://*/*']}); chrome.runtime.onInstalled.addListener((details) => { // Installs Update Listener // reason: install | update | browser_update | shared_module_update switch (true) { case details.reason === 'install': case details.reason === 'update' && /^(3\.|4\.|5\.5|5\.6)/.test(details.previousVersion): chrome.tabs.create({url: '/about.html?welcome'}); break; } }); // ----------------- User Preference ----------------------- chrome.storage.local.get(null, result => { // browserVersion is not used & runtime.getBrowserInfo() is not supported on Chrome // sync is NOT set or it is false, use this result ELSE get it from storage.sync // check both storage on start-up if (!Object.keys(result)[0]) { // local is empty, check sync chrome.storage.sync.get(null, syncResult => { if (!Object.keys(syncResult)[0]) { // sync is also empty storageArea = chrome.storage.local; // set storage as local process(result); } else { chrome.storage.local.set({sync: true}); // save sync as true storageArea = chrome.storage.sync; // set storage as sync process(syncResult); } }); } else { storageArea = result.sync ? chrome.storage.sync : chrome.storage.local; // cache for subsequent use !result.sync ? process(result) : chrome.storage.sync.get(null, process); } }); // ----------------- /User Preference ---------------------- function process(settings) { let update; let prefKeys = Object.keys(settings); if (!settings || !prefKeys[0]) { // create default settings if there are no settings // default settings = { mode: 'disabled', logging: { size: 100, active: false } }; update = true; } // update storage then add Change Listener if (update) { storageArea.set(settings, () => chrome.storage.onChanged.addListener(storageOnChanged)); } else { chrome.storage.onChanged.addListener(storageOnChanged); } logger = settings.logging ? new Logger(settings.logging.size, settings.logging.active) : new Logger(); setActiveSettings(settings); console.log('background.js: loaded proxy settings from storage.'); } function storageOnChanged(changes, area) { // console.log(changes); // update storageArea on sync on/off change from options if (changes.hasOwnProperty('sync') && changes.sync.newValue !== changes.sync.oldValue) { storageArea = changes.sync.newValue ? chrome.storage.sync : chrome.storage.local; } // update logger from log if (Object.keys(changes).length === 1 && changes.logging) { return; } // mode change from bg if(changes.mode && changes.mode.newValue === 'disabled' && bgDisable) { bgDisable = false; return; } // default: changes from popup | options storageArea.get(null, setActiveSettings); } function proxyRequest(requestInfo) { return findProxyMatch(requestInfo.url, activeSettings); } function setActiveSettings(settings) { browser.proxy.onRequest.hasListener(proxyRequest) && browser.proxy.onRequest.removeListener(proxyRequest); const pref = settings; const prefKeys = Object.keys(pref).filter(item => !['mode', 'logging', 'sync'].includes(item)); // not for these // --- cache credentials in authData (only those with user/pass) prefKeys.forEach(id => pref[id].username && pref[id].password && (authData[pref[id].address] = {username: pref[id].username, password: pref[id].password}) ); const mode = settings.mode; activeSettings = { // global mode, proxySettings: [] }; if (mode === 'disabled' || (FOXYPROXY_BASIC && mode === 'patterns')){ setDisabled(); return; } if (['patterns', 'random', 'roundrobin'].includes(mode)) { // we only support 'patterns' ATM // filter out the inactive proxy settings prefKeys.forEach(id => pref[id].active && activeSettings.proxySettings.push(pref[id])); activeSettings.proxySettings.sort((a, b) => a.index - b.index); // sort by index function processPatternObjects(patternObjects) { return patternObjects.reduce((accumulator, patternObject) => { patternObject = Utils.processPatternObject(patternObject); patternObject && accumulator.push(patternObject); return accumulator; }, []); } // Filter out the inactive patterns. that way, each comparison // is a little faster (doesn't even know about inactive patterns). Also convert all patterns to reg exps. for (const idx in activeSettings.proxySettings) { activeSettings.proxySettings[idx].blackPatterns = processPatternObjects(activeSettings.proxySettings[idx].blackPatterns); activeSettings.proxySettings[idx].whitePatterns = processPatternObjects(activeSettings.proxySettings[idx].whitePatterns); } browser.proxy.onRequest.addListener(proxyRequest, {urls: [""]}); Utils.updateIcon('images/icon.svg', null, 'patterns', true); console.log(activeSettings, "activeSettings in patterns mode"); } else { // User has selected a proxy for all URLs (not patterns, disabled, random, round-robin modes). // mode is set to the proxySettings id to use for all URLs. if (settings[mode]) { activeSettings.proxySettings = [settings[mode]]; browser.proxy.onRequest.addListener(proxyRequest, {urls: [""]}); const tmp = Utils.getProxyTitle(settings[mode]); Utils.updateIcon('images/icon.svg', settings[mode].color, tmp, false, tmp, false); console.log(activeSettings, "activeSettings in fixed mode"); } else { // This happens if user deletes the current proxy and mode is "use this proxy for all URLs" // Don't remove this block. bgDisable = true; storageArea.set({mode: 'disabled'}); // only in case of error, otherwise mode is already set setDisabled(); console.error(`Error: mode is set to ${mode} but no active proxySetting is found with that id. Disabling Due To Error`); } } } function setDisabled(isError) { browser.proxy.onRequest.hasListener(proxyRequest) && browser.proxy.onRequest.removeListener(proxyRequest); chrome.runtime.sendMessage({mode: 'disabled'}); // Update the options.html UI if it's open Utils.updateIcon('images/icon-off.svg', null, 'disabled', true); console.log('******* disabled mode'); } // ----------------- Proxy Authentication ------------------ // ----- session global let authData = {}; let authPending = {}; async function sendAuth(request) { // Do nothing if this not proxy auth request: // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/onAuthRequired // "Take no action: the listener can do nothing, just observing the request. If this happens, it will // have no effect on the handling of the request, and the browser will probably just ask the user to log in." if (!request.isProxy) return; // --- already sent once and pending if (authPending[request.requestId]) { return {cancel: true}; } // --- authData credentials not yet populated from storage if(!Object.keys(authData)[0]) { await getAuth(request); } // --- first authentication // According to https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/onAuthRequired : // "request.challenger.host is the requested host instead of the proxy requesting the authentication" // But in my tests (Fx 69.0.1 MacOS), it is indeed the proxy requesting the authentication // TODO: test in future Fx releases to see if that changes. // console.log(request.challenger.host, "challenger host"); if (authData[request.challenger.host]) { authPending[request.requestId] = 1; // prevent bad authentication loop return {authCredentials: authData[request.challenger.host]}; } // --- no user/pass set for the challenger.host, leave the authentication to the browser } async function getAuth(request) { await new Promise(resolve => { chrome.storage.local.get(null, result => { const host = result.hostData[request.challenger.host]; if (host && host.username) { // cache credentials in authData authData[host] = {username: host.username, password: host.password}; } resolve(); }); }); } function clearPending(request) { if(!authPending[request.requestId]) { return; } if (request.error) { const host = request.proxyInfo && request.proxyInfo.host ? request.proxyInfo.host : request.ip; Utils.notify(chrome.i18n.getMessage('authError', host)); console.error(request.error); return; // auth will be sent again } delete authPending[request.requestId]; // no error }