[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"
|
||||||
NSCL_SRC="$NSCL_SUBMOD/src/nscl"
|
NSCL_SRC="$NSCL_SUBMOD/src/nscl"
|
||||||
if [[ "$1" == "nscl" ]]; then
|
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..."
|
echo "Updating and synchronizing nscl..."
|
||||||
pushd "$NSCL_SUBMOD" && git submodule update --init && git fetch && git merge && popd || exit 1
|
pushd "$NSCL_SUBMOD" && git submodule update --init && git fetch && git merge && popd || exit 1
|
||||||
cp "$NSCL_SRC/common/tld.js" "$NSCL/common/"
|
nscl_cp common tld.js RequestKey.js Sites.js Permissions.js Policy.js
|
||||||
cp "$NSCL_SRC/content/patchWindow.js" "$NSCL/content/"
|
nscl_cp lib punycode.*
|
||||||
|
nscl_cp content patchWindow.js
|
||||||
git add "$NSCL" && git commit -m'[nscl] Updated NoScript Common Library inclusions.'
|
git add "$NSCL" && git commit -m'[nscl] Updated NoScript Common Library inclusions.'
|
||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -124,6 +124,9 @@ var Settings = {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.sync === null) {
|
if (settings.sync === null) {
|
||||||
|
// user is resetting options
|
||||||
|
policy = this.createDefaultDryPolicy();
|
||||||
|
|
||||||
// overriden defaults when user manually resets options
|
// overriden defaults when user manually resets options
|
||||||
|
|
||||||
// we want the reset options to stick (otherwise it gets very confusing)
|
// we want the reset options to stick (otherwise it gets very confusing)
|
||||||
|
@ -165,6 +168,28 @@ var Settings = {
|
||||||
if (reloadOptionsUI) await this.reloadOptionsUI();
|
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() {
|
export() {
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
policy: ns.policy.dry(),
|
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) {
|
} catch (e) {
|
||||||
error(e);
|
error(e);
|
||||||
}
|
}
|
||||||
return new Policy();
|
return new Policy(Settings.createDefaultDryPolicy());
|
||||||
},
|
},
|
||||||
|
|
||||||
extractLists(lists) {
|
extractLists(lists) {
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -40,7 +40,7 @@
|
||||||
"lib/SyncMessage.js",
|
"lib/SyncMessage.js",
|
||||||
"lib/log.js",
|
"lib/log.js",
|
||||||
"lib/include.js",
|
"lib/include.js",
|
||||||
"lib/punycode.js",
|
"nscl/lib/punycode.js",
|
||||||
"nscl/common/tld.js",
|
"nscl/common/tld.js",
|
||||||
"lib/LastListener.js",
|
"lib/LastListener.js",
|
||||||
"lib/Messages.js",
|
"lib/Messages.js",
|
||||||
|
@ -48,8 +48,10 @@
|
||||||
"lib/NetCSP.js",
|
"lib/NetCSP.js",
|
||||||
"lib/TabCache.js",
|
"lib/TabCache.js",
|
||||||
"common/CapsCSP.js",
|
"common/CapsCSP.js",
|
||||||
"common/RequestKey.js",
|
"/nscl/common/RequestKey.js",
|
||||||
"common/Policy.js",
|
"/nscl/common/Sites.js",
|
||||||
|
"/nscl/common/Permissions.js",
|
||||||
|
"/nscl/common/Policy.js",
|
||||||
"common/locale.js",
|
"common/locale.js",
|
||||||
"common/SyntaxChecker.js",
|
"common/SyntaxChecker.js",
|
||||||
"common/Storage.js",
|
"common/Storage.js",
|
||||||
|
@ -98,7 +100,7 @@
|
||||||
"lib/CSP.js",
|
"lib/CSP.js",
|
||||||
"nscl/content/patchWindow.js",
|
"nscl/content/patchWindow.js",
|
||||||
"common/CapsCSP.js",
|
"common/CapsCSP.js",
|
||||||
"common/RequestKey.js",
|
"/nscl/common/RequestKey.js",
|
||||||
"content/DocumentCSP.js",
|
"content/DocumentCSP.js",
|
||||||
"content/onScriptDisabled.js",
|
"content/onScriptDisabled.js",
|
||||||
"content/staticNS.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");
|
opt("amnesticUpdates", "local");
|
||||||
|
|
||||||
{
|
{
|
||||||
document.querySelector("#btn-reset").addEventListener("click", async () => {
|
document.querySelector("#btn-reset").addEventListener("click", async ev => {
|
||||||
if (confirm(_("reset_warning"))) {
|
if (confirm(_("reset_warning"))) {
|
||||||
policy = new Policy();
|
ev.target.disabled = true;
|
||||||
await UI.updateSettings({policy, local: null, sync: null, xssUserChoices: {}});
|
document.querySelector("#main-tabs").style.visibility = "hidden";
|
||||||
window.location.reload();
|
await UI.updateSettings({local: null, sync: null, xssUserChoices: {}});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
(async () => {
|
(async () => {
|
||||||
let [domain, tabId] = decodeURIComponent(location.hash.replace("#", "")).split(";");
|
let [domain, tabId] = decodeURIComponent(location.hash.replace("#", "")).split(";");
|
||||||
const BASE = "https://noscript.net";
|
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");
|
let {siteInfoConsent} = await Storage.get("sync", "siteInfoConsent");
|
||||||
if (!siteInfoConsent) {
|
if (!siteInfoConsent) {
|
||||||
await include('/common/locale.js');
|
await include('/common/locale.js');
|
||||||
|
|
|
@ -20,9 +20,11 @@ var UI = (() => {
|
||||||
let scripts = [
|
let scripts = [
|
||||||
"/ui/ui.css",
|
"/ui/ui.css",
|
||||||
"/lib/Messages.js",
|
"/lib/Messages.js",
|
||||||
"/lib/punycode.js",
|
"/nscl/lib/punycode.js",
|
||||||
"/nscl/common/tld.js",
|
"/nscl/common/tld.js",
|
||||||
"/common/Policy.js",
|
"/nscl/common/Sites.js",
|
||||||
|
"/nscl/common/Permissions.js",
|
||||||
|
"/nscl/common/Policy.js",
|
||||||
];
|
];
|
||||||
this.mobile = UA.mobile;
|
this.mobile = UA.mobile;
|
||||||
if (this.mobile) {
|
if (this.mobile) {
|
||||||
|
|
Loading…
Reference in New Issue