Further CSP refactoring and removal of obsolete fallbacks.
This commit is contained in:
parent
6e80d3f130
commit
e2b63cf982
|
@ -1,6 +1,7 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
{
|
{
|
||||||
let marker = JSON.stringify(uuid());
|
let marker = JSON.stringify(uuid());
|
||||||
|
let allUrls = ["<all_urls>"];
|
||||||
|
|
||||||
let Scripts = {
|
let Scripts = {
|
||||||
references: new Set(),
|
references: new Set(),
|
||||||
|
@ -10,27 +11,52 @@
|
||||||
matchAboutBlank: true,
|
matchAboutBlank: true,
|
||||||
runAt: "document_start"
|
runAt: "document_start"
|
||||||
},
|
},
|
||||||
|
async init() {
|
||||||
|
let opts = Object.assign({}, this.opts);
|
||||||
|
opts.js = [{file: "/content/dynamicNS.js"}];
|
||||||
|
opts.matches = allUrls;
|
||||||
|
delete opts.excludedMatches;
|
||||||
|
this._stubScript = await browser.contentScripts.register(opts);
|
||||||
|
|
||||||
|
this.init = this.forget;
|
||||||
|
},
|
||||||
forget() {
|
forget() {
|
||||||
for (let script of [...this.references]) {
|
for (let script of [...this.references]) {
|
||||||
script.unregister();
|
script.unregister();
|
||||||
this.references.delete(script);
|
this.references.delete(script);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
debug: false,
|
||||||
|
trace(code) {
|
||||||
|
return this.debug
|
||||||
|
? `console.debug("Executing child policy", ${JSON.stringify(code)});${code}`
|
||||||
|
: code
|
||||||
|
;
|
||||||
|
},
|
||||||
async register(code, matches, excludeMatches) {
|
async register(code, matches, excludeMatches) {
|
||||||
debug("Registering child policy.", code, matches, excludeMatches);
|
debug("Registering child policy.", code, matches, excludeMatches);
|
||||||
if (!matches.length) return;
|
if (!matches.length) return;
|
||||||
try {
|
try {
|
||||||
this.opts.js[0].code = code;
|
let opts = Object.assign({}, this.opts);
|
||||||
this.opts.matches = matches;
|
opts.js[0].code = this.trace(code);
|
||||||
|
opts.matches = matches;
|
||||||
if (excludeMatches && excludeMatches.length) {
|
if (excludeMatches && excludeMatches.length) {
|
||||||
this.opts.excludeMatches = excludeMatches;
|
opts.excludeMatches = excludeMatches;
|
||||||
} else {
|
|
||||||
delete this.opts.excludeMatches;
|
|
||||||
}
|
}
|
||||||
this.references.add(await browser.contentScripts.register(this.opts));
|
this.references.add(await browser.contentScripts.register(opts));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error(e);
|
error(e);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
buildPerms(perms, finalizeSetup = false) {
|
||||||
|
if (typeof perms !== "string") {
|
||||||
|
perms = JSON.stringify(perms);
|
||||||
|
}
|
||||||
|
return finalizeSetup
|
||||||
|
? `ns.setup(${perms}, ${marker});`
|
||||||
|
: `ns.config.CURRENT = ${perms};`
|
||||||
|
;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -69,12 +95,13 @@
|
||||||
error(e);
|
error(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async update(policy) {
|
async update(policy, debug) {
|
||||||
Scripts.forget();
|
if (debug !== "undefined") Scripts.debug = debug;
|
||||||
|
|
||||||
|
await Scripts.init();
|
||||||
|
|
||||||
if (!policy.enforced) {
|
if (!policy.enforced) {
|
||||||
await Scripts.register(`ns.setup(null, ${marker});`,
|
await Scripts.register(`ns.setup(null, ${marker});`, allUrls);
|
||||||
["<all_urls>"]);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,10 +149,27 @@
|
||||||
|
|
||||||
// register new content scripts
|
// register new content scripts
|
||||||
for (let [perms, keys] of [...permsMap]) {
|
for (let [perms, keys] of [...permsMap]) {
|
||||||
await Scripts.register(`ns.perms.CURRENT = ${perms};`, siteKeys2MatchPatterns(keys), excludeMap.get(perms));
|
await Scripts.register(Scripts.buildPerms(perms), siteKeys2MatchPatterns(keys), excludeMap.get(perms));
|
||||||
}
|
}
|
||||||
await Scripts.register(`ns.setup(${JSON.stringify(serialized.DEFAULT)}, ${marker});`,
|
await Scripts.register(Scripts.buildPerms(serialized.DEFAULT, true), allUrls);
|
||||||
["<all_urls>"]);
|
},
|
||||||
|
|
||||||
|
getForDocument(policy, url, context = null) {
|
||||||
|
return {
|
||||||
|
CURRENT: policy.get(url, context).perms.dry(),
|
||||||
|
DEFAULT: policy.DEFAULT.dry(),
|
||||||
|
MARKER: marker
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateFrame(tabId, frameId, perms, defaultPreset) {
|
||||||
|
let code = Scripts.buildPerms(perms) + Scripts.buildPerms(defaultPreset, true);
|
||||||
|
await browser.tabs.executeScript(tabId, {
|
||||||
|
code,
|
||||||
|
frameId,
|
||||||
|
matchAboutBlank: true,
|
||||||
|
runAt: "document_start"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
function ReportingCSP(reportURI, reportGroup) {
|
function ReportingCSP(reportURI, reportGroup) {
|
||||||
|
const REPORT_TO = {
|
||||||
|
name: "Report-To",
|
||||||
|
value: JSON.stringify({ "url": reportURI,
|
||||||
|
"group": reportGroup,
|
||||||
|
"max-age": 10886400 }),
|
||||||
|
};
|
||||||
return Object.assign(
|
return Object.assign(
|
||||||
new CapsCSP(new NetCSP(
|
new CapsCSP(new NetCSP(
|
||||||
`report-uri ${reportURI};`,
|
`report-uri ${reportURI};`,
|
||||||
|
@ -9,11 +15,32 @@ function ReportingCSP(reportURI, reportGroup) {
|
||||||
{
|
{
|
||||||
reportURI,
|
reportURI,
|
||||||
reportGroup,
|
reportGroup,
|
||||||
reportToHeader: {
|
patchHeaders(responseHeaders, capabilities) {
|
||||||
name: "Report-To",
|
let header = null;
|
||||||
value: JSON.stringify({ "url": reportURI,
|
let hasReportTo = false;
|
||||||
"group": reportGroup,
|
for (let h of responseHeaders) {
|
||||||
"max-age": 10886400 }),
|
if (this.isMine(h)) {
|
||||||
|
header = h;
|
||||||
|
h.value = this.inject(h.value, "");
|
||||||
|
} else if (h.name === REPORT_TO.name && h.value === REPORT_TO.value) {
|
||||||
|
hasReportTo = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let blocker = capabilities && this.buildFromCapabilities(capabilities);
|
||||||
|
if (blocker) {
|
||||||
|
if (!hasReportTo) {
|
||||||
|
responseHeaders.push(REPORT_TO);
|
||||||
|
}
|
||||||
|
if (header) {
|
||||||
|
header.value = this.inject(header.value, blocker);
|
||||||
|
} else {
|
||||||
|
header = this.asHeader(blocker);
|
||||||
|
responseHeaders.push(header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return header;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,12 +2,9 @@ var RequestGuard = (() => {
|
||||||
'use strict';
|
'use strict';
|
||||||
const VERSION_LABEL = `NoScript ${browser.runtime.getManifest().version}`;
|
const VERSION_LABEL = `NoScript ${browser.runtime.getManifest().version}`;
|
||||||
browser.browserAction.setTitle({title: VERSION_LABEL});
|
browser.browserAction.setTitle({title: VERSION_LABEL});
|
||||||
|
|
||||||
const REPORT_URI = "https://noscript-csp.invalid/__NoScript_Probe__/";
|
const REPORT_URI = "https://noscript-csp.invalid/__NoScript_Probe__/";
|
||||||
const REPORT_GROUP = "NoScript-Endpoint";
|
const REPORT_GROUP = "NoScript-Endpoint";
|
||||||
|
|
||||||
let csp = new ReportingCSP(REPORT_URI, REPORT_GROUP);
|
let csp = new ReportingCSP(REPORT_URI, REPORT_GROUP);
|
||||||
|
|
||||||
const policyTypesMap = {
|
const policyTypesMap = {
|
||||||
main_frame: "",
|
main_frame: "",
|
||||||
sub_frame: "frame",
|
sub_frame: "frame",
|
||||||
|
@ -25,7 +22,6 @@ var RequestGuard = (() => {
|
||||||
};
|
};
|
||||||
const allTypes = Object.keys(policyTypesMap);
|
const allTypes = Object.keys(policyTypesMap);
|
||||||
Object.assign(policyTypesMap, {"webgl": "webgl"}); // fake types
|
Object.assign(policyTypesMap, {"webgl": "webgl"}); // fake types
|
||||||
|
|
||||||
const TabStatus = {
|
const TabStatus = {
|
||||||
map: new Map(),
|
map: new Map(),
|
||||||
types: ["script", "object", "media", "frame", "font"],
|
types: ["script", "object", "media", "frame", "font"],
|
||||||
|
@ -36,13 +32,11 @@ var RequestGuard = (() => {
|
||||||
noscriptFrames: {},
|
noscriptFrames: {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
initTab(tabId, records = this.newRecords()) {
|
initTab(tabId, records = this.newRecords()) {
|
||||||
if (tabId < 0) return;
|
if (tabId < 0) return;
|
||||||
this.map.set(tabId, records);
|
this.map.set(tabId, records);
|
||||||
return records;
|
return records;
|
||||||
},
|
},
|
||||||
|
|
||||||
_record(request, what, optValue) {
|
_record(request, what, optValue) {
|
||||||
let {tabId, frameId, type, url, documentUrl} = request;
|
let {tabId, frameId, type, url, documentUrl} = request;
|
||||||
let policyType = policyTypesMap[type] || type;
|
let policyType = policyTypesMap[type] || type;
|
||||||
|
@ -54,7 +48,6 @@ var RequestGuard = (() => {
|
||||||
} else {
|
} else {
|
||||||
records = this.initTab(tabId);
|
records = this.initTab(tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (what === "noscriptFrame" && type !== "object") {
|
if (what === "noscriptFrame" && type !== "object") {
|
||||||
let nsf = records.noscriptFrames;
|
let nsf = records.noscriptFrames;
|
||||||
nsf[frameId] = optValue;
|
nsf[frameId] = optValue;
|
||||||
|
@ -76,7 +69,6 @@ var RequestGuard = (() => {
|
||||||
}
|
}
|
||||||
return records;
|
return records;
|
||||||
},
|
},
|
||||||
|
|
||||||
record(request, what, optValue) {
|
record(request, what, optValue) {
|
||||||
let {tabId} = request;
|
let {tabId} = request;
|
||||||
if (tabId < 0) return;
|
if (tabId < 0) return;
|
||||||
|
@ -85,9 +77,7 @@ var RequestGuard = (() => {
|
||||||
this.updateTab(request.tabId);
|
this.updateTab(request.tabId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_pendingTabs: new Set(),
|
_pendingTabs: new Set(),
|
||||||
|
|
||||||
updateTab(tabId) {
|
updateTab(tabId) {
|
||||||
if (tabId < 0) return;
|
if (tabId < 0) return;
|
||||||
if (this._pendingTabs.size === 0) {
|
if (this._pendingTabs.size === 0) {
|
||||||
|
@ -105,22 +95,18 @@ var RequestGuard = (() => {
|
||||||
let records = this.map.get(tabId) || this.initTab(tabId);
|
let records = this.map.get(tabId) || this.initTab(tabId);
|
||||||
let {allowed, blocked, noscriptFrames} = records;
|
let {allowed, blocked, noscriptFrames} = records;
|
||||||
let topAllowed = !(noscriptFrames && noscriptFrames[0]);
|
let topAllowed = !(noscriptFrames && noscriptFrames[0]);
|
||||||
|
|
||||||
let numAllowed = 0, numBlocked = 0, sum = 0;
|
let numAllowed = 0, numBlocked = 0, sum = 0;
|
||||||
let report = this.types.map(t => {
|
let report = this.types.map(t => {
|
||||||
let a = allowed[t] && allowed[t].length || 0, b = blocked[t] && blocked[t].length || 0, s = a + b;
|
let a = allowed[t] && allowed[t].length || 0, b = blocked[t] && blocked[t].length || 0, s = a + b;
|
||||||
numAllowed+= a, numBlocked += b, sum += s;
|
numAllowed+= a, numBlocked += b, sum += s;
|
||||||
return s && `<${t === "sub_frame" ? "frame" : t}>: ${b}/${s}`;
|
return s && `<${t === "sub_frame" ? "frame" : t}>: ${b}/${s}`;
|
||||||
}).filter(s => s).join("\n");
|
}).filter(s => s).join("\n");
|
||||||
|
|
||||||
let enforced = ns.isEnforced(tabId);
|
let enforced = ns.isEnforced(tabId);
|
||||||
|
|
||||||
let icon = topAllowed ?
|
let icon = topAllowed ?
|
||||||
(numBlocked ? "part"
|
(numBlocked ? "part"
|
||||||
: enforced ? "yes" : "global")
|
: enforced ? "yes" : "global")
|
||||||
: (numAllowed ? "sub" : "no");
|
: (numAllowed ? "sub" : "no");
|
||||||
let showBadge = ns.local.showCountBadge && numBlocked > 0;
|
let showBadge = ns.local.showCountBadge && numBlocked > 0;
|
||||||
|
|
||||||
let browserAction = browser.browserAction;
|
let browserAction = browser.browserAction;
|
||||||
browserAction.setIcon({tabId, path: {64: `/img/ui-${icon}64.png`}});
|
browserAction.setIcon({tabId, path: {64: `/img/ui-${icon}64.png`}});
|
||||||
browserAction.setBadgeText({tabId, text: showBadge ? numBlocked.toString() : ""});
|
browserAction.setBadgeText({tabId, text: showBadge ? numBlocked.toString() : ""});
|
||||||
|
@ -131,11 +117,9 @@ var RequestGuard = (() => {
|
||||||
: _("NotEnforced")}`
|
: _("NotEnforced")}`
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
totalize(sum, value) {
|
totalize(sum, value) {
|
||||||
return sum + value;
|
return sum + value;
|
||||||
},
|
},
|
||||||
|
|
||||||
async probe(tabId) {
|
async probe(tabId) {
|
||||||
if (tabId === undefined) {
|
if (tabId === undefined) {
|
||||||
(await browser.tabs.query({})).forEach(tab => TabStatus.probe(tab.id));
|
(await browser.tabs.query({})).forEach(tab => TabStatus.probe(tab.id));
|
||||||
|
@ -147,7 +131,6 @@ var RequestGuard = (() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
recordAll(tabId, seen) {
|
recordAll(tabId, seen) {
|
||||||
if (seen) {
|
if (seen) {
|
||||||
let records = TabStatus.map.get(tabId);
|
let records = TabStatus.map.get(tabId);
|
||||||
|
@ -156,17 +139,21 @@ var RequestGuard = (() => {
|
||||||
records.blocked = {};
|
records.blocked = {};
|
||||||
}
|
}
|
||||||
for (let thing of seen) {
|
for (let thing of seen) {
|
||||||
thing.request.tabId = tabId;
|
let {request, allowed} = thing;
|
||||||
TabStatus._record(thing.request, thing.allowed ? "allowed" : "blocked");
|
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);
|
this._updateTabNow(tabId);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async onActivatedTab(info) {
|
async onActivatedTab(info) {
|
||||||
let {tabId} = info;
|
let {tabId} = info;
|
||||||
let seen = await ns.collectSeen(tabId);
|
let seen = await ns.collectSeen(tabId);
|
||||||
|
|
||||||
TabStatus.recordAll(tabId, seen);
|
TabStatus.recordAll(tabId, seen);
|
||||||
},
|
},
|
||||||
onRemovedTab(tabId) {
|
onRemovedTab(tabId) {
|
||||||
|
@ -175,12 +162,9 @@ var RequestGuard = (() => {
|
||||||
}
|
}
|
||||||
browser.tabs.onActivated.addListener(TabStatus.onActivatedTab);
|
browser.tabs.onActivated.addListener(TabStatus.onActivatedTab);
|
||||||
browser.tabs.onRemoved.addListener(TabStatus.onRemovedTab);
|
browser.tabs.onRemoved.addListener(TabStatus.onRemovedTab);
|
||||||
|
|
||||||
if (!("setIcon" in browser.browserAction)) { // unsupported on Android
|
if (!("setIcon" in browser.browserAction)) { // unsupported on Android
|
||||||
TabStatus._updateTabNow = TabStatus.updateTab = () => {};
|
TabStatus._updateTabNow = TabStatus.updateTab = () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let messageHandler = {
|
let messageHandler = {
|
||||||
async pageshow(message, sender) {
|
async pageshow(message, sender) {
|
||||||
TabStatus.recordAll(sender.tab.id, message.seen);
|
TabStatus.recordAll(sender.tab.id, message.seen);
|
||||||
|
@ -215,7 +199,6 @@ var RequestGuard = (() => {
|
||||||
if (!capabilities.has(policyType)) {
|
if (!capabilities.has(policyType)) {
|
||||||
perms = new Permissions(new Set(capabilities), false);
|
perms = new Permissions(new Set(capabilities), false);
|
||||||
perms.capabilities.add(policyType);
|
perms.capabilities.add(policyType);
|
||||||
|
|
||||||
/* TODO: handle contextual permissions
|
/* TODO: handle contextual permissions
|
||||||
if (documentUrl) {
|
if (documentUrl) {
|
||||||
let context = new URL(documentUrl).origin;
|
let context = new URL(documentUrl).origin;
|
||||||
|
@ -228,23 +211,8 @@ var RequestGuard = (() => {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
async queryDocStatus(message, sender) {
|
|
||||||
let {frameId, tab} = sender;
|
|
||||||
let {url} = message;
|
|
||||||
let tabId = tab.id;
|
|
||||||
let records = TabStatus.map.get(tabId);
|
|
||||||
let noscriptFrames = records && records.noscriptFrames;
|
|
||||||
let canScript = !(noscriptFrames && noscriptFrames[sender.frameId]);
|
|
||||||
let shouldScript = !ns.isEnforced(tabId) || !url.startsWith("http") || ns.policy.can(url, "script");
|
|
||||||
debug("Frame %s %s of %o, canScript: %s, shouldScript: %s", frameId, url, noscriptFrames, canScript, shouldScript);
|
|
||||||
return {canScript, shouldScript};
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Content = {
|
const Content = {
|
||||||
|
|
||||||
async reportTo(request, allowed, policyType) {
|
async reportTo(request, allowed, policyType) {
|
||||||
let {requestId, tabId, frameId, type, url, documentUrl, originUrl} = request;
|
let {requestId, tabId, frameId, type, url, documentUrl, originUrl} = request;
|
||||||
let pending = pendingRequests.get(requestId); // null if from a CSP report
|
let pending = pendingRequests.get(requestId); // null if from a CSP report
|
||||||
|
@ -276,7 +244,6 @@ var RequestGuard = (() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const pendingRequests = new Map();
|
const pendingRequests = new Map();
|
||||||
function initPendingRequest(request) {
|
function initPendingRequest(request) {
|
||||||
let {requestId, url} = request;
|
let {requestId, url} = request;
|
||||||
|
@ -288,8 +255,6 @@ var RequestGuard = (() => {
|
||||||
});
|
});
|
||||||
return redirected;
|
return redirected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const ABORT = {cancel: true}, ALLOW = {};
|
const ABORT = {cancel: true}, ALLOW = {};
|
||||||
const INTERNAL_SCHEME = /^(?:chrome|resource|moz-extension|about):/;
|
const INTERNAL_SCHEME = /^(?:chrome|resource|moz-extension|about):/;
|
||||||
const listeners = {
|
const listeners = {
|
||||||
|
@ -307,7 +272,6 @@ var RequestGuard = (() => {
|
||||||
// livemark request or similar browser-internal, always allow;
|
// livemark request or similar browser-internal, always allow;
|
||||||
return ALLOW;
|
return ALLOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/^(?:data|blob):/.test(url)) {
|
if (/^(?:data|blob):/.test(url)) {
|
||||||
request._dataUrl = url;
|
request._dataUrl = url;
|
||||||
request.url = url = documentUrl;
|
request.url = url = documentUrl;
|
||||||
|
@ -316,7 +280,6 @@ var RequestGuard = (() => {
|
||||||
!ns.isEnforced(request.tabId) ||
|
!ns.isEnforced(request.tabId) ||
|
||||||
policy.can(url, policyType, originUrl);
|
policy.can(url, policyType, originUrl);
|
||||||
Content.reportTo(request, allowed, policyType);
|
Content.reportTo(request, allowed, policyType);
|
||||||
|
|
||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
debug(`Blocking ${policyType}`, request);
|
debug(`Blocking ${policyType}`, request);
|
||||||
TabStatus.record(request, "blocked");
|
TabStatus.record(request, "blocked");
|
||||||
|
@ -326,13 +289,10 @@ var RequestGuard = (() => {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error(e);
|
error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ALLOW;
|
return ALLOW;
|
||||||
},
|
},
|
||||||
|
|
||||||
async onHeadersReceived(request) {
|
async onHeadersReceived(request) {
|
||||||
// called for main_frame, sub_frame and object
|
// called for main_frame, sub_frame and object
|
||||||
|
|
||||||
// check for duplicate calls
|
// check for duplicate calls
|
||||||
let pending = pendingRequests.get(request.requestId);
|
let pending = pendingRequests.get(request.requestId);
|
||||||
if (pending) {
|
if (pending) {
|
||||||
|
@ -347,67 +307,28 @@ var RequestGuard = (() => {
|
||||||
pending = pendingRequests.get(request.requestId);
|
pending = pendingRequests.get(request.requestId);
|
||||||
}
|
}
|
||||||
pending.headersProcessed = true;
|
pending.headersProcessed = true;
|
||||||
|
|
||||||
let {url, documentUrl, statusCode, tabId, responseHeaders, type} = request;
|
let {url, documentUrl, statusCode, tabId, responseHeaders, type} = request;
|
||||||
|
|
||||||
let isMainFrame = type === "main_frame";
|
let isMainFrame = type === "main_frame";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let header;
|
let capabilities;
|
||||||
let content = {};
|
|
||||||
const REPORT_TO = csp.reportToHeader;
|
|
||||||
let hasReportTo = false;
|
|
||||||
for (let h of responseHeaders) {
|
|
||||||
if (csp.isMine(h)) {
|
|
||||||
header = h;
|
|
||||||
h.value = csp.inject(h.value, "");
|
|
||||||
} else if (/^\s*Content-(Type|Disposition)\s*$/i.test(h.name)) {
|
|
||||||
content[RegExp.$1.toLowerCase()] = h.value;
|
|
||||||
} else if (h.name === REPORT_TO.name && h.value === REPORT_TO.value) {
|
|
||||||
hasReportTo = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ns.isEnforced(tabId)) {
|
if (ns.isEnforced(tabId)) {
|
||||||
let policy = ns.policy;
|
let policy = ns.policy;
|
||||||
let perms = policy.get(url, documentUrl).perms;
|
let perms = policy.get(url, documentUrl).perms;
|
||||||
|
|
||||||
if (policy.autoAllowTop && isMainFrame && perms === policy.DEFAULT) {
|
if (policy.autoAllowTop && isMainFrame && perms === policy.DEFAULT) {
|
||||||
policy.set(Sites.optimalKey(url), perms = policy.TRUSTED.tempTwin);
|
policy.set(Sites.optimalKey(url), perms = policy.TRUSTED.tempTwin);
|
||||||
await ChildPolicies.update(policy);
|
await ChildPolicies.update(policy);
|
||||||
}
|
}
|
||||||
|
capabilities = perms.capabilities;
|
||||||
let blockHttp = !content.disposition &&
|
|
||||||
(!content.type || /^\s*(?:video|audio|application)\//.test(content.type));
|
|
||||||
if (blockHttp) {
|
|
||||||
debug(`Suspicious content type "%s" in request %o with capabilities %o`,
|
|
||||||
content.type, request, capabilities);
|
|
||||||
}
|
|
||||||
|
|
||||||
let blocker = csp.buildFromCapabilities(perms.capabilities, blockHttp);
|
|
||||||
if (blocker) {
|
|
||||||
if (!hasReportTo) {
|
|
||||||
responseHeaders.push(csp.reportToHeader);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (header) {
|
|
||||||
pending.cspHeader = header;
|
|
||||||
header.value = csp.inject(header.value, blocker);
|
|
||||||
} else {
|
|
||||||
header = csp.asHeader(blocker);
|
|
||||||
responseHeaders.push(header);
|
|
||||||
}
|
|
||||||
|
|
||||||
debug(`CSP blocker on %s:`, url, blocker, header.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMainFrame && !TabStatus.map.has(tabId)) {
|
|
||||||
debug("No TabStatus data yet for noscriptFrame", tabId);
|
|
||||||
TabStatus.record(request, "noscriptFrame", true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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);
|
||||||
if (header) {
|
if (header) {
|
||||||
|
pending.cspHeader = header;
|
||||||
|
debug(`CSP blocker on %s:`, url, header.value);
|
||||||
return {responseHeaders};
|
return {responseHeaders};
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -415,7 +336,6 @@ var RequestGuard = (() => {
|
||||||
}
|
}
|
||||||
return ALLOW;
|
return ALLOW;
|
||||||
},
|
},
|
||||||
|
|
||||||
onResponseStarted(request) {
|
onResponseStarted(request) {
|
||||||
debug("onResponseStarted", request);
|
debug("onResponseStarted", request);
|
||||||
let {requestId, url, tabId, frameId, type} = request;
|
let {requestId, url, tabId, frameId, type} = request;
|
||||||
|
@ -436,19 +356,9 @@ var RequestGuard = (() => {
|
||||||
debug("[WARNING] onHeadersReceived %s %o", frameId, tabId,
|
debug("[WARNING] onHeadersReceived %s %o", frameId, tabId,
|
||||||
pending.headersProcessed ? "has been overridden on": "could not process",
|
pending.headersProcessed ? "has been overridden on": "could not process",
|
||||||
request);
|
request);
|
||||||
|
|
||||||
if (tabId !== -1 && type !== "object") {
|
|
||||||
debug("[WARNING] Reloading %s frame %s of tab %s.", url, frameId, tabId);
|
|
||||||
browser.tabs.executeScript(tabId, {
|
|
||||||
runAt: "document_start",
|
|
||||||
code: "window.location.reload(false)",
|
|
||||||
frameId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onCompleted(request) {
|
onCompleted(request) {
|
||||||
let {requestId} = request;
|
let {requestId} = request;
|
||||||
if (pendingRequests.has(requestId)) {
|
if (pendingRequests.has(requestId)) {
|
||||||
|
@ -463,12 +373,10 @@ var RequestGuard = (() => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onErrorOccurred(request) {
|
onErrorOccurred(request) {
|
||||||
pendingRequests.delete(request.requestId);
|
pendingRequests.delete(request.requestId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function fakeRequestFromCSP(report, request) {
|
function fakeRequestFromCSP(report, request) {
|
||||||
let type = report["violated-directive"].split("-", 1)[0]; // e.g. script-src 'none' => script
|
let type = report["violated-directive"].split("-", 1)[0]; // e.g. script-src 'none' => script
|
||||||
if (type === "frame") type = "sub_frame";
|
if (type === "frame") type = "sub_frame";
|
||||||
|
@ -479,7 +387,6 @@ var RequestGuard = (() => {
|
||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onViolationReport(request) {
|
async function onViolationReport(request) {
|
||||||
try {
|
try {
|
||||||
let decoder = new TextDecoder("UTF-8");
|
let decoder = new TextDecoder("UTF-8");
|
||||||
|
@ -501,24 +408,17 @@ var RequestGuard = (() => {
|
||||||
}
|
}
|
||||||
return ABORT;
|
return ABORT;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RequestGuard = {
|
const RequestGuard = {
|
||||||
async start() {
|
async start() {
|
||||||
Messages.addHandler(messageHandler);
|
Messages.addHandler(messageHandler);
|
||||||
|
|
||||||
let wr = browser.webRequest;
|
let wr = browser.webRequest;
|
||||||
let listen = (what, ...args) => wr[what].addListener(listeners[what], ...args);
|
let listen = (what, ...args) => wr[what].addListener(listeners[what], ...args);
|
||||||
|
|
||||||
let allUrls = ["<all_urls>"];
|
let allUrls = ["<all_urls>"];
|
||||||
let docTypes = ["main_frame", "sub_frame", "object"];
|
let docTypes = ["main_frame", "sub_frame", "object"];
|
||||||
|
|
||||||
let filterDocs = {urls: allUrls, types: docTypes};
|
let filterDocs = {urls: allUrls, types: docTypes};
|
||||||
let filterAll = {urls: allUrls, types: allTypes};
|
let filterAll = {urls: allUrls, types: allTypes};
|
||||||
|
|
||||||
listen("onBeforeRequest", filterAll, ["blocking"]);
|
listen("onBeforeRequest", filterAll, ["blocking"]);
|
||||||
|
|
||||||
listen("onHeadersReceived", filterDocs, ["blocking", "responseHeaders"]);
|
listen("onHeadersReceived", filterDocs, ["blocking", "responseHeaders"]);
|
||||||
|
|
||||||
(listeners.onHeadersReceivedLast = new LastListener(wr.onHeadersReceived, request => {
|
(listeners.onHeadersReceivedLast = new LastListener(wr.onHeadersReceived, request => {
|
||||||
let {requestId, responseHeaders} = request;
|
let {requestId, responseHeaders} = request;
|
||||||
let pending = pendingRequests.get(request.requestId);
|
let pending = pendingRequests.get(request.requestId);
|
||||||
|
@ -541,18 +441,13 @@ var RequestGuard = (() => {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, filterDocs, ["blocking", "responseHeaders"])).install();
|
}, filterDocs, ["blocking", "responseHeaders"])).install();
|
||||||
|
|
||||||
listen("onResponseStarted", filterDocs, ["responseHeaders"]);
|
listen("onResponseStarted", filterDocs, ["responseHeaders"]);
|
||||||
listen("onCompleted", filterAll);
|
listen("onCompleted", filterAll);
|
||||||
listen("onErrorOccurred", filterAll);
|
listen("onErrorOccurred", filterAll);
|
||||||
|
|
||||||
|
|
||||||
wr.onBeforeRequest.addListener(onViolationReport,
|
wr.onBeforeRequest.addListener(onViolationReport,
|
||||||
{urls: [csp.reportURI], types: ["csp_report"]}, ["blocking", "requestBody"]);
|
{urls: [csp.reportURI], types: ["csp_report"]}, ["blocking", "requestBody"]);
|
||||||
|
|
||||||
TabStatus.probe();
|
TabStatus.probe();
|
||||||
},
|
},
|
||||||
|
|
||||||
stop() {
|
stop() {
|
||||||
let wr = browser.webRequest;
|
let wr = browser.webRequest;
|
||||||
for (let [name, listener] of Object.entries(listeners)) {
|
for (let [name, listener] of Object.entries(listeners)) {
|
||||||
|
@ -566,6 +461,5 @@ var RequestGuard = (() => {
|
||||||
Messages.removeHandler(messageHandler);
|
Messages.removeHandler(messageHandler);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return RequestGuard;
|
return RequestGuard;
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -23,10 +23,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
|
await include("/bg/defaults.js");
|
||||||
|
await ns.defaults;
|
||||||
|
|
||||||
let policyData = (await Storage.get("sync", "policy")).policy;
|
let policyData = (await Storage.get("sync", "policy")).policy;
|
||||||
if (policyData && policyData.DEFAULT) {
|
if (policyData && policyData.DEFAULT) {
|
||||||
ns.policy = new Policy(policyData);
|
ns.policy = new Policy(policyData);
|
||||||
await ChildPolicies.update(policyData);
|
await ChildPolicies.update(policyData, ns.local.debug);
|
||||||
} else {
|
} else {
|
||||||
await include("/legacy/Legacy.js");
|
await include("/legacy/Legacy.js");
|
||||||
ns.policy = await Legacy.createOrMigratePolicy();
|
ns.policy = await Legacy.createOrMigratePolicy();
|
||||||
|
@ -34,8 +37,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
await include("/bg/defaults.js");
|
|
||||||
await ns.defaults;
|
|
||||||
await include("/bg/RequestGuard.js");
|
await include("/bg/RequestGuard.js");
|
||||||
await RequestGuard.start();
|
await RequestGuard.start();
|
||||||
await XSS.start(); // we must start it anyway to initialize sub-objects
|
await XSS.start(); // we must start it anyway to initialize sub-objects
|
||||||
|
@ -136,6 +138,10 @@
|
||||||
return await Settings.import(data);
|
return await Settings.import(data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async fetchChildPolicy({url, contextUrl}) {
|
||||||
|
return ChildPolicies.getForDocument(ns.policy, url, contextUrl);
|
||||||
|
},
|
||||||
|
|
||||||
async openStandalonePopup() {
|
async openStandalonePopup() {
|
||||||
let win = await browser.windows.getLastFocused();
|
let win = await browser.windows.getLastFocused();
|
||||||
let [tab] = (await browser.tabs.query({
|
let [tab] = (await browser.tabs.query({
|
||||||
|
@ -203,7 +209,7 @@
|
||||||
|
|
||||||
async savePolicy() {
|
async savePolicy() {
|
||||||
if (this.policy) {
|
if (this.policy) {
|
||||||
await ChildPolicies.update(this.policy);
|
await ChildPolicies.update(this.policy, this.local.debug);
|
||||||
await Storage.set("sync", {
|
await Storage.set("sync", {
|
||||||
policy: this.policy.dry()
|
policy: this.policy.dry()
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,9 +6,9 @@ class DocumentCSP {
|
||||||
this.builder = new CapsCSP();
|
this.builder = new CapsCSP();
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(capabilities) {
|
apply(capabilities, embedding = CSP.isEmbedType(this.document.contentType)) {
|
||||||
let csp = this.builder;
|
let csp = this.builder;
|
||||||
let blocker = csp.buildFromCapabilities(capabilities);
|
let blocker = csp.buildFromCapabilities(capabilities, embedding);
|
||||||
if (!blocker) return;
|
if (!blocker) return;
|
||||||
|
|
||||||
let document = this.document;
|
let document = this.document;
|
||||||
|
@ -19,8 +19,11 @@ class DocumentCSP {
|
||||||
let parent = document.head || document.documentElement;
|
let parent = document.head || document.documentElement;
|
||||||
try {
|
try {
|
||||||
parent.insertBefore(meta, parent.firstChild);
|
parent.insertBefore(meta, parent.firstChild);
|
||||||
|
debug(`Failsafe <meta> CSP inserted in the DOM: "%s"`, header.value);
|
||||||
|
if (capabilities.has("script")) meta.remove();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error(e, "Error inserting CSP %s in the DOM", header && header.value);
|
error(e, "Error inserting CSP %s in the DOM", header && header.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,17 @@
|
||||||
var PlaceHolder = (() => {
|
var PlaceHolder = (() => {
|
||||||
const HANDLERS = new Map();
|
const HANDLERS = new Map();
|
||||||
|
|
||||||
|
let checkStyle = async () => {
|
||||||
|
checkStyle = () => {};
|
||||||
|
if (!ns.embeddingDocument) return;
|
||||||
|
let replacement = document.querySelector("a.__NoScript_PlaceHolder__");
|
||||||
|
if (!replacement) return;
|
||||||
|
if (window.getComputedStyle(replacement, null).opacity !== "0.8") {
|
||||||
|
document.head.appendChild(createHTMLElement("style")).textContent = await
|
||||||
|
(await fetch(browser.extension.getURL("/content/content.css"))).text();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Handler {
|
class Handler {
|
||||||
constructor(type, selector) {
|
constructor(type, selector) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
|
@ -9,6 +20,7 @@ var PlaceHolder = (() => {
|
||||||
HANDLERS.set(type, this);
|
HANDLERS.set(type, this);
|
||||||
}
|
}
|
||||||
filter(element, request) {
|
filter(element, request) {
|
||||||
|
if (request.embeddingDocument) return true;
|
||||||
let url = request.initialUrl || request.url;
|
let url = request.initialUrl || request.url;
|
||||||
return "data" in element ? element.data === url : element.src === url;
|
return "data" in element ? element.data === url : element.src === url;
|
||||||
}
|
}
|
||||||
|
@ -77,10 +89,14 @@ var PlaceHolder = (() => {
|
||||||
.filter(element => this.handler.filter(element, request))
|
.filter(element => this.handler.filter(element, request))
|
||||||
.forEach(element => this.replace(element));
|
.forEach(element => this.replace(element));
|
||||||
};
|
};
|
||||||
if (this.replacements.size) PlaceHolder.listen();
|
if (this.replacements.size) {
|
||||||
|
PlaceHolder.listen();
|
||||||
|
checkStyle();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
replace(element) {
|
replace(element) {
|
||||||
|
if (!element.parentElement) return;
|
||||||
let {
|
let {
|
||||||
url
|
url
|
||||||
} = this.request;
|
} = this.request;
|
||||||
|
@ -108,10 +124,10 @@ var PlaceHolder = (() => {
|
||||||
|
|
||||||
replacement._placeHolderObj = this;
|
replacement._placeHolderObj = this;
|
||||||
replacement._placeHolderElement = element;
|
replacement._placeHolderElement = element;
|
||||||
this.replacements.add(replacement);
|
|
||||||
|
|
||||||
if (element.parentNode) element.parentNode.replaceChild(replacement, element);
|
|
||||||
else document.body.appendChild(replacement);
|
element.parentNode.replaceChild(replacement, element);
|
||||||
|
this.replacements.add(replacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
async enable(replacement) {
|
async enable(replacement) {
|
||||||
|
|
|
@ -1,112 +1,12 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
// debug = () => {}; // REL_ONLY
|
||||||
|
|
||||||
// debug = () => {}; // REL_ONLY
|
var _ = browser.i18n.getMessage;
|
||||||
{
|
|
||||||
let listenersMap = new Map();
|
|
||||||
let backlog = new Set();
|
|
||||||
var ns = {
|
|
||||||
on(eventName, listener) {
|
|
||||||
let listeners = listenersMap.get(eventName);
|
|
||||||
if (!listeners) listenersMap.set(eventName, listeners = new Set());
|
|
||||||
listeners.add(listener);
|
|
||||||
if (backlog.has(eventName)) this.fire(eventName, listener);
|
|
||||||
},
|
|
||||||
detach(eventName, listener) {
|
|
||||||
let listeners = listenersMap.get(eventName);
|
|
||||||
if (listeners) listeners.delete(listener);
|
|
||||||
},
|
|
||||||
fire(eventName, listener = null) {
|
|
||||||
if (listener) {
|
|
||||||
listener({type:eventName, source: this});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let listeners = listenersMap.get(eventName);
|
|
||||||
if (listeners) {
|
|
||||||
for (let l of listeners) {
|
|
||||||
this.fire(eventName, l);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
backlog.add(eventName);
|
|
||||||
},
|
|
||||||
setup(DEFAULT, MARKER) {
|
|
||||||
this.perms.DEFAULT = DEFAULT;
|
|
||||||
if(!this.perms.CURRENT) this.perms.CURRENT = DEFAULT;
|
|
||||||
|
|
||||||
// ugly hack: since now we use registerContentScript instead of the
|
|
||||||
// filterRequest dynamic script injection hack, we use top.name
|
|
||||||
// to store per-tab information. We don't want web content to
|
|
||||||
// mess with it, though, so we wrap it around auto-hiding accessors
|
|
||||||
this.perms.MARKER = MARKER;
|
|
||||||
let eraseTabInfoRx = new RegExp(`[^]*${MARKER},?`);
|
|
||||||
if (eraseTabInfoRx.test(top.name)) {
|
|
||||||
let _name = top.name;
|
|
||||||
let tabInfoRx = new RegExp(`^${MARKER}\\[([^]*?)\\]${MARKER},`);
|
|
||||||
if (top === window) { // wrap to hide
|
|
||||||
Reflect.defineProperty(top.wrappedJSObject, "name", {
|
|
||||||
get: exportFunction(() => top.name.replace(eraseTabInfoRx, ""), top.wrappedJSObject),
|
|
||||||
set: exportFunction(value => {
|
|
||||||
let preamble = top.name.match(tabInfoRx);
|
|
||||||
top.name = `${preamble && preamble[0] || ""}${value}`;
|
|
||||||
return value;
|
|
||||||
}, top.wrappedJSObject)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let tabInfoMatch = _name.match(tabInfoRx);
|
|
||||||
if (tabInfoMatch) try {
|
|
||||||
this.perms.tabInfo = JSON.parse(tabInfoMatch[1]);
|
|
||||||
} catch (e) {
|
|
||||||
error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.perms.DEFAULT || this.perms.tabInfo.unrestricted) {
|
|
||||||
this.allows = () => true;
|
|
||||||
this.capabilities = Object.assign(
|
|
||||||
new Set(["script"]), { has() { return true; } });
|
|
||||||
} else {
|
|
||||||
let perms = this.perms.CURRENT || this.perms.DEFAULT;
|
|
||||||
this.capabilities = new Set(perms.capabilities);
|
|
||||||
new DocumentCSP(document).apply(this.capabilities);
|
|
||||||
}
|
|
||||||
ns.fire("perms");
|
|
||||||
},
|
|
||||||
perms: { DEFAULT: null, CURRENT: null, tabInfo: {}, MARKER: "" },
|
|
||||||
|
|
||||||
allows(cap) {
|
|
||||||
return this.capabilities && this.capabilities.has(cap);
|
|
||||||
},
|
|
||||||
|
|
||||||
getWindowName() {
|
|
||||||
return top !== window || !this.perms.MARKER ? window.name
|
|
||||||
: window.name.split(this.perms.MARKER + ",").pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var canScript = true, shouldScript = false;
|
|
||||||
|
|
||||||
let now = () => performance.now() + performance.timeOrigin;
|
|
||||||
|
|
||||||
function createHTMLElement(name) {
|
function createHTMLElement(name) {
|
||||||
return document.createElementNS("http://www.w3.org/1999/xhtml", name);
|
return document.createElementNS("http://www.w3.org/1999/xhtml", name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function probe() {
|
|
||||||
try {
|
|
||||||
debug("Probing execution...");
|
|
||||||
let s = document.createElement("script");
|
|
||||||
s.textContent = ";";
|
|
||||||
document.documentElement.appendChild(s);
|
|
||||||
s.remove();
|
|
||||||
} catch(e) {
|
|
||||||
debug(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = browser.i18n.getMessage;
|
|
||||||
|
|
||||||
var embeddingDocument = false;
|
|
||||||
|
|
||||||
var seen = {
|
var seen = {
|
||||||
_map: new Map(),
|
_map: new Map(),
|
||||||
_list: null,
|
_list: null,
|
||||||
|
@ -128,9 +28,8 @@ Messages.addHandler({
|
||||||
seen.record(event);
|
seen.record(event);
|
||||||
}
|
}
|
||||||
if (ownFrame) {
|
if (ownFrame) {
|
||||||
init();
|
|
||||||
if (!allowed && PlaceHolder.canReplace(policyType)) {
|
if (!allowed && PlaceHolder.canReplace(policyType)) {
|
||||||
request.embeddingDocument = embeddingDocument;
|
request.embeddingDocument = ns.embeddingDocument;
|
||||||
PlaceHolder.create(policyType, request);
|
PlaceHolder.create(policyType, request);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -142,87 +41,38 @@ Messages.addHandler({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (document.readyState !== "complete") {
|
|
||||||
let pageshown = e => {
|
debug(`Loading NoScript in document %s, scripting=%s, readyState %s`,
|
||||||
removeEventListener("pageshow", pageshown);
|
document.URL, ns.canScript, document.readyState);
|
||||||
init();
|
|
||||||
};
|
var notifyPage = async () => {
|
||||||
addEventListener("pageshow", pageshown);
|
|
||||||
} else {
|
|
||||||
init(true);
|
|
||||||
}
|
|
||||||
let notifyPage = async () => {
|
|
||||||
debug("Page %s shown, %s", document.URL, document.readyState);
|
debug("Page %s shown, %s", document.URL, document.readyState);
|
||||||
if (document.readyState === "complete") {
|
if (document.readyState === "complete") {
|
||||||
try {
|
try {
|
||||||
await Messages.send("pageshow", {seen: seen.list, canScript});
|
if (!("canScript" in ns)) {
|
||||||
|
let childPolicy = await Messages.send("fetchChildPolicy", {url: document.URL, contextUrl: top.location.href});
|
||||||
|
ns.config.CURRENT = childPolicy.CURRENT;
|
||||||
|
ns.setup(childPolicy.DEFAULT, childPolicy.MARKER);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Messages.send("pageshow", {seen: seen.list, canScript: ns.canScript});
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debug(e);
|
debug(e);
|
||||||
|
if (/Receiving end does not exist/.test(e.message)) {
|
||||||
|
window.setTimeout(notifyPage, 2000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var queryingStatus = false;
|
notifyPage();
|
||||||
|
|
||||||
function reload(noCache = false) {
|
window.addEventListener("pageshow", notifyPage);
|
||||||
init = () => {};
|
|
||||||
location.reload(noCache);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function init(oldPage = false) {
|
ns.on("capabilities", () => {
|
||||||
if (queryingStatus) return;
|
|
||||||
if (!document.URL.startsWith("http")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
queryingStatus = true;
|
|
||||||
|
|
||||||
debug(`init() called in document %s, contentType %s readyState %s, frameElement %o`,
|
|
||||||
document.URL, document.contentType, document.readyState, window.frameElement && frameElement.data);
|
|
||||||
|
|
||||||
try {
|
|
||||||
({canScript, shouldScript} = await Messages.send("queryDocStatus", {url: document.URL}));
|
|
||||||
debug(`document %s, canScript=%s, shouldScript=%s, readyState %s`, document.URL, canScript, shouldScript, document.readyState);
|
|
||||||
if (canScript) {
|
|
||||||
if (oldPage) {
|
|
||||||
probe();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!shouldScript &&
|
|
||||||
(document.readyState !== "complete" ||
|
|
||||||
now() - performance.timing.domContentLoadedEventStart < 5000)) {
|
|
||||||
// Something wrong: scripts can run, permissions say they shouldn't.
|
|
||||||
// Was webRequest bypassed by caching/session restore/service workers?
|
|
||||||
window.stop();
|
|
||||||
let noCache = !!navigator.serviceWorker.controller;
|
|
||||||
if (noCache) {
|
|
||||||
for (let r of await navigator.serviceWorker.getRegistrations()) {
|
|
||||||
await r.unregister();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
debug("Reloading %s (%s)", document.URL, noCache ? "no cache" : "cached");
|
|
||||||
reload(noCache);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
init = () => {};
|
|
||||||
} catch (e) {
|
|
||||||
debug("Error querying docStatus", e);
|
|
||||||
if (!oldPage &&
|
|
||||||
/Receiving end does not exist/.test(e.message)) {
|
|
||||||
// probably startup and bg page not ready yet, hence no CSP: reload!
|
|
||||||
debug("Reloading", document.URL);
|
|
||||||
reload();
|
|
||||||
} else {
|
|
||||||
setTimeout(() => init(oldPage), 100);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} finally {
|
|
||||||
queryingStatus = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!canScript) onScriptDisabled();
|
|
||||||
seen.record({
|
seen.record({
|
||||||
request: {
|
request: {
|
||||||
key: "noscript-probe",
|
key: "noscript-probe",
|
||||||
|
@ -230,21 +80,13 @@ async function init(oldPage = false) {
|
||||||
documentUrl: document.URL,
|
documentUrl: document.URL,
|
||||||
type: window === window.top ? "main_frame" : "script",
|
type: window === window.top ? "main_frame" : "script",
|
||||||
},
|
},
|
||||||
allowed: canScript
|
allowed: ns.canScript
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
debug(`Loading NoScript in document %s, scripting=%s, readyState %s`,
|
if (!ns.canScript) {
|
||||||
document.URL, canScript, document.readyState);
|
if (document.readyState !== "loading") onScriptDisabled();
|
||||||
|
window.addEventListener("DOMContentLoaded", onScriptDisabled);
|
||||||
if (/application|video|audio/.test(document.contentType)) {
|
|
||||||
debug("Embedding document detected");
|
|
||||||
embeddingDocument = true;
|
|
||||||
window.addEventListener("pageshow", e => {
|
|
||||||
debug("Active content still in document %s: %o", document.url, document.querySelectorAll("embed,object,video,audio"));
|
|
||||||
}, true);
|
|
||||||
// document.write("<plaintext>");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyPage();
|
notifyPage();
|
||||||
addEventListener("pageshow", notifyPage);
|
});
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// ensure the order which manifest scripts and dynamically registered scripts
|
||||||
|
// are executed in doesn't matter for initialization, by using a stub.
|
||||||
|
|
||||||
|
if (!this.ns) {
|
||||||
|
let deferredSetup = null;
|
||||||
|
let nsStub = this.ns = {
|
||||||
|
config: {},
|
||||||
|
setup(DEFAULT, MARKER) {
|
||||||
|
deferredSetup = [DEFAULT, MARKER];
|
||||||
|
},
|
||||||
|
merge: ns => {
|
||||||
|
ns.config = Object.assign(ns.config, nsStub.config);
|
||||||
|
this.ns = ns;
|
||||||
|
if (deferredSetup) {
|
||||||
|
ns.setup(...deferredSetup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
if (ns.embeddingDocument) {
|
||||||
|
ns.on("capabilities", () => {
|
||||||
|
for (let policyType of ["object", "media"]) {
|
||||||
|
if (!ns.allows(policyType)) {
|
||||||
|
let request = {
|
||||||
|
id: `noscript-${policyType}-doc`,
|
||||||
|
type: policyType,
|
||||||
|
url: document.URL,
|
||||||
|
documentUrl: document.URL,
|
||||||
|
embeddingDocument: true,
|
||||||
|
};
|
||||||
|
let ph = PlaceHolder.create(policyType, request);
|
||||||
|
if (ph.replacements.size > 0) {
|
||||||
|
debug(`Created placeholder for ${policyType} at ${document.URL}`);
|
||||||
|
seen.record({policyType, request, allowed: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
ns.on("perms", event => {
|
ns.on("capabilities", event => {
|
||||||
debug("Media Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.perms.CURRENT); // DEV_ONLY
|
debug("Media Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.capabilities); // DEV_ONLY
|
||||||
let mediaBlocker = !ns.allows("media");
|
let mediaBlocker = !ns.allows("media");
|
||||||
let unpatched = new Map();
|
let unpatched = new Map();
|
||||||
function patch(obj, methodName, replacement) {
|
function patch(obj, methodName, replacement) {
|
||||||
|
|
|
@ -0,0 +1,99 @@
|
||||||
|
'use strict';
|
||||||
|
{
|
||||||
|
let listenersMap = new Map();
|
||||||
|
let backlog = new Set();
|
||||||
|
|
||||||
|
let ns = {
|
||||||
|
debug: true, // DEV_ONLY
|
||||||
|
get embeddingDocument() {
|
||||||
|
delete this.embeddingDocument;
|
||||||
|
return this.embeddingDocument = CSP.isEmbedType(document.contentType);
|
||||||
|
},
|
||||||
|
on(eventName, listener) {
|
||||||
|
let listeners = listenersMap.get(eventName);
|
||||||
|
if (!listeners) listenersMap.set(eventName, listeners = new Set());
|
||||||
|
listeners.add(listener);
|
||||||
|
if (backlog.has(eventName)) this.fire(eventName, listener);
|
||||||
|
},
|
||||||
|
detach(eventName, listener) {
|
||||||
|
let listeners = listenersMap.get(eventName);
|
||||||
|
if (listeners) listeners.delete(listener);
|
||||||
|
},
|
||||||
|
fire(eventName, listener = null) {
|
||||||
|
if (listener) {
|
||||||
|
listener({type:eventName, source: this});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let listeners = listenersMap.get(eventName);
|
||||||
|
if (listeners) {
|
||||||
|
for (let l of listeners) {
|
||||||
|
this.fire(eventName, l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
backlog.add(eventName);
|
||||||
|
},
|
||||||
|
|
||||||
|
setup(DEFAULT, MARKER) {
|
||||||
|
this.config.DEFAULT = DEFAULT;
|
||||||
|
if(!this.config.CURRENT) this.config.CURRENT = DEFAULT;
|
||||||
|
|
||||||
|
// ugly hack: since now we use registerContentScript instead of the
|
||||||
|
// filterRequest dynamic script injection hack, we use top.name
|
||||||
|
// to store per-tab information. We don't want web content to
|
||||||
|
// mess with it, though, so we wrap it around auto-hiding accessors
|
||||||
|
this.config.MARKER = MARKER;
|
||||||
|
let eraseTabInfoRx = new RegExp(`[^]*${MARKER},?`);
|
||||||
|
if (eraseTabInfoRx.test(top.name)) {
|
||||||
|
let _name = top.name;
|
||||||
|
let tabInfoRx = new RegExp(`^${MARKER}\\[([^]*?)\\]${MARKER},`);
|
||||||
|
if (top === window) { // wrap to hide
|
||||||
|
Reflect.defineProperty(top.wrappedJSObject, "name", {
|
||||||
|
get: exportFunction(() => top.name.replace(eraseTabInfoRx, ""), top.wrappedJSObject),
|
||||||
|
set: exportFunction(value => {
|
||||||
|
let preamble = top.name.match(tabInfoRx);
|
||||||
|
top.name = `${preamble && preamble[0] || ""}${value}`;
|
||||||
|
return value;
|
||||||
|
}, top.wrappedJSObject)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let tabInfoMatch = _name.match(tabInfoRx);
|
||||||
|
if (tabInfoMatch) try {
|
||||||
|
this.config.tabInfo = JSON.parse(tabInfoMatch[1]);
|
||||||
|
} catch (e) {
|
||||||
|
error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.config.DEFAULT || this.config.tabInfo.unrestricted) {
|
||||||
|
this.allows = () => true;
|
||||||
|
this.capabilities = Object.assign(
|
||||||
|
new Set(["script"]), { has() { return true; } });
|
||||||
|
} else {
|
||||||
|
let perms = this.config.CURRENT;
|
||||||
|
this.capabilities = new Set(perms.capabilities);
|
||||||
|
new DocumentCSP(document).apply(this.capabilities, this.embeddingDocument);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.canScript = this.allows("script");
|
||||||
|
this.fire("capabilities");
|
||||||
|
},
|
||||||
|
config: { DEFAULT: null, CURRENT: null, tabInfo: {}, MARKER: "" },
|
||||||
|
|
||||||
|
allows(cap) {
|
||||||
|
return this.capabilities && this.capabilities.has(cap);
|
||||||
|
},
|
||||||
|
|
||||||
|
getWindowName() {
|
||||||
|
let marker = this.config.MARKER;
|
||||||
|
return (top === window && marker) ?
|
||||||
|
window.name.split(`${marker},`).pop()
|
||||||
|
: window.name;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.ns) {
|
||||||
|
this.ns.merge(ns);
|
||||||
|
} else {
|
||||||
|
this.ns = ns;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
ns.on("perms", event => {
|
ns.on("capabilities", event => {
|
||||||
debug("WebGL Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.perms.CURRENT); // DEV_ONLY
|
debug("WebGL Hook", document.URL, document.documentElement && document.documentElement.innerHTML, ns.capabilities); // DEV_ONLY
|
||||||
if (ns.allows("webgl")) return;
|
if (ns.allows("webgl")) return;
|
||||||
let proto = HTMLCanvasElement.prototype;
|
let proto = HTMLCanvasElement.prototype;
|
||||||
let getContext = proto.getContext;
|
let getContext = proto.getContext;
|
||||||
|
|
|
@ -19,4 +19,5 @@ class CSP {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CSP.isEmbedType = type => /\b(?:application|video|audio)\b/.test(type);
|
||||||
CSP.headerName = "content-security-policy";
|
CSP.headerName = "content-security-policy";
|
||||||
|
|
|
@ -27,4 +27,6 @@ class NetCSP extends CSP {
|
||||||
return `${this.start}${super.build(...directives)}${this.end}`;
|
return `${this.start}${super.build(...directives)}${this.end}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cleanup(headers) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,14 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["<all_urls>"],
|
||||||
|
"match_about_blank": true,
|
||||||
|
"all_frames": true,
|
||||||
|
"css": [
|
||||||
|
"/content/content.css"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"run_at": "document_start",
|
"run_at": "document_start",
|
||||||
"matches": ["<all_urls>"],
|
"matches": ["<all_urls>"],
|
||||||
|
@ -71,20 +79,14 @@
|
||||||
"common/CapsCSP.js",
|
"common/CapsCSP.js",
|
||||||
"content/DocumentCSP.js",
|
"content/DocumentCSP.js",
|
||||||
"content/onScriptDisabled.js",
|
"content/onScriptDisabled.js",
|
||||||
|
"content/staticNS.js",
|
||||||
"content/content.js",
|
"content/content.js",
|
||||||
"content/webglHook.js",
|
|
||||||
"content/PlaceHolder.js",
|
"content/PlaceHolder.js",
|
||||||
|
"content/embeddingDocument.js",
|
||||||
|
"content/webglHook.js",
|
||||||
"content/media.js"
|
"content/media.js"
|
||||||
]
|
]
|
||||||
},
|
}
|
||||||
{
|
|
||||||
"matches": ["<all_urls>"],
|
|
||||||
"match_about_blank": true,
|
|
||||||
"all_frames": true,
|
|
||||||
"css": [
|
|
||||||
"/content/content.css"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
|
|
||||||
"options_ui": {
|
"options_ui": {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
ns.on("perms", event => {
|
ns.on("capabilities", event => {
|
||||||
if (ns.allows("script")) {
|
if (ns.allows("script")) {
|
||||||
let name = ns.getWindowName();
|
let name = ns.getWindowName();
|
||||||
if (/[<"'\`(=:]/.test(name)) {
|
if (/[<"'\`(=:]/.test(name)) {
|
||||||
|
|
Loading…
Reference in New Issue