Bug 1161288 - Support app:// origins on Fetch API. r=baku,nsm
authorFernando Jimenez <ferjmoreno@gmail.com>
Thu, 07 May 2015 20:42:07 +0200
changeset 242819 c997b5af752d5f058b81bd0fcf074d0f92f3503a
parent 242818 9dfb28c444008452cb9bb5f99137b9c3747140f0
child 242820 0c9790f3db34ef8c4d18183ed19f4d84d6b22cb8
push id59522
push userferjmoreno@gmail.com
push dateThu, 07 May 2015 18:42:32 +0000
treeherdermozilla-inbound@c997b5af752d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku, nsm
bugs1161288
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1161288 - Support app:// origins on Fetch API. r=baku,nsm
dom/fetch/FetchDriver.cpp
dom/tests/mochitest/fetch/app-protocol/README.txt
dom/tests/mochitest/fetch/app-protocol/application.zip
dom/tests/mochitest/fetch/app-protocol/foo.txt
dom/tests/mochitest/fetch/app-protocol/index.html
dom/tests/mochitest/fetch/app-protocol/manifest.webapp
dom/tests/mochitest/fetch/app-protocol/update.webapp
dom/tests/mochitest/fetch/app-protocol/update.webapp^headers^
dom/tests/mochitest/fetch/mochitest.ini
dom/tests/mochitest/fetch/test_fetch_app_protocol.html
--- a/dom/fetch/FetchDriver.cpp
+++ b/dom/fetch/FetchDriver.cpp
@@ -7,16 +7,17 @@
 #include "mozilla/dom/FetchDriver.h"
 
 #include "nsIDocument.h"
 #include "nsIInputStream.h"
 #include "nsIOutputStream.h"
 #include "nsIHttpChannel.h"
 #include "nsIHttpChannelInternal.h"
 #include "nsIHttpHeaderVisitor.h"
+#include "nsIJARChannel.h"
 #include "nsIScriptSecurityManager.h"
 #include "nsIThreadRetargetableRequest.h"
 #include "nsIUploadChannel2.h"
 
 #include "nsContentPolicyUtils.h"
 #include "nsCORSListenerProxy.h"
 #include "nsDataHandler.h"
 #include "nsHostObjectProtocolHandler.h"
@@ -288,19 +289,19 @@ FetchDriver::BasicFetch()
           }
         }
       }
     }
 
     return FailWithNetworkError();
   }
 
-  if (scheme.LowerCaseEqualsLiteral("file")) {
-  } else if (scheme.LowerCaseEqualsLiteral("http") ||
-             scheme.LowerCaseEqualsLiteral("https")) {
+  if (scheme.LowerCaseEqualsLiteral("http") ||
+      scheme.LowerCaseEqualsLiteral("https") ||
+      scheme.LowerCaseEqualsLiteral("app")) {
     return HttpFetch();
   }
 
   return FailWithNetworkError();
 }
 
 // This function implements the "HTTP Fetch" algorithm from the Fetch spec.
 // Functionality is often split between here, the CORS listener proxy and the
@@ -481,18 +482,20 @@ FetchDriver::HttpFetch(bool aCORSFlag, b
       }
     }
   }
 
   // Set skip serviceworker flag.
   // While the spec also gates on the client being a ServiceWorker, we can't
   // infer that here. Instead we rely on callers to set the flag correctly.
   if (mRequest->SkipServiceWorker()) {
-    nsCOMPtr<nsIHttpChannelInternal> internalChan = do_QueryInterface(httpChan);
-    internalChan->ForceNoIntercept();
+    if (httpChan) {
+      nsCOMPtr<nsIHttpChannelInternal> internalChan = do_QueryInterface(httpChan);
+      internalChan->ForceNoIntercept();
+    }
   }
 
   nsCOMPtr<nsIStreamListener> listener = this;
 
   // Unless the cors mode is explicitly no-cors, we set up a cors proxy even in
   // the same-origin case, since the proxy does not enforce cors header checks
   // in the same-origin case.
   if (mRequest->Mode() != RequestMode::No_cors) {
@@ -520,17 +523,17 @@ FetchDriver::HttpFetch(bool aCORSFlag, b
     nsAutoTArray<nsCString, 5> unsafeHeaders;
     mRequest->Headers()->GetUnsafeHeaders(unsafeHeaders);
 
     rv = NS_StartCORSPreflight(chan, listener, mPrincipal,
                                useCredentials,
                                unsafeHeaders,
                                getter_AddRefs(preflightChannel));
   } else {
-   rv = chan->AsyncOpen(listener, nullptr);
+    rv = chan->AsyncOpen(listener, nullptr);
   }
 
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return FailWithNetworkError();
   }
 
   // Step 4 onwards of "HTTP Fetch" is handled internally by Necko.
   return NS_OK;
@@ -653,38 +656,41 @@ FetchDriver::OnStartRequest(nsIRequest* 
   MOZ_ASSERT(mObserver);
   nsresult rv;
   aRequest->GetStatus(&rv);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     FailWithNetworkError();
     return rv;
   }
 
-  nsCOMPtr<nsIHttpChannel> channel = do_QueryInterface(aRequest);
-  // For now we only support HTTP.
-  MOZ_ASSERT(channel);
+  nsRefPtr<InternalResponse> response;
+  nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest);
+  if (httpChannel) {
+    uint32_t responseStatus;
+    httpChannel->GetResponseStatus(&responseStatus);
 
-  aRequest->GetStatus(&rv);
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    FailWithNetworkError();
-    return rv;
-  }
+    nsAutoCString statusText;
+    httpChannel->GetResponseStatusText(statusText);
+
+    response = new InternalResponse(responseStatus, statusText);
 
-  uint32_t responseStatus;
-  channel->GetResponseStatus(&responseStatus);
-
-  nsAutoCString statusText;
-  channel->GetResponseStatusText(statusText);
+    nsRefPtr<FillResponseHeaders> visitor = new FillResponseHeaders(response);
+    rv = httpChannel->VisitResponseHeaders(visitor);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      NS_WARNING("Failed to visit all headers.");
+    }
+  } else {
+    nsCOMPtr<nsIJARChannel> jarChannel = do_QueryInterface(aRequest);
+    // If it is not an http channel, it has to be a jar one.
+    MOZ_ASSERT(jarChannel);
 
-  nsRefPtr<InternalResponse> response = new InternalResponse(responseStatus, statusText);
-
-  nsRefPtr<FillResponseHeaders> visitor = new FillResponseHeaders(response);
-  rv = channel->VisitResponseHeaders(visitor);
-  if (NS_WARN_IF(NS_FAILED(rv))) {
-    NS_WARNING("Failed to visit all headers.");
+    // We simulate the http protocol for jar/app requests
+    uint32_t responseStatus = 200;
+    nsAutoCString statusText;
+    response = new InternalResponse(responseStatus, NS_LITERAL_CSTRING("OK"));
   }
 
   // We open a pipe so that we can immediately set the pipe's read end as the
   // response's body. Setting the segment size to UINT32_MAX means that the
   // pipe has infinite space. The nsIChannel will continue to buffer data in
   // xpcom events even if we block on a fixed size pipe.  It might be possible
   // to suspend the channel and then resume when there is space available, but
   // for now use an infinite pipe to avoid blocking.
@@ -698,16 +704,17 @@ FetchDriver::OnStartRequest(nsIRequest* 
   if (NS_WARN_IF(NS_FAILED(rv))) {
     FailWithNetworkError();
     // Cancel request.
     return rv;
   }
   response->SetBody(pipeInputStream);
 
   nsCOMPtr<nsISupports> securityInfo;
+  nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
   rv = channel->GetSecurityInfo(getter_AddRefs(securityInfo));
   if (securityInfo) {
     response->SetSecurityInfo(securityInfo);
   }
 
   // Resolves fetch() promise which may trigger code running in a worker.  Make
   // sure the Response is fully initialized before calling this.
   mResponse = BeginAndGetFilteredResponse(response);
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/fetch/app-protocol/README.txt
@@ -0,0 +1,2 @@
+application.zip contains foo.txt index.html and manifest.webapp.
+Any change to one of these three files should be added to application.zip as well.
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e11c80dabe9edbf1c2055b1143e4a9bda0acba7e
GIT binary patch
literal 1730
zc$^FHW@Zs#-~htPDa+g#kN`V_07F`SzFtX1NoWKQ!+dA2(5Fsbq2Cxq7`)GVpFgQ{
zE<nTYthVP_Z$DOs0B?4Vccz@;LO_iIKpX%!_6C}<TtNBEyp+@my^NCF9FWN~fhNyz
zhM3$N;@dylKw$4@?Pco1rcGz%ZiR{~h3;M%;;MU5Yq}gS&%w3H%C}v1uljWVHNS_G
zdGuG7GkeU9-}e@Nd$+OT#=jeD7MND=>)G^y;Y8Gut<@XXw@LP$*3Dwx@_NBUu2XgF
z-ACJxzP%eet*z$3E3b{`8Y*8JsEeJAbmg6_^w*Q$MCCpIiY>fPyPqgWy7}sBwKM$G
zUdA7E<xK6_#yaP<FZt3x+@6-k8<XP9(@+$mrgrp~us}uqvOBlm2u{eJ!*u<qX+lZQ
zx!@~72{NzvmmGieyDQ>}>Zu6heG69JIv@MJCF<KMsYov0^(-cO(v#gz9kG!%^7iRg
z>Jxn>`Q&t>j`qzJa?c%XWJ<KF<8BK@ePR}}kGqiP+-WTONrpp9RaDaVL#kBP(Rr^-
zrWCGm|5y0tX;+4`@A}x8w`WUxOYCbur@HHd$DDv|KXPimC;9r>$lDa>W-Z{;`>|11
zP44UTO?5jB4ty`zxWN03#pP>kB5ba$>-zoqWWQbMvR=t`x;oo1{%B)S*#Gl&UaN$}
z9xS)@oo-TEKI7O-nKsLFipMmW8#%KliYiLqxOH^xopb*JAL+}k5`Wiz`SL<@uXi(#
zrC2C@JhPo!Io^6w_~-R=ced>OEWhuvh}qu8y&)X289b_2BdRBxZdqZ_dFSD&OCQpI
zcPq>0_nleq$EBA!ck*Thp#;5b{W&IgEb76jN$B)MKQ<-?hE5hpYT^Q?o(E{Di4&Nb
z;^TcCo&AF&^g)Sf2{0iofhH#0iBJ?-n_fKb^q8C=nw*}Hl<>jVC+q`%SVuqu(*y}-
zbpdI{LSupEAJLK>Onr)qjsa>O$9WW{xdtEV5D@o0cmArkj>ZYktJ*rgI$meAPoD8k
z)wp7AU|_Jt%+z$FiIJh_ma@}2UcRT-GiYu}V*vYiYwWcGAd_J$O700QM|C(q(BZj>
zd6{Xc#U*;>sY!_i1t2H80G-Pu0CDo_XF&l0r@YVjhHeT9DALggxWvf$L}l7D)$}9c
zD$`OLj(j!#92BJe#mK9uSmBfA%A+S;XR@l!{8_2+bHbHJLaj%dm|C~|QEFXssf5+p
z+1Oc`n_G2VlpffT$NRo2M*y9Xh}V%)sG&>1y#p1*j7*};h>{Pv@M34+fqBJYNh64Y
zmxCPeauC@7Q1U`fuUtq5Y-tP!QphQc1D?VlX5dQM2s8dVE(My5l+HQe=^Wh|$bJ+>
zamF2FBT&;kx{DCeiX7qmD8@}?X248|9PpS$Hwf8XQYZ#lu`u9GEy!lFvVk<Q0pS)#
K28JIjARYj|@GjH<
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/fetch/app-protocol/foo.txt
@@ -0,0 +1,1 @@
+english sentence
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/fetch/app-protocol/index.html
@@ -0,0 +1,51 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Test app for bug 1161288</title>
+    <script type='application/javascript;version=1.7'>
+function ok(aCondition, aMessage) {
+  if (aCondition) {
+    alert('OK: ' + aMessage);
+  } else {
+    alert('KO: ' + aMessage);
+  }
+}
+
+function done() {
+  alert('DONE');
+}
+
+function testFetchAppResource() {
+  return fetch('foo.txt').then((res) => {
+    ok(true, 'fetch should resolve');
+    if (res.type == 'error') {
+      ok(false, 'fetch failed');
+    }
+    ok(res.status == 200, 'status should be 200');
+    ok(res.statusText == 'OK', 'statusText should be OK');
+    return res.text().then((body) => {
+      ok(body == 'english sentence', 'body should match');
+    });
+  });
+}
+
+function testFetchInvalidAppResource() {
+  return fetch('foo.bar.invalid').then(() => {
+    ok(false, 'fetch should fail');
+  }, (e) => {
+    ok(e instanceof TypeError, 'fetch should fail');
+  });
+}
+
+function runTests() {
+  return Promise.resolve()
+    .then(testFetchAppResource)
+    .then(testFetchInvalidAppResource)
+    .then(done)
+    // Put more promise based tests here.
+}
+  </script>
+  </head>
+  <body onload='runTests()'>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/fetch/app-protocol/manifest.webapp
@@ -0,0 +1,5 @@
+{
+  "name": "App",
+  "launch_path": "/index.html",
+  "description": "Test app for bug 1161288"
+}
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/fetch/app-protocol/update.webapp
@@ -0,0 +1,6 @@
+{
+  "name": "App",
+  "launch_path": "/index.html",
+  "description": "Test app for bug 1161288",
+  "package_path": "application.zip"
+}
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/fetch/app-protocol/update.webapp^headers^
@@ -0,0 +1,1 @@
+Content-Type: application/manifest+json
--- a/dom/tests/mochitest/fetch/mochitest.ini
+++ b/dom/tests/mochitest/fetch/mochitest.ini
@@ -1,21 +1,23 @@
 [DEFAULT]
 support-files =
+  app-protocol/*
   fetch_test_framework.js
   test_fetch_basic.js
   test_fetch_basic_http.js
   test_fetch_cors.js
   test_formdataparsing.js
   test_headers_common.js
   test_request.js
   test_response.js
   utils.js
   worker_wrapper.js
 
 [test_headers.html]
 [test_headers_mainthread.html]
+[test_fetch_app_protocol.html]
 [test_fetch_basic.html]
 [test_fetch_basic_http.html]
 [test_fetch_cors.html]
 [test_formdataparsing.html]
 [test_request.html]
 [test_response.html]
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_app_protocol.html
@@ -0,0 +1,126 @@
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Bug 1161288 - Support app:// origins on Fetch API</title>
+  <script type='text/javascript' src='/tests/SimpleTest/SimpleTest.js'></script>
+  <link rel='stylesheet' type='text/css' href='/tests/SimpleTest/test.css' />
+</head>
+<body onload='runTests()'>
+<p id='display'></p>
+<div id='content' style='display: none'></div>
+<pre id='test'></pre>
+<script class='testbody' type='application/javascript;version=1.7'>
+
+SimpleTest.waitForExplicitFinish();
+
+const appManifestURL =
+  'http://mochi.test:8888/tests/dom/tests/mochitest/fetch/app-protocol/update.webapp';
+let gApp;
+
+function setup() {
+  info('Setting up');
+  return new Promise((resolve, reject) => {
+    SpecialPowers.setAllAppsLaunchable(true);
+    SpecialPowers.pushPrefEnv({'set': [
+      ['dom.mozBrowserFramesEnabled', true],
+      ['security.uri.allow_scheme_mismatch', true]
+    ]}, () => {
+      SpecialPowers.pushPermissions([
+        { 'type': 'webapps-manage', 'allow': 1, 'context': document },
+        { 'type': 'browser', 'allow': 1, 'context': document },
+        { 'type': 'embed-apps', 'allow': 1, 'context': document }
+      ], () => {
+        SpecialPowers.autoConfirmAppInstall(() => {
+          SpecialPowers.autoConfirmAppUninstall(resolve);
+        });
+      });
+    });
+  });
+}
+
+function installApp() {
+  return new Promise((resolve, reject) => {
+    let req = navigator.mozApps.installPackage(appManifestURL);
+    req.onsuccess = function() {
+      gApp = req.result;
+      is(req.result.manifestURL, appManifestURL, 'app installed');
+      if (req.result.installState == 'installed') {
+        is(req.result.installState, 'installed', 'app downloaded');
+        resolve()
+      } else {
+        req.result.ondownloadapplied = function() {
+          is(req.result.installState, 'installed', 'app downloaded');
+          resolve();
+        }
+      }
+    }
+    req.onerror = reject;
+  });
+}
+
+function launchApp() {
+  return new Promise((resolve, reject) => {
+    if (!gApp) {
+      ok(false, 'No test application to launch');
+      reject();
+    }
+
+    let expectedOkCount = 5;
+    let okCount = 0;
+    let iframe = document.createElement('iframe');
+    iframe.setAttribute('mozbrowser', 'true');
+    iframe.setAttribute('mozapp', gApp.manifestURL);
+    let domParent = document.getElementById('container');
+    iframe.addEventListener('mozbrowsershowmodalprompt', function listener(e) {
+      let message = e.detail.message;
+      info(message);
+      if (/OK/.exec(message)) {
+        ok(true, "Message from app: " + message);
+        okCount++;
+      } else if (/KO/.exec(message)) {
+        ok(false, "Message from app: " + message);
+      } else if (/DONE/.exec(message)) {
+        ok(true, "Messaging from app complete");
+        iframe.removeEventListener('mozbrowsershowmodalprompt', listener);
+        domParent.removeChild(iframe);
+        ok(okCount == expectedOkCount, "Everything's fine");
+        resolve();
+      }
+    }, false);
+    domParent.appendChild(iframe);
+    SpecialPowers.wrap(iframe.contentWindow).location =
+      gApp.origin + gApp.manifest.launch_path;
+  });
+}
+
+function uninstallApp() {
+  return new Promise((resolve, reject) => {
+    if (!gApp) {
+      return reject();
+    }
+    let req = navigator.mozApps.mgmt.uninstall(gApp);
+    req.onsuccess = resolve;
+    req.onerror = reject;
+  });
+}
+
+function runTests() {
+  setup()
+    .then(installApp)
+    .then(launchApp)
+    .then(uninstallApp)
+    .then(SimpleTest.finish)
+    .catch((e) => {
+      ok(false, 'Unexpected error ' + e);
+      SimpleTest.finish();
+    });
+}
+
+</script>
+<div id='container'></div>
+</body>
+</html>