Bug 1271354 support moz-extension in webrequests, r?kmag draft
authorShane Caraveo <scaraveo@mozilla.com>
Thu, 06 Jul 2017 13:08:31 -0700
changeset 604949 636767fe133fb4b65fc0a256d9816cab7dcd7b12
parent 604915 e23f55311ecb5e0cd14c5010d1b40663b3a2e7d0
child 636348 2bad04400ac1b254ff801b183da7e1cf5e2eafdb
push id67249
push usermixedpuppy@gmail.com
push dateThu, 06 Jul 2017 20:09:16 +0000
reviewerskmag
bugs1271354
milestone56.0a1
Bug 1271354 support moz-extension in webrequests, r?kmag MozReview-Commit-ID: AFP68jIdHHo
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/MatchPattern.cpp
toolkit/components/extensions/test/mochitest/chrome.ini
toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_mozextension.html
toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
toolkit/modules/addons/MatchPattern.jsm
toolkit/mozapps/extensions/test/xpcshell/test_ext_management.js
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -574,16 +574,23 @@ this.ExtensionData = class {
         whitelist.push(matcher);
         perm = matcher.pattern;
       } else if (type.api) {
         this.apiNames.add(type.api);
       }
 
       this.permissions.add(perm);
     }
+
+    // An extension always gets permission to its own url.
+    if (this.id) {
+      let matcher = new MatchPattern(this.getURL(), {ignorePath: true});
+      whitelist.push(matcher);
+    }
+
     this.whiteListedHosts = new MatchPatternSet(whitelist);
 
     for (let api of this.apiNames) {
       this.dependencies.add(`${api}@experiments.addons.mozilla.org`);
     }
 
     return this.manifest;
   }
--- a/toolkit/components/extensions/MatchPattern.cpp
+++ b/toolkit/components/extensions/MatchPattern.cpp
@@ -290,17 +290,17 @@ MatchPattern::Init(JSContext* aCx, const
   if (index <= 0) {
     aRv.Throw(NS_ERROR_INVALID_ARG);
     return;
   }
 
   nsCOMPtr<nsIAtom> scheme = NS_AtomizeMainThread(StringHead(aPattern, index));
   if (scheme == nsGkAtoms::_asterisk) {
     mSchemes = AtomSet::Get<WILDCARD_SCHEMES>();
-  } else if (permittedSchemes->Contains(scheme)) {
+  } else if (permittedSchemes->Contains(scheme) || scheme == nsGkAtoms::moz_extension) {
     mSchemes = new AtomSet({scheme});
   } else {
     aRv.Throw(NS_ERROR_INVALID_ARG);
     return;
   }
 
   /***************************************************************************
    * Host
--- a/toolkit/components/extensions/test/mochitest/chrome.ini
+++ b/toolkit/components/extensions/test/mochitest/chrome.ini
@@ -29,16 +29,17 @@ skip-if = os == 'android' # unsupported.
 skip-if = os == 'android' # Bug 1350559
 [test_chrome_ext_storage_cleanup.html]
 [test_chrome_ext_trackingprotection.html]
 [test_chrome_ext_trustworthy_origin.html]
 [test_chrome_ext_webnavigation_resolved_urls.html]
 [test_chrome_ext_webrequest_background_events.html]
 [test_chrome_ext_webrequest_errors.html]
 [test_chrome_ext_webrequest_host_permissions.html]
+[test_chrome_ext_webrequest_mozextension.html]
 [test_chrome_native_messaging_paths.html]
 skip-if = os != "mac" && os != "linux"
 [test_ext_cookies_expiry.html]
 [test_ext_cookies_permissions_bad.html]
 [test_ext_cookies_permissions_good.html]
 [test_ext_cookies_containers.html]
 [test_ext_jsversion.html]
 [test_ext_schema.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_webrequest_mozextension.html
@@ -0,0 +1,194 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test moz-extension protocol use</title>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="chrome_head.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+let peakAchu;
+add_task(async function setup() {
+  peakAchu = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: [
+        "webRequest",
+        "<all_urls>",
+      ],
+    },
+    background() {
+      // ID for the extension in the tests.  Try to observe it to ensure we cannot.
+      browser.webRequest.onBeforeRequest.addListener(details => {
+        browser.test.notifyFail(`PeakAchu onBeforeRequest ${details.url}`);
+      }, {urls: ["<all_urls>", "moz-extension://*/*"]});
+
+      browser.test.onMessage.addListener((msg, extensionUrl) => {
+        browser.test.log(`spying for ${extensionUrl}`);
+        browser.webRequest.onBeforeRequest.addListener(details => {
+          browser.test.notifyFail(`PeakAchu onBeforeRequest ${details.url}`);
+        }, {urls: [extensionUrl]});
+      });
+    },
+  });
+  await peakAchu.startup();
+});
+
+add_task(async function test_webRequest_no_mozextension_permission() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: [
+        "webRequest",
+        "tabs",
+        "moz-extension://c9e007e0-e518-ed4c-8202-83849981dd21/*",
+        "moz-extension://*/*",
+      ],
+    },
+    background() {
+      browser.test.notifyPass("loaded");
+    },
+  });
+
+  let messages = [
+    {message: /processing permissions\.2: Value "moz-extension:\/\/c9e007e0-e518-ed4c-8202-83849981dd21\/\*"/},
+    {message: /processing permissions\.3: Value "moz-extension:\/\/\*\/\*"/},
+  ];
+
+  let waitForConsole = new Promise(resolve => {
+    SimpleTest.monitorConsole(resolve, messages);
+  });
+
+  await extension.startup();
+  await extension.awaitFinish("loaded");
+  await extension.unload();
+
+  SimpleTest.endMonitorConsole();
+  await waitForConsole;
+});
+
+add_task(async function test_webRequest_mozextension_fetch() {
+  function background() {
+    let page = browser.extension.getURL("fetched.html");
+    browser.webRequest.onBeforeRequest.addListener(details => {
+      browser.test.assertEq(details.url, page, "got correct url in onBeforeRequest");
+      browser.test.sendMessage("request-started");
+    }, {urls: [browser.extension.getURL("*")]}, ["blocking"]);
+    browser.webRequest.onCompleted.addListener(details => {
+      browser.test.assertEq(details.url, page, "got correct url in onCompleted");
+      browser.test.sendMessage("request-complete");
+    }, {urls: [browser.extension.getURL("*")]});
+
+    browser.test.onMessage.addListener((msg, data) => {
+      fetch(page).then(() => {
+        browser.test.notifyPass("fetch success");
+        browser.test.sendMessage("done");
+      }, () => {
+        browser.test.fail("fetch failed");
+        browser.test.sendMessage("done");
+      });
+    });
+    browser.test.sendMessage("extensionUrl", browser.extension.getURL("*"));
+  }
+
+  // Use webrequest to monitor moz-extension:// requests
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: [
+        "webRequest",
+        "tabs",
+        "<all_urls>",
+      ],
+    },
+    files: {
+      "fetched.html": `
+        <!DOCTYPE html>
+        <html>
+          <head>
+            <meta charset="utf-8">
+          </head>
+          <body>
+            <h1>moz-extension file</h1>
+          </body>
+        </html>
+      `.trim(),
+    },
+    background,
+  });
+
+  await extension.startup();
+  // send the url for this extension to the monitoring extension
+  peakAchu.sendMessage("extensionUrl", await extension.awaitMessage("extensionUrl"));
+
+  extension.sendMessage("testFetch");
+  await extension.awaitMessage("request-started");
+  await extension.awaitMessage("request-complete");
+  await extension.awaitMessage("done");
+
+  await extension.unload();
+});
+
+add_task(async function test_webRequest_mozextension_tab_query() {
+  function background() {
+    browser.test.sendMessage("extensionUrl", browser.extension.getURL("*"));
+    let page = browser.extension.getURL("tab.html");
+
+    async function onUpdated(tabId, tabInfo, tab) {
+      if (tabInfo.status !== "complete") {
+        return;
+      }
+      browser.test.log(`tab created ${tabId} ${JSON.stringify(tabInfo)} ${tab.url}`);
+      let tabs = await browser.tabs.query({url: browser.extension.getURL("*")});
+      browser.test.assertEq(1, tabs.length, "got one tab");
+      browser.test.assertEq(tabs.length && tabs[0].id, tab.id, "got the correct tab");
+      browser.test.assertEq(tabs.length && tabs[0].url, page, "got correct url in tab");
+      browser.tabs.remove(tabId);
+      browser.tabs.onUpdated.removeListener(onUpdated);
+      browser.test.sendMessage("tabs-done");
+    }
+    browser.tabs.onUpdated.addListener(onUpdated);
+    browser.tabs.create({url: page});
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: [
+        "webRequest",
+        "tabs",
+        "<all_urls>",
+      ],
+    },
+    files: {
+      "tab.html": `
+        <!DOCTYPE html>
+        <html>
+          <head>
+            <meta charset="utf-8">
+          </head>
+          <body>
+            <h1>moz-extension file</h1>
+          </body>
+        </html>
+      `.trim(),
+    },
+    background,
+  });
+
+  await extension.startup();
+  peakAchu.sendMessage("extensionUrl", await extension.awaitMessage("extensionUrl"));
+  await extension.awaitMessage("tabs-done");
+  await extension.unload();
+});
+
+add_task(async function teardown() {
+  await peakAchu.unload();
+});
+</script>
+
+</body>
+</html>
--- a/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_permissions.js
@@ -53,16 +53,18 @@ add_task(async function test_permissions
   });
 
   await AddonTestUtils.promiseStartupManager();
 
   function background() {
     browser.test.onMessage.addListener(async (method, arg) => {
       if (method == "getAll") {
         let perms = await browser.permissions.getAll();
+        let url = browser.extension.getURL("*");
+        perms.origins = perms.origins.filter(i => i != url);
         browser.test.sendMessage("getAll.result", perms);
       } else if (method == "contains") {
         let result = await browser.permissions.contains(arg);
         browser.test.sendMessage("contains.result", result);
       } else if (method == "request") {
         try {
           let result = await browser.permissions.request(arg);
           browser.test.sendMessage("request.result", {status: "success", result});
@@ -198,16 +200,18 @@ add_task(async function test_permissions
 add_task(async function test_startup() {
   async function background() {
     browser.test.onMessage.addListener(async (perms) => {
       await browser.permissions.request(perms);
       browser.test.sendMessage("requested");
     });
 
     let all = await browser.permissions.getAll();
+    let url = browser.extension.getURL("*");
+    all.origins = all.origins.filter(i => i != url);
     browser.test.sendMessage("perms", all);
   }
 
   const PERMS1 = {
     permissions: ["clipboardRead", "tabs"],
   };
   const PERMS2 = {
     origins: ["https://site2.com/*"],
--- a/toolkit/modules/addons/MatchPattern.jsm
+++ b/toolkit/modules/addons/MatchPattern.jsm
@@ -14,17 +14,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
 this.EXPORTED_SYMBOLS = ["MatchPattern", "MatchGlobs", "MatchURLFilters"];
 
 /* globals MatchPattern, MatchGlobs */
 
 const PERMITTED_SCHEMES = ["http", "https", "file", "ftp", "data"];
-const PERMITTED_SCHEMES_REGEXP = PERMITTED_SCHEMES.join("|");
+const PERMITTED_SCHEMES_REGEXP = [...PERMITTED_SCHEMES, "moz-extension"].join("|");
 
 // The basic RE for matching patterns
 const PATTERN_REGEXP = new RegExp(`^(${PERMITTED_SCHEMES_REGEXP}|\\*)://(\\*|\\*\\.[^*/]+|[^*/]+|)(/.*)$`);
 
 // The schemes/protocols implied by a pattern that starts with *://
 const WILDCARD_SCHEMES = ["http", "https"];
 
 // This function converts a glob pattern (containing * and possibly ?
@@ -68,16 +68,23 @@ function SingleMatchPattern(pat) {
 
     // We allow the host to be empty for file URLs.
     if (match[2] == "" && this.schemes[0] != "file") {
       Cu.reportError(`Invalid match pattern: '${pat}'`);
       this.schemes = [];
       return;
     }
 
+    // We disallow the host to be * for moz-extension URLs.
+    if (match[2] == "*" && this.schemes[0] == "moz-extension") {
+      Cu.reportError(`Invalid match pattern: '${pat}'`);
+      this.schemes = [];
+      return;
+    }
+
     this.host = match[2];
     this.hostMatch = this.getHostMatcher(match[2]);
 
     let pathMatch = globToRegexp(match[3], false);
     this.pathMatch = pathMatch.test.bind(pathMatch);
   }
 }
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_ext_management.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_ext_management.js
@@ -4,16 +4,18 @@ add_task(async function setup() {
   createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "48", "48");
   startupManager();
 });
 
 /* eslint-disable no-undef */
 // Shared background function for getSelf tests
 function backgroundGetSelf() {
   browser.management.getSelf().then(extInfo => {
+    let url = browser.extension.getURL("*");
+    extInfo.hostPermissions = extInfo.hostPermissions.filter(i => i != url);
     browser.test.sendMessage("management-getSelf", extInfo);
   }, error => {
     browser.test.notifyFail(`getSelf rejected with error: ${error}`);
   });
 }
 /* eslint-enable no-undef */
 
 add_task(async function test_management_get_self_complete() {