Bug 1380186 test http redirects to moz-ext protocol, r?kmag draft
authorShane Caraveo <scaraveo@mozilla.com>
Thu, 27 Jul 2017 13:34:54 -0700
changeset 617057 ac9c82f83709cc7b34eb2f5ddbca43da5b5ab51b
parent 615176 32d9d1e81cc607320a36391845917f645f7a7f72
child 617058 3d8936a316d61e2f6f3bcc343d0e9079f09d54e6
push id70908
push usermixedpuppy@gmail.com
push dateThu, 27 Jul 2017 20:36:13 +0000
reviewerskmag
bugs1380186
milestone56.0a1
Bug 1380186 test http redirects to moz-ext protocol, r?kmag MozReview-Commit-ID: Kg8ELe3tV2z
toolkit/components/extensions/ExtensionXPCShellUtils.jsm
toolkit/components/extensions/test/xpcshell/test_ext_redirects.js
toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
--- a/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
+++ b/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
@@ -57,25 +57,25 @@ function frameScript() {
   Components.utils.import("resource://gre/modules/Services.jsm");
 
   Services.obs.notifyObservers(this, "tab-content-frameloader-created");
 }
 
 const FRAME_SCRIPT = `data:text/javascript,(${encodeURI(frameScript)}).call(this)`;
 
 let kungFuDeathGrip = new Set();
-function promiseBrowserLoaded(browser, url) {
+function promiseBrowserLoaded(browser, url, redirectUrl) {
   return new Promise(resolve => {
     const listener = {
       QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, Ci.nsIWebProgressListener]),
 
       onStateChange(webProgress, request, stateFlags, statusCode) {
         let requestUrl = request.URI ? request.URI.spec : webProgress.DOMWindow.location.href;
-
-        if (webProgress.isTopLevel && requestUrl === url &&
+        if (webProgress.isTopLevel &&
+            (requestUrl === url || requestUrl === redirectUrl) &&
             (stateFlags & Ci.nsIWebProgressListener.STATE_STOP)) {
           resolve();
           kungFuDeathGrip.delete(listener);
           browser.removeProgressListener(listener);
         }
       },
     };
 
@@ -126,21 +126,21 @@ class ContentPage {
 
     await awaitFrameLoader;
     browser.messageManager.loadFrameScript(FRAME_SCRIPT, true);
 
     this.browser = browser;
     return browser;
   }
 
-  async loadURL(url) {
+  async loadURL(url, redirectUrl = undefined) {
     await this.browserReady;
 
     this.browser.loadURI(url);
-    return promiseBrowserLoaded(this.browser, url);
+    return promiseBrowserLoaded(this.browser, url, redirectUrl);
   }
 
   async close() {
     await this.browserReady;
 
     this.browser = null;
 
     this.windowlessBrowser.close();
@@ -671,16 +671,16 @@ var ExtensionTestUtils = {
   get remoteContentScripts() {
     return REMOTE_CONTENT_SCRIPTS;
   },
 
   set remoteContentScripts(val) {
     REMOTE_CONTENT_SCRIPTS = !!val;
   },
 
-  loadContentPage(url, remote = undefined) {
+  loadContentPage(url, remote = undefined, redirectUrl = undefined) {
     let contentPage = new ContentPage(remote);
 
-    return contentPage.loadURL(url).then(() => {
+    return contentPage.loadURL(url, redirectUrl).then(() => {
       return contentPage;
     });
   },
 };
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_redirects.js
@@ -0,0 +1,247 @@
+"use strict";
+
+// Tests whether we can redirect to a moz-extension: url.
+XPCOMUtils.defineLazyModuleGetter(this, "TestUtils",
+                                  "resource://testing-common/TestUtils.jsm");
+const XMLHttpRequest = Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1", "nsIXMLHttpRequest");
+
+// nsIWebRequestListener is a nsIThreadRetargetableStreamListener that handles
+// forwarding of nsIRequestObserver for JS consumers.  It does nothing more
+// than that.
+let WebRequestListener = Components.Constructor("@mozilla.org/webextensions/webRequestListener;1",
+                                                "nsIWebRequestListener", "init");
+
+const server = createHttpServer();
+const gServerUrl = `http://localhost:${server.identity.primaryPort}`;
+
+server.registerPathHandler("/redirect", (request, response) => {
+  let params = new URLSearchParams(request.queryString);
+  response.setStatusLine(request.httpVersion, 302, "Moved Temporarily");
+  response.setHeader("Location", params.get("redirect_uri"));
+});
+
+server.registerPathHandler("/dummy", (request, response) => {
+  response.setStatusLine(request.httpVersion, 200, "OK");
+  response.write("ok");
+});
+
+function onStopListener(channel) {
+  return new Promise(resolve => {
+    new WebRequestListener({
+      QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver,
+                                             Ci.nsIStreamListener]),
+      getFinalURI(request) {
+        let {loadInfo} = request;
+        return (loadInfo && loadInfo.resultPrincipalURI) || request.originalURI;
+      },
+      onStartRequest(request, context) {
+      },
+      onStopRequest(request, context, statusCode) {
+        let URI = this.getFinalURI(request.QueryInterface(Ci.nsIChannel));
+        resolve(URI && URI.spec);
+      },
+    }, channel);
+  });
+}
+
+async function onModifyListener(originUrl, redirectToUrl) {
+  return TestUtils.topicObserved("http-on-modify-request", (subject, data) => {
+    let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+    return channel.URI && channel.URI.spec == originUrl;
+  }).then(([subject, data]) => {
+    let channel = subject.QueryInterface(Ci.nsIHttpChannel);
+    if (redirectToUrl) {
+      channel.redirectTo(Services.io.newURI(redirectToUrl));
+    }
+    return channel;
+  });
+}
+
+function getExtension(accessible = false, background = undefined) {
+  let manifest = {
+    "permissions": [
+      "webRequest",
+      "webRequestBlocking",
+      "<all_urls>",
+    ],
+  };
+  if (accessible) {
+    manifest.web_accessible_resources = ["finished.html"];
+  }
+  if (!background) {
+    background = () => {
+      // send the extensions public uri to the test.
+      let exturi = browser.extension.getURL("finished.html");
+      browser.test.sendMessage("redirectURI", exturi);
+    };
+  }
+  return ExtensionTestUtils.loadExtension({
+    manifest,
+    files: {
+      "finished.html": `
+        <!DOCTYPE html>
+        <html>
+          <head>
+            <meta charset="utf-8">
+          </head>
+          <body>
+            <h1>redirected!</h1>
+          </body>
+        </html>
+      `.trim(),
+    },
+    background,
+  });
+}
+
+async function redirection_test(url, channelRedirectUrl) {
+  // setup our observer
+  let watcher = onModifyListener(url, channelRedirectUrl).then(channel => {
+    return onStopListener(channel);
+  });
+  let xhr = new XMLHttpRequest();
+  xhr.open("GET", url);
+  xhr.send();
+  return watcher;
+}
+
+// This test verifies failure without web_accessible_resources.
+add_task(async function test_redirect_to_non_accessible_resource() {
+  let extension = getExtension();
+  await extension.startup();
+  let redirectUrl = await extension.awaitMessage("redirectURI");
+  let url = `${gServerUrl}/redirect?redirect_uri=${redirectUrl}`;
+  let result = await redirection_test(url);
+  equal(result, url, `expected no redirect`);
+  await extension.unload();
+});
+
+// This test makes a request against a server that redirects with a 302.
+add_task(async function test_302_redirect_to_extension() {
+  let extension = getExtension(true);
+  await extension.startup();
+  let redirectUrl = await extension.awaitMessage("redirectURI");
+  let url = `${gServerUrl}/redirect?redirect_uri=${redirectUrl}`;
+  let result = await redirection_test(url);
+  equal(result, redirectUrl, "redirect request is finished");
+  await extension.unload();
+});
+
+// This test uses channel.redirectTo during http-on-modify to redirect to the
+// moz-extension url.
+add_task(async function test_channel_redirect_to_extension() {
+  let extension = getExtension(true);
+  await extension.startup();
+  let redirectUrl = await extension.awaitMessage("redirectURI");
+  let url = `${gServerUrl}/dummy?r=${Math.random()}`;
+  let result = await redirection_test(url, redirectUrl);
+  equal(result, redirectUrl, "redirect request is finished");
+  await extension.unload();
+});
+
+// This test verifies failure without web_accessible_resources.
+add_task(async function test_content_redirect_to_non_accessible_resource() {
+  let extension = getExtension();
+  await extension.startup();
+  let redirectUrl = await extension.awaitMessage("redirectURI");
+  let url = `${gServerUrl}/redirect?redirect_uri=${redirectUrl}`;
+  let watcher = onModifyListener(url).then(channel => {
+    return onStopListener(channel);
+  });
+  let contentPage = await ExtensionTestUtils.loadContentPage(url, undefined, "about:blank");
+  equal(contentPage.browser.documentURI.spec, "about:blank", `expected no redirect`);
+  equal(await watcher, url, "expected no redirect");
+  await contentPage.close();
+  await extension.unload();
+});
+
+// This test makes a request against a server that redirects with a 302.
+add_task(async function test_content_302_redirect_to_extension() {
+  let extension = getExtension(true);
+  await extension.startup();
+  let redirectUrl = await extension.awaitMessage("redirectURI");
+  let url = `${gServerUrl}/redirect?redirect_uri=${redirectUrl}`;
+  let contentPage = await ExtensionTestUtils.loadContentPage(url, undefined, redirectUrl);
+  equal(contentPage.browser.documentURI.spec, redirectUrl, `expected redirect`);
+  await contentPage.close();
+  await extension.unload();
+});
+
+// This test uses channel.redirectTo during http-on-modify to redirect to the
+// moz-extension url.
+add_task(async function test_content_channel_redirect_to_extension() {
+  let extension = getExtension(true);
+  await extension.startup();
+  let redirectUrl = await extension.awaitMessage("redirectURI");
+  let url = `${gServerUrl}/dummy?r=${Math.random()}`;
+  onModifyListener(url, redirectUrl);
+  let contentPage = await ExtensionTestUtils.loadContentPage(url, undefined, redirectUrl);
+  equal(contentPage.browser.documentURI.spec, redirectUrl, `expected redirect`);
+  await contentPage.close();
+  await extension.unload();
+});
+
+// This test makes a request against a server and tests webrequest.  Currently
+// disabled due to NS_BINDING_ABORTED happening.
+add_task(async function test_extension_302_redirect() {
+  let extension = getExtension(true, () => {
+    let myuri = browser.extension.getURL("*");
+    let exturi = browser.extension.getURL("finished.html");
+    browser.webRequest.onBeforeRedirect.addListener(details => {
+      browser.test.assertEq(details.redirectUrl, exturi, "redirect matches");
+    }, {urls: ["<all_urls>", myuri]});
+    browser.webRequest.onCompleted.addListener(details => {
+      browser.test.assertEq(details.url, exturi, "expected url received");
+      browser.test.notifyPass("requestCompleted");
+    }, {urls: ["<all_urls>", myuri]});
+    browser.webRequest.onErrorOccurred.addListener(details => {
+      browser.test.log(`onErrorOccurred ${JSON.stringify(details)}`);
+      browser.test.notifyFail("requestCompleted");
+    }, {urls: ["<all_urls>", myuri]});
+    // send the extensions public uri to the test.
+    browser.test.sendMessage("redirectURI", exturi);
+  });
+  await extension.startup();
+  let redirectUrl = await extension.awaitMessage("redirectURI");
+  let completed = extension.awaitFinish("requestCompleted");
+  let url = `${gServerUrl}/redirect?r=${Math.random()}&redirect_uri=${redirectUrl}`;
+  let contentPage = await ExtensionTestUtils.loadContentPage(url, undefined, redirectUrl);
+  equal(contentPage.browser.documentURI.spec, redirectUrl, `expected content redirect`);
+  await completed;
+  await contentPage.close();
+  await extension.unload();
+}).skip();
+
+// This test makes a request and uses onBeforeRequet to redirect to moz-ext.
+// Currently disabled due to NS_BINDING_ABORTED happening.
+add_task(async function test_extension_redirect() {
+  let extension = getExtension(true, () => {
+    let myuri = browser.extension.getURL("*");
+    let exturi = browser.extension.getURL("finished.html");
+    browser.webRequest.onBeforeRequest.addListener(details => {
+      return {redirectUrl: exturi};
+    }, {urls: ["<all_urls>", myuri]}, ["blocking"]);
+    browser.webRequest.onBeforeRedirect.addListener(details => {
+      browser.test.assertEq(details.redirectUrl, exturi, "redirect matches");
+    }, {urls: ["<all_urls>", myuri]});
+    browser.webRequest.onCompleted.addListener(details => {
+      browser.test.assertEq(details.url, exturi, "expected url received");
+      browser.test.notifyPass("requestCompleted");
+    }, {urls: ["<all_urls>", myuri]});
+    browser.webRequest.onErrorOccurred.addListener(details => {
+      browser.test.log(`onErrorOccurred ${JSON.stringify(details)}`);
+      browser.test.notifyFail("requestCompleted");
+    }, {urls: ["<all_urls>", myuri]});
+    // send the extensions public uri to the test.
+    browser.test.sendMessage("redirectURI", exturi);
+  });
+  await extension.startup();
+  let redirectUrl = await extension.awaitMessage("redirectURI");
+  let completed = extension.awaitFinish("requestCompleted");
+  let url = `${gServerUrl}/dummy?r=${Math.random()}`;
+  let contentPage = await ExtensionTestUtils.loadContentPage(url, undefined, redirectUrl);
+  equal(contentPage.browser.documentURI.spec, redirectUrl, `expected redirect`);
+  await completed;
+  await contentPage.close();
+  await extension.unload();
+}).skip();
--- a/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell-common.ini
@@ -34,16 +34,17 @@ skip-if = os == "android"
 [test_ext_localStorage.js]
 [test_ext_management.js]
 [test_ext_management_uninstall_self.js]
 [test_ext_onmessage_removelistener.js]
 skip-if = true # This test no longer tests what it is meant to test.
 [test_ext_privacy.js]
 [test_ext_privacy_disable.js]
 [test_ext_privacy_update.js]
+[test_ext_redirects.js]
 [test_ext_runtime_connect_no_receiver.js]
 [test_ext_runtime_getBrowserInfo.js]
 [test_ext_runtime_getPlatformInfo.js]
 [test_ext_runtime_onInstalled_and_onStartup.js]
 [test_ext_runtime_sendMessage.js]
 [test_ext_runtime_sendMessage_errors.js]
 [test_ext_runtime_sendMessage_no_receiver.js]
 [test_ext_runtime_sendMessage_self.js]