1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
|
'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
}
|