diff options
Diffstat (limited to 'src/common')
-rw-r--r-- | src/common/Entities.js | 30 | ||||
-rw-r--r-- | src/common/Policy.js | 390 | ||||
-rw-r--r-- | src/common/Storage.js | 24 | ||||
-rw-r--r-- | src/common/SyntaxChecker.js | 29 | ||||
-rw-r--r-- | src/common/locale.js | 45 |
5 files changed, 518 insertions, 0 deletions
diff --git a/src/common/Entities.js b/src/common/Entities.js new file mode 100644 index 0000000..03a5d11 --- /dev/null +++ b/src/common/Entities.js @@ -0,0 +1,30 @@ +var Entities = { + get htmlNode() { + delete this.htmlNode; + return this.htmlNode = document.implementation.createHTMLDocument("") + .createElement("body"); + }, + convert: function(e) { + try { + this.htmlNode.innerHTML = e; + var child = this.htmlNode.firstChild || null; + return child && child.nodeValue || e; + } catch(ex) { + return e; + } + }, + convertAll: function(s) { + return s.replace(/[\\&][^<>]+/g, function(e) { return Entities.convert(e) }); + }, + convertDeep: function(s) { + for (var prev = null; (s = this.convertAll(s)) !== prev || (s = unescape(s)) !== prev; prev = s); + return s; + }, + neutralize: function(e, whitelist) { + var c = this.convert(e); + return (c == e) ? c : (whitelist && whitelist.test(c) ? e : e.replace(";", ",")); + }, + neutralizeAll: function(s, whitelist) { + return s.replace(/&[\w#-]*?;/g, function(e) { return Entities.neutralize(e, whitelist || null); }); + } +}; diff --git a/src/common/Policy.js b/src/common/Policy.js new file mode 100644 index 0000000..6adc2ae --- /dev/null +++ b/src/common/Policy.js @@ -0,0 +1,390 @@ +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 SKIP_RX = /^(?:(?:about|chrome|resource|moz-.*):|\[System)/; + + class Sites extends Map { + static secureDomainKey(domain) { + return domain.includes(":") ? 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 /^(?:https?:(?:\/\/)?)?([\w\u0100-\uf000][\w\u0100-\uf000.-]*)?[\w\u0100-\uf000](?::\d+)?$/.test(site); + } + + static parse(site) { + let url, siteKey = ""; + if (site instanceof URL) { + url = site; + } else { + try { + url = new URL(site); + } catch (e) { + siteKey = typeof site === "string" ? site : site.toString(); + } + } + if (url) { + let path = url.pathname; + siteKey = url.origin; + 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 url && url.origin || siteKey; + } + + static origin(site) { + try { + return new URL(site).origin; + } catch (e) {}; + return site; + } + + 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 || SKIP_RX.test(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 (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; + + let secure = protocol === "https:"; + for (let domain = hostname;;) { + if (this.has(domain)) { + return domain; + } + if (secure) { + let ssDomain = Sites.secureDomainKey(domain); + if (this.has(ssDomain)) { + return ssDomain; + } + } + let dotPos = domain.indexOf("."); + if (dotPos === -1) { + break; + } + domain = domain.substring(dotPos + 1); // sub + 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; + } + + get tempTwin() { + return this._tempTwin || (this._tempTwin = new Permissions(this.capabilities, true, this.contextual)); + } + } + + Permissions.ALL = ["script", "object", "media", "frame", "font", "webgl", "fetch", "other"]; + Permissions.IMMUTABLE = { + UNTRUSTED: { + "script": false, + "object": false, + "webgl": false, + "fetch": false, + "other": 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]); + } + + 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(key, 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); + + if (perms === this.UNTRUSTED) { + cascade = true; + Sites.toggleSecureDomainKey(siteKey, false); + } + 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)); + } + + equals(other) { + this.snapshot === other.snapshot; + } + } + + return {Permissions, Policy, Sites}; +})(); diff --git a/src/common/Storage.js b/src/common/Storage.js new file mode 100644 index 0000000..4555a28 --- /dev/null +++ b/src/common/Storage.js @@ -0,0 +1,24 @@ +var Storage = { + + async safeOp(op, type, keys) { + try { + return await browser.storage[type][op](keys); + } catch (e) { + if (type === "sync") { + debug("Sync disabled? Falling back to local storage (%s %o)", op, keys); + } else { + error(e); + throw e; + } + } + return await browser.storage.local[op](keys); + }, + + async get(type, keys) { + return await this.safeOp("get", type, keys); + }, + + async set(type, keys) { + return await this.safeOp("set", type, keys); + } +} diff --git a/src/common/SyntaxChecker.js b/src/common/SyntaxChecker.js new file mode 100644 index 0000000..8646901 --- /dev/null +++ b/src/common/SyntaxChecker.js @@ -0,0 +1,29 @@ +class SyntaxChecker { + constructor() { + this.lastError = null; + this.lastFunction = null; + this.lastScript = ""; + } + check(script) { + this.lastScript = script; + try { + return !!(this.lastFunction = new Function(script)); + } catch(e) { + this.lastError = e; + this.lastFunction = null; + } + return false; + } + unquote(s, q) { + // check that this is really a double or a single quoted string... + if (s.length > 1 && s.startsWith(q) && s.endsWith(q) && + // if nothing is left if you remove all he escapes and all the stuff between quotes + s.replace(/\\./g, '').replace(/^(['"])[^\n\r]*?\1/, '') === '') { + try { + return eval(s); + } catch (e) { + } + } + return null; + } +} diff --git a/src/common/locale.js b/src/common/locale.js new file mode 100644 index 0000000..60498a2 --- /dev/null +++ b/src/common/locale.js @@ -0,0 +1,45 @@ +'use strict'; +var _ = browser.i18n.getMessage; +var i18n = (() => { + var i18n = { + // derived from http://github.com/piroor/webextensions-lib-l10n + + updateString(aString) { + return aString.replace(/__MSG_(.+?)__/g, function(aMatched) { + var key = aMatched.slice(6, -2); + return _(key); + }); + }, + updateDOM(rootNode = document) { + var texts = document.evaluate( + 'descendant::text()[contains(self::text(), "__MSG_")]', + rootNode, + null, + XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, + null + ); + for (let i = 0, maxi = texts.snapshotLength; i < maxi; i++) + { + let text = texts.snapshotItem(i); + text.nodeValue = this.updateString(text.nodeValue); + } + + var attributes = document.evaluate( + 'descendant::*/attribute::*[contains(., "__MSG_")]', + rootNode, + null, + XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, + null + ); + for (let i = 0, maxi = attributes.snapshotLength; i < maxi; i++) + { + let attribute = attributes.snapshotItem(i); + debug('apply', attribute); + attribute.value = this.updateString(attribute.value); + } + } + }; + + document.addEventListener('DOMContentLoaded', e => i18n.updateDOM()); + return i18n; +})() |