Merge pull request #1903 from mozilla/proxy-support
Updated per-container proxy support
This commit is contained in:
commit
98e3412d68
11 changed files with 321 additions and 59 deletions
|
@ -1,6 +1,6 @@
|
|||
module.exports = {
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 8
|
||||
"ecmaVersion": 2018
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
|
@ -18,7 +18,8 @@ module.exports = {
|
|||
"XPCOMUtils": true,
|
||||
"OS": true,
|
||||
"ADDON_UNINSTALL": true,
|
||||
"ADDON_DISABLE": true
|
||||
"ADDON_DISABLE": true,
|
||||
"proxifiedContainers": true
|
||||
},
|
||||
"plugins": [
|
||||
"promise",
|
||||
|
|
|
@ -586,7 +586,7 @@ manage things like container crud */
|
|||
}
|
||||
|
||||
.edit-container-panel fieldset:last-of-type {
|
||||
margin-block-end: 0;
|
||||
margin-block-start: 16px;
|
||||
}
|
||||
|
||||
.edit-container-panel input[type="text"] {
|
||||
|
@ -886,12 +886,11 @@ input {
|
|||
|
||||
.site-isolation {
|
||||
inset-block-end: auto;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.options-label {
|
||||
cursor: pointer;
|
||||
padding-inline-start: 25px;
|
||||
padding-inline-start: 4px;
|
||||
}
|
||||
|
||||
.manage-assigned-sites-list {
|
||||
|
|
|
@ -184,6 +184,17 @@ window.assignManager = {
|
|||
return true;
|
||||
},
|
||||
|
||||
async handleProxifiedRequest(requestInfo) {
|
||||
// The following blocks potentially dangerous requests for privacy that come without a tabId
|
||||
if(requestInfo.tabId === -1)
|
||||
return Utils.getBogusProxy();
|
||||
|
||||
const tab = await browser.tabs.get(requestInfo.tabId);
|
||||
const proxy = await proxifiedContainers.retrieveFromBackground(tab.cookieStoreId);
|
||||
|
||||
return proxy;
|
||||
},
|
||||
|
||||
// Before a request is handled by the browser we decide if we should
|
||||
// route through a different container
|
||||
async onBeforeRequest(options) {
|
||||
|
@ -368,6 +379,9 @@ window.assignManager = {
|
|||
this._onClickedHandler(info, tab);
|
||||
});
|
||||
|
||||
// Before anything happens we decide if the request should be proxified
|
||||
browser.proxy.onRequest.addListener(this.handleProxifiedRequest, {urls: ["<all_urls>"]});
|
||||
|
||||
// Before a request is handled by the browser we decide if we should
|
||||
// route through a different container
|
||||
this.canceledRequests = {};
|
||||
|
|
|
@ -45,6 +45,10 @@ const backgroundLogic = {
|
|||
await browser.contextualIdentities.remove(this.cookieStoreId(userContextId));
|
||||
}
|
||||
assignManager.deleteContainer(userContextId);
|
||||
|
||||
// Now remove the identity->proxy association in proxifiedContainers also
|
||||
proxifiedContainers.delete(this.cookieStoreId(userContextId));
|
||||
|
||||
return {done: true, userContextId};
|
||||
},
|
||||
|
||||
|
@ -55,8 +59,17 @@ const backgroundLogic = {
|
|||
this.cookieStoreId(options.userContextId),
|
||||
options.params
|
||||
);
|
||||
|
||||
proxifiedContainers.set(this.cookieStoreId(options.userContextId), options.proxy);
|
||||
} else {
|
||||
donePromise = browser.contextualIdentities.create(options.params);
|
||||
|
||||
// We cannot yet access the new cookieStoreId via this.cookieStoreId(...), so we take this from the resolved promise
|
||||
donePromise.then((identity) => {
|
||||
proxifiedContainers.set(identity.cookieStoreId, options.proxy);
|
||||
}).catch(() => {
|
||||
// Empty because this should never happen theoretically.
|
||||
});
|
||||
}
|
||||
await donePromise;
|
||||
},
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
"js/background/messageHandler.js",
|
||||
]
|
||||
-->
|
||||
<script type="text/javascript" src="../utils.js"></script>
|
||||
<script type="text/javascript" src="../proxified-containers.js"></script>
|
||||
<script type="text/javascript" src="backgroundLogic.js"></script>
|
||||
<script type="text/javascript" src="assignManager.js"></script>
|
||||
<script type="text/javascript" src="badge.js"></script>
|
||||
|
|
|
@ -307,6 +307,10 @@ const Logic = {
|
|||
return Utils.userContextId(identity.cookieStoreId);
|
||||
},
|
||||
|
||||
cookieStoreId(userContextId) {
|
||||
return `firefox-container-${userContextId}`;
|
||||
},
|
||||
|
||||
currentCookieStoreId() {
|
||||
const identity = Logic.currentIdentity();
|
||||
return identity.cookieStoreId;
|
||||
|
@ -1348,8 +1352,9 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
|||
params: {
|
||||
name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(),
|
||||
icon: formValues.get("container-icon") || DEFAULT_ICON,
|
||||
color: formValues.get("container-color") || DEFAULT_COLOR,
|
||||
}
|
||||
color: formValues.get("container-color") || DEFAULT_COLOR
|
||||
},
|
||||
proxy: proxifiedContainers.parseProxy(document.getElementById("edit-container-panel-proxy").value) || Utils.DEFAULT_PROXY
|
||||
}
|
||||
});
|
||||
await Logic.refreshIdentities();
|
||||
|
@ -1423,6 +1428,37 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
|||
iconInput.checked = iconInput.value === identity.icon;
|
||||
});
|
||||
|
||||
// Clear the proxy field before doing the retrieval requests below
|
||||
document.querySelector("#edit-container-panel-proxy").value = "";
|
||||
|
||||
const edit_proxy_dom = function(result) {
|
||||
const proxyInput = document.querySelector("#edit-container-panel-proxy");
|
||||
if (result.type === "direct" || typeof result.type === "undefined") {
|
||||
proxyInput.value = "";
|
||||
return;
|
||||
}
|
||||
proxyInput.value = `${result.type}://${result.host}:${result.port}`;
|
||||
};
|
||||
|
||||
proxifiedContainers.retrieve(identity.cookieStoreId).then((result) => {
|
||||
edit_proxy_dom(result.proxy);
|
||||
}, (error) => {
|
||||
if(error.error === "uninitialized" || error.error === "doesnotexist") {
|
||||
proxifiedContainers.set(identity.cookieStoreId, Utils.DEFAULT_PROXY, error.error === "uninitialized").then((result) => {
|
||||
edit_proxy_dom(result);
|
||||
}, (error) => {
|
||||
proxifiedContainers.report_proxy_error(error, "popup.js: unexpected set(...) error");
|
||||
}).catch((error) => {
|
||||
proxifiedContainers.report_proxy_error(error, "popup.js: unexpected set(...) exception");
|
||||
});
|
||||
}
|
||||
else {
|
||||
proxifiedContainers.report_proxy_error(error, "popup.js: unknown error");
|
||||
}
|
||||
}).catch((err) => {
|
||||
proxifiedContainers.report_proxy_error(err, "popup.js: unexpected retrieve error");
|
||||
});
|
||||
|
||||
const deleteButton = document.getElementById("delete-container-button");
|
||||
Utils.addEnterHandler(deleteButton, () => {
|
||||
Logic.showPanel(P_CONTAINER_DELETE, identity);
|
||||
|
|
147
src/js/proxified-containers.js
Normal file
147
src/js/proxified-containers.js
Normal file
|
@ -0,0 +1,147 @@
|
|||
// This object allows other scripts to access the list mapping containers to their proxies
|
||||
proxifiedContainers = {
|
||||
|
||||
// Slightly modified version of 'retrieve' which returns a direct proxy whenever an error is met.
|
||||
retrieveFromBackground(cookieStoreId = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
proxifiedContainers.retrieve(cookieStoreId).then((success) => {
|
||||
resolve(success.proxy);
|
||||
}, function() {
|
||||
resolve(Utils.DEFAULT_PROXY);
|
||||
}).catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
report_proxy_error(error, identifier = null) {
|
||||
// Currently I print to console but this is inefficient
|
||||
const relevant_id_str = identifier === null ? "" : ` call supplied with id: ${identifier.toString()}`;
|
||||
browser.extension.getBackgroundPage().console.log(`proxifiedContainers error occured ${relevant_id_str}: ${JSON.stringify(error)}`);
|
||||
},
|
||||
|
||||
// Resolves to a proxy object which can be used in the return of the listener required for browser.proxy.onRequest.addListener
|
||||
retrieve(cookieStoreId = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
browser.storage.local.get("proxifiedContainersKey").then((results) => {
|
||||
// Steps to test:
|
||||
// 1. Is result empty? If so we must inform the caller to intialize proxifiedContainersStore with some initial info.
|
||||
// 2. Is cookieStoreId null? This means the caller probably wants everything currently in the proxifiedContainersStore object store
|
||||
// 3. If there doesn't exist an entry for the associated cookieStoreId, inform the caller of this
|
||||
// 4. Normal operation - if the cookieStoreId exists in the map, we can simply resolve with the correct proxy value
|
||||
|
||||
const results_array = results["proxifiedContainersKey"];
|
||||
|
||||
if (Object.getOwnPropertyNames(results).length === 0) {
|
||||
reject({
|
||||
error: "uninitialized",
|
||||
message: ""
|
||||
});
|
||||
} else if (cookieStoreId === null) {
|
||||
resolve(results_array);
|
||||
} else {
|
||||
const val = results_array.find(o => o.cookieStoreId === cookieStoreId);
|
||||
|
||||
if (typeof val !== "object" || val === null) {
|
||||
reject({
|
||||
error: "doesnotexist",
|
||||
message: ""
|
||||
});
|
||||
} else {
|
||||
resolve(val);
|
||||
}
|
||||
}
|
||||
|
||||
}, (error) => {
|
||||
reject({
|
||||
error: "internal",
|
||||
message: error
|
||||
});
|
||||
}).catch((error) => {
|
||||
proxifiedContainers.report_proxy_error(error, "proxified-containers.js: error 1");
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
set(cookieStoreId, proxy, initialize = false) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (initialize === true) {
|
||||
const proxifiedContainersStore = [];
|
||||
proxifiedContainersStore.push({
|
||||
cookieStoreId: cookieStoreId,
|
||||
proxy: proxy
|
||||
});
|
||||
|
||||
browser.storage.local.set({
|
||||
proxifiedContainersKey: proxifiedContainersStore
|
||||
});
|
||||
|
||||
resolve(proxy);
|
||||
}
|
||||
|
||||
// Assumes proxy is a properly formatted object
|
||||
proxifiedContainers.retrieve().then((proxifiedContainersStore) => {
|
||||
let index = proxifiedContainersStore.findIndex(i => i.cookieStoreId === cookieStoreId);
|
||||
|
||||
if (index === -1) {
|
||||
proxifiedContainersStore.push({
|
||||
cookieStoreId: cookieStoreId,
|
||||
proxy: proxy
|
||||
});
|
||||
index = proxifiedContainersStore.length - 1;
|
||||
} else {
|
||||
proxifiedContainersStore[index] = {
|
||||
cookieStoreId: cookieStoreId,
|
||||
proxy: proxy
|
||||
};
|
||||
}
|
||||
|
||||
browser.storage.local.set({
|
||||
proxifiedContainersKey: proxifiedContainersStore
|
||||
});
|
||||
|
||||
resolve(proxifiedContainersStore[index]);
|
||||
}, (errorObj) => {
|
||||
reject(errorObj);
|
||||
}).catch((error) => {
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
//Parses a proxy description string of the format type://host[:port] or type://username:password@host[:port] (port is optional)
|
||||
parseProxy(proxy_str) {
|
||||
const proxyRegexp = /(?<type>(https?)|(socks4?)):\/\/(\b(?<username>\w+):(?<password>\w+)@)?(?<host>((?:\d{1,3}\.){3}\d{1,3}\b)|(\b([\w.-]+)(\.([\w.-]+))+))(:(?<port>\d+))?/;
|
||||
if (proxyRegexp.test(proxy_str) !== true) {
|
||||
return false;
|
||||
}
|
||||
const matches = proxyRegexp.exec(proxy_str);
|
||||
return matches.groups;
|
||||
},
|
||||
|
||||
// Deletes the proxy information object for a specified cookieStoreId [useful for cleaning]
|
||||
delete(cookieStoreId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Assumes proxy is a properly formatted object
|
||||
proxifiedContainers.retrieve().then((proxifiedContainersStore) => {
|
||||
const index = proxifiedContainersStore.findIndex(i => i.cookieStoreId === cookieStoreId);
|
||||
|
||||
if (index === -1) {
|
||||
reject({error: "not-found", message: `Container '${cookieStoreId}' not found.`});
|
||||
} else {
|
||||
proxifiedContainersStore.splice(index, 1);
|
||||
}
|
||||
|
||||
browser.storage.local.set({
|
||||
proxifiedContainersKey: proxifiedContainersStore
|
||||
});
|
||||
|
||||
resolve();
|
||||
}, (errorObj) => {
|
||||
reject(errorObj);
|
||||
}).catch((error) => {
|
||||
throw error;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
|
@ -1,3 +1,5 @@
|
|||
/*global getBogusProxy */
|
||||
|
||||
const DEFAULT_FAVICON = "/img/blank-favicon.svg";
|
||||
|
||||
// TODO use export here instead of globals
|
||||
|
@ -19,6 +21,40 @@ const Utils = {
|
|||
imageElement.addEventListener("load", loadListener);
|
||||
return imageElement;
|
||||
},
|
||||
|
||||
// See comment in PR #313 - so far the (hacky) method being used to block proxies is to produce a sufficiently long random address
|
||||
getBogusProxy() {
|
||||
const bogusFailover = 1;
|
||||
const bogusType = "socks4";
|
||||
const bogusPort = 9999;
|
||||
const bogusUsername = "foo";
|
||||
if(typeof window.Utils.pregeneratedString !== "undefined")
|
||||
{
|
||||
return {type:bogusType, host:`w.${window.Utils.pregeneratedString}.coo`, port:bogusPort, username:bogusUsername, failoverTimeout:bogusFailover};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Initialize Utils.pregeneratedString
|
||||
window.Utils.pregeneratedString = "";
|
||||
|
||||
// We generate a cryptographically random string (of length specified in bogusLength), but we only do so once - thus negating any time delay caused
|
||||
const bogusLength = 8;
|
||||
const array = new Uint8Array(bogusLength);
|
||||
window.crypto.getRandomValues(array);
|
||||
for(let i = 0; i < bogusLength; i++)
|
||||
{
|
||||
const s = array[i].toString(16);
|
||||
if(s.length === 1)
|
||||
window.Utils.pregeneratedString += `0${s}`;
|
||||
else
|
||||
window.Utils.pregeneratedString += s;
|
||||
}
|
||||
|
||||
// The only issue with this approach is that if (for some unknown reason) pregeneratedString is not saved, it will result in an infinite loop - but better than a privacy leak!
|
||||
return getBogusProxy();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Escapes any occurances of &, ", <, > or / with XML entities.
|
||||
*
|
||||
|
@ -130,8 +166,16 @@ const Utils = {
|
|||
false
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
window.Utils = Utils;
|
||||
|
||||
// The following creates a fake (but convincing) constant Utils.DEFAULT_PROXY
|
||||
Object.defineProperty(window.Utils, "DEFAULT_PROXY", {
|
||||
value: Object.freeze({type: "direct"}),
|
||||
writable: false,
|
||||
enumerable: true,
|
||||
|
||||
// Setting configurable to false avoids deletion of Utils.DEFAULT_PROXY
|
||||
configurable: false
|
||||
});
|
||||
|
|
|
@ -28,7 +28,8 @@
|
|||
"unlimitedStorage",
|
||||
"tabs",
|
||||
"webRequestBlocking",
|
||||
"webRequest"
|
||||
"webRequest",
|
||||
"proxy"
|
||||
],
|
||||
"optional_permissions": [
|
||||
"bookmarks"
|
||||
|
|
|
@ -277,6 +277,10 @@
|
|||
<fieldset id="edit-container-panel-choose-icon" class="radio-choice">
|
||||
<legend class="form-header" data-i18n-message-id="icon"></legend>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Proxy (Optional)</legend>
|
||||
<input type="text" name="container-proxy" id="edit-container-panel-proxy" maxlength="50" placeholder="type://host:port"/>
|
||||
</fieldset>
|
||||
</form>
|
||||
<div id="edit-container-options">
|
||||
<div class="options-header" data-i18n-message-id="options"></div>
|
||||
|
@ -332,6 +336,7 @@
|
|||
</div>
|
||||
|
||||
<script src="js/utils.js"></script>
|
||||
<script src="js/proxified-containers.js"></script>
|
||||
<script src="js/popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -9,4 +9,4 @@ module.exports = {
|
|||
"rules": {
|
||||
"no-restricted-globals": ["error", "browser"]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue