diff options
author | Jesús <heckyel@hyperbola.info> | 2019-12-30 15:55:13 -0500 |
---|---|---|
committer | Jesús <heckyel@hyperbola.info> | 2019-12-30 15:55:13 -0500 |
commit | 288df6a7bf8b933e2dc499e38f4915fcf974c14b (patch) | |
tree | 77bba994f260c064d3ee7f76c427ddfaa4f91710 /lib | |
parent | a2c9deaa145b780722e93b3899600f287c8094a4 (diff) | |
download | ematrix-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.jsm | 92 | ||||
-rw-r--r-- | lib/CookieCache.jsm | 171 | ||||
-rw-r--r-- | lib/FrameModule.jsm | 337 | ||||
-rw-r--r-- | lib/HttpRequestHeaders.jsm | 102 | ||||
-rw-r--r-- | lib/LiquidDict.jsm | 175 | ||||
-rw-r--r-- | lib/PendingRequests.jsm | 97 | ||||
-rw-r--r-- | lib/PublicSuffixList.jsm | 308 | ||||
-rw-r--r-- | lib/Punycode.jsm | 305 | ||||
-rw-r--r-- | lib/UriTools.jsm | 405 |
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(); + }, +}; |