diff --git a/.gitignore b/.gitignore index 5205c64..0614f9f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules/ .DS_Store *.swp -content_scripts/dist/*.js \ No newline at end of file +content_scripts/dist/*.js +web-ext-artifacts/ diff --git a/README.md b/README.md index b1685ef..0d0e772 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ Web API Manager === + Overview --- This extension allows users to selectively allow different hosts on the web @@ -9,6 +10,7 @@ security and privacy sensitive web users to limit the attack surface presented to websites, and to limit websites to the functionality they actually need to carry out user-serving purposes. + Background --- Web browsers gain staggering numbers of new features, without their users diff --git a/background_scripts/background.js b/background_scripts/background.js index ae840ca..b0b7b2f 100644 --- a/background_scripts/background.js +++ b/background_scripts/background.js @@ -1,8 +1,9 @@ /*jslint es6: true*/ -/*global chrome, browser, window, URI*/ +/*global window*/ (function () { + "use strict"; - const {packingLib, standards, storageLib} = window.WEB_API_MANAGER; + const {packingLib, standards, storageLib, domainMatcherLib} = window.WEB_API_MANAGER; const rootObject = window.browser || window.chrome; const defaultKey = "(default)"; @@ -11,6 +12,9 @@ // that should be blocked on matching domains. let domainRules; + // The extension depends on this fetch happening before the DOM on any + // pages is loaded. The Chrome and Firefox docs *do not* promise this, + // but in testing this is always the case. storageLib.get(function (loadedDomainRules) { domainRules = loadedDomainRules; }); @@ -18,7 +22,7 @@ // Manage the state of the browser activity, by displaying the number // of origins / frames const updateBrowserActionBadge = function (activeInfo) { - const {tabId, windowId} = activeInfo; + const tabId = activeInfo.tabId; rootObject.tabs.executeScript( tabId, { @@ -50,39 +54,8 @@ rootObject.tabs.onActivated.addListener(updateBrowserActionBadge); rootObject.windows.onFocusChanged.addListener(updateBrowserActionBadge); - // Inject the blocking settings for each visited domain / frame. - const extractHostFromUrl = function (url) { - const uri = URI(url); - return uri.hostname(); - }; - - const matchingUrlReduceFunction = function (domain, prev, next) { - if (prev) { - return prev; - } - - const domainRegex = new RegExp(next); - if (domainRegex.test(domain)) { - return next; - } - - return prev; - }; - - const whichDomainRuleMatches = function (hostName) { - // of the URL being requested. - const matchingUrlReduceFunctionBound = matchingUrlReduceFunction.bind(undefined, hostName); - const matchingPattern = Object - .keys(domainRules) - .filter((aRule) => aRule !== defaultKey) - .sort() - .reduce(matchingUrlReduceFunctionBound, undefined); - - return matchingPattern || defaultKey; - }; - // Listen for updates to the domain rules from the config page. - rootObject.runtime.onMessage.addListener(function (request, sender, sendResponse) { + rootObject.runtime.onMessage.addListener(function (request, ignore, sendResponse) { const [label, data] = request; if (label === "rulesUpdate") { domainRules = data; @@ -90,12 +63,16 @@ } if (label === "rulesForDomains") { - const ruleForDomain = data.map(whichDomainRuleMatches); - const mapping = {}; - for (let i = 0; i < ruleForDomain.length; i += 1) { - mapping[data[i]] = ruleForDomain[i]; - } - sendResponse(mapping); + + const matchHostNameBound = domainMatcherLib.matchHostName.bind(undefined, Object.keys(domainRules)); + const rulesForDomains = data.map(matchHostNameBound); + const domainToRuleMapping = {}; + + data.forEach(function (aHostName, index) { + domainToRuleMapping[aHostName] = rulesForDomains[index] || defaultKey; + }); + + sendResponse(domainToRuleMapping); return; } }); @@ -106,15 +83,15 @@ }; const requestOptions = ["blocking", "responseHeaders"]; - chrome.webRequest.onHeadersReceived.addListener(function (details) { + // Inject the blocking settings for each visited domain / frame. + rootObject.webRequest.onHeadersReceived.addListener(function (details) { const url = details.url; - const hostName = extractHostFromUrl(url); // Decide which set of blocking rules to use, depending on the host // of the URL being requested. - const matchingDomainKey = whichDomainRuleMatches(hostName); - const standardsToBlock = domainRules[matchingDomainKey]; + const matchingDomainRule = domainMatcherLib.matchUrl(Object.keys(domainRules), url); + const standardsToBlock = domainRules[matchingDomainRule || defaultKey]; const options = Object.keys(standards); const packedValues = packingLib.pack(options, standardsToBlock); @@ -129,4 +106,4 @@ }; }, requestFilter, requestOptions); -}()); \ No newline at end of file +}()); diff --git a/config/index.html b/config/index.html index 2ceffb5..20f2e0e 100644 --- a/config/index.html +++ b/config/index.html @@ -34,9 +34,9 @@ - + diff --git a/config/js/components/domain-rules.vue.js b/config/js/components/domain-rules.vue.js index 7875c97..83e688a 100644 --- a/config/js/components/domain-rules.vue.js +++ b/config/js/components/domain-rules.vue.js @@ -1,5 +1,5 @@ -/*jslint es6: true*/ -/*global window, browser, Vue*/ +/*jslint es6: true, this: true*/ +/*global window, Vue*/ (function () { "use strict"; @@ -65,6 +65,6 @@ isDefault: function (domainName) { return domainName === "(default)"; } - }, + } }); }()); \ No newline at end of file diff --git a/config/js/components/web-api-standards.vue.js b/config/js/components/web-api-standards.vue.js index acc78d9..0a0e9a8 100644 --- a/config/js/components/web-api-standards.vue.js +++ b/config/js/components/web-api-standards.vue.js @@ -1,6 +1,7 @@ -/*jslint es6: true*/ -/*global window, browser, Vue*/ +/*jslint es6: true, this: true*/ +/*global window, Vue*/ (function () { + "use strict"; const standardsDefaults = window.WEB_API_MANAGER.defaults; diff --git a/config/js/config.js b/config/js/config.js index 79f694d..4f86cfb 100644 --- a/config/js/config.js +++ b/config/js/config.js @@ -6,25 +6,15 @@ const rootObject = (window.browser || window.chrome); const doc = window.document; const standards = window.WEB_API_MANAGER.standards; - const defaultConservativeRules = window.WEB_API_MANAGER.defaults.conservative; const {storageLib, stateLib} = window.WEB_API_MANAGER; const defaultDomain = "(default)"; const state = stateLib.generateStateObject(defaultDomain, standards); - const onSettingsLoaded = function (settingsResults) { - - let loadedDomainRules; - - if (Object.keys(settingsResults).length !== 0) { - loadedDomainRules = settingsResults; - } else { - loadedDomainRules = Object.create(null); - loadedDomainRules[defaultDomain] = defaultConservativeRules; - } + const onSettingsLoaded = function (loadedDomainRules) { state.setDomainRules(loadedDomainRules); - + const vm = new Vue({ el: doc.body, data: state diff --git a/config/js/state.js b/config/js/state.js index d8da92b..8dfba14 100644 --- a/config/js/state.js +++ b/config/js/state.js @@ -1,6 +1,7 @@ -/*jslint es6: true*/ +/*jslint es6: true, this: true*/ /*global window, browser, Vue*/ (function () { + "use strict"; window.WEB_API_MANAGER.stateLib = {}; @@ -12,27 +13,27 @@ domainRules: {}, domainNames: [], selectedStandards: [], - + setDomainRules: function (newDomainRules) { this.domainRules = newDomainRules; this.domainNames = Object.keys(newDomainRules); this.selectedStandards = this.domainRules[this.selectedDomain]; }, - + setSelectedDomain: function (newDomain) { this.selectedDomain = newDomain; this.selectedStandards = this.domainRules[newDomain]; }, - + setSelectedStandards: function (selectedStandards) { this.selectedStandards = selectedStandards; this.domainRules[this.selectedDomain] = selectedStandards; }, - + deleteDomainRule: function (domainToDelete) { delete this.domainRules[domainToDelete]; this.domainNames = Object.keys(this.domainRules); - + // If we're deleted the domain thats currently selected, then // select the default domain. if (this.selectedDomain === domainToDelete) { diff --git a/lib/domainmatcher.js b/lib/domainmatcher.js new file mode 100644 index 0000000..5e5986d --- /dev/null +++ b/lib/domainmatcher.js @@ -0,0 +1,46 @@ +/*jslint es6: true*/ +/*global window*/ +(function () { + "use strict"; + const defaultKey = "(default)"; + + const extractHostNameFromUrl = function (url) { + const uri = window.URI(url); + return uri.hostname(); + }; + + const matchingUrlReduceFunction = function (hostName, prev, next) { + if (prev) { + return prev; + } + + const domainRegex = new RegExp(next); + if (domainRegex.test(hostName)) { + return next; + } + + return prev; + }; + + const matchHostName = function (domainRegExes, hostName) { + // of the URL being requested. + const matchingUrlReduceFunctionBound = matchingUrlReduceFunction.bind(undefined, hostName); + const matchingPattern = domainRegExes + .filter((aRule) => aRule !== defaultKey) + .sort() + .reduce(matchingUrlReduceFunctionBound, undefined); + + return matchingPattern || undefined; + }; + + const matchUrl = function (domainRegExes, url) { + const hostName = extractHostNameFromUrl(url); + return matchHostName(domainRegExes, hostName); + }; + + window.WEB_API_MANAGER.domainMatcherLib = { + matchHostName, + matchUrl + }; + +}()); \ No newline at end of file diff --git a/lib/init.js b/lib/init.js index 8585f2b..1e772b3 100644 --- a/lib/init.js +++ b/lib/init.js @@ -1,3 +1,7 @@ +/*global window*/ // Initial content script for the Web API manager extension, that creates // the "namespace" we'll use for all the content scripts in the extension. -window.WEB_API_MANAGER = {}; \ No newline at end of file +(function () { + "use strict"; + window.WEB_API_MANAGER = {}; +}()); diff --git a/lib/pack.js b/lib/pack.js index 2504eeb..ec580c6 100644 --- a/lib/pack.js +++ b/lib/pack.js @@ -1,8 +1,9 @@ -/*jslint es6: true*/ +/*jslint es6: true, for: true, bitwise: true*/ /*global window*/ (function () { "use strict"; + const {btoa, atob} = window; const bucketSize = 8; const bufferToBase64 = function (buf) { @@ -16,7 +17,7 @@ const binstr = atob(base64); const buf = new Uint8Array(binstr.length); Array.prototype.forEach.call(binstr, function (ch, i) { - buf[i] = ch.charCodeAt(0); + buf[i] = ch.charCodeAt(0); }); return buf; }; @@ -47,11 +48,13 @@ const binnedOptions = options.reduce(binToBucketSizeFunc, []); const bitFields = new Uint8Array(numBuckets); - for (let i = 0; i < numBuckets; i += 1) { + let i, j; + + for (i = 0; i < numBuckets; i += 1) { let bitfield = 0; let currentBucket = binnedOptions[i]; - for (let j = 0; j < currentBucket.length; j += 1) { + for (j = 0; j < currentBucket.length; j += 1) { let currentOption = currentBucket[j]; if (selected.indexOf(currentOption) !== -1) { @@ -68,7 +71,6 @@ const unpack = function (options, data) { - const numBuckets = Math.ceil(options.length / bucketSize); const binToBucketSizeFunc = binOptionsReduceFunction.bind(undefined, bucketSize); options.sort(); @@ -77,11 +79,13 @@ const result = []; - for (let i = 0; i < bitFields.length; i += 1) { + let i, j; + + for (i = 0; i < bitFields.length; i += 1) { let currentBitField = bitFields[i]; let currentOptionsBin = binnedOptions[i]; - for (let j = 0; j < bucketSize; j += 1) { + for (j = 0; j < bucketSize; j += 1) { if (currentBitField & (1 << j)) { let currentOption = currentOptionsBin[j]; result.push(currentOption); @@ -93,6 +97,7 @@ }; window.WEB_API_MANAGER.packingLib = { - pack, unpack + pack, + unpack }; }()); \ No newline at end of file diff --git a/lib/storage.js b/lib/storage.js index 3ffbd87..b8e93d7 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -3,13 +3,25 @@ (function () { "use strict"; - const browserObj = window.browser || window.chrome; + const rootObject = window.browser || window.chrome; + const defaultConservativeRules = window.WEB_API_MANAGER.defaults.conservative; const webApiManagerKeySettingsKey = "webApiManagerDomainRules"; - const storageObject = browserObj.storage; + const storageObject = rootObject.storage; const get = function (callback) { storageObject.local.get(webApiManagerKeySettingsKey, function (results) { - callback(results && results[webApiManagerKeySettingsKey]); + + let loadedDomainRules = results && results[webApiManagerKeySettingsKey]; + + // If there are no currently saved domain rules, then create + // a stubbed out one, using the conservative blocking rule set. + if (!loadedDomainRules || Object.keys(loadedDomainRules).length === 0) { + loadedDomainRules = { + "(default)": defaultConservativeRules + }; + } + + callback(loadedDomainRules); }); }; diff --git a/manifest.json b/manifest.json index 84ee897..a3026d7 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "WebAPI Manager", - "version": "0.5", + "version": "0.7", "description": "Improves browser security by restricting page access to parts of the Web API.", "icons": { "48": "images/uic-48.png", @@ -37,20 +37,21 @@ "lib/init.js", "lib/js.cookie.js", "lib/storage.js", - "lib/URI.js" + "lib/URI.js", + "content_scripts/dist/defaults.js" ], "background": { "scripts": [ "lib/init.js", "lib/pack.js", - "lib/storage.js", "lib/URI.js", "content_scripts/dist/standards.js", + "content_scripts/dist/defaults.js", + "lib/storage.js", "background_scripts/background.js" ] }, "options_ui": { - "page": "config/index.html", - "chrome_style": true + "page": "config/index.html" } } diff --git a/popup/js/popup.js b/popup/js/popup.js index 27cfc4c..a0473ae 100644 --- a/popup/js/popup.js +++ b/popup/js/popup.js @@ -1,8 +1,11 @@ /*jslint es6: true*/ /*global window*/ (function () { + "use strict"; + const rootObject = window.browser || window.chrome; - const configureButton = window.document.getElementById("config-page-link"); + const doc = window.document; + const configureButton = doc.getElementById("config-page-link"); configureButton.addEventListener("click", function (event) { rootObject.runtime.openOptionsPage(); @@ -21,26 +24,26 @@ const message = ["rulesForDomains", uniqueDomains]; rootObject.runtime.sendMessage(message, function (response) { - const listGroupElm = document.querySelector("ul.list-group"); + const listGroupElm = doc.querySelector("ul.list-group"); const domainNames = Object.keys(response); domainNames.forEach(function (aDomain) { const domainRule = response[aDomain]; - - const liElm = document.createElement("li"); + + const liElm = doc.createElement("li"); liElm.className = "list-group-item"; if (domainRule !== "(default)") { liElm.className += " list-group-item-success"; } - const spanElm = document.createElement("span"); + const spanElm = doc.createElement("span"); spanElm.className = "badge"; - const badgeText = document.createTextNode(domainRule); + const badgeText = doc.createTextNode(domainRule); spanElm.appendChild(badgeText); liElm.appendChild(spanElm); - const textElm = document.createTextNode(aDomain); + const textElm = doc.createTextNode(aDomain); liElm.appendChild(textElm); listGroupElm.appendChild(liElm); });