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 = {
|
module.exports = {
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 8
|
"ecmaVersion": 2018
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
|
@ -18,7 +18,8 @@ module.exports = {
|
||||||
"XPCOMUtils": true,
|
"XPCOMUtils": true,
|
||||||
"OS": true,
|
"OS": true,
|
||||||
"ADDON_UNINSTALL": true,
|
"ADDON_UNINSTALL": true,
|
||||||
"ADDON_DISABLE": true
|
"ADDON_DISABLE": true,
|
||||||
|
"proxifiedContainers": true
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"promise",
|
"promise",
|
||||||
|
|
|
@ -586,7 +586,7 @@ manage things like container crud */
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-container-panel fieldset:last-of-type {
|
.edit-container-panel fieldset:last-of-type {
|
||||||
margin-block-end: 0;
|
margin-block-start: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-container-panel input[type="text"] {
|
.edit-container-panel input[type="text"] {
|
||||||
|
@ -886,12 +886,11 @@ input {
|
||||||
|
|
||||||
.site-isolation {
|
.site-isolation {
|
||||||
inset-block-end: auto;
|
inset-block-end: auto;
|
||||||
position: fixed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.options-label {
|
.options-label {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding-inline-start: 25px;
|
padding-inline-start: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.manage-assigned-sites-list {
|
.manage-assigned-sites-list {
|
||||||
|
|
|
@ -184,6 +184,17 @@ window.assignManager = {
|
||||||
return true;
|
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
|
// Before a request is handled by the browser we decide if we should
|
||||||
// route through a different container
|
// route through a different container
|
||||||
async onBeforeRequest(options) {
|
async onBeforeRequest(options) {
|
||||||
|
@ -368,6 +379,9 @@ window.assignManager = {
|
||||||
this._onClickedHandler(info, tab);
|
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
|
// Before a request is handled by the browser we decide if we should
|
||||||
// route through a different container
|
// route through a different container
|
||||||
this.canceledRequests = {};
|
this.canceledRequests = {};
|
||||||
|
|
|
@ -45,6 +45,10 @@ const backgroundLogic = {
|
||||||
await browser.contextualIdentities.remove(this.cookieStoreId(userContextId));
|
await browser.contextualIdentities.remove(this.cookieStoreId(userContextId));
|
||||||
}
|
}
|
||||||
assignManager.deleteContainer(userContextId);
|
assignManager.deleteContainer(userContextId);
|
||||||
|
|
||||||
|
// Now remove the identity->proxy association in proxifiedContainers also
|
||||||
|
proxifiedContainers.delete(this.cookieStoreId(userContextId));
|
||||||
|
|
||||||
return {done: true, userContextId};
|
return {done: true, userContextId};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -55,8 +59,17 @@ const backgroundLogic = {
|
||||||
this.cookieStoreId(options.userContextId),
|
this.cookieStoreId(options.userContextId),
|
||||||
options.params
|
options.params
|
||||||
);
|
);
|
||||||
|
|
||||||
|
proxifiedContainers.set(this.cookieStoreId(options.userContextId), options.proxy);
|
||||||
} else {
|
} else {
|
||||||
donePromise = browser.contextualIdentities.create(options.params);
|
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;
|
await donePromise;
|
||||||
},
|
},
|
||||||
|
@ -183,7 +196,7 @@ const backgroundLogic = {
|
||||||
index: -1
|
index: -1
|
||||||
});
|
});
|
||||||
} else {
|
} 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({
|
newWindowObj = await browser.windows.create({
|
||||||
});
|
});
|
||||||
hiddenDefaultTabToClose = true;
|
hiddenDefaultTabToClose = true;
|
||||||
|
|
|
@ -13,6 +13,8 @@
|
||||||
"js/background/messageHandler.js",
|
"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="backgroundLogic.js"></script>
|
||||||
<script type="text/javascript" src="assignManager.js"></script>
|
<script type="text/javascript" src="assignManager.js"></script>
|
||||||
<script type="text/javascript" src="badge.js"></script>
|
<script type="text/javascript" src="badge.js"></script>
|
||||||
|
|
|
@ -307,6 +307,10 @@ const Logic = {
|
||||||
return Utils.userContextId(identity.cookieStoreId);
|
return Utils.userContextId(identity.cookieStoreId);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cookieStoreId(userContextId) {
|
||||||
|
return `firefox-container-${userContextId}`;
|
||||||
|
},
|
||||||
|
|
||||||
currentCookieStoreId() {
|
currentCookieStoreId() {
|
||||||
const identity = Logic.currentIdentity();
|
const identity = Logic.currentIdentity();
|
||||||
return identity.cookieStoreId;
|
return identity.cookieStoreId;
|
||||||
|
@ -1348,8 +1352,9 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||||
params: {
|
params: {
|
||||||
name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(),
|
name: document.getElementById("edit-container-panel-name-input").value || Logic.generateIdentityName(),
|
||||||
icon: formValues.get("container-icon") || DEFAULT_ICON,
|
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();
|
await Logic.refreshIdentities();
|
||||||
|
@ -1423,6 +1428,37 @@ Logic.registerPanel(P_CONTAINER_EDIT, {
|
||||||
iconInput.checked = iconInput.value === identity.icon;
|
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");
|
const deleteButton = document.getElementById("delete-container-button");
|
||||||
Utils.addEnterHandler(deleteButton, () => {
|
Utils.addEnterHandler(deleteButton, () => {
|
||||||
Logic.showPanel(P_CONTAINER_DELETE, identity);
|
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";
|
const DEFAULT_FAVICON = "/img/blank-favicon.svg";
|
||||||
|
|
||||||
// TODO use export here instead of globals
|
// TODO use export here instead of globals
|
||||||
|
@ -19,6 +21,40 @@ const Utils = {
|
||||||
imageElement.addEventListener("load", loadListener);
|
imageElement.addEventListener("load", loadListener);
|
||||||
return imageElement;
|
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.
|
* Escapes any occurances of &, ", <, > or / with XML entities.
|
||||||
*
|
*
|
||||||
|
@ -130,8 +166,16 @@ const Utils = {
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
window.Utils = Utils;
|
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",
|
"unlimitedStorage",
|
||||||
"tabs",
|
"tabs",
|
||||||
"webRequestBlocking",
|
"webRequestBlocking",
|
||||||
"webRequest"
|
"webRequest",
|
||||||
|
"proxy"
|
||||||
],
|
],
|
||||||
"optional_permissions": [
|
"optional_permissions": [
|
||||||
"bookmarks"
|
"bookmarks"
|
||||||
|
|
|
@ -277,6 +277,10 @@
|
||||||
<fieldset id="edit-container-panel-choose-icon" class="radio-choice">
|
<fieldset id="edit-container-panel-choose-icon" class="radio-choice">
|
||||||
<legend class="form-header" data-i18n-message-id="icon"></legend>
|
<legend class="form-header" data-i18n-message-id="icon"></legend>
|
||||||
</fieldset>
|
</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>
|
</form>
|
||||||
<div id="edit-container-options">
|
<div id="edit-container-options">
|
||||||
<div class="options-header" data-i18n-message-id="options"></div>
|
<div class="options-header" data-i18n-message-id="options"></div>
|
||||||
|
@ -332,6 +336,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/utils.js"></script>
|
<script src="js/utils.js"></script>
|
||||||
|
<script src="js/proxified-containers.js"></script>
|
||||||
<script src="js/popup.js"></script>
|
<script src="js/popup.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -9,4 +9,4 @@ module.exports = {
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-restricted-globals": ["error", "browser"]
|
"no-restricted-globals": ["error", "browser"]
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
Loading…
Add table
Reference in a new issue