Merge pull request #1903 from mozilla/proxy-support

Updated per-container proxy support
This commit is contained in:
luke crouch 2021-09-29 10:07:21 -05:00 committed by GitHub
commit 98e3412d68
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 321 additions and 59 deletions

View file

@ -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",

View file

@ -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 {

View file

@ -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 = {};

View file

@ -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;
},
@ -183,7 +196,7 @@ const backgroundLogic = {
index: -1
});
} else {
//As we get a blank tab here we will need to await the tabs creation
// As we get a blank tab here we will need to await the tabs creation
newWindowObj = await browser.windows.create({
});
hiddenDefaultTabToClose = true;

View file

@ -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>

View file

@ -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);

View 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;
});
});
}
};

View file

@ -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
});

View file

@ -28,7 +28,8 @@
"unlimitedStorage",
"tabs",
"webRequestBlocking",
"webRequest"
"webRequest",
"proxy"
],
"optional_permissions": [
"bookmarks"

View file

@ -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>

View file

@ -9,4 +9,4 @@ module.exports = {
"rules": {
"no-restricted-globals": ["error", "browser"]
}
}
};