📝 Added entire config directory for backup purposes
This commit is contained in:
parent
4b38e59bb6
commit
9b946e2d14
11091 changed files with 1440953 additions and 0 deletions
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
|
||||
*
|
||||
* Copyright (C) 2005-2021 Giorgio Maone <https://maone.net>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation, either version 3 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var Defaults = {
|
||||
async init() {
|
||||
let defaults = {
|
||||
local: {
|
||||
debug: false,
|
||||
showCtxMenuItem: true,
|
||||
showCountBadge: true,
|
||||
showFullAddresses: false,
|
||||
amnesticUpdates: false,
|
||||
},
|
||||
sync: {
|
||||
global: false,
|
||||
xss: true,
|
||||
cascadeRestrictions : false,
|
||||
overrideTorBrowserPolicy: false, // note: Settings.update() on reset will flip this to true
|
||||
}
|
||||
};
|
||||
let defaultsClone = JSON.parse(JSON.stringify(defaults));
|
||||
|
||||
for (let [k, v] of Object.entries(defaults)) {
|
||||
let store = await Storage.get(k, k);
|
||||
if (k in store) {
|
||||
Object.assign(v, store[k]);
|
||||
}
|
||||
v.storage = k;
|
||||
}
|
||||
|
||||
Object.assign(ns, defaults);
|
||||
|
||||
// dynamic settings
|
||||
if (!ns.local.uuid) {
|
||||
ns.local.uuid = uuid();
|
||||
await ns.save(ns.local);
|
||||
}
|
||||
|
||||
return ns.defaults = defaultsClone;
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
/*
|
||||
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
|
||||
*
|
||||
* Copyright (C) 2005-2021 Giorgio Maone <https://maone.net>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation, either version 3 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var LifeCycle = (() => {
|
||||
|
||||
const AES = "AES-GCM",
|
||||
keyUsages = ["encrypt", "decrypt"];
|
||||
|
||||
function toBase64(bytes) {
|
||||
return btoa(Array.from(bytes).map(b => String.fromCharCode(b)).join(''));
|
||||
}
|
||||
function fromBase64(string) {
|
||||
return Uint8Array.from((Array.from(atob(string)).map(c => c.charCodeAt(0))));
|
||||
}
|
||||
async function encrypt(clearText) {
|
||||
let key = await crypto.subtle.generateKey({
|
||||
name: AES,
|
||||
length: 256,
|
||||
},
|
||||
true,
|
||||
keyUsages,
|
||||
);
|
||||
let iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
let encoded = new TextEncoder().encode(clearText);
|
||||
let cypherText = await crypto.subtle.encrypt({
|
||||
name: AES,
|
||||
iv
|
||||
}, key, encoded);
|
||||
return {cypherText, key: await crypto.subtle.exportKey("jwk", key), iv};
|
||||
}
|
||||
|
||||
var LifeBoat = {
|
||||
url: "about:blank",
|
||||
async createAndStore() {
|
||||
let allSeen = {};
|
||||
let tab;
|
||||
await Promise.all((await browser.tabs.query({})).map(
|
||||
async t => {
|
||||
let seen = await ns.collectSeen(t.id);
|
||||
if (seen) {
|
||||
allSeen[t.id] = seen;
|
||||
if (!tab || !tab.incognito && t.incognito) {
|
||||
tab = t;
|
||||
}
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
|
||||
if (!tab) { // no suitable existing tab, let's open a new one
|
||||
if (!UA.isMozilla) {
|
||||
// injecting new about:blank tabs is supported only by Mozilla: let's bailout
|
||||
return;
|
||||
}
|
||||
let {url} = LifeBoat;
|
||||
let tabInfo = {
|
||||
url,
|
||||
active: false,
|
||||
};
|
||||
if (browser.windows) { // it may be missing on mobile
|
||||
// check if an incognito window exists and open our "survival" tab there
|
||||
for (let w of await browser.windows.getAll()) {
|
||||
if (w.incognito) {
|
||||
tabInfo.windowId = w.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (;!tab;) {
|
||||
try {
|
||||
tab = await browser.tabs.create(tabInfo);
|
||||
} catch (e) {
|
||||
error(e);
|
||||
if (tabInfo.windowId) {
|
||||
// we might not have incognito permissions, let's try using any window
|
||||
delete tabInfo.windowId;
|
||||
} else {
|
||||
return; // bailout
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tabId = tab.id;
|
||||
let {url} = tab;
|
||||
let {cypherText, key, iv} = await encrypt(JSON.stringify({
|
||||
policy: ns.policy.dry(true),
|
||||
allSeen,
|
||||
unrestrictedTabs: [...ns.unrestrictedTabs]
|
||||
}));
|
||||
|
||||
let attr;
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
let l = async (tabId, changeInfo) => {
|
||||
if (!!attr || tabId !== tab.id) return;
|
||||
debug("Survival tab updating", changeInfo);
|
||||
if (changeInfo.status !== "complete") return;
|
||||
try {
|
||||
attr = await Messages.send("store", {url, data: toBase64(new Uint8Array(cypherText))}, {tabId, frameId: 0});
|
||||
resolve();
|
||||
debug("Survival tab updated");
|
||||
} catch (e) {
|
||||
if (!Messages.isMissingEndpoint(e)) {
|
||||
error(e, "Survival tab failed");
|
||||
reject(e);
|
||||
} // otherwise we keep waiting for further updates from the tab until content script is ready to answer
|
||||
return false;
|
||||
};
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
l(tabId, tab).then(r => {
|
||||
if (!r) browser.tabs.onUpdated.addListener(l);
|
||||
});
|
||||
});
|
||||
|
||||
await Storage.set("local", { "updateInfo": {key, iv: toBase64(iv), tabId, url, attr}});
|
||||
tabId = -1;
|
||||
debug("Ready to reload...", await Storage.get("local", "updateInfo"));
|
||||
} finally {
|
||||
if (tabId !== -1 && url === LifeBoat.url && !ns.local.debug) {
|
||||
browser.tabs.remove(tabId); // cleanup on failure unless we want to debug a post-mortem
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async retrieveAndDestroy() {
|
||||
let {updateInfo} = await Storage.get("local", "updateInfo");
|
||||
if (!updateInfo) return;
|
||||
await Storage.remove("local", "updateInfo");
|
||||
let {key, iv, tabId, attr, url} = updateInfo;
|
||||
|
||||
let destroyIfNeeded = url === LifeBoat.url ? (keepIfDebug = false) => {
|
||||
if (tabId === -1 || url !== LifeBoat.url) return;
|
||||
if (keepIfDebug && ns.local.debug) {
|
||||
debug("Failed survival tab %s left open for debugging.", tabId);
|
||||
} else {
|
||||
browser.tabs.remove(tabId);
|
||||
}
|
||||
tabId = -1;
|
||||
} : () => {};
|
||||
|
||||
try {
|
||||
key = await crypto.subtle.importKey("jwk", key, AES, true, keyUsages);
|
||||
iv = fromBase64(iv);
|
||||
let cypherText;
|
||||
for (let attempts = 3; attempts-- > 0;) {
|
||||
try {
|
||||
cypherText = await Messages.send("retrieve", {url, attr}, {tabId, frameId: 0});
|
||||
break;
|
||||
} catch (e) {
|
||||
if (Messages.isMissingEndpoint(e)) {
|
||||
debug("Cannot retrieve survival tab data, maybe content script not loaded yet. Retrying...");
|
||||
await ns.initializing;
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!cypherText) {
|
||||
throw new Error("Could not retrieve survival tab data!");
|
||||
}
|
||||
cypherText = fromBase64(cypherText);
|
||||
let encoded = await crypto.subtle.decrypt({
|
||||
name: AES,
|
||||
iv
|
||||
}, key, cypherText
|
||||
);
|
||||
let {policy, allSeen, unrestrictedTabs} = JSON.parse(new TextDecoder().decode(encoded));
|
||||
if (!policy) {
|
||||
throw new error("Ephemeral policy not found in survival tab %s!", tabId);
|
||||
}
|
||||
ns.unrestrictedTabs = new Set(unrestrictedTabs);
|
||||
destroyIfNeeded();
|
||||
if (ns.initializing) await ns.initializing;
|
||||
ns.policy = new Policy(policy);
|
||||
await Promise.all(
|
||||
Object.entries(allSeen).map(
|
||||
async ([tabId, seen]) => {
|
||||
try {
|
||||
debug("Restoring seen %o to tab %s", seen, tabId);
|
||||
await Messages.send("allSeen", {seen}, {tabId, frameId: 0});
|
||||
} catch (e) {
|
||||
error(e, "Cannot send previously seen data to tab", tabId);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
error(e);
|
||||
} finally {
|
||||
destroyIfNeeded(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
async onInstalled(details) {
|
||||
browser.runtime.onInstalled.removeListener(this.onInstalled);
|
||||
|
||||
if (!UA.isMozilla) {
|
||||
// Chromium does not inject content scripts at startup automatically for already loaded pages,
|
||||
// let's hack it manually.
|
||||
let contentScripts = browser.runtime.getManifest().content_scripts.find(s =>
|
||||
s.js && s.matches.includes("<all_urls>") && s.all_frames && s.match_about_blank).js;
|
||||
|
||||
await Promise.all((await browser.tabs.query({})).map(async tab => {
|
||||
for (let file of contentScripts) {
|
||||
try {
|
||||
await browser.tabs.executeScript(tab.id, {file, allFrames: true, matchAboutBlank: true});
|
||||
} catch (e) {
|
||||
await include("/nscl/common/restricted.js");
|
||||
if (!isRestrictedURL(tab.url)) {
|
||||
error(e, "Can't run content script on tab", tab);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
let {reason, previousVersion} = details;
|
||||
if (reason === "update") {
|
||||
try {
|
||||
await LifeBoat.retrieveAndDestroy();
|
||||
} catch (e) {
|
||||
error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!previousVersion) return;
|
||||
|
||||
await include("/nscl/common/Ver.js");
|
||||
previousVersion = new Ver(previousVersion);
|
||||
let currentVersion = new Ver(browser.runtime.getManifest().version);
|
||||
let upgrading = Ver.is(previousVersion, "<=", currentVersion);
|
||||
if (!upgrading) return;
|
||||
|
||||
// put here any version specific upgrade adjustment in stored data
|
||||
|
||||
let forEachPreset = async (callback, presetNames = "*") => {
|
||||
await ns.initializing;
|
||||
let changed = false;
|
||||
for (let p of ns.policy.getPresets(presetNames)) {
|
||||
if (callback(p)) changed = true;
|
||||
if (p.contextual) {
|
||||
for (let ctxP of p.contextual.values()) {
|
||||
if (callback(ctxP)) changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
await ns.savePolicy();
|
||||
}
|
||||
};
|
||||
|
||||
let configureNewCap = async (cap, presetNames, capsFilter) => {
|
||||
log(`Upgrading from ${previousVersion}: configure the "${cap}" capability.`);
|
||||
await forEachPreset(({capabilities}) => {
|
||||
if (capsFilter(capabilities) && !capabilities.has(cap)) {
|
||||
capabilities.add(cap);
|
||||
return true;
|
||||
}
|
||||
}, presetNames);
|
||||
};
|
||||
|
||||
let renameCap = async (oldName, newName) => {
|
||||
log(`Upgrading from ${previousVersion}: rename capability "${oldName}" to "${newName}`);
|
||||
await forEachPreset(({capabilities}) => {
|
||||
if (capabilities.has(oldName)) {
|
||||
capabilities.delete(oldName);
|
||||
capabilities.add(newName);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (Ver.is(previousVersion, "<=", "11.0.10")) {
|
||||
await configureNewCap("ping", ["TRUSTED"]);
|
||||
}
|
||||
if (Ver.is(previousVersion, "<=", "11.2.1")) {
|
||||
await configureNewCap("noscript", ["DEFAULT", "TRUSTED", "CUSTOM"])
|
||||
}
|
||||
if (Ver.is(previousVersion, "<=", "11.2.4")) {
|
||||
// add the unchecked_css capability to any preset which already has the script capability
|
||||
await configureNewCap("unchecked_css", ["DEFAULT", "TRUSTED", "CUSTOM"], caps => caps.has("script"));
|
||||
}
|
||||
if (Ver.is(previousVersion, "<=", "11.2.5rc1")) {
|
||||
await renameCap("csspp0", "unchecked_css");
|
||||
}
|
||||
if (Ver.is(previousVersion, "<=", "11.3rc2")) {
|
||||
// add the lan capability to any preset which already has the script capability
|
||||
await configureNewCap("lan", ["DEFAULT", "TRUSTED", "CUSTOM"], caps => caps.has("script"));
|
||||
}
|
||||
|
||||
if (Ver.is(previousVersion, "<=", "11.4.1rc3")) {
|
||||
// show theme switcher on update unless user has already chosen between Vintage Blue and Modern Red
|
||||
(async () => {
|
||||
let isVintage = await Themes.isVintage();
|
||||
if (typeof isVintage === "boolean") return;
|
||||
await ns.init;
|
||||
ns.openOptionsPage({tab: 2, focus: "#opt-vintageTheme", hilite: "#sect-themes"});
|
||||
})();
|
||||
}
|
||||
},
|
||||
|
||||
async onUpdateAvailable(details) {
|
||||
try {
|
||||
if (ns.local.amnesticUpdates) {
|
||||
// user doesn't want us to remember temporary settings across updates: bail out
|
||||
return;
|
||||
}
|
||||
await include("/nscl/common/Ver.js");
|
||||
if (Ver.is(details.version, "<", browser.runtime.getManifest().version)) {
|
||||
// downgrade: temporary survival might not be supported, and we don't care
|
||||
return;
|
||||
}
|
||||
await LifeBoat.createAndStore();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
browser.runtime.reload(); // apply update
|
||||
}
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
|
||||
*
|
||||
* Copyright (C) 2005-2021 Giorgio Maone <https://maone.net>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation, either version 3 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
function ReportingCSP(marker, reportURI = "") {
|
||||
const DOM_SUPPORTED = "SecurityPolicyViolationEvent" in window;
|
||||
|
||||
if (DOM_SUPPORTED) reportURI = "";
|
||||
|
||||
return Object.assign(
|
||||
new CapsCSP(new NetCSP(
|
||||
reportURI ? `report-uri ${reportURI}` : marker
|
||||
)),
|
||||
{
|
||||
reportURI,
|
||||
patchHeaders(responseHeaders, capabilities) {
|
||||
let header = null;
|
||||
let blocker;
|
||||
if (capabilities) {
|
||||
let contentType = responseHeaders.filter(h => h.name.toLowerCase() === "content-type");
|
||||
let blockHTTP = contentType.length === 0 || contentType.some(h => !/^(?:text|application)\/\S*\b(?:x?ht|x)ml\b/i.test(h.value));
|
||||
blocker = this.buildFromCapabilities(capabilities, blockHTTP);
|
||||
}
|
||||
let extras = [];
|
||||
responseHeaders.forEach((h, index) => {
|
||||
if (this.isMine(h)) {
|
||||
header = h;
|
||||
if (h.value === blocker) {
|
||||
// make this equivalent but different than the original, otherwise
|
||||
// it won't be (re)set when deleted, see
|
||||
// https://dxr.mozilla.org/mozilla-central/rev/882de07e4cbe31a0617d1ae350236123dfdbe17f/toolkit/components/extensions/webrequest/WebRequest.jsm#138
|
||||
blocker += " ";
|
||||
} else {
|
||||
extras.push(...this.unmergeExtras(h));
|
||||
}
|
||||
responseHeaders.splice(index, 1);
|
||||
} else if (blocker && /^(Location|Refresh)$/i.test(h.name)) {
|
||||
// neutralize any HTTP redirection to data: URLs, like Chromium
|
||||
let url = /^R/i.test(h.name)
|
||||
? h.value.replace(/^[^,;]*[,;](?:\W*url[^=]*=)?[^!#$%&()*+,/:;=?@[\]\w.,~-]*/i, "") : h.value;
|
||||
if (/^data:/i.test(url)) {
|
||||
h.value = h.value.slice(0, -url.length) + "data:";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (blocker) {
|
||||
header = this.asHeader(blocker);
|
||||
responseHeaders.push(header);
|
||||
}
|
||||
|
||||
if (extras.length) {
|
||||
responseHeaders.push(...extras);
|
||||
}
|
||||
|
||||
return header;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,820 @@
|
|||
/*
|
||||
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
|
||||
*
|
||||
* Copyright (C) 2005-2022 Giorgio Maone <https://maone.net>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation, either version 3 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
var RequestGuard = (() => {
|
||||
'use strict';
|
||||
const VERSION_LABEL = `NoScript ${browser.runtime.getManifest().version}`;
|
||||
browser.browserAction.setTitle({title: VERSION_LABEL});
|
||||
const CSP_REPORT_URI = "https://noscript-csp.invalid/__NoScript_Probe__/";
|
||||
const CSP_MARKER = "noscript-marker";
|
||||
let csp = new ReportingCSP(CSP_MARKER, CSP_REPORT_URI);
|
||||
const policyTypesMap = {
|
||||
main_frame: "",
|
||||
sub_frame: "frame",
|
||||
script: "script",
|
||||
xslt: "script",
|
||||
xbl: "script",
|
||||
font: "font",
|
||||
object: "object",
|
||||
object_subrequest: "fetch",
|
||||
xmlhttprequest: "fetch",
|
||||
ping: "ping",
|
||||
beacon: "ping",
|
||||
media: "media",
|
||||
other: "",
|
||||
};
|
||||
|
||||
Object.assign(policyTypesMap, {"webgl": "webgl"}); // fake types
|
||||
const TabStatus = {
|
||||
map: new Map(),
|
||||
_originsCache: new Map(),
|
||||
types: ["script", "object", "media", "frame", "font"],
|
||||
newRecords() {
|
||||
return {
|
||||
allowed: {},
|
||||
blocked: {},
|
||||
noscriptFrames: {},
|
||||
origins: new Set(),
|
||||
}
|
||||
},
|
||||
hasOrigin(tabId, url) {
|
||||
let records = this.map.get(tabId);
|
||||
return records && records.origins.has(Sites.origin(url));
|
||||
},
|
||||
addOrigin(tabId, url) {
|
||||
if (tabId < 0) return;
|
||||
let origin = Sites.origin(url);
|
||||
if (!origin) return;
|
||||
let {origins} = this.map.get(tabId) || this.initTab(tabId);
|
||||
if (!origins.has(origin)) {
|
||||
origins.add(origin);
|
||||
this._originsCache.clear();
|
||||
}
|
||||
},
|
||||
|
||||
findTabsByOrigin(origin) {
|
||||
let tabIds = this._originsCache.get(origin);
|
||||
if (!tabIds) {
|
||||
tabIds = [];
|
||||
for(let [tabId, {origins}] of [...this.map]) {
|
||||
if (origins.has(origin)) tabIds.push(tabId);
|
||||
}
|
||||
this._originsCache.set(origin, tabIds);
|
||||
}
|
||||
return tabIds;
|
||||
},
|
||||
initTab(tabId, records = this.newRecords()) {
|
||||
if (tabId < 0) return;
|
||||
this.map.set(tabId, records);
|
||||
return records;
|
||||
},
|
||||
_record(request, what, optValue) {
|
||||
let {tabId, frameId, type, url, documentUrl} = request;
|
||||
let policyType = policyTypesMap[type] || type;
|
||||
let requestKey = Policy.requestKey(url, policyType, documentUrl);
|
||||
let {map} = this;
|
||||
let records = map.has(tabId) ? map.get(tabId) : this.initTab(tabId);
|
||||
if (what === "noscriptFrame" && type !== "object") {
|
||||
let nsf = records.noscriptFrames;
|
||||
nsf[frameId] = optValue;
|
||||
what = optValue ? "blocked" : "allowed";
|
||||
if (frameId === 0) {
|
||||
request.type = type = "main_frame";
|
||||
Content.reportTo(request, optValue, type);
|
||||
}
|
||||
}
|
||||
if (type.endsWith("frame")) {
|
||||
this.addOrigin(tabId, url);
|
||||
} else if (documentUrl) {
|
||||
this.addOrigin(tabId, documentUrl);
|
||||
}
|
||||
let collection = records[what];
|
||||
if (collection) {
|
||||
if (type in collection) {
|
||||
if (!collection[type].includes(requestKey)) {
|
||||
collection[type].push(requestKey);
|
||||
}
|
||||
} else {
|
||||
collection[type] = [requestKey];
|
||||
}
|
||||
}
|
||||
return records;
|
||||
},
|
||||
record(request, what, optValue) {
|
||||
let {tabId} = request;
|
||||
if (tabId < 0) return;
|
||||
let records = this._record(request, what, optValue);
|
||||
if (records) {
|
||||
this.updateTab(request.tabId);
|
||||
}
|
||||
},
|
||||
_pendingTabs: new Set(),
|
||||
updateTab(tabId) {
|
||||
if (tabId < 0) return;
|
||||
if (this._pendingTabs.size === 0) {
|
||||
window.setTimeout(() => { // clamp UI updates
|
||||
for (let tabId of this._pendingTabs) {
|
||||
this._updateTabNow(tabId);
|
||||
}
|
||||
this._pendingTabs.clear();
|
||||
}, 200);
|
||||
}
|
||||
this._pendingTabs.add(tabId);
|
||||
},
|
||||
_updateTabNow(tabId) {
|
||||
this._pendingTabs.delete(tabId);
|
||||
let records = this.map.get(tabId) || this.initTab(tabId);
|
||||
|
||||
let {allowed, blocked, noscriptFrames} = records;
|
||||
let topAllowed = !(noscriptFrames && noscriptFrames[0]);
|
||||
let numAllowed = 0, numBlocked = 0, sum = 0;
|
||||
let report = this.types.map(t => {
|
||||
let a = allowed[t] && allowed[t].length || 0,
|
||||
b = blocked[t] && blocked[t].length || 0,
|
||||
s = a + b;
|
||||
numAllowed += a;
|
||||
numBlocked += b;
|
||||
sum += s;
|
||||
return s && `<${t === "sub_frame" ? "frame" : t}>: ${b}/${s}`;
|
||||
}).filter(s => s).join("\n");
|
||||
let enforced = ns.isEnforced(tabId);
|
||||
let icon = enforced ?
|
||||
(topAllowed ? (numBlocked ? "part" : "yes")
|
||||
: (numAllowed ? "sub" : "no")) // not topAllowed
|
||||
: "global"; // not enforced
|
||||
let showBadge = ns.local.showCountBadge && numBlocked > 0;
|
||||
let browserAction = browser.browserAction;
|
||||
if (!browserAction.setIcon) { // Fennec
|
||||
browserAction.setTitle({tabId, title: `NoScript (${numBlocked})`});
|
||||
return;
|
||||
}
|
||||
(async () => {
|
||||
let iconPath = (await Themes.isVintage()) ? '/img/vintage' : '/img';
|
||||
browserAction.setIcon({tabId, path: {64: `${iconPath}/ui-${icon}64.png`}});
|
||||
})();
|
||||
browserAction.setBadgeText({tabId, text: showBadge ? numBlocked.toString() : ""});
|
||||
browserAction.setBadgeBackgroundColor({tabId, color: [128, 0, 0, 160]});
|
||||
browserAction.setTitle({tabId,
|
||||
title: UA.mobile ? "NoScript" : `${VERSION_LABEL} \n${enforced ?
|
||||
_("BlockedItems", [numBlocked, numAllowed + numBlocked]) + ` \n${report}`
|
||||
: _("NotEnforced")}`
|
||||
});
|
||||
},
|
||||
async probe(tabId) {
|
||||
if (tabId === undefined) {
|
||||
(await browser.tabs.query({})).forEach(tab => TabStatus.probe(tab.id));
|
||||
} else {
|
||||
try {
|
||||
TabStatus.recordAll(tabId, await ns.collectSeen(tabId));
|
||||
} catch (e) {
|
||||
error(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
recordAll(tabId, seen) {
|
||||
if (seen) {
|
||||
let records = TabStatus.map.get(tabId);
|
||||
if (records) {
|
||||
records.allowed = {};
|
||||
records.blocked = {};
|
||||
}
|
||||
for (let thing of seen) {
|
||||
let {request, allowed} = thing;
|
||||
request.tabId = tabId;
|
||||
debug(`Recording`, request);
|
||||
TabStatus._record(request, allowed ? "allowed" : "blocked");
|
||||
if (request.key === "noscript-probe" && request.type === "main_frame" ) {
|
||||
request.frameId = 0;
|
||||
TabStatus._record(request, "noscriptFrame", !allowed);
|
||||
}
|
||||
}
|
||||
this._updateTabNow(tabId);
|
||||
}
|
||||
},
|
||||
async onActivatedTab(info) {
|
||||
let {tabId} = info;
|
||||
let seen = await ns.collectSeen(tabId);
|
||||
TabStatus.recordAll(tabId, seen);
|
||||
},
|
||||
onUpdatedTab(tabId, changeInfo) {
|
||||
if (changeInfo.url) {
|
||||
TabStatus.initTab(tabId);
|
||||
}
|
||||
},
|
||||
onRemovedTab(tabId) {
|
||||
TabStatus.map.delete(tabId);
|
||||
TabStatus._originsCache.clear();
|
||||
TabStatus._pendingTabs.delete(tabId);
|
||||
},
|
||||
}
|
||||
for (let event of ["Activated", "Updated", "Removed"]) {
|
||||
browser.tabs[`on${event}`].addListener(TabStatus[`on${event}Tab`]);
|
||||
}
|
||||
|
||||
let messageHandler = {
|
||||
async pageshow(message, sender) {
|
||||
if (sender.frameId === 0) {
|
||||
TabStatus.recordAll(sender.tab.id, message.seen);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
violation({url, type}, sender) {
|
||||
let tabId = sender.tab.id;
|
||||
let {frameId} = sender;
|
||||
let r = {
|
||||
url, type, tabId, frameId, documentUrl: sender.url
|
||||
};
|
||||
Content.reportTo(r, false, policyTypesMap[type]);
|
||||
debug("Violation", type, url, tabId, frameId);
|
||||
if (type === "script" && url === sender.url) {
|
||||
TabStatus.record(r, "noscriptFrame", true);
|
||||
} else {
|
||||
TabStatus.record(r, "blocked");
|
||||
}
|
||||
},
|
||||
async blockedObjects(message, sender) {
|
||||
let {url, documentUrl, policyType} = message;
|
||||
let TAG = `<${policyType.toUpperCase()}>`;
|
||||
let origin = Sites.origin(url);
|
||||
let {siteKey} = Sites.parse(url);
|
||||
let options;
|
||||
if (siteKey === origin) {
|
||||
origin = new URL(url).protocol;
|
||||
}
|
||||
options = [
|
||||
{label: _("allowLocal", siteKey), checked: true},
|
||||
{label: _("allowLocal", origin)},
|
||||
{label: _("CollapseBlockedObjects")},
|
||||
];
|
||||
let t = u => `${TAG}@${u}`;
|
||||
let ret = await Prompts.prompt({
|
||||
title: _("BlockedObjects"),
|
||||
message: _("allowLocal", TAG),
|
||||
options});
|
||||
debug(`Prompt returned`, ret, sender);
|
||||
if (ret.button !== 0) return;
|
||||
if (ret.option === 2) {
|
||||
return {collapse: "all"};
|
||||
}
|
||||
let key = [siteKey, origin][ret.option || 0];
|
||||
if (!key) return;
|
||||
let contextUrl = sender.tab.url || documentUrl;
|
||||
let {siteMatch, contextMatch, perms} = ns.policy.get(key, contextUrl);
|
||||
let {capabilities} = perms;
|
||||
if (!capabilities.has(policyType)) {
|
||||
let temp = sender.tab.incognito; // we don't want to store in PBM
|
||||
perms = new Permissions(new Set(capabilities), temp);
|
||||
perms.capabilities.add(policyType);
|
||||
/* TODO: handle contextual permissions
|
||||
if (contextUrl) {
|
||||
let context = Sites.optimalKey(contextUrl);
|
||||
let contextualSites = new Sites([[context, perms]]);
|
||||
perms = new Permissions(new Set(capabilities), false, contextualSites);
|
||||
}
|
||||
*/
|
||||
ns.policy.set(key, perms);
|
||||
await ns.savePolicy();
|
||||
}
|
||||
return {enable: key};
|
||||
},
|
||||
}
|
||||
const Content = {
|
||||
async reportTo(request, allowed, policyType) {
|
||||
let {requestId, tabId, frameId, type, url, documentUrl, originUrl} = request;
|
||||
let pending = pendingRequests.get(requestId); // null if from a CSP report
|
||||
let initialUrl = pending ? pending.initialUrl : request.url;
|
||||
request = {
|
||||
key: Policy.requestKey(url, type, documentUrl || "", /^(media|object|frame)$/.test(type)),
|
||||
type, url, documentUrl, originUrl
|
||||
};
|
||||
if (tabId < 0) {
|
||||
if ((policyType === "script" || policyType === "fetch") &&
|
||||
url.startsWith("https://") && documentUrl && documentUrl.startsWith("https://")) {
|
||||
// service worker request ?
|
||||
let payload = {request, allowed, policyType, serviceWorker: Sites.origin(documentUrl)};
|
||||
let recipient = {frameId: 0};
|
||||
for (let tabId of TabStatus.findTabsByOrigin(payload.serviceWorker)) {
|
||||
recipient.tabId = tabId;
|
||||
try {
|
||||
Messages.send("seen", payload, recipient);
|
||||
} catch (e) {
|
||||
// likely a privileged tab where our content script couldn't run
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (pending) request.initialUrl = pending.initialUrl;
|
||||
if (type !== "sub_frame") { // we couldn't deliver it to frameId, since it's generally not loaded yet
|
||||
try {
|
||||
await Messages.send("seen",
|
||||
{request, allowed, policyType, ownFrame: true},
|
||||
{tabId, frameId}
|
||||
);
|
||||
} catch (e) {
|
||||
debug(`Couldn't deliver "seen" message for ${type}@${url} ${allowed ? "A" : "F" } to document ${documentUrl} (${frameId}/${tabId})`, e);
|
||||
}
|
||||
}
|
||||
if (frameId === 0) return;
|
||||
try {
|
||||
await Messages.send("seen",
|
||||
{request, allowed, policyType},
|
||||
{tabId, frameId: 0}
|
||||
);
|
||||
} catch (e) {
|
||||
debug(`Couldn't deliver "seen" message to top frame containing ${documentUrl} (${frameId}/${tabId}`, e);
|
||||
}
|
||||
}
|
||||
};
|
||||
const pendingRequests = new Map();
|
||||
function initPendingRequest(request) {
|
||||
let {requestId, url} = request;
|
||||
let redirected = pendingRequests.get(requestId);
|
||||
let initialUrl = redirected ? redirected.initialUrl : url;
|
||||
pendingRequests.set(requestId, {
|
||||
initialUrl, url, redirected,
|
||||
onCompleted: new Set(),
|
||||
});
|
||||
return redirected;
|
||||
}
|
||||
|
||||
let normalizeRequest = UA.isMozilla ? () => {} : request => {
|
||||
if ("initiator" in request && !("originUrl" in request)) {
|
||||
request.originUrl = request.initiator;
|
||||
if (request.type !== "main_frame" && !("documentUrl" in request)) {
|
||||
request.documentUrl = request.initiator;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function intersectCapabilities(perms, request) {
|
||||
let {frameId, frameAncestors, tabId} = request;
|
||||
if (frameId !== 0 && ns.sync.cascadeRestrictions) {
|
||||
let topUrl = frameAncestors && frameAncestors.length
|
||||
&& frameAncestors[frameAncestors.length - 1].url;
|
||||
if (!topUrl) {
|
||||
let tab = TabCache.get(tabId);
|
||||
if (tab) topUrl = tab.url;
|
||||
}
|
||||
if (topUrl) {
|
||||
return ns.policy.cascadeRestrictions(perms, topUrl).capabilities;
|
||||
}
|
||||
}
|
||||
return perms.capabilities;
|
||||
}
|
||||
|
||||
const ABORT = {cancel: true}, ALLOW = {};
|
||||
const recent = {
|
||||
MAX_AGE: 500,
|
||||
_pendingGC: 0,
|
||||
_byUrl: new Map(),
|
||||
find(request, last = this._byUrl.get(request.url)) {
|
||||
if (!last) return null;
|
||||
for (let j = last.length; j-- > 0;) {
|
||||
let other = last[j];
|
||||
if (request.timeStamp - other.timeStamp > this.MAX_AGE) {
|
||||
last.splice(0, ++j);
|
||||
if (last.length === 0) this._byUrl.delete(other.url);
|
||||
break;
|
||||
}
|
||||
if (request.url && other.type === request.type && other.documentUrl === request.documentUrl
|
||||
&& other.tabId === request.tabId && other.frameId === request.frameId) {
|
||||
return other;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
add(request) {
|
||||
let last = this._byUrl.get(request.url);
|
||||
if (!last) {
|
||||
last = [request];
|
||||
this._byUrl.set(request.url, last);
|
||||
} else {
|
||||
last.push(request);
|
||||
}
|
||||
this._gc();
|
||||
return;
|
||||
},
|
||||
_gc(now) {
|
||||
if (!now && this._pendingGC) return;
|
||||
debug("Recent requests garbage collection.");
|
||||
let request = {timeStamp: Date.now()};
|
||||
for (let last of this._byUrl.values()) {
|
||||
this.find(request, last);
|
||||
}
|
||||
this._pendingGC = this._byUrl.size ?
|
||||
setTimeout(() => this._gc(true), 1000)
|
||||
: 0;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function blockLANRequest(request) {
|
||||
debug("WAN->LAN request blocked", request);
|
||||
let r = Object.assign({}, request);
|
||||
r.url = request.originUrl; // we want to report the origin as needing the permission
|
||||
Content.reportTo(r, false, "lan")
|
||||
return ABORT;
|
||||
}
|
||||
|
||||
function checkLANRequest(request) {
|
||||
if (!ns.isEnforced(request.tabId)) return ALLOW;
|
||||
let {originUrl, url} = request;
|
||||
if (originUrl && !Sites.isInternal(originUrl) && url.startsWith("http") &&
|
||||
!ns.policy.can(originUrl, "lan", ns.policyContext(request))) {
|
||||
// we want to block any request whose origin resolves to at least one external WAN IP
|
||||
// and whose destination resolves to at least one LAN IP
|
||||
let {proxyInfo} = request; // see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/proxy/ProxyInfo
|
||||
let neverDNS = (proxyInfo && (proxyInfo.type && proxyInfo.type.startsWith("http") || proxyInfo.proxyDNS))
|
||||
|| !(UA.isMozilla && DNS.supported);
|
||||
if (neverDNS) {
|
||||
// On Chromium we must do it synchronously: we need to sacrifice DNS resolution and check just numeric addresses :(
|
||||
// (the Tor Browser, on the other hand, does DNS resolution and boundary checks on its own and breaks the DNS API)
|
||||
return iputil.isLocalURI(url, false, neverDNS) && !iputil.isLocalURI(originUrl, true, neverDNS)
|
||||
? blockLANRequest(request)
|
||||
: ALLOW;
|
||||
}
|
||||
// Firefox does support asynchronous webRequest: let's return a Promise and perform DNS resolution.
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
resolve(await iputil.isLocalURI(url, false) && !(await iputil.isLocalURI(originUrl, true))
|
||||
? blockLANRequest(request)
|
||||
: ALLOW
|
||||
);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const listeners = {
|
||||
onBeforeRequest(request) {
|
||||
try {
|
||||
if (browser.runtime.onSyncMessage && browser.runtime.onSyncMessage.isMessageRequest(request)) return ALLOW;
|
||||
normalizeRequest(request);
|
||||
initPendingRequest(request);
|
||||
|
||||
let {policy} = ns
|
||||
let {tabId, type, url, originUrl} = request;
|
||||
|
||||
if (type in policyTypesMap) {
|
||||
let previous = recent.find(request);
|
||||
if (previous) {
|
||||
debug("Rapid fire request", previous);
|
||||
return previous.return;
|
||||
}
|
||||
(previous = request).return = ALLOW;
|
||||
recent.add(previous);
|
||||
|
||||
let policyType = policyTypesMap[type];
|
||||
let {documentUrl} = request;
|
||||
if (!ns.isEnforced(tabId)) {
|
||||
if (ns.unrestrictedTabs.has(tabId) && type.endsWith("frame") && url.startsWith("https:")) {
|
||||
TabStatus.addOrigin(tabId, url);
|
||||
}
|
||||
if (type !== "main_frame") {
|
||||
Content.reportTo(request, true, policyType);
|
||||
}
|
||||
return ALLOW;
|
||||
}
|
||||
let isFetch = "fetch" === policyType;
|
||||
|
||||
if ((isFetch || "frame" === policyType) &&
|
||||
(((isFetch && !originUrl
|
||||
|| url === originUrl) && originUrl === documentUrl
|
||||
// some extensions make them both undefined,
|
||||
// see https://github.com/eight04/image-picka/issues/150
|
||||
) ||
|
||||
Sites.isInternal(originUrl))
|
||||
) {
|
||||
// livemark request or similar browser-internal, always allow;
|
||||
return ALLOW;
|
||||
}
|
||||
|
||||
if (/^(?:data|blob):/.test(url)) {
|
||||
request._dataUrl = url;
|
||||
request.url = url = documentUrl || originUrl;
|
||||
}
|
||||
|
||||
let allowed = Sites.isInternal(url);
|
||||
if (!allowed) {
|
||||
if (tabId < 0 && documentUrl && documentUrl.startsWith("https:")) {
|
||||
allowed = [...ns.unrestrictedTabs]
|
||||
.some(tabId => TabStatus.hasOrigin(tabId, documentUrl));
|
||||
}
|
||||
if (!allowed) {
|
||||
let capabilities = intersectCapabilities(
|
||||
policy.get(url, ns.policyContext(request)).perms,
|
||||
request);
|
||||
allowed = !policyType || capabilities.has(policyType);
|
||||
if (allowed && request._dataUrl && type.endsWith("frame")) {
|
||||
let blocker = csp.buildFromCapabilities(capabilities);
|
||||
if (blocker) {
|
||||
let redirectUrl = CSP.patchDataURI(request._dataUrl, blocker);
|
||||
if (redirectUrl !== request._dataUrl) {
|
||||
return previous.return = {redirectUrl};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (type !== "main_frame") {
|
||||
Content.reportTo(request, allowed, policyType);
|
||||
}
|
||||
if (!allowed) {
|
||||
debug(`Blocking ${policyType}`, request);
|
||||
TabStatus.record(request, "blocked");
|
||||
return previous.return = ABORT;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
error(e);
|
||||
}
|
||||
return ALLOW;
|
||||
},
|
||||
onBeforeSendHeaders(request) {
|
||||
normalizeRequest(request);
|
||||
return checkLANRequest(request);
|
||||
},
|
||||
onHeadersReceived(request) {
|
||||
// called for main_frame, sub_frame and object
|
||||
|
||||
// check for duplicate calls
|
||||
let pending = pendingRequests.get(request.requestId);
|
||||
if (pending) {
|
||||
if (pending.headersProcessed) {
|
||||
if (!request.fromCache) {
|
||||
debug("Headers already processed, skipping ", request);
|
||||
return ALLOW;
|
||||
}
|
||||
debug("Reprocessing headers for cached request ", request);
|
||||
} else {
|
||||
debug("onHeadersReceived", request);
|
||||
}
|
||||
} else {
|
||||
debug("[WARNING] no pending information for ", request);
|
||||
initPendingRequest(request);
|
||||
pending = pendingRequests.get(request.requestId);
|
||||
}
|
||||
if (request.fromCache && listeners.onHeadersReceived.resetCSP && !pending.resetCachedCSP) {
|
||||
debug("Resetting CSP Headers");
|
||||
pending.resetCachedCSP = true;
|
||||
let {responseHeaders} = request;
|
||||
let headersCount = responseHeaders.length;
|
||||
let purged = false;
|
||||
responseHeaders.forEach((h, index) => {
|
||||
if (csp.isMine(h)) {
|
||||
responseHeaders.splice(index, 1);
|
||||
}
|
||||
});
|
||||
if (headersCount > responseHeaders.length) {
|
||||
debug("Resetting cached NoScript CSP header(s)", request);
|
||||
return {responseHeaders};
|
||||
}
|
||||
}
|
||||
|
||||
normalizeRequest(request);
|
||||
let result = ALLOW;
|
||||
let promises = [];
|
||||
let headersModified = false;
|
||||
|
||||
pending.headersProcessed = true;
|
||||
let {url, documentUrl, tabId, responseHeaders, type} = request;
|
||||
let isMainFrame = type === "main_frame";
|
||||
try {
|
||||
let capabilities;
|
||||
if (ns.isEnforced(tabId)) {
|
||||
let policy = ns.policy;
|
||||
let {perms} = policy.get(url, ns.policyContext(request));
|
||||
if (isMainFrame) {
|
||||
if (policy.autoAllowTop && perms === policy.DEFAULT) {
|
||||
policy.set(Sites.optimalKey(url), perms = policy.TRUSTED.tempTwin);
|
||||
}
|
||||
capabilities = perms.capabilities;
|
||||
} else {
|
||||
capabilities = intersectCapabilities(perms, request);
|
||||
}
|
||||
} // else unrestricted, either globally or per-tab
|
||||
if (isMainFrame && !TabStatus.map.has(tabId)) {
|
||||
debug("No TabStatus data yet for noscriptFrame", tabId);
|
||||
TabStatus.record(request, "noscriptFrame",
|
||||
capabilities && !capabilities.has("script"));
|
||||
}
|
||||
let header = csp.patchHeaders(responseHeaders, capabilities);
|
||||
/*
|
||||
// Uncomment me to disable networking-level CSP for debugging purposes
|
||||
header = null;
|
||||
*/
|
||||
if (header) {
|
||||
pending.cspHeader = header;
|
||||
debug(`CSP blocker on %s:`, url, header.value);
|
||||
headersModified = true;
|
||||
}
|
||||
if (headersModified) {
|
||||
result = {responseHeaders};
|
||||
debug("Headers changed ", request);
|
||||
}
|
||||
} catch (e) {
|
||||
error(e, "Error in onHeadersReceived", request);
|
||||
}
|
||||
|
||||
promises = promises.filter(p => p instanceof Promise);
|
||||
if (promises.length > 0) {
|
||||
return Promise.all(promises).then(() => result);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
onResponseStarted(request) {
|
||||
normalizeRequest(request);
|
||||
debug("onResponseStarted", request);
|
||||
let {requestId, url, tabId, frameId, type} = request;
|
||||
if (type === "main_frame") {
|
||||
TabStatus.initTab(tabId);
|
||||
}
|
||||
let scriptBlocked = request.responseHeaders.some(
|
||||
h => csp.isMine(h) && csp.blocks(h.value, "script")
|
||||
);
|
||||
debug("%s scriptBlocked=%s setting noscriptFrame on ", url, scriptBlocked, tabId, frameId);
|
||||
TabStatus.record(request, "noscriptFrame", scriptBlocked);
|
||||
let pending = pendingRequests.get(requestId);
|
||||
if (pending) {
|
||||
|
||||
pending.scriptBlocked = scriptBlocked;
|
||||
if (!(pending.headersProcessed &&
|
||||
(scriptBlocked || ns.requestCan(request, "script"))
|
||||
)) {
|
||||
debug("[WARNING] onHeadersReceived %s %o", frameId, tabId,
|
||||
pending.headersProcessed ? "has been overridden on": "could not process",
|
||||
request);
|
||||
}
|
||||
}
|
||||
},
|
||||
onCompleted(request) {
|
||||
let {requestId} = request;
|
||||
if (pendingRequests.has(requestId)) {
|
||||
let r = pendingRequests.get(requestId);
|
||||
pendingRequests.delete(requestId);
|
||||
for (let callback of r.onCompleted) {
|
||||
try {
|
||||
callback(request, r);
|
||||
} catch (e) {
|
||||
error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onErrorOccurred(request) {
|
||||
pendingRequests.delete(request.requestId);
|
||||
}
|
||||
};
|
||||
function fakeRequestFromCSP(report, request) {
|
||||
let type = report["violated-directive"].split("-", 1)[0]; // e.g. script-src 'none' => script
|
||||
if (type === "frame") type = "sub_frame";
|
||||
let url = report['blocked-uri'];
|
||||
if (!url || url === 'self') url = request.documentUrl;
|
||||
return Object.assign({}, request, {
|
||||
url,
|
||||
type,
|
||||
});
|
||||
}
|
||||
|
||||
let utf8Decoder = new TextDecoder("UTF-8");
|
||||
function onViolationReport(request) {
|
||||
try {
|
||||
let text = utf8Decoder.decode(request.requestBody.raw[0].bytes);
|
||||
if (text.includes(`"inline"`)) return ABORT;
|
||||
let report = JSON.parse(text)["csp-report"];
|
||||
let originalPolicy = report["original-policy"]
|
||||
debug("CSP report", report);
|
||||
let blockedURI = report['blocked-uri'];
|
||||
if (blockedURI && blockedURI !== 'self') {
|
||||
let r = fakeRequestFromCSP(report, request);
|
||||
if (!/:/.test(r.url)) r.url = request.documentUrl;
|
||||
Content.reportTo(r, false, policyTypesMap[r.type]);
|
||||
TabStatus.record(r, "blocked");
|
||||
} else if (report["violated-directive"].startsWith("script-src") && (originalPolicy.includes("script-src 'none'"))) {
|
||||
let r = fakeRequestFromCSP(report, request);
|
||||
Content.reportTo(r, false, "script"); // NEW
|
||||
TabStatus.record(r, "noscriptFrame", true);
|
||||
}
|
||||
} catch(e) {
|
||||
error(e);
|
||||
}
|
||||
return ABORT;
|
||||
}
|
||||
|
||||
function injectPolicyScript(details) {
|
||||
let {url, tabId, frameId} = details;
|
||||
let policy = ns.computeChildPolicy({url}, {tab: {id: tabId}, frameId});
|
||||
policy.navigationURL = url;
|
||||
let debugStatement = ns.local.debug ? `
|
||||
let mark = Date.now() + ":" + Math.random();
|
||||
console.debug("domPolicy", domPolicy, document.readyState, location.href, mark, window.ns);` : '';
|
||||
return `
|
||||
let domPolicy = ${JSON.stringify(policy)};
|
||||
let {ns} = window;
|
||||
if (ns) {
|
||||
ns.domPolicy = domPolicy;
|
||||
if (ns.setup) {
|
||||
if (ns.syncSetup) ns.syncSetup(domPolicy);
|
||||
else ns.setup(domPolicy);
|
||||
} ;
|
||||
} else {
|
||||
window.ns = {domPolicy}
|
||||
}
|
||||
${debugStatement}`;
|
||||
}
|
||||
|
||||
const RequestGuard = {
|
||||
async start() {
|
||||
Messages.addHandler(messageHandler);
|
||||
let wr = browser.webRequest;
|
||||
let listen = (what, ...args) => wr[what].addListener(listeners[what], ...args);
|
||||
let allUrls = ["<all_urls>"];
|
||||
let docTypes = ["main_frame", "sub_frame", "object"];
|
||||
let filterDocs = {urls: allUrls, types: docTypes};
|
||||
let filterAll = {urls: allUrls};
|
||||
listen("onBeforeRequest", filterAll, ["blocking"]);
|
||||
listen("onBeforeSendHeaders", filterAll, ["blocking"]);
|
||||
|
||||
let mergingCSP = "getBrowserInfo" in browser.runtime;
|
||||
if (mergingCSP) {
|
||||
let {vendor, version} = await browser.runtime.getBrowserInfo();
|
||||
mergingCSP = vendor === "Mozilla" && parseInt(version) >= 77;
|
||||
}
|
||||
if (mergingCSP) {
|
||||
// In Gecko>=77 (https://bugzilla.mozilla.org/show_bug.cgi?id=1462989)
|
||||
// we need to cleanup our own cached headers in a dedicated listener :(
|
||||
// see also https://trac.torproject.org/projects/tor/ticket/34305
|
||||
wr.onHeadersReceived.addListener(
|
||||
listeners.onHeadersReceived.resetCSP = request => {
|
||||
return listeners.onHeadersReceived(request);
|
||||
}, filterDocs, ["blocking", "responseHeaders"]);
|
||||
}
|
||||
listen("onHeadersReceived", filterDocs, ["blocking", "responseHeaders"]);
|
||||
// Still, other extensions may accidentally delete our CSP header
|
||||
// if called before us, hence we try our best reinjecting it in the end
|
||||
(listeners.onHeadersReceivedLast =
|
||||
new LastListener(wr.onHeadersReceived, request => {
|
||||
let {requestId, responseHeaders} = request;
|
||||
let pending = pendingRequests.get(request.requestId);
|
||||
if (pending && pending.headersProcessed) {
|
||||
let {cspHeader} = pending;
|
||||
if (cspHeader) {
|
||||
responseHeaders.push(cspHeader);
|
||||
return {responseHeaders};
|
||||
}
|
||||
} else {
|
||||
debug("[WARNING] onHeadersReceived not called (yet?)", request);
|
||||
}
|
||||
return ALLOW;
|
||||
}, filterDocs, ["blocking", "responseHeaders"])).install();
|
||||
|
||||
listen("onResponseStarted", filterDocs, ["responseHeaders"]);
|
||||
listen("onCompleted", filterAll);
|
||||
listen("onErrorOccurred", filterAll);
|
||||
if (csp.reportURI) {
|
||||
wr.onBeforeRequest.addListener(onViolationReport,
|
||||
{urls: [csp.reportURI], types: ["csp_report"]}, ["blocking", "requestBody"]);
|
||||
}
|
||||
DocStartInjection.register(injectPolicyScript);
|
||||
TabStatus.probe();
|
||||
},
|
||||
stop() {
|
||||
let wr = browser.webRequest;
|
||||
for (let [name, listener] of Object.entries(listeners)) {
|
||||
if (typeof listener === "function") {
|
||||
wr[name].removeListener(listener);
|
||||
} else if (listener instanceof LastListener) {
|
||||
listener.uninstall();
|
||||
}
|
||||
}
|
||||
wr.onBeforeRequest.removeListener(onViolationReport);
|
||||
if (listeners.onHeadersReceived.resetCSP) {
|
||||
wr.onHeadersReceived.removeListener(listeners.onHeadersReceived.resetCSP);
|
||||
}
|
||||
DocStartInjection.unregister(injectPolicyScript);
|
||||
Messages.removeHandler(messageHandler);
|
||||
}
|
||||
};
|
||||
return RequestGuard;
|
||||
})();
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
/*
|
||||
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
|
||||
*
|
||||
* Copyright (C) 2005-2021 Giorgio Maone <https://maone.net>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation, either version 3 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
var Settings = {
|
||||
|
||||
async import(data) {
|
||||
|
||||
// figure out whether it's just a whitelist, a legacy backup or a "Quantum" export
|
||||
try {
|
||||
let json = JSON.parse(data);
|
||||
if (json.whitelist) {
|
||||
return await this.importLegacy(json);
|
||||
}
|
||||
if (json.trusted) {
|
||||
return await this.importPolicy(json);
|
||||
}
|
||||
if (json.policy) {
|
||||
return await this.importSettings(json);
|
||||
}
|
||||
} catch (e) {
|
||||
if (data.includes("[UNTRUSTED]")) await this.importLists(data);
|
||||
else throw e;
|
||||
}
|
||||
},
|
||||
|
||||
async importLegacy(json) {
|
||||
await include("/legacy/Legacy.js");
|
||||
if (await Legacy.import(json)) {
|
||||
try {
|
||||
ns.policy = Legacy.migratePolicy();
|
||||
await ns.savePolicy();
|
||||
await Legacy.persist();
|
||||
return true;
|
||||
} catch (e) {
|
||||
error(e, "Importing legacy settings");
|
||||
Legacy.migrated = Legacy.undo;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
async importLists(data) {
|
||||
await include("/legacy/Legacy.js");
|
||||
try {
|
||||
let [trusted, untrusted] = Legacy.extractLists(data.split("[UNTRUSTED]"));
|
||||
let policy = ns.policy;
|
||||
for (let site of trusted) {
|
||||
policy.set(site, policy.TRUSTED);
|
||||
}
|
||||
for (let site of untrusted) {
|
||||
policy.set(site, policy.UNTRUSTED, true);
|
||||
}
|
||||
await ns.savePolicy();
|
||||
} catch (e) {
|
||||
error(e, "Importing white/black lists %s", data);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
async importPolicy(json) {
|
||||
try {
|
||||
ns.policy = new Policy(json);
|
||||
await ns.savePolicy();
|
||||
return true;
|
||||
} catch (e) {
|
||||
error(e, "Importing policy %o", json);
|
||||
}
|
||||
},
|
||||
|
||||
async importSettings(json) {
|
||||
try {
|
||||
await this.update(json);
|
||||
return true;
|
||||
} catch (e) {
|
||||
error(e, "Importing settings %o", json);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
async update(settings) {
|
||||
let {
|
||||
policy,
|
||||
xssUserChoices,
|
||||
tabId,
|
||||
unrestrictedTab,
|
||||
reloadAffected,
|
||||
isTorBrowser,
|
||||
} = settings;
|
||||
debug("Received settings ", settings);
|
||||
let oldDebug = ns.local.debug;
|
||||
|
||||
let reloadOptionsUI = false;
|
||||
|
||||
if (isTorBrowser) {
|
||||
// Tor Browser-specific settings
|
||||
ns.defaults.local.isTorBrowser = true; // prevents reset from forgetting
|
||||
ns.defaults.sync.cascadeRestrictions = true; // we want this to be the default even on reset
|
||||
Sites.onionSecure = true;
|
||||
|
||||
if (!this.gotTorBrowserInit) {
|
||||
// First initialization message from the Tor Browser
|
||||
this.gotTorBrowserInit = true;
|
||||
if (ns.sync.overrideTorBrowserPolicy) {
|
||||
// If the user chose to override Tor Browser's policy we skip
|
||||
// copying the Security Level preset on startup (only).
|
||||
// Manually changing the security level works as usual.
|
||||
ns.local.isTorBrowser = true;
|
||||
await ns.save(ns.local);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
reloadOptionsUI = true;
|
||||
}
|
||||
|
||||
let torBrowserSettings = {
|
||||
local: {
|
||||
isTorBrowser: true,
|
||||
},
|
||||
sync: {
|
||||
cascadeRestrictions: true,
|
||||
}
|
||||
}
|
||||
for (let [storage, prefs] of Object.entries(torBrowserSettings)) {
|
||||
settings[storage] = Object.assign(settings[storage] || {}, prefs);
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.sync === null) {
|
||||
// user is resetting options
|
||||
policy = this.createDefaultDryPolicy();
|
||||
|
||||
// overriden defaults when user manually resets options
|
||||
|
||||
// we want the reset options to stick (otherwise it gets very confusing)
|
||||
ns.defaults.sync.overrideTorBrowserPolicy = true;
|
||||
reloadOptionsUI = true;
|
||||
}
|
||||
|
||||
await Promise.all(["local", "sync"].map(
|
||||
async storage => (settings[storage] || // changed or...
|
||||
settings[storage] === null // ... needs reset to default
|
||||
) && await ns.save(settings[storage]
|
||||
? Object.assign(ns[storage], settings[storage]) : ns[storage] = Object.assign({}, ns.defaults[storage]))
|
||||
));
|
||||
if (ns.local.debug !== oldDebug) {
|
||||
await include("/nscl/common/log.js");
|
||||
if (oldDebug) debug = () => {};
|
||||
}
|
||||
|
||||
if (policy) {
|
||||
ns.policy = new Policy(policy);
|
||||
await ns.savePolicy();
|
||||
}
|
||||
|
||||
if (typeof unrestrictedTab === "boolean") {
|
||||
ns.unrestrictedTabs[unrestrictedTab ? "add" : "delete"](tabId);
|
||||
}
|
||||
if (reloadAffected) {
|
||||
browser.tabs.reload(tabId);
|
||||
}
|
||||
|
||||
if (xssUserChoices) await XSS.saveUserChoices(xssUserChoices);
|
||||
|
||||
if (ns.sync.xss) {
|
||||
XSS.start();
|
||||
} else {
|
||||
XSS.stop();
|
||||
}
|
||||
|
||||
if (reloadOptionsUI) await this.reloadOptionsUI();
|
||||
},
|
||||
|
||||
createDefaultDryPolicy() {
|
||||
let dp = new Policy().dry();
|
||||
dp.sites.trusted = `
|
||||
addons.mozilla.org
|
||||
afx.ms ajax.aspnetcdn.com
|
||||
ajax.googleapis.com bootstrapcdn.com
|
||||
code.jquery.com firstdata.com firstdata.lv gfx.ms
|
||||
google.com googlevideo.com gstatic.com
|
||||
hotmail.com live.com live.net
|
||||
maps.googleapis.com mozilla.net
|
||||
netflix.com nflxext.com nflximg.com nflxvideo.net
|
||||
noscript.net
|
||||
outlook.com passport.com passport.net passportimages.com
|
||||
paypal.com paypalobjects.com
|
||||
securecode.com securesuite.net sfx.ms tinymce.cachefly.net
|
||||
wlxrs.com
|
||||
yahoo.com yahooapis.com
|
||||
yimg.com youtube.com ytimg.com
|
||||
`.trim().split(/\s+/).map(Sites.secureDomainKey);
|
||||
return dp;
|
||||
},
|
||||
|
||||
export() {
|
||||
return JSON.stringify({
|
||||
policy: ns.policy.dry(),
|
||||
local: ns.local,
|
||||
sync: ns.sync,
|
||||
xssUserChoices: XSS.getUserChoices(),
|
||||
}, null, 2);
|
||||
},
|
||||
|
||||
async reloadOptionsUI() {
|
||||
try {
|
||||
for (let t of await browser.tabs.query({url: browser.runtime.getURL(
|
||||
browser.runtime.getManifest().options_ui.page) })
|
||||
) {
|
||||
browser.tabs.reload(t.id);
|
||||
};
|
||||
} catch (e) {
|
||||
error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
|
||||
*
|
||||
* Copyright (C) 2005-2021 Giorgio Maone <https://maone.net>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation, either version 3 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
function deferWebTraffic(promiseToWaitFor, next) {
|
||||
debug("deferWebTraffic on %o", promiseToWaitFor);
|
||||
let seenTabs = new Set();
|
||||
function checkNavigation(nav) {
|
||||
if (nav.tabId !== browser.tabs.TAB_ID_NONE && nav.url.startsWith("http")) {
|
||||
let seen = seenTabs.has(nav.tabId);
|
||||
debug(`%s navigation %o`, seen ? "seen" : "unseen", nav);
|
||||
if (!seen) {
|
||||
reloadTab(nav.tabId);
|
||||
}
|
||||
}
|
||||
}
|
||||
browser.webNavigation.onCommitted.addListener(checkNavigation);
|
||||
function reloadTab(tabId) {
|
||||
seenTabs.add(tabId);
|
||||
try {
|
||||
browser.tabs.executeScript(tabId, {
|
||||
runAt: "document_start",
|
||||
code: "if (performance.now() < 60000) window.location.reload(false)"
|
||||
});
|
||||
debug("Reloading tab", tabId);
|
||||
} catch (e) {
|
||||
error(e, "Can't reload tab", tabId);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitFor(request) {
|
||||
let {type, documentUrl, url, tabId, frameId} = request;
|
||||
if (tabId === browser.tabs.TAB_ID_NONE) return;
|
||||
if (!seenTabs.has(tabId)) {
|
||||
if (type === "main_frame") {
|
||||
seenTabs.add(tabId);
|
||||
} else if (documentUrl) {
|
||||
if (frameId !== 0 && request.frameAncestors) {
|
||||
documentUrl = request.frameAncestors.pop().url;
|
||||
}
|
||||
reloadTab(tabId);
|
||||
}
|
||||
}
|
||||
debug("Deferring %s %s from %s", type, url, documentUrl);
|
||||
try {
|
||||
await promiseToWaitFor;
|
||||
} catch (e) {
|
||||
error(e);
|
||||
}
|
||||
debug("Green light to %s %s from %s", type, url, documentUrl);
|
||||
}
|
||||
|
||||
function spyTabs(request) {
|
||||
debug("Spying request %o", request);
|
||||
}
|
||||
|
||||
browser.webRequest.onHeadersReceived.addListener(spyTabs, {
|
||||
urls: ["<all_urls>"],
|
||||
types: ["main_frame"],
|
||||
}, ["blocking", "responseHeaders"]);
|
||||
browser.webRequest.onBeforeRequest.addListener(waitFor, {
|
||||
urls: ["<all_urls>"]
|
||||
}, ["blocking"]);
|
||||
|
||||
(async () => {
|
||||
await promiseToWaitFor;
|
||||
browser.webNavigation.onCommitted.removeListener(checkNavigation);
|
||||
browser.webRequest.onBeforeRequest.removeListener(waitFor);
|
||||
browser.webRequest.onHeadersReceived.removeListener(spyTabs);
|
||||
if (next) next();
|
||||
})();
|
||||
}
|
||||
|
|
@ -0,0 +1,384 @@
|
|||
/*
|
||||
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
|
||||
*
|
||||
* Copyright (C) 2005-2021 Giorgio Maone <https://maone.net>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation, either version 3 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
{
|
||||
'use strict';
|
||||
{
|
||||
for (let event of ["onInstalled", "onUpdateAvailable"]) {
|
||||
browser.runtime[event].addListener(async details => {
|
||||
await include("/bg/LifeCycle.js");
|
||||
LifeCycle[event](details);
|
||||
});
|
||||
}
|
||||
}
|
||||
let popupURL = browser.runtime.getURL("/ui/popup.html");
|
||||
let popupFor = tabId => `${popupURL}#tab${tabId}`;
|
||||
|
||||
let ctxMenuId = "noscript-ctx-menu";
|
||||
|
||||
async function toggleCtxMenuItem(show = ns.local.showCtxMenuItem) {
|
||||
if (!("contextMenus" in browser)) return;
|
||||
let id = ctxMenuId;
|
||||
try {
|
||||
await browser.contextMenus.remove(id);
|
||||
} catch (e) {}
|
||||
|
||||
if (show) {
|
||||
browser.contextMenus.create({
|
||||
id,
|
||||
title: "NoScript",
|
||||
contexts: ["all"]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await Defaults.init();
|
||||
|
||||
if (!ns.policy) { // it could have been already retrieved by LifeCycle
|
||||
let policyData = (await Storage.get("sync", "policy")).policy;
|
||||
if (policyData && policyData.DEFAULT) {
|
||||
ns.policy = new Policy(policyData);
|
||||
if (ns.local.enforceOnRestart && !ns.policy.enforced) {
|
||||
ns.policy.enforced = true;
|
||||
await ns.savePolicy();
|
||||
}
|
||||
} else {
|
||||
await include("/legacy/Legacy.js");
|
||||
ns.policy = await Legacy.createOrMigratePolicy();
|
||||
await ns.savePolicy();
|
||||
}
|
||||
}
|
||||
|
||||
let {isTorBrowser} = ns.local;
|
||||
Sites.onionSecure = isTorBrowser;
|
||||
|
||||
if (!isTorBrowser) {
|
||||
await include("/nscl/service/prefetchCSSResources.js");
|
||||
}
|
||||
|
||||
await RequestGuard.start();
|
||||
await XSS.start(); // we must start it anyway to initialize sub-objects
|
||||
if (!ns.sync.xss) {
|
||||
XSS.stop();
|
||||
}
|
||||
|
||||
Messages.addHandler(messageHandler);
|
||||
|
||||
try {
|
||||
await Messages.send("started");
|
||||
} catch (e) {
|
||||
// no embedder to answer us
|
||||
}
|
||||
log("STARTED");
|
||||
await include("/bg/popupHandler.js");
|
||||
};
|
||||
|
||||
let Commands = {
|
||||
async openPageUI() {
|
||||
if (ns.popupOpening) return;
|
||||
ns.popupOpening = true;
|
||||
ns.popupOpened = false;
|
||||
let openPanel = async () => {
|
||||
ns.popupOpening = false;
|
||||
if (ns.popupOpened) return;
|
||||
messageHandler.openStandalonePopup();
|
||||
};
|
||||
try {
|
||||
await browser.browserAction.openPopup();
|
||||
setTimeout(openPanel, 500);
|
||||
return;
|
||||
} catch (e) {
|
||||
openPanel();
|
||||
debug(e);
|
||||
}
|
||||
},
|
||||
|
||||
togglePermissions() {},
|
||||
install() {
|
||||
if ("command" in browser) {
|
||||
// keyboard shortcuts
|
||||
browser.commands.onCommand.addListener(cmd => {
|
||||
if (cmd in Commands) {
|
||||
Commands[cmd]();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ("contextMenus" in browser) {
|
||||
toggleCtxMenuItem();
|
||||
browser.contextMenus.onClicked.addListener((info, tab) => {
|
||||
if (info.menuItemId == ctxMenuId) {
|
||||
this.openPageUI();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// wiring main UI
|
||||
let ba = browser.browserAction;
|
||||
if ("setIcon" in ba) {
|
||||
//desktop or Fenix
|
||||
ba.setPopup({
|
||||
popup: popupURL
|
||||
});
|
||||
} else {
|
||||
// Fennec
|
||||
ba.onClicked.addListener(async tab => {
|
||||
try {
|
||||
await browser.tabs.remove(await browser.tabs.query({
|
||||
url: popupURL
|
||||
}));
|
||||
} catch (e) {}
|
||||
await browser.tabs.create({
|
||||
url: popupFor(tab.id)
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let messageHandler = {
|
||||
async updateSettings(settings, sender) {
|
||||
await Settings.update(settings);
|
||||
toggleCtxMenuItem();
|
||||
},
|
||||
|
||||
async broadcastSettings({
|
||||
tabId = -1
|
||||
}) {
|
||||
let policy = ns.policy.dry(true);
|
||||
let seen = tabId !== -1 ? await ns.collectSeen(tabId) : null;
|
||||
let xssUserChoices = await XSS.getUserChoices();
|
||||
await Messages.send("settings", {
|
||||
policy,
|
||||
seen,
|
||||
xssUserChoices,
|
||||
local: ns.local,
|
||||
sync: ns.sync,
|
||||
unrestrictedTab: ns.unrestrictedTabs.has(tabId),
|
||||
tabId,
|
||||
xssBlockedInTab: XSS.getBlockedInTab(tabId),
|
||||
});
|
||||
},
|
||||
|
||||
async exportSettings() {
|
||||
return Settings.export();
|
||||
},
|
||||
|
||||
async importSettings({data}) {
|
||||
return await Settings.import(data);
|
||||
},
|
||||
|
||||
async fetchChildPolicy({url, contextUrl}, sender) {
|
||||
await ns.initializing;
|
||||
return (messageHandler.fetchChildPolicy =
|
||||
ns.computeChildPolicy)(...arguments);
|
||||
},
|
||||
|
||||
async openStandalonePopup() {
|
||||
let [tab] = (await browser.tabs.query({
|
||||
currentWindow: true,
|
||||
active: true
|
||||
}));
|
||||
|
||||
if (!tab || tab.id === -1) {
|
||||
log("No tab found to open the UI for");
|
||||
return;
|
||||
}
|
||||
let win = await browser.windows.getCurrent();
|
||||
browser.windows.create({
|
||||
url: popupFor(tab.id),
|
||||
width: 800,
|
||||
height: 600,
|
||||
top: win.top + 48,
|
||||
left: win.left + 48,
|
||||
type: "panel"
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function onSyncMessage(msg, sender) {
|
||||
switch(msg.id) {
|
||||
case "fetchChildPolicy":
|
||||
return messageHandler.fetchChildPolicy(msg, sender);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var ns = {
|
||||
running: false,
|
||||
policy: null,
|
||||
local: null,
|
||||
sync: null,
|
||||
initializing: null,
|
||||
unrestrictedTabs: new Set(),
|
||||
isEnforced(tabId = -1) {
|
||||
return this.policy.enforced && (tabId === -1 || !this.unrestrictedTabs.has(tabId));
|
||||
},
|
||||
policyContext(contextualData) {
|
||||
// contextualData (e.g. a request details object) must contain a tab, a tabId or a documentUrl
|
||||
// (used as a fallback if tab's top URL cannot be retrieved, e.g. in service workers)
|
||||
let {tab, tabId, documentUrl, url} = contextualData;
|
||||
if (!tab) {
|
||||
if (contextualData.type === "main_frame") return url;
|
||||
tab = tabId !== -1 && TabCache.get(tabId);
|
||||
}
|
||||
return tab && tab.url || documentUrl || url;
|
||||
},
|
||||
requestCan(request, capability) {
|
||||
return !this.isEnforced(request.tabId) || this.policy.can(request.url, capability, this.policyContext(request));
|
||||
},
|
||||
|
||||
computeChildPolicy({url, contextUrl}, sender) {
|
||||
let {tab, frameId} = sender;
|
||||
let policy = ns.policy;
|
||||
let {isTorBrowser} = ns.local;
|
||||
if (!policy) {
|
||||
console.log("Policy is null, initializing: %o, sending fallback.", ns.initializing);
|
||||
return {
|
||||
permissions: new Permissions(Permissions.DEFAULT).dry(),
|
||||
unrestricted: false,
|
||||
cascaded: false,
|
||||
fallback: true,
|
||||
isTorBrowser,
|
||||
};
|
||||
}
|
||||
|
||||
let tabId = tab ? tab.id : -1;
|
||||
let topUrl;
|
||||
if (frameId === 0) {
|
||||
topUrl = url;
|
||||
} else if (tab) {
|
||||
if (!tab.url) tab = TabCache.get(tabId);
|
||||
if (tab) topUrl = tab.url;
|
||||
}
|
||||
if (!topUrl) topUrl = url;
|
||||
if (!contextUrl) contextUrl = topUrl;
|
||||
|
||||
if (Sites.isInternal(url) || !ns.isEnforced(tabId)) {
|
||||
policy = null;
|
||||
}
|
||||
|
||||
let permissions, unrestricted, cascaded;
|
||||
if (policy) {
|
||||
let perms = policy.get(url, contextUrl).perms;
|
||||
cascaded = topUrl && ns.sync.cascadeRestrictions;
|
||||
if (cascaded) {
|
||||
perms = policy.cascadeRestrictions(perms, topUrl);
|
||||
}
|
||||
permissions = perms.dry();
|
||||
} else {
|
||||
// otherwise either internal URL or unrestricted
|
||||
permissions = new Permissions(Permissions.ALL).dry();
|
||||
unrestricted = true;
|
||||
cascaded = false;
|
||||
}
|
||||
return {permissions, unrestricted, cascaded, isTorBrowser};
|
||||
},
|
||||
|
||||
start() {
|
||||
if (this.running) return;
|
||||
this.running = true;
|
||||
browser.runtime.onSyncMessage.addListener(onSyncMessage);
|
||||
deferWebTraffic(this.initializing = init(),
|
||||
async () => {
|
||||
Commands.install();
|
||||
try {
|
||||
this.devMode = (await browser.management.getSelf()).installType === "development";
|
||||
} catch(e) {}
|
||||
if (!(this.local.debug || this.devMode)) {
|
||||
debug = () => {}; // suppress verbosity
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
stop() {
|
||||
if (!this.running) return;
|
||||
this.running = false;
|
||||
browser.runtime.onSyncMessage.removeListener(onSyncMessage);
|
||||
Messages.removeHandler(messageHandler);
|
||||
RequestGuard.stop();
|
||||
log("STOPPED");
|
||||
},
|
||||
|
||||
test() {
|
||||
include("/test/run.js");
|
||||
},
|
||||
|
||||
async testIC(callbackOrUrl) {
|
||||
await include("xss/InjectionChecker.js");
|
||||
let IC = await XSS.InjectionChecker;
|
||||
let ic = new IC();
|
||||
ic.logEnabled = true;
|
||||
return (typeof callbackOrUrl === "function")
|
||||
? await callbackOrUrl(ic)
|
||||
: ic.checkUrl(callbackOrUrl);
|
||||
},
|
||||
|
||||
async savePolicy() {
|
||||
if (this.policy) {
|
||||
await Storage.set("sync", {
|
||||
policy: this.policy.dry()
|
||||
});
|
||||
await browser.webRequest.handlerBehaviorChanged()
|
||||
}
|
||||
return this.policy;
|
||||
},
|
||||
|
||||
openOptionsPage({tab, focus, hilite}) {
|
||||
let url = new URL(browser.runtime.getManifest().options_ui.page);
|
||||
if (tab !== undefined) {
|
||||
url.hash += `;tab-main-tabs=${tab}`;
|
||||
}
|
||||
let search = new URLSearchParams(url.search);
|
||||
if (focus) search.set("focus", focus);
|
||||
if (hilite) search.set("hilite", hilite);
|
||||
url.search = search;
|
||||
browser.tabs.create({url: url.toString() });
|
||||
},
|
||||
|
||||
async save(obj) {
|
||||
if (obj && obj.storage) {
|
||||
let toBeSaved = {
|
||||
[obj.storage]: obj
|
||||
};
|
||||
await Storage.set(obj.storage, toBeSaved);
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
async collectSeen(tabId) {
|
||||
try {
|
||||
let seen = Array.from(await Messages.send("collect", {uuid: ns.local.uuid}, {tabId, frameId: 0}));
|
||||
debug("Collected seen", seen);
|
||||
return seen;
|
||||
} catch (e) {
|
||||
await include("/nscl/common/restricted.js");
|
||||
if (!isRestrictedURL((await browser.tabs.get(tabId)).url)) {
|
||||
// probably a page where content scripts cannot run, let's open the options instead
|
||||
error(e, "Cannot collect noscript activity data");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
ns.start();
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* NoScript - a Firefox extension for whitelist driven safe JavaScript execution
|
||||
*
|
||||
* Copyright (C) 2005-2021 Giorgio Maone <https://maone.net>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by the Free Software
|
||||
* Foundation, either version 3 of the License, or (at your option) any later
|
||||
* version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
browser.runtime.onConnect.addListener(port => {
|
||||
if (port.name === "noscript.popup") {
|
||||
ns.popupOpened = true;
|
||||
let pendingReload = false;
|
||||
let tabId = -1;
|
||||
port.onMessage.addListener(m => {
|
||||
if ("pendingReload" in m) {
|
||||
tabId = m.tabId;
|
||||
pendingReload = m.pendingReload;
|
||||
}
|
||||
});
|
||||
port.onDisconnect.addListener(() => {
|
||||
ns.popupOpened = false;
|
||||
if (pendingReload) {
|
||||
browser.tabs.reload(tabId);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue