aboutsummaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorJesús <heckyel@hyperbola.info>2019-12-30 15:55:13 -0500
committerJesús <heckyel@hyperbola.info>2019-12-30 15:55:13 -0500
commit288df6a7bf8b933e2dc499e38f4915fcf974c14b (patch)
tree77bba994f260c064d3ee7f76c427ddfaa4f91710 /lib
parenta2c9deaa145b780722e93b3899600f287c8094a4 (diff)
downloadematrix-288df6a7bf8b933e2dc499e38f4915fcf974c14b.tar.lz
ematrix-288df6a7bf8b933e2dc499e38f4915fcf974c14b.tar.xz
ematrix-288df6a7bf8b933e2dc499e38f4915fcf974c14b.zip
backport
- Flush caches on upgrade - Properly handle FrameModule's unloading - Use the new module and remove the old implementation
Diffstat (limited to 'lib')
-rw-r--r--lib/CallbackWrapper.jsm92
-rw-r--r--lib/CookieCache.jsm171
-rw-r--r--lib/FrameModule.jsm337
-rw-r--r--lib/HttpRequestHeaders.jsm102
-rw-r--r--lib/LiquidDict.jsm175
-rw-r--r--lib/PendingRequests.jsm97
-rw-r--r--lib/PublicSuffixList.jsm308
-rw-r--r--lib/Punycode.jsm305
-rw-r--r--lib/UriTools.jsm405
9 files changed, 1992 insertions, 0 deletions
diff --git a/lib/CallbackWrapper.jsm b/lib/CallbackWrapper.jsm
new file mode 100644
index 0000000..1d3664e
--- /dev/null
+++ b/lib/CallbackWrapper.jsm
@@ -0,0 +1,92 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://gitlab.com/vannilla/ematrix
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+'use strict';
+
+var EXPORTED_SYMBOLS = ['CallbackWrapper'];
+
+var junkyard = [];
+
+var CallbackWrapper = function (messageManager, channelName,
+ listenerId, requestId) {
+ // This allows to avoid creating a closure for every single
+ // message which expects an answer. Having a closure created
+ // each time a message is processed has been always bothering
+ // me. Another benefit of the implementation here is to reuse
+ // the callback proxy object, so less memory churning.
+ //
+ // https://developers.google.com/speed/articles/optimizing-javascript
+ // "Creating a closure is significantly slower then creating
+ // an inner function without a closure, and much slower than
+ // reusing a static function"
+ //
+ // http://hacksoflife.blogspot.ca/2015/01/the-four-horsemen-of-performance.html
+ // "the dreaded 'uniformly slow code' case where every
+ // function takes 1% of CPU and you have to make one hundred
+ // separate performance optimizations to improve performance
+ // at all"
+ //
+ // http://jsperf.com/closure-no-closure/2
+ this.callback = this.proxy.bind(this); // bind once
+ this.init(messageManager, channelName, listenerId, requestId);
+};
+
+CallbackWrapper.factory = function (messageManager, channelName,
+ listenerId, requestId) {
+ let wrapper = junkyard.pop();
+ if (wrapper) {
+ wrapper.init(messageManager, channelName, listenerId, requestId);
+ return wrapper;
+ }
+
+ return new CallbackWrapper(messageManager, channelName,
+ listenerId, requestId);
+};
+
+CallbackWrapper.prototype.init = function (messageManager, channelName,
+ listenerId, requestId) {
+ this.messageManager = messageManager;
+ this.channelName = channelName;
+ this.listenerId = listenerId;
+ this.requestId = requestId;
+};
+
+CallbackWrapper.prototype.proxy = function (response) {
+ let message = JSON.stringify({
+ requestId: this.requestId,
+ channelName: this.channelName,
+ msg: response !== undefined ? response : null
+ });
+
+ if (this.messageManager.sendAsyncMessage) {
+ this.messageManager.sendAsyncMessage(this.listenerId, message);
+ } else {
+ this.messageManager.broadcastAsyncMessage(this.listenerId, message);
+ }
+
+ // Mark for reuse
+ this.messageManager = this.channelName =
+ this.requestId = this.listenerId = null;
+
+ junkyard.push(this);
+};
diff --git a/lib/CookieCache.jsm b/lib/CookieCache.jsm
new file mode 100644
index 0000000..7b6fe6f
--- /dev/null
+++ b/lib/CookieCache.jsm
@@ -0,0 +1,171 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://gitlab.com/vannilla/ematrix
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+'use strict';
+
+Components.utils.import('chrome://ematrix/content/lib/UriTools.jsm');
+
+var EXPORTED_SYMBOLS = ['CookieCache', 'CookieUtils'];
+
+var junkyard = [];
+var dict = new Map();
+
+var CookieEntry = function (cookie) {
+ this.usedOn = new Set();
+ this.init(cookie);
+};
+
+CookieEntry.prototype.init = function (cookie) {
+ this.secure = cookie.secure;
+ this.session = cookie.session;
+ this.anySubdomain = cookie.domain.charAt(0) === '.';
+ this.hostname = this.anySubdomain ? cookie.domain.slice(1) : cookie.domain;
+ this.domain = UriTools.domainFromHostname(this.hostname) || this.hostname;
+ this.path = cookie.path;
+ this.name = cookie.name;
+ this.value = cookie.value;
+ this.tstamp = Date.now();
+ this.usedOn.clear();
+ return this;
+};
+
+CookieEntry.prototype.dispose = function () {
+ this.hostname = '';
+ this.domain = '';
+ this.path = '';
+ this.name = '';
+ this.value = '';
+ this.usedOn.clear();
+ return this;
+};
+
+var CookieUtils = {
+ keyFromCookie: function (cookie) {
+ let cb = [];
+
+ cb[0] = cookie.secure ? 'https' : 'http';
+ cb[1] = '://';
+ cb[2] = cookie.domain.charAt(0) === '.' ?
+ cookie.domain.slice(1) :
+ cookie.domain;
+ cb[3] = cookie.path;
+ cb[4] = '{';
+ cb[5] = cookie.session ? 'session' : 'persistent';
+ cb[6] = '-cookie:';
+ cb[7] = cookie.name;
+ cb[8] = '}';
+
+ return cb.join('');
+ },
+ keyFromURL: function (url, type, name) {
+ if (typeof url !== 'object') {
+ throw new Error('Invalid URL parameter');
+ }
+
+ let cb = [];
+
+ cb[0] = url.scheme;
+ cb[1] = '://';
+ cb[2] = url.hostname;
+ cb[3] = url.path;
+ cb[4] = '{';
+ cb[5] = type;
+ cb[6] = '-cookie:';
+ cb[7] = name;
+ cb[8] = '}';
+
+ return cb.join('');
+ },
+ urlFromEntry: function (entry) {
+ if (!entry) {
+ return '';
+ }
+
+ return (entry.secure ? 'https://' : 'http://')
+ + entry.hostname
+ + entry.path;
+ },
+ matchDomains: function (key, allHosts) {
+ let entry = CookieCache.get(key);
+
+ if (entry === undefined) {
+ return false;
+ }
+
+ if (allHosts.indexOf(' '+entry.hostname+' ') < 0) {
+ if (!entry.anySubdomain) {
+ return false;
+ }
+
+ if (allHosts.indexOf('.'+entry.hostname+' ') < 0) {
+ return false;
+ }
+ }
+
+ return true;
+ },
+};
+
+var CookieCache = {
+ add: function (cookie) {
+ let key = CookieUtils.keyFromCookie(cookie);
+ let value = dict.get(key);
+
+ if (value === undefined) {
+ value = junkyard.pop();
+ if (value) {
+ value.init(cookie);
+ } else {
+ value = new CookieEntry(cookie);
+ }
+ dict.set(key, value);
+ }
+
+ return value;
+ },
+ addVector: function (vector) {
+ for (let i=vector.length-1; i>=0; --i) {
+ CookieCache.add(vector[i]);
+ }
+ },
+ remove: function (cookie) {
+ let value = dict.get(cookie);
+ if (cookie === undefined) {
+ return false;
+ }
+
+ dict.delete(cookie);
+
+ if (junkyard.length < 25) {
+ junkyard.push(value.dispose());
+ }
+
+ return true;
+ },
+ get: function (key) {
+ return dict.get(key);
+ },
+ keys: function () {
+ return dict.keys();
+ }
+};
diff --git a/lib/FrameModule.jsm b/lib/FrameModule.jsm
new file mode 100644
index 0000000..ef149ae
--- /dev/null
+++ b/lib/FrameModule.jsm
@@ -0,0 +1,337 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 The µBlock authors
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://gitlab.com/vannilla/ematrix
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+'use strict';
+
+// https://github.com/gorhill/uBlock/issues/800#issuecomment-146580443
+var EXPORTED_SYMBOLS = ['contentObserver', 'LocationChangeListener'];
+
+const {interfaces: Ci, utils: Cu} = Components;
+Cu.import('resource://gre/modules/Services.jsm');
+Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+
+var hostName = Services.io.newURI(Components.stack.filename, null, null).host;
+
+// Cu.import('resource://gre/modules/Console.jsm');
+
+const getMessageManager = function (win) {
+ let iface = win
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .sameTypeRootTreeItem
+ .QueryInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIInterfaceRequestor);
+
+ try {
+ return iface.getInterface(Ci.nsIContentFrameMessageManager);
+ } catch (ex) {
+ // This can throw. It appears `shouldLoad` can be called *after* a
+ // tab has been closed. For example, a case where this happens all
+ // the time (FF38):
+ // - Open twitter.com (assuming you have an account and are logged in)
+ // - Close twitter.com
+ // There will be an exception raised when `shouldLoad` is called
+ // to process a XMLHttpRequest with URL `https://twitter.com/i/jot`
+ // fired from `https://twitter.com/`, *after* the tab is closed.
+ // In such case, `win` is `about:blank`.
+ }
+ return null;
+};
+
+var contentObserver = {
+ classDescription: 'content-policy for ' + hostName,
+ classID: Components.ID('{c84283d4-9975-41b7-b1a4-f106af56b51d}'),
+ contractID: '@' + hostName + '/content-policy;1',
+ ACCEPT: Ci.nsIContentPolicy.ACCEPT,
+ MAIN_FRAME: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ contentBaseURI: 'chrome://' + hostName + '/content/js/',
+ cpMessageName: hostName + ':shouldLoad',
+ uniqueSandboxId: 1,
+ modernFirefox:
+ (Services.appinfo.ID === '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}'
+ || Services.appinfo.ID === '{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}')
+ && Services.vc.compare(Services.appinfo.version, '44') > 0,
+
+ get componentRegistrar() {
+ return Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ },
+
+ get categoryManager() {
+ return Components.classes['@mozilla.org/categorymanager;1']
+ .getService(Ci.nsICategoryManager);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIFactory,
+ Ci.nsIObserver,
+ Ci.nsIContentPolicy,
+ Ci.nsISupportsWeakReference
+ ]),
+
+ createInstance: function (outer, iid) {
+ if (outer) {
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+ }
+
+ return this.QueryInterface(iid);
+ },
+
+ register: function () {
+ Services.obs.addObserver(this, 'document-element-inserted', true);
+
+ if (!this.modernFirefox) {
+ this.componentRegistrar
+ .registerFactory(this.classID,
+ this.classDescription,
+ this.contractID,
+ this);
+ this.categoryManager
+ .addCategoryEntry('content-policy',
+ this.contractID,
+ this.contractID,
+ false,
+ true);
+ }
+ },
+
+ unregister: function () {
+ Services.obs.removeObserver(this, 'document-element-inserted');
+
+ if (!this.modernFirefox) {
+ this.componentRegistrar
+ .unregisterFactory(this.classID,
+ this);
+ this.categoryManager
+ .deleteCategoryEntry('content-policy',
+ this.contractID,
+ false);
+ }
+ },
+
+ // https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIContentPolicy
+ // https://bugzil.la/612921
+ shouldLoad: function (type, location, origin, context) {
+ if (!context) {
+ return this.ACCEPT;
+ }
+
+ if (!location.schemeIs('http') && !location.schemeIs('https')) {
+ return this.ACCEPT;
+ }
+
+ let contextWindow;
+ if (type === this.MAIN_FRAME) {
+ contextWindow = context.contentWindow || context;
+ } else if (type === this.SUB_FRAME) {
+ contextWindow = context.contentWindow;
+ } else {
+ contextWindow = (context.ownerDocument || context).defaultView;
+ }
+
+ // https://github.com/gorhill/uMatrix/issues/706
+ if (!contextWindow) {
+ return this.ACCEPT;
+ }
+
+ // The context for the toolbar popup is an iframe element here,
+ // so check context.top instead of context
+ if (!contextWindow.top || !contextWindow.location) {
+ return this.ACCEPT;
+ }
+
+ let messageManager = getMessageManager(contextWindow);
+ if (messageManager === null) {
+ return this.ACCEPT;
+ }
+
+ let details = {
+ rawType: type,
+ url: location.asciiSpec
+ };
+
+ if (typeof messageManager.sendRpcMessage === 'function') {
+ // https://bugzil.la/1092216
+ messageManager.sendRpcMessage(this.cpMessageName, details);
+ } else {
+ // Compatibility for older versions
+ messageManager.sendSyncMessage(this.cpMessageName, details);
+ }
+
+ return this.ACCEPT;
+ },
+
+ initContentScripts: function (win, sandbox) {
+ let messager = getMessageManager(win);
+ let sandboxId = hostName + ':sb:' + this.uniqueSandboxId++;
+
+ if (sandbox) {
+ let sandboxName = [
+ win.location.href.slice(0, 100),
+ win.document.title.slice(0, 100)
+ ].join(' | ');
+
+ // https://github.com/gorhill/uMatrix/issues/325
+ // "Pass sameZoneAs to sandbox constructor to make GCs cheaper"
+ sandbox = Cu.Sandbox([win], {
+ sameZoneAs: win.top,
+ sandboxName: sandboxId + '[' + sandboxName + ']',
+ sandboxPrototype: win,
+ wantComponents: false,
+ wantXHRConstructor: false
+ });
+
+ sandbox.injectScript = function (script) {
+ Services.scriptloader.loadSubScript(script, sandbox);
+ };
+ }
+ else {
+ sandbox = win;
+ }
+
+ sandbox._sandboxId_ = sandboxId;
+ sandbox.sendAsyncMessage = messager.sendAsyncMessage;
+
+ sandbox.addMessageListener = function (callback) {
+ if (sandbox._messageListener_) {
+ sandbox.removeMessageListener();
+ }
+
+ sandbox._messageListener_ = function (message) {
+ callback(message.data);
+ };
+
+ messager.addMessageListener(sandbox._sandboxId_,
+ sandbox._messageListener_);
+ messager.addMessageListener(hostName + ':broadcast',
+ sandbox._messageListener_);
+ };
+
+ sandbox.removeMessageListener = function () {
+ try {
+ messager.removeMessageListener(sandbox._sandboxId_,
+ sandbox._messageListener_);
+ messager.removeMessageListener(hostName + ':broadcast',
+ sandbox._messageListener_);
+ } catch (ex) {
+ // It throws sometimes, mostly when the popup closes
+ }
+
+ sandbox._messageListener_ = null;
+ };
+
+ return sandbox;
+ },
+
+ observe: function (doc) {
+ let win = doc.defaultView;
+ if (!win) {
+ return;
+ }
+
+ let loc = win.location;
+ if (!loc) {
+ return;
+ }
+
+ // https://github.com/gorhill/uBlock/issues/260
+ // TODO: We may have to skip more types, for now let's be
+ // conservative, i.e. let's not test against `text/html`.
+ if (doc.contentType.lastIndexOf('image/', 0) === 0) {
+ return;
+ }
+
+ if (loc.protocol !== 'http:'
+ && loc.protocol !== 'https:'
+ && loc.protocol !== 'file:') {
+ if (loc.protocol === 'chrome:' && loc.host === hostName) {
+ this.initContentScripts(win);
+ }
+
+ // What about data: and about:blank?
+ return;
+ }
+
+ let lss = Services.scriptloader.loadSubScript;
+ let sandbox = this.initContentScripts(win, true);
+
+ // Can throw with attempts at injecting into non-HTML document.
+ // Example: https://a.pomf.se/avonjf.webm
+ try {
+ lss(this.contentBaseURI + 'vapi-client.js', sandbox);
+ lss(this.contentBaseURI + 'contentscript-start.js', sandbox);
+ } catch (ex) {
+ return; // don't further try to inject anything
+ }
+
+ let docReady = (e) => {
+ let doc = e.target;
+ doc.removeEventListener(e.type, docReady, true);
+ lss(this.contentBaseURI + 'contentscript.js', sandbox);
+ };
+
+ if (doc.readyState === 'loading') {
+ doc.addEventListener('DOMContentLoaded', docReady, true);
+ } else {
+ docReady({
+ target: doc,
+ type: 'DOMContentLoaded',
+ });
+ }
+ }
+};
+
+var locationChangedMessageName = hostName + ':locationChanged';
+
+var LocationChangeListener = function (docShell) {
+ if (!docShell) {
+ return;
+ }
+
+ var requestor = docShell.QueryInterface(Ci.nsIInterfaceRequestor);
+ var ds = requestor.getInterface(Ci.nsIWebProgress);
+ var mm = requestor.getInterface(Ci.nsIContentFrameMessageManager);
+
+ if (ds && mm && typeof mm.sendAsyncMessage === 'function') {
+ this.docShell = ds;
+ this.messageManager = mm;
+ ds.addProgressListener(this, Ci.nsIWebProgress.NOTIFY_LOCATION);
+ }
+};
+
+LocationChangeListener.prototype.QueryInterface = XPCOMUtils.generateQI([
+ 'nsIWebProgressListener',
+ 'nsISupportsWeakReference'
+]);
+
+LocationChangeListener.prototype.onLocationChange =
+ function (webProgress, request, location, flags) {
+ if (!webProgress.isTopLevel) {
+ return;
+ }
+ this.messageManager.sendAsyncMessage(locationChangedMessageName, {
+ url: location.asciiSpec,
+ flags: flags,
+ });
+ };
+
+contentObserver.register();
diff --git a/lib/HttpRequestHeaders.jsm b/lib/HttpRequestHeaders.jsm
new file mode 100644
index 0000000..4c125eb
--- /dev/null
+++ b/lib/HttpRequestHeaders.jsm
@@ -0,0 +1,102 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://gitlab.com/vannilla/ematrix
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+'use strict';
+
+var EXPORTED_SYMBOLS = ['HTTPRequestHeaders'];
+
+var junkyard = [];
+
+var HTTPRequestHeaders = function (channel) {
+ this.init(channel);
+};
+
+HTTPRequestHeaders.factory = function (channel) {
+ let entry = junkyard.pop();
+ if (entry) {
+ return entry.init(channel);
+ }
+
+ return new HTTPRequestHeaders(channel);
+}
+
+HTTPRequestHeaders.prototype.init = function (channel) {
+ this.channel = channel;
+ this.headers = new Array();
+ this.originalHeaderNames = new Array();
+
+ channel.visitRequestHeaders({
+ visitHeader: function (name, value) {
+ this.headers.push({name: name, value: value});
+ this.originalHeaderNames.push(name);
+ }.bind(this)
+ });
+
+ return this;
+};
+
+HTTPRequestHeaders.prototype.dispose = function () {
+ this.channel = null;
+ this.headers = null;
+ this.originalHeaderNames = null;
+ junkyard.push(this);
+};
+
+HTTPRequestHeaders.prototype.update = function () {
+ let newHeaderNames = new Set();
+ for (let header of this.headers) {
+ this.setHeader(header.name, header.value, true);
+ newHeaderNames.add(header.name);
+ }
+
+ //Clear any headers that were removed
+ for (let name of this.originalHeaderNames) {
+ if (!newHeaderNames.has(name)) {
+ this.channel.setRequestHeader(name, '', false);
+ }
+ }
+};
+
+HTTPRequestHeaders.prototype.getHeader = function (name) {
+ try {
+ return this.channel.getRequestHeader(name);
+ } catch (e) {
+ // Ignore
+ }
+
+ return '';
+};
+
+HTTPRequestHeaders.prototype.setHeader = function (name, newValue, create) {
+ let oldValue = this.getHeader(name);
+ if (newValue === oldValue) {
+ return false;
+ }
+
+ if (oldValue === '' && create !== true) {
+ return false;
+ }
+
+ this.channel.setRequestHeader(name, newValue, false);
+ return true;
+};
diff --git a/lib/LiquidDict.jsm b/lib/LiquidDict.jsm
new file mode 100644
index 0000000..65de9c5
--- /dev/null
+++ b/lib/LiquidDict.jsm
@@ -0,0 +1,175 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://gitlab.com/vannilla/ematrix
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+'use strict';
+
+var EXPORTED_SYMBOLS = ['LiquidDict'];
+
+var LiquidDict = function () {
+ this.dict = {};
+ this.count = 0;
+ this.duplicateCount = 0;
+ this.bucketCount = 0;
+ this.frozenCount = 0;
+ this.cutoff = 500;
+};
+
+function meltBucket(dict, len, bucket) {
+ let map = {};
+
+ --dict.frozenCount;
+
+ if (bucket.charAt(0) === ' ') {
+ bucket.trim().split(' ').map(function (e) {
+ map[e] = true;
+ });
+ } else {
+ for (let i=0; i<bucket.length; i+=len) {
+ map[bucket.substring(i, len)] = true;
+ }
+ }
+
+ return map;
+}
+
+function melt(dict) {
+ let buckets = dict.dict;
+ for (let key in buckets) {
+ let bucket = buckets[key];
+ if (typeof bucket === 'string') {
+ buckets[key] = meltBucket(dict, key.charCodeAt(0) & 0xFF, bucket);
+ }
+ }
+}
+
+function freezeBucket(dict, bucket) {
+ let words = Object.keys(bucket);
+ let wlen = words[0].length;
+
+ ++dict.frozenCount;
+
+ if (wlen * words.length < dict.cutoff) {
+ return ' ' + words.join(' ') + ' ';
+ }
+
+ return words.sort().join('');
+}
+
+LiquidDict.prototype.makeKey = function (word) {
+ let len = word.length;
+ if (len > 255) {
+ len = 255;
+ }
+
+ let i = len >> 2;
+ return String.fromCharCode((word.charCodeAt(0) & 0x03) << 14
+ | (word.charCodeAt(i) & 0x03) << 12
+ | (word.charCodeAt(i+i) & 0x03) << 10
+ | (word.charCodeAt(i+i+i) & 0x03) << 8
+ | len);
+};
+
+LiquidDict.prototype.test = function (word) {
+ let key = this.makeKey(word);
+ let bucket = this.dict[key];
+
+ if (bucket === undefined) {
+ return false;
+ }
+
+ if (typeof bucket === 'object') {
+ return bucket[word] !== undefined;
+ }
+
+ if (bucket.charAt(0) === ' ') {
+ return bucket.indexOf(' ' + word + ' ') >= 0;
+ }
+
+ let len = word.length;
+ let left = 0;
+ let right = ~~(bucket.length / len + 0.5);
+
+ while (left < right) {
+ let i = left + right >> 1;
+ let needle = bucket.substr(len * i, len);
+
+ if (word < needle) {
+ right = i;
+ } else if (word > needle) {
+ left = i + 1;
+ } else {
+ return true;
+ }
+ }
+
+ return false;
+};
+
+LiquidDict.prototype.add = function (word) {
+ let key = this.makeKey(word);
+ if (key === undefined) {
+ return false;
+ }
+
+ let bucket = this.dict[key];
+ if (bucket === undefined) {
+ this.dict[key] = bucket = {};
+ ++this.bucketCount;
+ bucket[word] = true;
+ ++this.count;
+
+ return true;
+ } else if (typeof bucket === 'string') {
+ this.dict[key] = bucket = meltBucket(this, word.len, bucket);
+ }
+
+ if (bucket[word] === undefined) {
+ bucket[word] = true;
+ ++this.count;
+
+ return true;
+ }
+
+ ++this.duplicateCount;
+
+ return false;
+};
+
+LiquidDict.prototype.freeze = function () {
+ let buckets = this.dict;
+
+ for (let key in buckets) {
+ let bucket = buckets[key];
+ if (typeof bucket === 'object') {
+ buckets[key] = freezeBucket(this, bucket);
+ }
+ }
+};
+
+LiquidDict.prototype.reset = function () {
+ this.dict = {};
+ this.count = 0;
+ this.duplicateCount = 0;
+ this.bucketCount = 0;
+ this.frozenBucketCount = 0;
+}
diff --git a/lib/PendingRequests.jsm b/lib/PendingRequests.jsm
new file mode 100644
index 0000000..1214c3c
--- /dev/null
+++ b/lib/PendingRequests.jsm
@@ -0,0 +1,97 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://gitlab.com/vannilla/ematrix
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+'use strict';
+
+var EXPORTED_SYMBOLS = ['PendingRequestBuffer'];
+
+function PendingRequest() {
+ this.rawType = 0;
+ this.tabId = 0;
+ this._key = '';
+}
+
+var urlToIndex = new Map();
+var writePointer = 0;
+var ringBuffer = new Array(256);
+
+var PendingRequestBuffer = (function () {
+ for (let i=ringBuffer.length-1; i>=0; --i) {
+ ringBuffer[i] = new PendingRequest();
+ }
+
+ return {
+ createRequest: function (url) {
+ // URL to ring buffer index map:
+ // { k = URL, s = ring buffer indices }
+ //
+ // s is a string which character codes map to ring buffer
+ // indices -- for when the same URL is received multiple times
+ // by shouldLoadListener() before the existing one is serviced
+ // by the network request observer. I believe the use of a
+ // string in lieu of an array reduces memory churning.
+ let bucket;
+ let i = writePointer;
+ writePointer = i + 1 & 255;
+
+ let req = ringBuffer[i];
+ let str = String.fromCharCode(i);
+
+ if (req._key !== '') {
+ bucket = urlToIndex.get(req._key);
+ if (bucket.lenght === 1) {
+ urlToIndex.delete(req._key);
+ } else {
+ let pos = bucket.indexOf(str);
+ urlToIndex.set(req._key,
+ bucket.slice(0, pos)+bucket.slice(pos+1));
+ }
+ }
+
+ bucket = urlToIndex.get(url);
+ urlToIndex.set(url,
+ (bucket === undefined) ? str : bucket + str);
+ req._key = url;
+
+ return req;
+ },
+ lookupRequest: function (url) {
+ let bucket = urlToIndex.get(url);
+ if (bucket === undefined) {
+ return null;
+ }
+
+ let i = bucket.charCodeAt(0);
+ if (bucket.length === 1) {
+ urlToIndex.delete(url);
+ } else {
+ urlToIndex.set(url, bucket.slice(1));
+ }
+
+ let req = ringBuffer[i];
+ req._key = '';
+
+ return req;
+ },
+ }
+})();
diff --git a/lib/PublicSuffixList.jsm b/lib/PublicSuffixList.jsm
new file mode 100644
index 0000000..72b0669
--- /dev/null
+++ b/lib/PublicSuffixList.jsm
@@ -0,0 +1,308 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 The uMatrix/uBlock Origin authors
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://gitlab.com/vannilla/ematrix
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+'use strict';
+
+var EXPORTED_SYMBOLS = ['publicSuffixList'];
+
+var exceptions = {};
+var rules = {};
+var magic = 'iscjsfsaolnm';
+
+// This value dictate how the search will be performed:
+// < cutoffLength → indexOf()
+// >= cutoffLength → binary search
+var cutoffLength = 256;
+
+var reMustPunycode = /[^\w.*-]/;
+
+var onChangedListeners = [];
+
+function search(store, hostname) {
+ let pos = hostname.lastIndexOf('.');
+ let tld;
+ let remainder;
+
+ if (pos < 0) {
+ tld = hostname;
+ remainder = hostname;
+ } else {
+ tld = hostname.slice(pos+1);
+ remainder = hostname.slice(0, pos);
+ }
+
+ let sub = store[tld];
+ if (!sub) {
+ return false;
+ }
+
+ if (typeof sub === 'string') {
+ return (sub.indexOf(' '+remainder+' ') >= 0);
+ }
+
+ let l = remainder.length;
+ let val = sub[l];
+ if (!val) {
+ return false;
+ }
+
+ let left = 0;
+ let right = Math.floor(val.length/l+0.5);
+
+ while (left < right) {
+ let i = left+right >> 1;
+ let key = val.substr(l*i, l);
+ if (remainder < key) {
+ right = i;
+ } else if (remainder > key) {
+ left = i+1;
+ } else {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+function getPublicSuffix(hostname) {
+ if (!hostname) {
+ return '';
+ }
+
+ while (true) {
+ let pos = hostname.indexOf('.');
+ if (pos < 0) {
+ return hostname;
+ }
+
+ if (search(exceptions, hostname)) {
+ return hostname.slice(pos+1);
+ }
+
+ if (search(rules, hostname)) {
+ return hostname;
+ }
+
+ if (search(rules, '*'+hostname.slice(pos))) {
+ return hostname;
+ }
+
+ hostname = hostname.slice(pos+1);
+ }
+}
+
+function getDomain(hostname) {
+ if (!hostname || hostname.charAt(0) == '.') {
+ return '';
+ }
+
+ hostname = hostname.toLowerCase();
+
+ let suffix = getPublicSuffix(hostname);
+ if (suffix === hostname) {
+ return '';
+ }
+
+ let len = hostname.length-suffix.length;
+ let pos = hostname.lastIndexOf('.', hostname.lastIndexOf('.', len) - 1);
+ if (pos <= 0) {
+ return hostname;
+ }
+
+ return hostname.slice(pos+1);
+}
+
+function crystallize(store) {
+ for (let tld in store) {
+ if (!store.hasOwnProperty(tld)) {
+ continue;
+ }
+
+ let suff = store[tld].join(' ');
+ if (!suff) {
+ store[tld] = '';
+ continue;
+ }
+
+ if (suff.length < cutoffLength) {
+ store[tld] = ' ' + suff + ' ';
+ continue;
+ }
+
+ suff = [];
+ for (let i=store[tld].length-1; i>=0; --i) {
+ let s = store[tld][i];
+ let l = s.length;
+ if (!suff[l]) {
+ suff[l] = [];
+ }
+ suff[l].push(s);
+ }
+
+ for (let i=suff.length-1; i>=0; --i) {
+ if (suff[i]) {
+ suff[i] = suff[i].sort().join('');
+ }
+ }
+
+ store[tld] = suff;
+ }
+
+ return store;
+}
+
+function parse(text, toAscii) {
+ exceptions = {};
+ rules = {};
+
+ let beg = 0;
+ let end = 0;
+ let tend = text.length;
+
+ while (beg < tend) {
+ end = text.indexOf('\n', beg);
+ if (end < 0) {
+ end = text.indexOf('\r', beg);
+ if (end < 0) {
+ end = tend;
+ }
+ }
+
+ let line = text.slice(beg, end).trim();
+ beg = end+1;
+
+ if (line.length === 0) {
+ continue;
+ }
+
+ let pos = line.indexOf('//');
+ if (pos >= 0) {
+ line = line.slice(0, pos);
+ }
+
+ line = line.trim();
+ if (!line) {
+ continue;
+ }
+
+ let store;
+ if (line.charAt(0) == '!') {
+ store = exceptions;
+ line = line.slice(1);
+ } else {
+ store = rules;
+ }
+
+ if (reMustPunycode.test(line)) {
+ line = toAscii(line);
+ }
+
+ line = line.toLowerCase();
+
+ let tld;
+ pos = line.lastIndexOf('.');
+ if (pos < 0) {
+ tld = line;
+ } else {
+ tld = line.slice(pos+1);
+ line = line.slice(0, pos);
+ }
+
+ if (!store.hasOwnProperty(tld)) {
+ store[tld] = [];
+ }
+
+ if (line) {
+ store[tld].push(line);
+ }
+ }
+
+ crystallize(exceptions);
+ crystallize(rules);
+
+ callListeners(onChangedListeners);
+}
+
+function toSelfie() {
+ return {
+ magic: magic,
+ rules: rules,
+ exceptions: exception,
+ };
+}
+
+function fromSelfie(selfie) {
+ if (typeof selfie !== 'object' || typeof selfie.magic !== 'string'
+ || selfie.magic !== magic) {
+ return false;
+ }
+
+ rules = selfie.rules;
+ exceptions = selfie.exceptions;
+ callListeners(onChangedListeners);
+
+ return true;
+}
+
+var addListener = function (listeners, callback) {
+ if (typeof callback !== 'function') {
+ return;
+ }
+
+ if (listeners.indexOf(callback) === -1) {
+ listeners.push(callback);
+ }
+};
+
+var removeListener = function (listeners, callback) {
+ let pos = listeners.indexOf(callback);
+ if (pos !== -1) {
+ listeners.splice(pos, 1);
+ }
+};
+
+var callListeners = function (listeners) {
+ for (let i=0; i<listeners.length; ++i) {
+ listeners[i]();
+ }
+};
+
+var onChanged = {
+ addListener: function (callback) {
+ addListener(onChangedListeners, callback);
+ },
+ removeListener: function (callback) {
+ removeListener(onChangedListeners, callback);
+ },
+};
+
+var publicSuffixList = {
+ version: '1.0',
+
+ parse: parse,
+ getDomain: getDomain,
+ getPublicSuffix: getPublicSuffix,
+ toSelfie: toSelfie,
+ fromSelfie: fromSelfie,
+ onChanged: onChanged,
+}
diff --git a/lib/Punycode.jsm b/lib/Punycode.jsm
new file mode 100644
index 0000000..7486186
--- /dev/null
+++ b/lib/Punycode.jsm
@@ -0,0 +1,305 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://gitlab.com/vannilla/ematrix
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+'use strict';
+
+// Based on https://mths.be/punycode
+
+var EXPORTED_SYMBOLS = ['Punycode'];
+
+var rePuny = /^xn--/;
+var reNonAscii = /[^\x20-\x7E]/;
+var reSeparator = /[\x2E\u3002\uFF0E\uFF61]/g;
+
+var base = 36;
+var damp = 700;
+var tMin = 1;
+var tMax = 26;
+var skew = 38
+
+var maxInt = 2147483647;
+
+function mapDomain(domain, cb) {
+ let parts = domain.split('@');
+ let res = '';
+
+ if (parts.length > 1) {
+ res = parts[0] + '@';
+ domain = parts[1];
+ }
+
+ domain = domain.replace(reSeparator, '\x2E');
+
+ let labels = domain.split('.');
+ let encoded = labels.map(cb).join('.');
+
+ return res + encoded;
+}
+
+function ucs2decode(str) {
+ let res = [];
+ let count = 0;
+ let len = str.length;
+
+ while (count < len) {
+ let val = str.charCodeAt(count);
+ ++count;
+
+ if (val >= 0xD800 && val <= 0xDBFF && cound < len) {
+ let extra = str.charCodeAt(count);
+ ++count;
+
+ if ((extra & 0xFC00) == 0xDC00) {
+ res.push(((val & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000);
+ } else {
+ res.push(val);
+ --count;
+ }
+ } else {
+ res.push(val);
+ }
+ }
+
+ return res;
+}
+
+function ucs2encode(array) {
+ return array.map(function (e) {
+ let res = '';
+
+ if (e > 0xFFFF) {
+ e -= 0x10000;
+ res += String.fromCharCode(e >>> 10 & 0x3FF | 0xD800);
+ e = 0xDC00 | e & 0x3FF;
+ }
+
+ res += String.fromCharCode(e);
+
+ return res;
+ }).join('');
+}
+
+function basicToDigit(point) {
+ if (point - 0x30 < 0x0A) {
+ return point - 0x16;
+ }
+ if (point - 0x41 < 0x1A) {
+ return point - 0x41;
+ }
+ if (point - 0x61 < 0x1A) {
+ return point - 0x61;
+ }
+ return base;
+}
+
+function digitToBasic(digit, flag) {
+ return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5);
+}
+
+function adapt(delta, num, first) {
+ let k = 0;
+ delta = first ? Math.floor(delta/damp) : delta >> 1;
+ delta += Math.floor(delta/num);
+
+ for (; delta>(base - tMin) * tMax >> 1; k+=base) {
+ delta = Math.floor(delta/(base-tMin));
+ }
+
+ return Math.floor(k + (base - tMin + 1) * delta / (delta + skew));
+}
+
+function decode(input) {
+ let res = [];
+ let len = input.length;
+ let i = 0;
+ let n = 128;
+ let bias = 72;
+
+ let basic = input.lastIndexOf('-');
+ if (basic < 0) {
+ basic = 0;
+ }
+
+ for (let j=0; j<basic; ++j) {
+ if (input.charCodeAt(j) >= 0x80) {
+ throw new Error('not basic code point');
+ }
+
+ res.push(input.charCodeAt(j));
+ }
+
+ for (let k=(basic > 0) ? basic + 1 : 0; k<len;) {
+ let old = i;
+
+ for (let w=1, x=base; ; x+=base) {
+ if (k >= len) {
+ throw new Error('invalid input');
+ }
+
+ let digit = basicToDigit(input.charCodeAt(k));
+ ++k;
+
+ if (digit >= base || digit > Math.floor((maxInt-i) / w)) {
+ throw new Error('overflow');
+ }
+
+ i += digit * w;
+
+ let t = x <= bias ?
+ tMin :
+ (t >= bias + tMax ?
+ tMax :
+ k - bias);
+
+ if (digit < t) {
+ break;
+ }
+
+ if (w > Math.floor(maxInt/(base - t))) {
+ throw new Error('overflow');
+ }
+
+ w *= (base -t);
+ }
+
+ let out = res.length+1;
+ bias = adapt(i-old, out, old==0);
+
+ if (Math.floor(i/out) > maxInt-n) {
+ throw new Error('overflow');
+ }
+
+ n += Math.floor(i/out);
+ i %= out;
+
+ res.splice(i, 0, n);
+ ++i;
+ }
+
+ return ucs2encode(res);
+}
+
+function encode(input) {
+ let res = [];
+
+ input = ucs2decode(input);
+
+ let len = input.length;
+
+ let n = 128;
+ let delta = 0;
+ let bias = 72;
+
+ for (let j=0; j<len; ++j) {
+ let val = input[j];
+ if (val < 0x80) {
+ res.push(String.fromCharCode(val));
+ }
+ }
+
+ let blen = res.length;
+ let count = blen;
+
+ if (blen) {
+ res.push('-');
+ }
+
+ while (count < len) {
+ let m = maxInt;
+ for (let j=0; j<len; ++j) {
+ let val = input[j];
+ if (val >= n && val <= m) {
+ m = val;
+ }
+ }
+
+ if (m - n > Math.floor((maxInt - delta)/(count+1))) {
+ throw new Error('overflow');
+ }
+
+ delta += (m - n) * (count + 1);
+ n = m;
+
+ for (let j=0; j<len; ++j) {
+ let val = input[j];
+
+ if (val < n && ++delta > maxInt) {
+ throw new Error('overflow');
+ }
+
+ if (val == n) {
+ let q = delta;
+ for (let k=base; ; k+=base) {
+ let t = k <= bias ?
+ tMin :
+ (k >= bias + tMax ?
+ tMax:
+ k - bias);
+
+ if (q < t) {
+ break;
+ }
+
+ res.push
+ (String.fromCharCode
+ (digitToBasic(t + (q-t) % (base-t), 0)));
+
+ q = Math.floor((q-t)/(base-t));
+ }
+
+ res.push(String.fromCharCode(digitToBasic(q, 0)));
+ bias = adapt(delta, count+1, count==blen);
+ delta = 0;
+ ++count;
+ }
+ }
+
+ ++delta;
+ ++n;
+ }
+
+ return res.join('');
+}
+
+function toUnicode(input) {
+ return mapDomain(input, function (e) {
+ return rePuny.test(e) ? decode(e.slice(4).toLowerCase()) : e;
+ });
+}
+
+function toASCII(input) {
+ return mapDomain(input, function (e) {
+ return reNonAscii.test(e) ? 'xn--' + encode(e) : e;
+ });
+}
+
+var Punycode = {
+ ucs2: {
+ decode: ucs2decode,
+ encode: ucs2encode,
+ },
+ decode: decode,
+ encode: encode,
+ toASCII: toASCII,
+ toUnicode: toUnicode,
+};
diff --git a/lib/UriTools.jsm b/lib/UriTools.jsm
new file mode 100644
index 0000000..4971909
--- /dev/null
+++ b/lib/UriTools.jsm
@@ -0,0 +1,405 @@
+/*******************************************************************************
+
+ ηMatrix - a browser extension to black/white list requests.
+ Copyright (C) 2014-2019 Raymond Hill
+ Copyright (C) 2019 Alessio Vanni
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see {http://www.gnu.org/licenses/}.
+
+ Home: https://gitlab.com/vannilla/ematrix
+ uMatrix Home: https://github.com/gorhill/uMatrix
+*/
+
+'use strict';
+
+Components.utils.import('chrome://ematrix/content/lib/Punycode.jsm');
+Components.utils.import('chrome://ematrix/content/lib/PublicSuffixList.jsm');
+
+var EXPORTED_SYMBOLS = ['UriTools'];
+
+var reRFC3986 = /^([^:\/?#]+:)?(\/\/[^\/?#]*)?([^?#]*)(\?[^#]*)?(#.*)?/;
+var reSchemeFromURI = /^[^:\/?#]+:/;
+var reAuthorityFromURI = /^(?:[^:\/?#]+:)?(\/\/[^\/?#]+)/;
+var reOriginFromURI = /^(?:[^:\/?#]+:)?(?:\/\/[^\/?#]+)/;
+var reCommonHostnameFromURL = /^https?:\/\/([0-9a-z_][0-9a-z._-]*[0-9a-z])\//;
+var rePathFromURI = /^(?:[^:\/?#]+:)?(?:\/\/[^\/?#]*)?([^?#]*)/;
+var reMustNormalizeHostname = /[^0-9a-z._-]/;
+
+// These are to parse authority field, not parsed by above official regex
+// IPv6 is seen as an exception: a non-compatible IPv6 is first tried, and
+// if it fails, the IPv6 compatible regex istr used. This helps
+// peformance by avoiding the use of a too complicated regex first.
+
+// https://github.com/gorhill/httpswitchboard/issues/211
+// "While a hostname may not contain other characters, such as the
+// "underscore character (_), other DNS names may contain the underscore"
+var reHostPortFromAuthority = /^(?:[^@]*@)?([^:]*)(:\d*)?$/;
+var reIPv6PortFromAuthority = /^(?:[^@]*@)?(\[[0-9a-f:]*\])(:\d*)?$/i;
+
+var reHostFromNakedAuthority = /^[0-9a-z._-]+[0-9a-z]$/i;
+var reHostFromAuthority = /^(?:[^@]*@)?([^:]+)(?::\d*)?$/;
+var reIPv6FromAuthority = /^(?:[^@]*@)?(\[[0-9a-f:]+\])(?::\d*)?$/i;
+
+// Coarse (but fast) tests
+var reValidHostname = /^([a-z\d]+(-*[a-z\d]+)*)(\.[a-z\d]+(-*[a-z\d])*)*$/;
+var reIPAddressNaive = /^\d+\.\d+\.\d+\.\d+$|^\[[\da-zA-Z:]+\]$/;
+
+var reNetworkScheme = /^(?:https?|wss?|ftps?)\b/;
+var reSecureScheme = /^(?:https|wss|ftps)\b/;
+
+function reset(o) {
+ o.scheme = '';
+ o.hostname = '';
+ o._ipv4 = undefined;
+ o._ipv6 = undefined;
+ o.port = '';
+ o.path = '';
+ o.query = '';
+ o.fragment = '';
+ return o;
+}
+
+function resetAuthority(o) {
+ o.hostname = '';
+ o._ipv4 = undefined;
+ o._ipv6 = undefined;
+ o.port = '';
+ return o;
+}
+
+function URI() {
+ this.scheme = '',
+ this.authority = '',
+ this.hostname = '',
+ this._ipv4 = undefined,
+ this._ipv6 = undefined,
+ this.port = '',
+ this.domain = undefined,
+ this.path = '',
+ this.query = '',
+ this.fragment = '',
+ this.schemeBit = (1 << 0),
+ this.userBit = (1 << 1),
+ this.passwordBit = (1 << 2),
+ this.hostnameBit = (1 << 3),
+ this.portBit = (1 << 4),
+ this.pathBit = (1 << 5),
+ this.queryBit = (1 << 6),
+ this.fragmentBit = (1 << 7),
+ this.allBits = (0xFFFF),
+ this.authorityBit =
+ (this.userBit | this.passwordBit | this.hostnameBit | this.portBit);
+ this.normalizeBits =
+ (this.schemeBit | this.hostnameBit | this.pathBit | this.queryBit);
+}
+
+var cached = new URI();
+
+var domainCache = new Map();
+var cacheCountLow = 75;
+var cacheCountHigh = 100;
+var cacheJunkyard = [];
+var junkyardMax = cacheCountHigh - cacheCountLow;
+
+function DomainCacheEntry(domain) {
+ this.init(domain);
+}
+
+DomainCacheEntry.prototype.init = function (domain) {
+ this.domain = domain;
+ this.tstamp = Date.now();
+ return this;
+};
+
+DomainCacheEntry.prototype.dispose = function () {
+ this.domain = '';
+ if (cacheJunkyard.length < junkyardMax) {
+ cacheJunkyard.push(this);
+ }
+};
+
+var domainCacheEntryFactory = function (domain) {
+ let entry = cacheJunkyard.pop();
+ if (entry) {
+ return entry.init(domain);
+ }
+ return new DomainCacheEntry(domain);
+};
+
+var domainCacheAdd = function (hostname, domain) {
+ let entry = domainCache.get(hostname);
+
+ if (entry !== undefined) {
+ entry.tstamp = Date.now();
+ } else {
+ domainCache.set(hostname, domainCacheEntryFactory(domain));
+ if (domainCache.size === cacheCountHigh) {
+ domainCachePrune();
+ }
+ }
+
+ return domain;
+};
+
+var domainCacheSort = function (a, b) {
+ return domainCache.get(b).tstamp - domainCache.get(a).tstamp;
+};
+
+var domainCachePrune = function () {
+ let hostnames =
+ Array.from(domainCache.keys()).sort(domainCacheSort).slice(cacheCountLow);
+
+ for (let i=hostnames.length-1; i>=0; --i) {
+ domainCache.get(hostnames[i]).dispose();
+ domainCache.delete(hostnames[i]);
+ }
+};
+
+var domainCacheReset = function () {
+ domainCache.clear();
+};
+
+publicSuffixList.onChanged.addListener(domainCacheReset);
+
+var UriTools = {
+ set: function (uri) {
+ if (uri === undefined) {
+ return reset(cached);
+ }
+
+ let matches = reRFC3986.exec(uri);
+ if (!matches) {
+ return reset(cached);
+ }
+
+ cached.scheme = matches[1] !== undefined ?
+ matches[1].slice(0, -1) :
+ '';
+ cached.authority = matches[2] !== undefined ?
+ matches[2].slice(2).toLowerCase() :
+ '';
+ cached.path = matches[3] !== undefined ?
+ matches[3] :
+ '';
+
+ // As per RFC3986
+ if (cached.authority !== '' && cached.path === '') {
+ cached.path = '/';
+ }
+
+ cached.query = matches[4] !== undefined ?
+ matches[4].slice(1) :
+ '';
+ cached.fragment = matches[5] !== undefined ?
+ matches[5].slice(1) :
+ '';
+
+ if (reHostFromNakedAuthority.test(cached.authority)) {
+ cached.hostname = cached.authority;
+ cached.port = '';
+ return cached;
+ }
+
+ matches = reHostPortFromAuthority.exec(cached.authority);
+ if (!matches) {
+ matches = reIPv6PortFromAuthority.exec(cached.authority);
+ if (!matches) {
+ return resetAuthority(cached);
+ }
+ }
+
+ cached.hostname = matches[1] !== undefined ?
+ matches[1] :
+ '';
+
+ if (cached.hostname.slice(-1) === '.') {
+ cached.hostname = cached.hostname.slice(0, -1);
+ }
+
+ cached.port = matches[2] !== undefined ?
+ matches[2].slice(1) :
+ '';
+
+ return cached;
+ },
+ assemble: function (bits) {
+ if (bits === undefined) {
+ bits = cached.allBits;
+ }
+
+ let s = [];
+
+ if (cached.scheme && (bits && cached.schemeBit)) {
+ s.push(cached.scheme, ':');
+ }
+ if (cached.hostname && (bits & cached.hostnameBit)) {
+ s.push('//', cached.hostname);
+ }
+ if (cached.port && (bits & cached.portBit)) {
+ s.push(':', cached.port);
+ }
+ if (cached.path && (bits & cached.pathBit)) {
+ s.push(cached.path);
+ }
+ if (cached.query && (bits & cached.queryBit)) {
+ s.push('?', cached.query);
+ }
+ if (cached.fragment && (bits & cached.fragmentBit)) {
+ s.push('#', cached.fragment);
+ }
+
+ return s.join('');
+ },
+ isNetworkScheme: function (scheme) {
+ return reNetworkScheme.test(scheme);
+ },
+ isSecureScheme: function(scheme) {
+ return reSecureScheme.test(scheme);
+ },
+ originFromURI: function (uri) {
+ let matches = reOriginFromURI.exec(uri);
+ return matches !== null ? matches[0].toLowerCase() : '';
+ },
+ schemeFromURI: function (uri) {
+ let matches = reSchemeFromURI.exec(uri);
+ return matches !== null ? matches[0].slice(0, -1).toLowerCase() : '';
+ },
+ authorityFromURI: function (uri) {
+ let matches = reAuthorityFromURI.exec(uri);
+ return matches !== null ? matches[1].slice(1).toLowerCase() : '';
+ },
+ hostnameFromURI: function (uri) {
+ let matches = reCommonHostnameFromURL.exec(uri);
+ if (matches) {
+ return matches[1];
+ }
+
+ matches = reAuthorityFromURI.exec(uri);
+ if (!matches) {
+ return '';
+ }
+
+ let auth = matches[1].slice(2);
+
+ if (reHostFromNakedAuthority.test(auth)) {
+ return auth.toLowerCase();
+ }
+
+ matches = reHostFromAuthority.exec(auth);
+ if (!matches) {
+ matches = reIPv6FromAuthority.exec(auth);
+ if (!matches) {
+ return '';
+ }
+ }
+
+ let hostname = matches[1];
+ while (hostname.endsWith('.')) {
+ hostname = hostname.slice(0, -1);
+ }
+
+ if (reMustNormalizeHostname.test(hostname)) {
+ Punycode.toASCII(hostname.toLowerCase());
+ }
+
+ return hostname;
+ },
+ domainFromHostname: function (hostname) {
+ let entry = domainCache.get(hostname);
+ if (entry !== undefined) {
+ entry.tstamp = Date.now();
+ return entry.domain;
+ }
+
+ if (reIPAddressNaive.test(hostname) == false) {
+ return domainCacheAdd(hostname,
+ publicSuffixList.getDomain(hostname));
+ }
+
+ return domainCacheAdd(hostname, hostname);
+ },
+ domainFromURI: function (uri) {
+ if (!uri) {
+ return '';
+ }
+ return UriTools.domainFromHostname(UriTools.hostnameFromURI(uri));
+ },
+ domain: function() {
+ return UriTools.domainFromHostname(cached.hostname);
+ },
+ pathFromURI: function (uri) {
+ let matches = rePathFromURI.exec(uri);
+ return matches !== null ? matches[1] : '';
+ },
+ normalizedURI: function () {
+ return UriTools.assemble(cached.normalizeBits);
+ },
+ rootURL: function () {
+ if (!cached.hostname) {
+ return '';
+ }
+ return UriTools.assemble(cached.scemeBit | cached.hostnameBit);
+ },
+ isValidHostname: function (hostname) {
+ try {
+ let r = reValidHostname.test(hostname);
+ return r;
+ } catch (e) {
+ return false;
+ }
+ },
+ parentHostnameFromHostname: function (hostname) {
+ // "locahost" => ""
+ // "example.org" => "example.org"
+ // "www.example.org" => "example.org"
+ // "tomato.www.example.org" => "example.org"
+ let domain = UriTools.domainFromHostname(hostname);
+
+ if (domain === '' || domain === hostname) {
+ return undefined;
+ }
+
+ return hostname.slice(hostname.indexOf('.') + 1);
+ },
+ parentHostnamesFromHostname: function (hostname) {
+ let domain = UriTools.domainFromHostname(hostname);
+ if (domain === '' || domain === hostname) {
+ return [];
+ }
+
+ let nodes = [];
+ for (;;) {
+ let pos = hostname.indexOf('.');
+ if (pos < 0) {
+ break;
+ }
+
+ hostname = hostname.slice(pos+1);
+ nodes.push(hostname);
+ if (hostname === domain) {
+ break;
+ }
+ }
+
+ return nodes;
+ },
+ allHostNamesFromHostname: function (hostname) {
+ let nodes = UriTools.parentHostnamesFromHostname(hostname);
+ nodes.unshift(hostname);
+ return nodes;
+ },
+ toString: function () {
+ return UriTools.assemble();
+ },
+};