[nscl] Refactoring to use Policy and its dependencies from the NoScript Commons Library.
This commit is contained in:
parent
4a8d6ef2b4
commit
9fad0842f7
15
build.sh
15
build.sh
|
@ -67,10 +67,21 @@ NSCL_SUBMOD="$BASE/nscl"
|
|||
NSCL="$SRC/nscl"
|
||||
NSCL_SRC="$NSCL_SUBMOD/src/nscl"
|
||||
if [[ "$1" == "nscl" ]]; then
|
||||
nscl_cp() {
|
||||
nscl_from="$NSCL_SRC/$1"
|
||||
nscl_to="$NSCL/$1/"
|
||||
mkdir -p "$nscl_to"
|
||||
pushd "$nscl_from" >/dev/null 2>&1
|
||||
shift
|
||||
echo "Copying $@ to $nscl_to..."
|
||||
cp -Rp $@ "$nscl_to"
|
||||
popd >/dev/null 2>&1
|
||||
}
|
||||
echo "Updating and synchronizing nscl..."
|
||||
pushd "$NSCL_SUBMOD" && git submodule update --init && git fetch && git merge && popd || exit 1
|
||||
cp "$NSCL_SRC/common/tld.js" "$NSCL/common/"
|
||||
cp "$NSCL_SRC/content/patchWindow.js" "$NSCL/content/"
|
||||
nscl_cp common tld.js RequestKey.js Sites.js Permissions.js Policy.js
|
||||
nscl_cp lib punycode.*
|
||||
nscl_cp content patchWindow.js
|
||||
git add "$NSCL" && git commit -m'[nscl] Updated NoScript Common Library inclusions.'
|
||||
exit
|
||||
fi
|
||||
|
|
|
@ -124,6 +124,9 @@ var Settings = {
|
|||
}
|
||||
|
||||
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)
|
||||
|
@ -165,6 +168,28 @@ var Settings = {
|
|||
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(),
|
||||
|
|
|
@ -1,496 +0,0 @@
|
|||
var {Permissions, Policy, Sites} = (() => {
|
||||
'use strict';
|
||||
const SECURE_DOMAIN_PREFIX = "§:";
|
||||
const SECURE_DOMAIN_RX = new RegExp(`^${SECURE_DOMAIN_PREFIX}`);
|
||||
const DOMAIN_RX = new RegExp(`(?:^\\w+://|${SECURE_DOMAIN_PREFIX})?([^/]*)`, "i");
|
||||
const IPV4_RX = /^(?:\d+\.){1,3}\d+/;
|
||||
const INTERNAL_SITE_RX = /^(?:(?:about|chrome|resource|(?:moz|chrome)-.*):|\[System)/;
|
||||
const VALID_SITE_RX = /^(?:(?:(?:(?:http|ftp|ws)s?|file):)(?:(?:\/\/)[\w\u0100-\uf000][\w\u0100-\uf000.-]*[\w\u0100-\uf000.](?:$|\/))?|[\w\u0100-\uf000][\w\u0100-\uf000.-]*[\w\u0100-\uf000]$)/;
|
||||
|
||||
let rxQuote = s => s.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&");
|
||||
|
||||
class Sites extends Map {
|
||||
static secureDomainKey(domain) {
|
||||
return /^[§\w]+:/.test(domain) ? domain : `${SECURE_DOMAIN_PREFIX}${domain}`;
|
||||
}
|
||||
static isSecureDomainKey(domain) {
|
||||
return domain.startsWith(SECURE_DOMAIN_PREFIX);
|
||||
}
|
||||
static toggleSecureDomainKey(domain, b = !Sites.isSecureDomainKey(domain)) {
|
||||
return b ? Sites.secureDomainKey(domain) : domain.replace(SECURE_DOMAIN_RX, '');
|
||||
}
|
||||
|
||||
static isValid(site) {
|
||||
return VALID_SITE_RX.test(site);
|
||||
}
|
||||
|
||||
static isInternal(site) {
|
||||
return INTERNAL_SITE_RX.test(site);
|
||||
}
|
||||
|
||||
static originImplies(originKey, site) {
|
||||
return originKey === site || site.startsWith(`${originKey}/`);
|
||||
}
|
||||
|
||||
static domainImplies(domainKey, site, protocol ="https?") {
|
||||
if (Sites.isSecureDomainKey(domainKey)) {
|
||||
protocol = "https";
|
||||
domainKey = Sites.toggleSecureDomainKey(domainKey, false);
|
||||
}
|
||||
if (!site.includes(domainKey)) return false;
|
||||
try {
|
||||
return new RegExp(`^${protocol}://([^/?#:]+\\.)?${rxQuote(domainKey)}(?:[:/]|$)`)
|
||||
.test(site);
|
||||
} catch (e) {
|
||||
error(e, `Cannot check if ${domainKey} implies ${site}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static isImplied(site, byKey) {
|
||||
return byKey.includes("://")
|
||||
? Sites.originImplies(byKey, site)
|
||||
: Sites.domainImplies(byKey, site);
|
||||
}
|
||||
|
||||
static parse(site) {
|
||||
let url, siteKey = "";
|
||||
if (site instanceof URL) {
|
||||
url = site;
|
||||
} else {
|
||||
try {
|
||||
url = new URL(site);
|
||||
} catch (e) {
|
||||
siteKey = site ? (typeof site === "string" ? site : site.toString()) : "";
|
||||
}
|
||||
}
|
||||
if (url) {
|
||||
if (Sites.onionSecure && url.protocol === "http:" && url.hostname.endsWith(".onion")) {
|
||||
url.protocol = "https:";
|
||||
}
|
||||
let path = url.pathname;
|
||||
siteKey = url.origin;
|
||||
if (siteKey === "null") {
|
||||
([siteKey] = site.split(/[?#]/)); // drop any search / hash segment
|
||||
} else if (path !== '/') {
|
||||
siteKey += path;
|
||||
}
|
||||
}
|
||||
return {url, siteKey};
|
||||
}
|
||||
|
||||
static optimalKey(site) {
|
||||
let {url, siteKey} = Sites.parse(site);
|
||||
if (url && url.protocol === "https:") return Sites.secureDomainKey(tld.getDomain(url.hostname));
|
||||
return Sites.origin(url) || siteKey;
|
||||
}
|
||||
|
||||
static origin(site) {
|
||||
if (!site) return "";
|
||||
try {
|
||||
let objUrl = (typeof site === "object" && "origin" in site) ? site : site.startsWith("chrome:") ? {origin: "chrome:" } : new URL(site);
|
||||
let {origin} = objUrl;
|
||||
return origin === "null" ? Sites.cleanUrl(objUrl) || site : origin;
|
||||
} catch (e) {
|
||||
error(e);
|
||||
};
|
||||
return site.origin || site;
|
||||
}
|
||||
|
||||
static cleanUrl(url) {
|
||||
try {
|
||||
url = new URL(url);
|
||||
if (!tld.preserveFQDNs && url.hostname) {
|
||||
url.hostname = tld.normalize(url.hostname);
|
||||
}
|
||||
url.port = "";
|
||||
url.search = "";
|
||||
url.hash = "";
|
||||
return url.href;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static toExternal(url) { // domains are stored in punycode internally
|
||||
let s = typeof url === "string" ? url : url && url.toString() || "";
|
||||
if (s.startsWith(SECURE_DOMAIN_PREFIX)) s = s.substring(SECURE_DOMAIN_PREFIX.length);
|
||||
let [,domain] = DOMAIN_RX.exec(s);
|
||||
return domain.startsWith("xn--") ?
|
||||
s.replace(domain, punycode.toUnicode(domain))
|
||||
: s;
|
||||
}
|
||||
|
||||
set(k, v) {
|
||||
if (!k || Sites.isInternal(k) || k === "§:") return this;
|
||||
let [,domain] = DOMAIN_RX.exec(k);
|
||||
if (/[^\u0000-\u007f]/.test(domain)) {
|
||||
k = k.replace(domain, punycode.toASCII(domain));
|
||||
}
|
||||
return super.set(k, v);
|
||||
}
|
||||
|
||||
match(site) {
|
||||
if (site && this.size) {
|
||||
if (site instanceof URL) site = site.href;
|
||||
if (this.has(site)) return site;
|
||||
|
||||
let {url, siteKey} = Sites.parse(site);
|
||||
|
||||
if (site !== siteKey && this.has(siteKey)) {
|
||||
return siteKey;
|
||||
}
|
||||
|
||||
if (url) {
|
||||
let {origin} = url;
|
||||
if (origin && origin !== "null" && origin < siteKey && this.has(origin)) {
|
||||
return origin;
|
||||
}
|
||||
let domain = this.domainMatch(url);
|
||||
if (domain) return domain;
|
||||
let protocol = url.protocol;
|
||||
if (this.has(protocol)) {
|
||||
return protocol;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
domainMatch(url) {
|
||||
let {protocol, hostname} = url;
|
||||
if (!hostname) return null;
|
||||
if (!tld.preserveFQDNs) hostname = tld.normalize(hostname);
|
||||
let secure = protocol === "https:";
|
||||
let isIPv4 = IPV4_RX.test(hostname);
|
||||
for (let domain = hostname;;) {
|
||||
if (this.has(domain)) {
|
||||
return domain;
|
||||
}
|
||||
if (secure) {
|
||||
let ssDomain = Sites.secureDomainKey(domain);
|
||||
if (this.has(ssDomain)) {
|
||||
return ssDomain;
|
||||
}
|
||||
}
|
||||
|
||||
if (isIPv4) {
|
||||
// subnet shortcuts
|
||||
let dotPos = domain.lastIndexOf(".");
|
||||
if (!(dotPos > 3 || domain.indexOf(".") < dotPos)) {
|
||||
break; // we want at least the 2 most significant bytes
|
||||
}
|
||||
domain = domain.substring(0, dotPos);
|
||||
} else {
|
||||
// (sub)domain matching
|
||||
let dotPos = domain.indexOf(".");
|
||||
if (dotPos === -1) {
|
||||
break;
|
||||
}
|
||||
domain = domain.substring(dotPos + 1); // upper level
|
||||
if (!domain) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
dry() {
|
||||
let dry;
|
||||
if (this.size) {
|
||||
dry = Object.create(null);
|
||||
for (let [key, perms] of this) {
|
||||
dry[key] = perms.dry();
|
||||
}
|
||||
}
|
||||
return dry;
|
||||
}
|
||||
|
||||
static hydrate(dry, obj = new Sites()) {
|
||||
if (dry) {
|
||||
for (let [key, dryPerms] of Object.entries(dry)) {
|
||||
obj.set(key, Permissions.hydrate(dryPerms));
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
class Permissions {
|
||||
|
||||
constructor(capabilities, temp = false, contextual = null) {
|
||||
this.capabilities = new Set(capabilities);
|
||||
this.temp = temp;
|
||||
this.contextual = contextual instanceof Sites ? contextual : new Sites(contextual);
|
||||
}
|
||||
|
||||
dry() {
|
||||
return {capabilities: [...this.capabilities], contextual: this.contextual.dry(), temp: this.temp};
|
||||
}
|
||||
|
||||
static hydrate(dry = {}, obj = null) {
|
||||
let capabilities = new Set(dry.capabilities);
|
||||
let contextual = Sites.hydrate(dry.contextual);
|
||||
let temp = dry.temp;
|
||||
return obj ? Object.assign(obj, {capabilities, temp, contextual, _tempTwin: undefined})
|
||||
: new Permissions(capabilities, temp, contextual);
|
||||
}
|
||||
|
||||
static typed(capability, type) {
|
||||
let [capName] = capability.split(":");
|
||||
return `${capName}:${type}`;
|
||||
}
|
||||
|
||||
allowing(capability) {
|
||||
return this.capabilities.has(capability);
|
||||
}
|
||||
|
||||
set(capability, enabled = true) {
|
||||
if (enabled) {
|
||||
this.capabilities.add(capability);
|
||||
} else {
|
||||
this.capabilities.delete(capability);
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
sameAs(otherPerms) {
|
||||
let otherCaps = new Set(otherPerms.capabilities);
|
||||
let theseCaps = this.capabilities;
|
||||
for (let c of theseCaps) {
|
||||
if (!otherCaps.delete(c)) return false;
|
||||
}
|
||||
for (let c of otherCaps) {
|
||||
if (!theseCaps.has(c)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
clone() {
|
||||
return new Permissions(this.capabilities, this.temp, this.contextual);
|
||||
}
|
||||
get tempTwin() {
|
||||
return this._tempTwin || (this._tempTwin = new Permissions(this.capabilities, true, this.contextual));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Permissions.ALL = ["script", "object", "media", "frame", "font", "webgl", "fetch", "ping", "other"];
|
||||
Permissions.IMMUTABLE = {
|
||||
UNTRUSTED: {
|
||||
"script": false,
|
||||
"object": false,
|
||||
"webgl": false,
|
||||
"fetch": false,
|
||||
"other": false,
|
||||
"ping": false,
|
||||
},
|
||||
TRUSTED: {
|
||||
"script": true,
|
||||
}
|
||||
};
|
||||
|
||||
Object.freeze(Permissions.ALL);
|
||||
|
||||
function defaultOptions() {
|
||||
return {
|
||||
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`.split(/\s+/).map(Sites.secureDomainKey),
|
||||
untrusted: [],
|
||||
custom: {},
|
||||
},
|
||||
DEFAULT: new Permissions(["frame", "fetch", "other"]),
|
||||
TRUSTED: new Permissions(Permissions.ALL),
|
||||
UNTRUSTED: new Permissions(),
|
||||
enforced: true,
|
||||
autoAllowTop: false,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePolicyOptions(dry) {
|
||||
let options = Object.assign({}, dry);
|
||||
for (let p of ["DEFAULT", "TRUSTED", "UNTRUSTED"]) {
|
||||
options[p] = dry[p] instanceof Permissions ? dry[p] : Permissions.hydrate(dry[p]);
|
||||
options[p].temp = false; // preserve immutability of presets persistence
|
||||
}
|
||||
if (typeof dry.sites === "object" && !(dry.sites instanceof Sites)) {
|
||||
let {trusted, untrusted, temp, custom} = dry.sites;
|
||||
let sites = Sites.hydrate(custom);
|
||||
for (let key of trusted) {
|
||||
sites.set(key, options.TRUSTED);
|
||||
}
|
||||
for (let key of untrusted) {
|
||||
sites.set(Sites.toggleSecureDomainKey(key, false), options.UNTRUSTED);
|
||||
}
|
||||
if (temp) {
|
||||
let tempPreset = options.TRUSTED.tempTwin;
|
||||
for (let key of temp) sites.set(key, tempPreset);
|
||||
}
|
||||
options.sites = sites;
|
||||
}
|
||||
enforceImmutable(options);
|
||||
return options;
|
||||
}
|
||||
|
||||
function enforceImmutable(policy) {
|
||||
for (let [preset, filter] of Object.entries(Permissions.IMMUTABLE)) {
|
||||
let presetCaps = policy[preset].capabilities;
|
||||
for (let [cap, value] of Object.entries(filter)) {
|
||||
if (value) presetCaps.add(cap);
|
||||
else presetCaps.delete(cap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Policy {
|
||||
|
||||
constructor(options = defaultOptions()) {
|
||||
Object.assign(this, normalizePolicyOptions(options));
|
||||
}
|
||||
|
||||
static hydrate(dry, policyObj) {
|
||||
return policyObj ? Object.assign(policyObj, normalizePolicyOptions(dry))
|
||||
: new Policy(dry);
|
||||
}
|
||||
|
||||
dry(includeTemp = false) {
|
||||
let trusted = [],
|
||||
temp = [],
|
||||
untrusted = [],
|
||||
custom = Object.create(null);
|
||||
|
||||
const {DEFAULT, TRUSTED, UNTRUSTED} = this;
|
||||
for(let [key, perms] of this.sites) {
|
||||
if (!includeTemp && perms.temp) {
|
||||
continue;
|
||||
}
|
||||
switch(perms) {
|
||||
case TRUSTED:
|
||||
trusted.push(key);
|
||||
break;
|
||||
case TRUSTED.tempTwin:
|
||||
temp.push(key);
|
||||
break;
|
||||
case UNTRUSTED:
|
||||
untrusted.push(key);
|
||||
break;
|
||||
case DEFAULT:
|
||||
break;
|
||||
default:
|
||||
custom[key] = perms.dry();
|
||||
}
|
||||
}
|
||||
|
||||
let sites = {
|
||||
trusted,
|
||||
untrusted,
|
||||
custom
|
||||
};
|
||||
if (includeTemp) {
|
||||
sites.temp = temp;
|
||||
}
|
||||
enforceImmutable(this);
|
||||
return {
|
||||
DEFAULT: DEFAULT.dry(),
|
||||
TRUSTED: TRUSTED.dry(),
|
||||
UNTRUSTED: UNTRUSTED.dry(),
|
||||
sites,
|
||||
enforced: this.enforced,
|
||||
autoAllowTop: this.autoAllowTop,
|
||||
};
|
||||
}
|
||||
|
||||
static requestKey(url, type, documentUrl, includePath = false) {
|
||||
url = includePath ? Sites.parse(url).siteKey : Sites.origin(url);
|
||||
return `${type}@${url}<${Sites.origin(documentUrl)}`;
|
||||
}
|
||||
|
||||
static explodeKey(requestKey) {
|
||||
let [, type, url, documentUrl] = /(\w+)@([^<]+)<(.*)/.exec(requestKey);
|
||||
return {url, type, documentUrl};
|
||||
}
|
||||
|
||||
set(site, perms, cascade = false) {
|
||||
let sites = this.sites;
|
||||
let {url, siteKey} = Sites.parse(site);
|
||||
|
||||
sites.delete(siteKey);
|
||||
let wideSiteKey = Sites.toggleSecureDomainKey(siteKey, false);
|
||||
|
||||
if (perms === this.UNTRUSTED) {
|
||||
cascade = true;
|
||||
siteKey = wideSiteKey;
|
||||
} else {
|
||||
if (wideSiteKey !== siteKey) {
|
||||
sites.delete(wideSiteKey);
|
||||
}
|
||||
}
|
||||
if (cascade && !url) {
|
||||
for (let subMatch; (subMatch = sites.match(siteKey));) {
|
||||
sites.delete(subMatch);
|
||||
}
|
||||
}
|
||||
|
||||
if (!perms || perms === this.DEFAULT) {
|
||||
perms = this.DEFAULT;
|
||||
} else {
|
||||
sites.set(siteKey, perms);
|
||||
}
|
||||
return {siteKey, perms};
|
||||
}
|
||||
|
||||
get(site, ctx = null) {
|
||||
let perms, contextMatch;
|
||||
let siteMatch = !(this.onlySecure && /^\w+tp:/i.test(site)) && this.sites.match(site);
|
||||
if (siteMatch) {
|
||||
perms = this.sites.get(siteMatch);
|
||||
if (ctx) {
|
||||
contextMatch = perms.contextual.match(ctx);
|
||||
if (contextMatch) perms = perms.contextual.get(ctx);
|
||||
}
|
||||
} else {
|
||||
perms = this.DEFAULT;
|
||||
}
|
||||
|
||||
return {perms, siteMatch, contextMatch};
|
||||
}
|
||||
|
||||
can(url, capability = "script", ctx = null) {
|
||||
return !this.enforced ||
|
||||
this.get(url, ctx).perms.allowing(capability);
|
||||
}
|
||||
|
||||
get snapshot() {
|
||||
return JSON.stringify(this.dry(true));
|
||||
}
|
||||
|
||||
cascadeRestrictions(perms, topUrl) {
|
||||
let topPerms = this.get(topUrl, topUrl).perms;
|
||||
if (topPerms !== perms) {
|
||||
let topCaps = topPerms.capabilities;
|
||||
perms = new Permissions([...perms.capabilities].filter(c => topCaps.has(c)),
|
||||
perms.temp, perms.contextual);
|
||||
}
|
||||
return perms;
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
this.snapshot === other.snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
return {Permissions, Policy, Sites};
|
||||
})();
|
|
@ -53,7 +53,7 @@ var Legacy = {
|
|||
} catch (e) {
|
||||
error(e);
|
||||
}
|
||||
return new Policy();
|
||||
return new Policy(Settings.createDefaultDryPolicy());
|
||||
},
|
||||
|
||||
extractLists(lists) {
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -40,7 +40,7 @@
|
|||
"lib/SyncMessage.js",
|
||||
"lib/log.js",
|
||||
"lib/include.js",
|
||||
"lib/punycode.js",
|
||||
"nscl/lib/punycode.js",
|
||||
"nscl/common/tld.js",
|
||||
"lib/LastListener.js",
|
||||
"lib/Messages.js",
|
||||
|
@ -48,8 +48,10 @@
|
|||
"lib/NetCSP.js",
|
||||
"lib/TabCache.js",
|
||||
"common/CapsCSP.js",
|
||||
"common/RequestKey.js",
|
||||
"common/Policy.js",
|
||||
"/nscl/common/RequestKey.js",
|
||||
"/nscl/common/Sites.js",
|
||||
"/nscl/common/Permissions.js",
|
||||
"/nscl/common/Policy.js",
|
||||
"common/locale.js",
|
||||
"common/SyntaxChecker.js",
|
||||
"common/Storage.js",
|
||||
|
@ -98,7 +100,7 @@
|
|||
"lib/CSP.js",
|
||||
"nscl/content/patchWindow.js",
|
||||
"common/CapsCSP.js",
|
||||
"common/RequestKey.js",
|
||||
"/nscl/common/RequestKey.js",
|
||||
"content/DocumentCSP.js",
|
||||
"content/onScriptDisabled.js",
|
||||
"content/staticNS.js",
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
var Permissions = (() => {
|
||||
'use strict';
|
||||
/**
|
||||
* This class models an extensible set of browser capabilities, to be assigned to a certain site,
|
||||
* possibly tied to a set of parent sites (contextual permissions).
|
||||
* Depends on Sites.js.
|
||||
*/
|
||||
class Permissions {
|
||||
/**
|
||||
* Creates a Permissions object
|
||||
* @param {Set/array} capabilities the capability enabled by this Permissions
|
||||
* @param {boolean} temp are these permissions marked as temporary (volatile?)
|
||||
* @param {Sites/array} contextual (optional) the parent sites which these permissions are tied to
|
||||
*/
|
||||
constructor(capabilities, temp = false, contextual = null) {
|
||||
this.capabilities = new Set(capabilities);
|
||||
this.temp = temp;
|
||||
this.contextual = contextual instanceof Sites ? contextual : new Sites(contextual);
|
||||
}
|
||||
|
||||
dry() {
|
||||
return {capabilities: [...this.capabilities], contextual: this.contextual.dry(), temp: this.temp};
|
||||
}
|
||||
|
||||
static hydrate(dry = {}, obj = null) {
|
||||
let capabilities = new Set(dry.capabilities);
|
||||
let contextual = Sites.hydrate(dry.contextual);
|
||||
let temp = dry.temp;
|
||||
return obj ? Object.assign(obj, {capabilities, temp, contextual, _tempTwin: undefined})
|
||||
: new Permissions(capabilities, temp, contextual);
|
||||
}
|
||||
|
||||
static typed(capability, type) {
|
||||
let [capName] = capability.split(":");
|
||||
return `${capName}:${type}`;
|
||||
}
|
||||
|
||||
allowing(capability) {
|
||||
return this.capabilities.has(capability);
|
||||
}
|
||||
|
||||
set(capability, enabled = true) {
|
||||
if (enabled) {
|
||||
this.capabilities.add(capability);
|
||||
} else {
|
||||
this.capabilities.delete(capability);
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
sameAs(otherPerms) {
|
||||
let otherCaps = new Set(otherPerms.capabilities);
|
||||
let theseCaps = this.capabilities;
|
||||
for (let c of theseCaps) {
|
||||
if (!otherCaps.delete(c)) return false;
|
||||
}
|
||||
for (let c of otherCaps) {
|
||||
if (!theseCaps.has(c)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
clone() {
|
||||
return new Permissions(this.capabilities, this.temp, this.contextual);
|
||||
}
|
||||
get tempTwin() {
|
||||
return this._tempTwin || (this._tempTwin = new Permissions(this.capabilities, true, this.contextual));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Permissions.ALL = ["script", "object", "media", "frame", "font", "webgl", "fetch", "ping", "other"];
|
||||
Permissions.IMMUTABLE = {
|
||||
UNTRUSTED: {
|
||||
"script": false,
|
||||
"object": false,
|
||||
"webgl": false,
|
||||
"fetch": false,
|
||||
"other": false,
|
||||
"ping": false,
|
||||
},
|
||||
TRUSTED: {
|
||||
"script": true,
|
||||
}
|
||||
};
|
||||
|
||||
Object.freeze(Permissions.ALL);
|
||||
|
||||
return Permissions;
|
||||
})();
|
|
@ -0,0 +1,198 @@
|
|||
var Policy = (() => {
|
||||
'use strict';
|
||||
|
||||
function defaultOptions() {
|
||||
return {
|
||||
sites:{
|
||||
trusted: [],
|
||||
untrusted: [],
|
||||
custom: {},
|
||||
},
|
||||
DEFAULT: new Permissions(["frame", "fetch", "other"]),
|
||||
TRUSTED: new Permissions(Permissions.ALL),
|
||||
UNTRUSTED: new Permissions(),
|
||||
enforced: true,
|
||||
autoAllowTop: false,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePolicyOptions(dry) {
|
||||
let options = Object.assign({}, dry);
|
||||
for (let p of ["DEFAULT", "TRUSTED", "UNTRUSTED"]) {
|
||||
options[p] = dry[p] instanceof Permissions ? dry[p] : Permissions.hydrate(dry[p]);
|
||||
options[p].temp = false; // preserve immutability of presets persistence
|
||||
}
|
||||
if (typeof dry.sites === "object" && !(dry.sites instanceof Sites)) {
|
||||
let {trusted, untrusted, temp, custom} = dry.sites;
|
||||
let sites = Sites.hydrate(custom);
|
||||
for (let key of trusted) {
|
||||
sites.set(key, options.TRUSTED);
|
||||
}
|
||||
for (let key of untrusted) {
|
||||
sites.set(Sites.toggleSecureDomainKey(key, false), options.UNTRUSTED);
|
||||
}
|
||||
if (temp) {
|
||||
let tempPreset = options.TRUSTED.tempTwin;
|
||||
for (let key of temp) sites.set(key, tempPreset);
|
||||
}
|
||||
options.sites = sites;
|
||||
}
|
||||
enforceImmutable(options);
|
||||
return options;
|
||||
}
|
||||
|
||||
function enforceImmutable(policy) {
|
||||
for (let [preset, filter] of Object.entries(Permissions.IMMUTABLE)) {
|
||||
let presetCaps = policy[preset].capabilities;
|
||||
for (let [cap, value] of Object.entries(filter)) {
|
||||
if (value) presetCaps.add(cap);
|
||||
else presetCaps.delete(cap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A browser-independent class representing all the restrictions to content
|
||||
* loading and script execution we want to apply globally and per-site,
|
||||
* providing methods to set, query and serialize these settings.
|
||||
* Depends on Permissions.js and Sites.js.
|
||||
*/
|
||||
class Policy {
|
||||
|
||||
constructor(options = defaultOptions()) {
|
||||
Object.assign(this, normalizePolicyOptions(options));
|
||||
}
|
||||
|
||||
static hydrate(dry, policyObj) {
|
||||
return policyObj ? Object.assign(policyObj, normalizePolicyOptions(dry))
|
||||
: new Policy(dry);
|
||||
}
|
||||
|
||||
dry(includeTemp = false) {
|
||||
let trusted = [],
|
||||
temp = [],
|
||||
untrusted = [],
|
||||
custom = Object.create(null);
|
||||
|
||||
const {DEFAULT, TRUSTED, UNTRUSTED} = this;
|
||||
for(let [key, perms] of this.sites) {
|
||||
if (!includeTemp && perms.temp) {
|
||||
continue;
|
||||
}
|
||||
switch(perms) {
|
||||
case TRUSTED:
|
||||
trusted.push(key);
|
||||
break;
|
||||
case TRUSTED.tempTwin:
|
||||
temp.push(key);
|
||||
break;
|
||||
case UNTRUSTED:
|
||||
untrusted.push(key);
|
||||
break;
|
||||
case DEFAULT:
|
||||
break;
|
||||
default:
|
||||
custom[key] = perms.dry();
|
||||
}
|
||||
}
|
||||
|
||||
let sites = {
|
||||
trusted,
|
||||
untrusted,
|
||||
custom
|
||||
};
|
||||
if (includeTemp) {
|
||||
sites.temp = temp;
|
||||
}
|
||||
enforceImmutable(this);
|
||||
return {
|
||||
DEFAULT: DEFAULT.dry(),
|
||||
TRUSTED: TRUSTED.dry(),
|
||||
UNTRUSTED: UNTRUSTED.dry(),
|
||||
sites,
|
||||
enforced: this.enforced,
|
||||
autoAllowTop: this.autoAllowTop,
|
||||
};
|
||||
}
|
||||
|
||||
static requestKey(url, type, documentUrl, includePath = false) {
|
||||
url = includePath ? Sites.parse(url).siteKey : Sites.origin(url);
|
||||
return `${type}@${url}<${Sites.origin(documentUrl)}`;
|
||||
}
|
||||
|
||||
static explodeKey(requestKey) {
|
||||
let [, type, url, documentUrl] = /(\w+)@([^<]+)<(.*)/.exec(requestKey);
|
||||
return {url, type, documentUrl};
|
||||
}
|
||||
|
||||
set(site, perms, cascade = false) {
|
||||
let sites = this.sites;
|
||||
let {url, siteKey} = Sites.parse(site);
|
||||
|
||||
sites.delete(siteKey);
|
||||
let wideSiteKey = Sites.toggleSecureDomainKey(siteKey, false);
|
||||
|
||||
if (perms === this.UNTRUSTED) {
|
||||
cascade = true;
|
||||
siteKey = wideSiteKey;
|
||||
} else {
|
||||
if (wideSiteKey !== siteKey) {
|
||||
sites.delete(wideSiteKey);
|
||||
}
|
||||
}
|
||||
if (cascade && !url) {
|
||||
for (let subMatch; (subMatch = sites.match(siteKey));) {
|
||||
sites.delete(subMatch);
|
||||
}
|
||||
}
|
||||
|
||||
if (!perms || perms === this.DEFAULT) {
|
||||
perms = this.DEFAULT;
|
||||
} else {
|
||||
sites.set(siteKey, perms);
|
||||
}
|
||||
return {siteKey, perms};
|
||||
}
|
||||
|
||||
get(site, ctx = null) {
|
||||
let perms, contextMatch;
|
||||
let siteMatch = !(this.onlySecure && /^\w+tp:/i.test(site)) && this.sites.match(site);
|
||||
if (siteMatch) {
|
||||
perms = this.sites.get(siteMatch);
|
||||
if (ctx) {
|
||||
contextMatch = perms.contextual.match(ctx);
|
||||
if (contextMatch) perms = perms.contextual.get(ctx);
|
||||
}
|
||||
} else {
|
||||
perms = this.DEFAULT;
|
||||
}
|
||||
|
||||
return {perms, siteMatch, contextMatch};
|
||||
}
|
||||
|
||||
can(url, capability = "script", ctx = null) {
|
||||
return !this.enforced ||
|
||||
this.get(url, ctx).perms.allowing(capability);
|
||||
}
|
||||
|
||||
get snapshot() {
|
||||
return JSON.stringify(this.dry(true));
|
||||
}
|
||||
|
||||
cascadeRestrictions(perms, topUrl) {
|
||||
let topPerms = this.get(topUrl, topUrl).perms;
|
||||
if (topPerms !== perms) {
|
||||
let topCaps = topPerms.capabilities;
|
||||
perms = new Permissions([...perms.capabilities].filter(c => topCaps.has(c)),
|
||||
perms.temp, perms.contextual);
|
||||
}
|
||||
return perms;
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
this.snapshot === other.snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
return Policy;
|
||||
})();
|
|
@ -0,0 +1,224 @@
|
|||
var Sites = (() => {
|
||||
'use strict';
|
||||
const SECURE_DOMAIN_PREFIX = "§:";
|
||||
const SECURE_DOMAIN_RX = new RegExp(`^${SECURE_DOMAIN_PREFIX}`);
|
||||
const DOMAIN_RX = new RegExp(`(?:^\\w+://|${SECURE_DOMAIN_PREFIX})?([^/]*)`, "i");
|
||||
const IPV4_RX = /^(?:\d+\.){1,3}\d+/;
|
||||
const INTERNAL_SITE_RX = /^(?:(?:about|chrome|resource|(?:moz|chrome)-.*):|\[System)/;
|
||||
const VALID_SITE_RX = /^(?:(?:(?:(?:http|ftp|ws)s?|file):)(?:(?:\/\/)[\w\u0100-\uf000][\w\u0100-\uf000.-]*[\w\u0100-\uf000.](?:$|\/))?|[\w\u0100-\uf000][\w\u0100-\uf000.-]*[\w\u0100-\uf000]$)/;
|
||||
|
||||
let rxQuote = s => s.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&");
|
||||
|
||||
/**
|
||||
* a Map whose keys are (partial) URLs, used by Policy to store per-site Permissions
|
||||
* and providing several utility functions for URL/origin manipulation and mapping.
|
||||
*/
|
||||
class Sites extends Map {
|
||||
static secureDomainKey(domain) {
|
||||
return /^[§\w]+:/.test(domain) ? domain : `${SECURE_DOMAIN_PREFIX}${domain}`;
|
||||
}
|
||||
static isSecureDomainKey(domain) {
|
||||
return domain.startsWith(SECURE_DOMAIN_PREFIX);
|
||||
}
|
||||
static toggleSecureDomainKey(domain, b = !Sites.isSecureDomainKey(domain)) {
|
||||
return b ? Sites.secureDomainKey(domain) : domain.replace(SECURE_DOMAIN_RX, '');
|
||||
}
|
||||
|
||||
static isValid(site) {
|
||||
return VALID_SITE_RX.test(site);
|
||||
}
|
||||
|
||||
static isInternal(site) {
|
||||
return INTERNAL_SITE_RX.test(site);
|
||||
}
|
||||
|
||||
static originImplies(originKey, site) {
|
||||
return originKey === site || site.startsWith(`${originKey}/`);
|
||||
}
|
||||
|
||||
static domainImplies(domainKey, site, protocol ="https?") {
|
||||
if (Sites.isSecureDomainKey(domainKey)) {
|
||||
protocol = "https";
|
||||
domainKey = Sites.toggleSecureDomainKey(domainKey, false);
|
||||
}
|
||||
if (!site.includes(domainKey)) return false;
|
||||
try {
|
||||
return new RegExp(`^${protocol}://([^/?#:]+\\.)?${rxQuote(domainKey)}(?:[:/]|$)`)
|
||||
.test(site);
|
||||
} catch (e) {
|
||||
error(e, `Cannot check if ${domainKey} implies ${site}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static isImplied(site, byKey) {
|
||||
return byKey.includes("://")
|
||||
? Sites.originImplies(byKey, site)
|
||||
: Sites.domainImplies(byKey, site);
|
||||
}
|
||||
|
||||
static parse(site) {
|
||||
let url, siteKey = "";
|
||||
if (site instanceof URL) {
|
||||
url = site;
|
||||
} else {
|
||||
try {
|
||||
url = new URL(site);
|
||||
} catch (e) {
|
||||
siteKey = site ? (typeof site === "string" ? site : site.toString()) : "";
|
||||
}
|
||||
}
|
||||
if (url) {
|
||||
if (Sites.onionSecure && url.protocol === "http:" && url.hostname.endsWith(".onion")) {
|
||||
url.protocol = "https:";
|
||||
}
|
||||
let path = url.pathname;
|
||||
siteKey = url.origin;
|
||||
if (siteKey === "null") {
|
||||
([siteKey] = site.split(/[?#]/)); // drop any search / hash segment
|
||||
} else if (path !== '/') {
|
||||
siteKey += path;
|
||||
}
|
||||
}
|
||||
return {url, siteKey};
|
||||
}
|
||||
|
||||
static optimalKey(site) {
|
||||
let {url, siteKey} = Sites.parse(site);
|
||||
if (url && url.protocol === "https:") return Sites.secureDomainKey(tld.getDomain(url.hostname));
|
||||
return Sites.origin(url) || siteKey;
|
||||
}
|
||||
|
||||
static origin(site) {
|
||||
if (!site) return "";
|
||||
try {
|
||||
let objUrl = (typeof site === "object" && "origin" in site) ? site : site.startsWith("chrome:") ? {origin: "chrome:" } : new URL(site);
|
||||
let {origin} = objUrl;
|
||||
return origin === "null" ? Sites.cleanUrl(objUrl) || site : origin;
|
||||
} catch (e) {
|
||||
error(e);
|
||||
};
|
||||
return site.origin || site;
|
||||
}
|
||||
|
||||
static cleanUrl(url) {
|
||||
try {
|
||||
url = new URL(url);
|
||||
if (!tld.preserveFQDNs && url.hostname) {
|
||||
url.hostname = tld.normalize(url.hostname);
|
||||
}
|
||||
url.port = "";
|
||||
url.search = "";
|
||||
url.hash = "";
|
||||
return url.href;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static toExternal(url) { // domains are stored in punycode internally
|
||||
let s = typeof url === "string" ? url : url && url.toString() || "";
|
||||
if (s.startsWith(SECURE_DOMAIN_PREFIX)) s = s.substring(SECURE_DOMAIN_PREFIX.length);
|
||||
let [,domain] = DOMAIN_RX.exec(s);
|
||||
return domain.startsWith("xn--") ?
|
||||
s.replace(domain, punycode.toUnicode(domain))
|
||||
: s;
|
||||
}
|
||||
|
||||
set(k, v) {
|
||||
if (!k || Sites.isInternal(k) || k === "§:") return this;
|
||||
let [,domain] = DOMAIN_RX.exec(k);
|
||||
if (/[^\u0000-\u007f]/.test(domain)) {
|
||||
k = k.replace(domain, punycode.toASCII(domain));
|
||||
}
|
||||
return super.set(k, v);
|
||||
}
|
||||
|
||||
match(site) {
|
||||
if (site && this.size) {
|
||||
if (site instanceof URL) site = site.href;
|
||||
if (this.has(site)) return site;
|
||||
|
||||
let {url, siteKey} = Sites.parse(site);
|
||||
|
||||
if (site !== siteKey && this.has(siteKey)) {
|
||||
return siteKey;
|
||||
}
|
||||
|
||||
if (url) {
|
||||
let {origin} = url;
|
||||
if (origin && origin !== "null" && origin < siteKey && this.has(origin)) {
|
||||
return origin;
|
||||
}
|
||||
let domain = this.domainMatch(url);
|
||||
if (domain) return domain;
|
||||
let protocol = url.protocol;
|
||||
if (this.has(protocol)) {
|
||||
return protocol;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
domainMatch(url) {
|
||||
let {protocol, hostname} = url;
|
||||
if (!hostname) return null;
|
||||
if (!tld.preserveFQDNs) hostname = tld.normalize(hostname);
|
||||
let secure = protocol === "https:";
|
||||
let isIPv4 = IPV4_RX.test(hostname);
|
||||
for (let domain = hostname;;) {
|
||||
if (this.has(domain)) {
|
||||
return domain;
|
||||
}
|
||||
if (secure) {
|
||||
let ssDomain = Sites.secureDomainKey(domain);
|
||||
if (this.has(ssDomain)) {
|
||||
return ssDomain;
|
||||
}
|
||||
}
|
||||
|
||||
if (isIPv4) {
|
||||
// subnet shortcuts
|
||||
let dotPos = domain.lastIndexOf(".");
|
||||
if (!(dotPos > 3 || domain.indexOf(".") < dotPos)) {
|
||||
break; // we want at least the 2 most significant bytes
|
||||
}
|
||||
domain = domain.substring(0, dotPos);
|
||||
} else {
|
||||
// (sub)domain matching
|
||||
let dotPos = domain.indexOf(".");
|
||||
if (dotPos === -1) {
|
||||
break;
|
||||
}
|
||||
domain = domain.substring(dotPos + 1); // upper level
|
||||
if (!domain) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
dry() {
|
||||
let dry;
|
||||
if (this.size) {
|
||||
dry = Object.create(null);
|
||||
for (let [key, perms] of this) {
|
||||
dry[key] = perms.dry();
|
||||
}
|
||||
}
|
||||
return dry;
|
||||
}
|
||||
|
||||
static hydrate(dry, obj = new Sites()) {
|
||||
if (dry) {
|
||||
for (let [key, dryPerms] of Object.entries(dry)) {
|
||||
obj.set(key, Permissions.hydrate(dryPerms));
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
return Sites;
|
||||
})();
|
|
@ -0,0 +1,20 @@
|
|||
Copyright Mathias Bynens <https://mathiasbynens.be/>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -41,11 +41,11 @@
|
|||
opt("amnesticUpdates", "local");
|
||||
|
||||
{
|
||||
document.querySelector("#btn-reset").addEventListener("click", async () => {
|
||||
document.querySelector("#btn-reset").addEventListener("click", async ev => {
|
||||
if (confirm(_("reset_warning"))) {
|
||||
policy = new Policy();
|
||||
await UI.updateSettings({policy, local: null, sync: null, xssUserChoices: {}});
|
||||
window.location.reload();
|
||||
ev.target.disabled = true;
|
||||
document.querySelector("#main-tabs").style.visibility = "hidden";
|
||||
await UI.updateSettings({local: null, sync: null, xssUserChoices: {}});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
(async () => {
|
||||
let [domain, tabId] = decodeURIComponent(location.hash.replace("#", "")).split(";");
|
||||
const BASE = "https://noscript.net";
|
||||
await include(['/lib/punycode.js', '/common/Storage.js']);
|
||||
await include(['/nscl/lib/punycode.js', '/common/Storage.js']);
|
||||
let {siteInfoConsent} = await Storage.get("sync", "siteInfoConsent");
|
||||
if (!siteInfoConsent) {
|
||||
await include('/common/locale.js');
|
||||
|
|
|
@ -20,9 +20,11 @@ var UI = (() => {
|
|||
let scripts = [
|
||||
"/ui/ui.css",
|
||||
"/lib/Messages.js",
|
||||
"/lib/punycode.js",
|
||||
"/nscl/lib/punycode.js",
|
||||
"/nscl/common/tld.js",
|
||||
"/common/Policy.js",
|
||||
"/nscl/common/Sites.js",
|
||||
"/nscl/common/Permissions.js",
|
||||
"/nscl/common/Policy.js",
|
||||
];
|
||||
this.mobile = UA.mobile;
|
||||
if (this.mobile) {
|
||||
|
|
Loading…
Reference in New Issue