275 lines
No EOL
11 KiB
JavaScript
275 lines
No EOL
11 KiB
JavaScript
'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: ["<all_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: ["<all_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
|
|
} |