diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..a6fa254 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,183 @@ + +{ + "env": { + "browser": true, + "es6": true + }, + "parserOptions": { + "ecmaVersion": 6, + "ecmaFeatures": { + "modules": true + } + }, + "extends": [ + "eslint:recommended" + ], + "rules": { + // Possible Errors + "comma-dangle": [2, "never"], + "computed-property-spacing": [2, "never"], + "no-cond-assign": 2, + "no-console": 0, // allow console + "no-constant-condition": 2, + "no-control-regex": 2, + "no-debugger": 2, + "no-dupe-keys": 2, + "no-empty": 2, + "no-empty-character-class": 2, + "no-ex-assign": 2, + "no-extra-boolean-cast": 2, + "no-extra-parens": 0, + "no-extra-semi": 2, + "no-func-assign": 2, + "no-inner-declarations": 2, + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-negated-in-lhs": 2, + "no-obj-calls": 2, + "no-regex-spaces": 2, + "no-reserved-keys": 0, + "no-sparse-arrays": 2, + "no-unreachable": 2, + "use-isnan": 2, + "valid-jsdoc": 0, + "valid-typeof": 2, + // Best Practices + "block-scoped-var": 2, + "complexity": 0, + "consistent-return": 2, + "curly": 2, + "default-case": 2, + "dot-notation": 2, + "eqeqeq": 2, + "guard-for-in": 2, + "no-alert": 2, + "no-caller": 2, + "no-confusing-arrow": 2, + "no-div-regex": 2, + "no-else-return": 2, + "no-eq-null": 2, + "no-eval": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-implied-eval": 2, + "no-iterator": 2, + "no-labels": 2, + "no-lone-blocks": 2, + "no-loop-func": 2, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-native-reassign": 2, + "no-new": 2, + "no-new-func": 2, + "no-new-wrappers": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-mixed-operators": 2, + "no-process-env": 2, + "no-proto": 2, + "no-redeclare": 2, + "no-return-assign": 2, + "no-script-url": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-unused-expressions": 2, + "no-void": 0, + "no-warning-comments": 2, + "no-with": 2, + "prefer-arrow-callback": 2, + "radix": 2, + "vars-on-top": 0, + "wrap-iife": 2, + "yoda": 2, + // Strict Mode + "strict": [2, "global"], + // Variables + "prefer-const": 2, + "no-catch-shadow": 2, + "no-const-assign": 2, + "no-delete-var": 2, + "no-label-var": 2, + "no-shadow": 2, + "no-shadow-restricted-names": 2, + "no-undef": 2, + "no-undef-init": 2, + "no-undefined": 2, + "no-unused-vars": 2, + "no-use-before-define": 2, + "no-var": 2, + // Stylistic Issues + "indent": [2, 2, { + "SwitchCase": 1 + }], + "arrow-body-style": [2, "as-needed"], + "arrow-parens": [2, "as-needed"], + "arrow-spacing": 2, + "brace-style": 2, + "camelcase": 0, + "comma-spacing": 2, + "comma-style": 2, + "consistent-this": 0, + "eol-last": 2, + "func-names": 0, + "func-style": 0, + "key-spacing": [2, { + "beforeColon": false, + "afterColon": true + }], + "max-nested-callbacks": 0, + "new-cap": 2, + "new-parens": 2, + "no-array-constructor": 2, + "no-inline-comments": 0, + "no-lonely-if": 2, + "no-mixed-spaces-and-tabs": 2, + "no-nested-ternary": 2, + "no-new-object": 2, + "semi-spacing": [2, { + "before": false, + "after": true + }], + "no-spaced-func": 2, + "no-ternary": 0, + "no-trailing-spaces": 2, + "no-multiple-empty-lines": 2, + "no-underscore-dangle": 0, + "one-var": 0, + "operator-assignment": [2, "always"], + "padded-blocks": [2, { "blocks": "never", "classes": "never", "switches": "never" }], + "quotes": [2, "single"], + "quote-props": [2, "as-needed"], + "semi": [2, "always"], + "sort-vars": [2, {"ignoreCase": true}], + "keyword-spacing": 2, + "space-before-blocks": 2, + "object-curly-spacing": [2, "never"], + "array-bracket-spacing": [2, "never"], + "space-in-parens": 2, + "space-infix-ops": 2, + "space-unary-ops": 2, + "spaced-comment": 2, + "wrap-regex": 0, + "space-before-function-paren": ["error", { + "anonymous": "always", + "named": "never", + "asyncArrow": "always" + }], + // Legacy + "max-depth": 0, + "max-len": [2, 80, { + "ignoreStrings": false, + "ignoreTemplateLiterals": false, + "ignoreComments": false + }], + "max-params": 0, + "max-statements": 0, + "no-plusplus": 0, + "no-prototype-builtins": 2, + "prefer-template": 2, + "template-curly-spacing": [2, "never"] + } +} \ No newline at end of file diff --git a/background-script.js b/background-script.js index 1fdb16a..be6f157 100644 --- a/background-script.js +++ b/background-script.js @@ -16,223 +16,230 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with DNSSEC-HSTS. If not, see . */ +/* global chrome, browser */ +'use strict'; -function queryUpgradeNative(requestDetails, resolve, reject) { +let unifiedBrowser; +// let isFirefox; +let isChrome; + +// Firefox supports both browser and chrome; Chromium only supports chrome; +// Edge only supports browser. See https://stackoverflow.com/a/45985333 +if (typeof browser !== 'undefined') { + console.log('Testing for browser/chrome: browser'); + // isFirefox = true; + unifiedBrowser = browser; +} else { + console.log('Testing for browser/chrome: chrome'); + isChrome = true; + unifiedBrowser = chrome; +} + +const httpLookupApiUrl = 'http://127.0.0.1:8080/lookup'; +const matchHostPattern = 'http://*.bit/*'; +const nativeLookupAppName = 'org.namecoin.dnssec_hsts'; +const pages = { + error: unifiedBrowser.runtime.getURL('/pages/lookup_error/index.html') +}; +const pendingUpgradeChecks = new Map(); +let communicationType = 'native'; +let nativePort; // Only used with native messaging + + +function queryUpgradeNative(requestDetails, resolve) { const url = new URL(requestDetails.url); - const host = url.host; - const hostname = url.hostname; - const port = url.port; - if(! pendingUpgradeChecks.has(host)) { + const {host, hostname, port} = url; + if (!pendingUpgradeChecks.has(host)) { pendingUpgradeChecks.set(host, new Set()); - const message = {"host": host, "hostname": hostname, "port": port}; + const message = {host, hostname, port}; // Send message to the native DNSSEC app nativePort.postMessage(message); } - pendingUpgradeChecks.get(host).add(resolve); -} - -// upgradeAsync function returns a Promise -// which is resolved with the upgrade after the native DNSSEC app replies -function upgradeAsync(requestDetails) { - var asyncCancel = new Promise((resolve, reject) => { - queryUpgradeNative(requestDetails, resolve, reject); + pendingUpgradeChecks.get(host).add({ + url: url, + callback: resolve }); - - return asyncCancel; } // Adapted from Tagide/chrome-bit-domain-extension // Returns true if timed out, returns false if hostname showed up function sleep(milliseconds, queryFinishedRef) { - // synchronous XMLHttpRequests from Chrome extensions are not blocking event handlers. That's why we use this - // pretty little sleep function to try to get the API response before the request times out. - var start = new Date().getTime(); - for (var i = 0; i < 1e7; i++) { - if ((new Date().getTime() - start) > milliseconds) { - return true; + // synchronous XMLHttpRequests from Chrome extensions are not blocking event + // handlers. That's why we use this pretty little sleep function to try to get + // the API response before the request times out. + const start = Date.now(); + let lock = true; + let timeout; + do { + if ((Date.now() - start) > milliseconds) { + timeout = true; + lock = false; + } + if (queryFinishedRef.val) { + timeout = false; + lock = false; } - if (queryFinishedRef["val"]) { - return false; + } while (lock); + return timeout; +} + +function buildBlockingResponse(url, upgrade, lookupError) { + if (lookupError) { + return {redirectUrl: pages.error}; + } + if (upgrade) { + if (!isChrome) { + return {upgradeToSecure: true}; } + url.protocol = 'https:'; + // Chromium and Edge don't support "upgradeToSecure", + // so we use "redirectUrl" instead + return {redirectUrl: url.toString()}; } + return {}; } // Compatibility for Chromium/Edge, which don't support async onBeforeRequest // See Chromium Bug 904365 -function upgradeSync(requestDetails) { +function upgradeSyncOverHttp(requestDetails) { const url = new URL(requestDetails.url); - const host = url.host; - const hostname = url.hostname; - const port = url.port; + const {host, hostname} = url; - var certResponse; - var queryFinishedRef = {"val": false}; + let certResponse; + const queryFinishedRef = {val: false}; - var upgrade = false; - var lookupError = false; + let upgrade = false; + let lookupError = false; // Adapted from Tagide/chrome-bit-domain-extension // Get the TLSA records from the API - var xhr = new XMLHttpRequest(); - var apiUrl = "http://127.0.0.1:8080/lookup?domain="+encodeURIComponent(hostname); + const xhr = new XMLHttpRequest(); + const apiUrl = `${httpLookupApiUrl}?domain=${encodeURIComponent(hostname)}`; // synchronous XMLHttpRequest is actually asynchronous // check out https://developer.chrome.com/extensions/webRequest - xhr.open("GET", apiUrl, false); - xhr.onreadystatechange = function() { - if (xhr.readyState == 4) { - if (xhr.status != 200) { - console.log("Error received from API: status " + xhr.status); + xhr.open('GET', apiUrl, false); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + if (xhr.status !== 200) { + console.error(`Error received from API: status ${xhr.status}`); lookupError = true; } - // Get the certs returned from the API server. certResponse = xhr.responseText; // Notify the sleep function that we're ready to proceed - queryFinishedRef["val"] = true; + queryFinishedRef.val = true; } - } + }; + try { xhr.send(); } catch (e) { - console.log("Error reaching API: " + e.toString()); + console.error(`Error reaching API: ${e.toString()}`); lookupError = true; } // block the request until the API response is received. Block for up to two // seconds. if (sleep(2000, queryFinishedRef)) { - console.log("API timed out"); + console.log('API timed out'); lookupError = true; } // Check if any certs exist in the result - var result = certResponse; - if (result.trim() != "") { - console.log("Upgraded via TLSA: " + host); + const result = certResponse; + if (result.trim()) { + console.info(`Upgraded via TLSA: ${host}`); upgrade = true; } return buildBlockingResponse(url, upgrade, lookupError); } -function upgradeCompat(requestDetails) { - if (onFirefox()) { - return upgradeAsync(requestDetails); - } else { - return upgradeSync(requestDetails); - } -} - -function buildBlockingResponse(url, upgrade, lookupError) { - if (lookupError) { - return {"redirectUrl": compatBrowser.runtime.getURL("/pages/lookup_error/index.html")}; - } - if (upgrade) { - if (onFirefox()) { - return {"upgradeToSecure": true}; - } - url.protocol = "https:"; - // Chromium and Edge don't support "upgradeToSecure", so we use "redirectUrl" instead - return {"redirectUrl": url.toString()}; - } - return {}; -} - -// Only use this on initial extension startup; afterwards you should use -// resetRequestListener instead. -function attachRequestListener() { - // This shim function is a hack so that we can add a new listener before we - // remove the old one. In theory JavaScript's single-threaded nature makes - // that irrelevant, but I don't trust browsers to behave sanely on this. - currentRequestListener = function(requestDetails) { - return upgradeCompat(requestDetails); - }; - - // add the listener, - // passing the filter argument and "blocking" - compatBrowser.webRequest.onBeforeRequest.addListener( - currentRequestListener, - {urls: [buildPattern(matchHost)]}, - ["blocking"] - ); -} - -// Attaches a new listener based on the current matchHost, and then removes the -// old listener. The ordering is intended to prevent race conditions where the -// protection is disabled. -function resetRequestListener() { - var oldListener = currentRequestListener; - - attachRequestListener(); - - compatBrowser.webRequest.onBeforeRequest.removeListener(oldListener); -} - -// Builds a match pattern for all HTTP URL's for the specified host -function buildPattern(host) { - return "http://" + host + "/*"; +// upgradeAsync function returns a Promise +// which is resolved with the upgrade after the native DNSSEC app replies +function upgradeAsyncNative(requestDetails) { + return new Promise((resolve, reject) => { + queryUpgradeNative(requestDetails, resolve, reject); + }); } -// Based on https://stackoverflow.com/a/45985333 -function onFirefox() { - if (typeof chrome !== "undefined" && typeof browser !== "undefined") { - return true; +function upgradeUnified(requestDetails, chromiumAsyncResolve) { + switch (communicationType) { + case 'native': + if (isChrome) { + if (typeof chromiumAsyncResolve === 'function') { + upgradeAsyncNative(requestDetails).then(chromiumAsyncResolve); + return false; + } + // chromiumAsyncResolve not found, fallback to sync HTTP + return upgradeSyncOverHttp(requestDetails); + } + return upgradeAsyncNative(requestDetails); + default: + return upgradeSyncOverHttp(requestDetails); } - return false; } -console.log("Testing for Firefox: " + onFirefox()); - -var compatBrowser; -// Firefox supports both browser and chrome; Chromium only supports chrome; -// Edge only supports browser. See https://stackoverflow.com/a/45985333 -if (typeof browser !== "undefined") { - console.log("Testing for browser/chrome: browser"); - compatBrowser = browser; -} else { - console.log("Testing for browser/chrome: chrome"); - compatBrowser = chrome; -} - -// Only used with native messaging -var nativePort; -var pendingUpgradeChecks = new Map(); - -// host for match pattern for the URLs to upgrade -var matchHost = "*.bit"; -var currentRequestListener; - -// Firefox is the only browser that supports async onBeforeRequest, and -// therefore is the only browser that we can use native messaging with. -if (onFirefox()) { +function connectNative() { /* On startup, connect to the Namecoin "dnssec_hsts" app. */ - nativePort = compatBrowser.runtime.connectNative("org.namecoin.dnssec_hsts"); + nativePort = unifiedBrowser.runtime.connectNative(nativeLookupAppName); /* Listen for messages from the native DNSSEC app. */ - nativePort.onMessage.addListener((response) => { - const host = response["host"]; - const hasTLSA = response["hasTLSA"]; - const ok = response["ok"]; + nativePort.onMessage.addListener(response => { + const {host, hasTLSA, ok} = response; if (!ok) { - console.log("Native DNSSEC app error: " + host); + console.log(`Native DNSSEC app error: ${host}`); } - if(! pendingUpgradeChecks.has(host)) { + if (!pendingUpgradeChecks.has(host)) { return; } - for (let item of pendingUpgradeChecks.get(host)) { - item(buildBlockingResponse(null, hasTLSA, !ok)); + for (const query of pendingUpgradeChecks.get(host)) { + query.callback(buildBlockingResponse(query.url, hasTLSA, !ok)); } - pendingUpgradeChecks.delete(host); }); } -attachRequestListener(); +function getExtraInfoSpecOptional() { + const extraInfoSpecOptional = ['blocking']; + if (isChrome && communicationType === 'native') { + extraInfoSpecOptional[0] = 'asyncBlocking'; + let validated = false; + extraInfoSpecOptional.__defineGetter__(0, () => { + if (!validated) { + validated = true; + return 'blocking'; + } + return 'asyncBlocking'; + }); + } + return extraInfoSpecOptional; +} + +function attachRequestListener() { + unifiedBrowser.webRequest.onBeforeRequest.addListener( + upgradeUnified, + {urls: [matchHostPattern]}, + getExtraInfoSpecOptional() + ); +} + +try { + if (communicationType === 'native') { + connectNative(); + } + attachRequestListener(); +} catch (e) { + console.warn( + 'Exception while attaching listener, fallback to sync HTTP lookup', e); + communicationType = 'sync_over_http'; + attachRequestListener(); +}