summaryrefslogtreecommitdiffstats
path: root/src/scripts/background.js
blob: a60546bd774cf1025645e8e538ed3e1357c73143 (plain)
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
}