Bug 1173811 - Part 1: Propagate the response URL to intercepted channels when necessary (non-e10s). r=mayhemer,bkelly
authorJosh Matthews <josh@joshmatthews.net>
Thu, 22 Oct 2015 09:23:39 -0400
changeset 268983 4ee908af4695ef2c0a171b36fba84f4b82cb84c1
parent 268982 5125b09f7f0ee46ad53955820a13db6d4affbcfa
child 268984 01c1f46069567aa2477b4fe70b24fbf14c6f3459
push id66977
push userjosh@joshmatthews.net
push dateThu, 22 Oct 2015 13:24:07 +0000
treeherdermozilla-inbound@01c1f4606956 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmayhemer, bkelly
bugs1173811
milestone44.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 1173811 - Part 1: Propagate the response URL to intercepted channels when necessary (non-e10s). r=mayhemer,bkelly
dom/workers/ServiceWorkerEvents.cpp
modules/libjar/InterceptedJARChannel.cpp
netwerk/base/nsINetworkInterceptController.idl
netwerk/protocol/http/HttpBaseChannel.cpp
netwerk/protocol/http/HttpChannelChild.cpp
netwerk/protocol/http/HttpChannelChild.h
netwerk/protocol/http/HttpChannelParent.cpp
netwerk/protocol/http/InterceptedChannel.cpp
netwerk/protocol/http/InterceptedChannel.h
netwerk/protocol/http/nsHttpChannel.cpp
netwerk/protocol/http/nsHttpChannel.h
netwerk/protocol/http/nsIHttpChannelInternal.idl
netwerk/test/unit/test_synthesized_response.js
testing/web-platform/mozilla/meta/MANIFEST.json
testing/web-platform/mozilla/meta/service-workers/service-worker/fetch-request-css-base-url.https.html.ini
testing/web-platform/mozilla/meta/service-workers/service-worker/resources/worker-interception-iframe.https.html.ini
testing/web-platform/mozilla/tests/service-workers/service-worker/fetch-canvas-tainting-cache.https.html
testing/web-platform/mozilla/tests/service-workers/service-worker/fetch-event-redirect.https.html
testing/web-platform/mozilla/tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html
testing/web-platform/mozilla/tests/service-workers/service-worker/resources/fetch-event-redirect-iframe.html
testing/web-platform/mozilla/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js
--- a/dom/workers/ServiceWorkerEvents.cpp
+++ b/dom/workers/ServiceWorkerEvents.cpp
@@ -104,26 +104,29 @@ FetchEvent::Constructor(const GlobalObje
 namespace {
 
 class FinishResponse final : public nsRunnable
 {
   nsMainThreadPtrHandle<nsIInterceptedChannel> mChannel;
   RefPtr<InternalResponse> mInternalResponse;
   ChannelInfo mWorkerChannelInfo;
   const nsCString mScriptSpec;
+  const nsCString mResponseURLSpec;
 
 public:
   FinishResponse(nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel,
                  InternalResponse* aInternalResponse,
                  const ChannelInfo& aWorkerChannelInfo,
-                 const nsACString& aScriptSpec)
+                 const nsACString& aScriptSpec,
+                 const nsACString& aResponseURLSpec)
     : mChannel(aChannel)
     , mInternalResponse(aInternalResponse)
     , mWorkerChannelInfo(aWorkerChannelInfo)
     , mScriptSpec(aScriptSpec)
+    , mResponseURLSpec(aResponseURLSpec)
   {
   }
 
   NS_IMETHOD
   Run()
   {
     AssertIsOnMainThread();
 
@@ -149,17 +152,17 @@ public:
                                mInternalResponse->GetUnfilteredStatusText());
 
     nsAutoTArray<InternalHeaders::Entry, 5> entries;
     mInternalResponse->UnfilteredHeaders()->GetEntries(entries);
     for (uint32_t i = 0; i < entries.Length(); ++i) {
        mChannel->SynthesizeHeader(entries[i].mName, entries[i].mValue);
     }
 
-    rv = mChannel->FinishSynthesizedResponse();
+    rv = mChannel->FinishSynthesizedResponse(mResponseURLSpec);
     NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), "Failed to finish synthesized response");
     return rv;
   }
 
   bool CSPPermitsResponse()
   {
     AssertIsOnMainThread();
 
@@ -227,38 +230,42 @@ private:
 };
 
 struct RespondWithClosure
 {
   nsMainThreadPtrHandle<nsIInterceptedChannel> mInterceptedChannel;
   RefPtr<InternalResponse> mInternalResponse;
   ChannelInfo mWorkerChannelInfo;
   const nsCString mScriptSpec;
+  const nsCString mResponseURLSpec;
 
   RespondWithClosure(nsMainThreadPtrHandle<nsIInterceptedChannel>& aChannel,
                      InternalResponse* aInternalResponse,
                      const ChannelInfo& aWorkerChannelInfo,
-                     const nsCString& aScriptSpec)
+                     const nsCString& aScriptSpec,
+                     const nsACString& aResponseURLSpec)
     : mInterceptedChannel(aChannel)
     , mInternalResponse(aInternalResponse)
     , mWorkerChannelInfo(aWorkerChannelInfo)
     , mScriptSpec(aScriptSpec)
+    , mResponseURLSpec(aResponseURLSpec)
   {
   }
 };
 
 void RespondWithCopyComplete(void* aClosure, nsresult aStatus)
 {
   nsAutoPtr<RespondWithClosure> data(static_cast<RespondWithClosure*>(aClosure));
   nsCOMPtr<nsIRunnable> event;
   if (NS_SUCCEEDED(aStatus)) {
     event = new FinishResponse(data->mInterceptedChannel,
                                data->mInternalResponse,
                                data->mWorkerChannelInfo,
-                               data->mScriptSpec);
+                               data->mScriptSpec,
+                               data->mResponseURLSpec);
   } else {
     event = new CancelChannelRunnable(data->mInterceptedChannel,
                                       NS_ERROR_INTERCEPTION_FAILED);
   }
   MOZ_ALWAYS_TRUE(NS_SUCCEEDED(NS_DispatchToMainThread(event)));
 }
 
 class MOZ_STACK_CLASS AutoCancel
@@ -351,19 +358,32 @@ RespondWithHandler::ResolvedCallback(JSC
     return;
   }
 
   RefPtr<InternalResponse> ir = response->GetInternalResponse();
   if (NS_WARN_IF(!ir)) {
     return;
   }
 
+  // When an opaque response is encountered, we need the original channel's principal
+  // to reflect the final URL. Non-opaque responses are either same-origin or CORS-enabled
+  // cross-origin responses, which are treated as same-origin by consumers.
+  nsCString responseURL;
+  if (response->Type() == ResponseType::Opaque) {
+    ir->GetUnfilteredUrl(responseURL);
+    if (NS_WARN_IF(responseURL.IsEmpty())) {
+      autoCancel.SetCancelStatus(NS_ERROR_INTERCEPTION_FAILED);
+      return;
+    }
+  }
+
   nsAutoPtr<RespondWithClosure> closure(new RespondWithClosure(mInterceptedChannel, ir,
                                                                worker->GetChannelInfo(),
-                                                               mScriptSpec));
+                                                               mScriptSpec,
+                                                               responseURL));
   nsCOMPtr<nsIInputStream> body;
   ir->GetUnfilteredBody(getter_AddRefs(body));
   // Errors and redirects may not have a body.
   if (body) {
     response->SetBodyUsed();
 
     nsCOMPtr<nsIOutputStream> responseBody;
     rv = mInterceptedChannel->GetResponseBody(getter_AddRefs(responseBody));
--- a/modules/libjar/InterceptedJARChannel.cpp
+++ b/modules/libjar/InterceptedJARChannel.cpp
@@ -73,22 +73,28 @@ InterceptedJARChannel::SynthesizeHeader(
 {
   if (aName.LowerCaseEqualsLiteral("content-type")) {
     mContentType = aValue;
   }
   return NS_OK;
 }
 
 NS_IMETHODIMP
-InterceptedJARChannel::FinishSynthesizedResponse()
+InterceptedJARChannel::FinishSynthesizedResponse(const nsACString& aFinalURLSpec)
 {
   if (NS_WARN_IF(!mChannel)) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
+  if (!aFinalURLSpec.IsEmpty()) {
+    // We don't support rewriting responses for JAR channels where the principal
+    // needs to be modified.
+    return NS_ERROR_NOT_IMPLEMENTED;
+  }
+
   mChannel->OverrideWithSynthesizedResponse(mSynthesizedInput, mContentType);
 
   mResponseBody = nullptr;
   mChannel = nullptr;
   return NS_OK;
 }
 
 NS_IMETHODIMP
--- a/netwerk/base/nsINetworkInterceptController.idl
+++ b/netwerk/base/nsINetworkInterceptController.idl
@@ -22,17 +22,17 @@ class ChannelInfo;
 
 /**
  * Interface to allow implementors of nsINetworkInterceptController to control the behaviour
  * of intercepted channels without tying implementation details of the interception to
  * the actual channel. nsIInterceptedChannel is expected to be implemented by objects
  * which do not implement nsIChannel.
  */
 
-[scriptable, uuid(6be00c37-2e85-42ee-b53c-e6508ce4cef0)]
+[scriptable, uuid(afe6aae6-a80d-405b-856e-df36c19bf3c8)]
 interface nsIInterceptedChannel : nsISupports
 {
     /**
      * Instruct a channel that has been intercepted to continue with the original
      * network request.
      */
     void resetInterception();
 
@@ -46,19 +46,21 @@ interface nsIInterceptedChannel : nsISup
      * Attach a header name/value pair to the forthcoming synthesized response.
      * Overwrites any existing header value.
      */
     void synthesizeHeader(in ACString name, in ACString value);
 
     /**
      * Instruct a channel that has been intercepted that a response has been
      * synthesized and can now be read. No further header modification is allowed
-     * after this point.
+     * after this point. The caller may optionally pass a spec for a URL that
+     * this response originates from; an empty string will cause the original
+     * intercepted request's URL to be used instead.
      */
-    void finishSynthesizedResponse();
+    void finishSynthesizedResponse(in ACString finalURLSpec);
 
     /**
      * Cancel the pending intercepted request.
      * @return NS_ERROR_FAILURE if the response has already been synthesized or
      *         the original request has been instructed to continue.
      */
     void cancel(in nsresult status);
 
--- a/netwerk/protocol/http/HttpBaseChannel.cpp
+++ b/netwerk/protocol/http/HttpBaseChannel.cpp
@@ -262,16 +262,31 @@ HttpBaseChannel::GetLoadFlags(nsLoadFlag
   NS_ENSURE_ARG_POINTER(aLoadFlags);
   *aLoadFlags = mLoadFlags;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 HttpBaseChannel::SetLoadFlags(nsLoadFlags aLoadFlags)
 {
+  bool synthesized = false;
+  nsresult rv = GetResponseSynthesized(&synthesized);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // If this channel is marked as awaiting a synthesized response,
+  // modifying certain load flags can interfere with the implementation
+  // of the network interception logic. This takes care of a couple
+  // known cases that attempt to mark channels as anonymous due
+  // to cross-origin redirects; since the response is entirely synthesized
+  // this is an unnecessary precaution.
+  // This should be removed when bug 1201683 is fixed.
+  if (synthesized && aLoadFlags != mLoadFlags) {
+    aLoadFlags &= ~LOAD_ANONYMOUS;
+  }
+
   mLoadFlags = aLoadFlags;
   mForceMainDocumentChannel = (aLoadFlags & LOAD_DOCUMENT_URI);
   return NS_OK;
 }
 
 //-----------------------------------------------------------------------------
 // HttpBaseChannel::nsIChannel
 //-----------------------------------------------------------------------------
--- a/netwerk/protocol/http/HttpChannelChild.cpp
+++ b/netwerk/protocol/http/HttpChannelChild.cpp
@@ -2439,16 +2439,22 @@ HttpChannelChild::OverrideWithSynthesize
 
 NS_IMETHODIMP
 HttpChannelChild::ForceIntercepted()
 {
   mShouldParentIntercept = true;
   return NS_OK;
 }
 
+NS_IMETHODIMP
+HttpChannelChild::ForceIntercepted(uint64_t aInterceptionID)
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
+
 bool
 HttpChannelChild::RecvIssueDeprecationWarning(const uint32_t& warning,
                                               const bool& asError)
 {
   nsCOMPtr<nsIDeprecationWarner> warner;
   GetCallback(warner);
   if (warner) {
     warner->IssueWarning(warning, asError);
--- a/netwerk/protocol/http/HttpChannelChild.h
+++ b/netwerk/protocol/http/HttpChannelChild.h
@@ -83,16 +83,17 @@ public:
   NS_IMETHOD SetEmptyRequestHeader(const nsACString& aHeader) override;
   NS_IMETHOD RedirectTo(nsIURI *newURI) override;
   // nsIHttpChannelInternal
   NS_IMETHOD SetupFallbackChannel(const char *aFallbackKey) override;
   NS_IMETHOD GetLocalAddress(nsACString& addr) override;
   NS_IMETHOD GetLocalPort(int32_t* port) override;
   NS_IMETHOD GetRemoteAddress(nsACString& addr) override;
   NS_IMETHOD GetRemotePort(int32_t* port) override;
+  NS_IMETHOD ForceIntercepted(uint64_t aInterceptionID) override;
   // nsISupportsPriority
   NS_IMETHOD SetPriority(int32_t value) override;
   // nsIClassOfService
   NS_IMETHOD SetClassFlags(uint32_t inFlags) override;
   NS_IMETHOD AddClassFlags(uint32_t inFlags) override;
   NS_IMETHOD ClearClassFlags(uint32_t inFlags) override;
   // nsIResumableChannel
   NS_IMETHOD ResumeAt(uint64_t startPos, const nsACString& entityID) override;
--- a/netwerk/protocol/http/HttpChannelParent.cpp
+++ b/netwerk/protocol/http/HttpChannelParent.cpp
@@ -198,17 +198,19 @@ class FinishSynthesizedResponse : public
 public:
   explicit FinishSynthesizedResponse(nsIInterceptedChannel* aChannel)
   : mChannel(aChannel)
   {
   }
 
   NS_IMETHOD Run()
   {
-    mChannel->FinishSynthesizedResponse();
+    // The URL passed as an argument here doesn't matter, since the child will
+    // receive a redirection notification as a result of this synthesized response.
+    mChannel->FinishSynthesizedResponse(EmptyCString());
     return NS_OK;
   }
 };
 
 class ResponseSynthesizer final : public nsIFetchEventDispatcher
 {
 public:
   ResponseSynthesizer(nsIInterceptedChannel* aChannel,
--- a/netwerk/protocol/http/InterceptedChannel.cpp
+++ b/netwerk/protocol/http/InterceptedChannel.cpp
@@ -174,17 +174,17 @@ InterceptedChannelChrome::SynthesizeHead
   if (!mSynthesizedCacheEntry) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   return DoSynthesizeHeader(aName, aValue);
 }
 
 NS_IMETHODIMP
-InterceptedChannelChrome::FinishSynthesizedResponse()
+InterceptedChannelChrome::FinishSynthesizedResponse(const nsACString& aFinalURLSpec)
 {
   if (!mChannel) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   EnsureSynthesizedResponse();
 
   // If the synthesized response is a redirect, then we want to respect
@@ -203,33 +203,48 @@ InterceptedChannelChrome::FinishSynthesi
   nsresult rv = mChannel->GetSecurityInfo(getter_AddRefs(securityInfo));
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = DoAddCacheEntryHeaders(mChannel, mSynthesizedCacheEntry,
                               mChannel->GetRequestHead(),
                               mSynthesizedResponseHead.ref(), securityInfo);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  nsCOMPtr<nsIURI> uri;
-  mChannel->GetURI(getter_AddRefs(uri));
-
-  bool usingSSL = false;
-  uri->SchemeIs("https", &usingSSL);
+  nsCOMPtr<nsIURI> originalURI;
+  mChannel->GetURI(getter_AddRefs(originalURI));
 
-  // Then we open a real cache entry to read the synthesized response from.
-  rv = mChannel->OpenCacheEntry(usingSSL);
-  NS_ENSURE_SUCCESS(rv, rv);
-
-  mSynthesizedCacheEntry = nullptr;
-
-  if (!mChannel->AwaitingCacheCallbacks()) {
-    rv = mChannel->ContinueConnect();
+  nsCOMPtr<nsIURI> responseURI;
+  if (!aFinalURLSpec.IsEmpty()) {
+    nsresult rv = NS_NewURI(getter_AddRefs(responseURI), aFinalURLSpec);
     NS_ENSURE_SUCCESS(rv, rv);
+  } else {
+    responseURI = originalURI;
   }
 
+  bool equal = false;
+  originalURI->Equals(responseURI, &equal);
+  if (!equal) {
+    nsresult rv =
+        mChannel->StartRedirectChannelToURI(responseURI, nsIChannelEventSink::REDIRECT_INTERNAL);
+    NS_ENSURE_SUCCESS(rv, rv);
+  } else {
+    bool usingSSL = false;
+    responseURI->SchemeIs("https", &usingSSL);
+
+    // Then we open a real cache entry to read the synthesized response from.
+    rv = mChannel->OpenCacheEntry(usingSSL);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    mSynthesizedCacheEntry = nullptr;
+
+    if (!mChannel->AwaitingCacheCallbacks()) {
+      rv = mChannel->ContinueConnect();
+      NS_ENSURE_SUCCESS(rv, rv);
+    }
+  }
   mChannel = nullptr;
   return NS_OK;
 }
 
 NS_IMETHODIMP
 InterceptedChannelChrome::Cancel(nsresult aStatus)
 {
   MOZ_ASSERT(NS_FAILED(aStatus));
@@ -325,17 +340,17 @@ InterceptedChannelContent::SynthesizeHea
   if (!mResponseBody) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   return DoSynthesizeHeader(aName, aValue);
 }
 
 NS_IMETHODIMP
-InterceptedChannelContent::FinishSynthesizedResponse()
+InterceptedChannelContent::FinishSynthesizedResponse(const nsACString& aFinalURLSpec)
 {
   if (NS_WARN_IF(!mChannel)) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   EnsureSynthesizedResponse();
 
   mChannel->OverrideWithSynthesizedResponse(mSynthesizedResponseHead.ref(),
--- a/netwerk/protocol/http/InterceptedChannel.h
+++ b/netwerk/protocol/http/InterceptedChannel.h
@@ -67,17 +67,17 @@ class InterceptedChannelChrome : public 
   // ResetInterception is called.
   bool mOldApplyConversion;
 public:
   InterceptedChannelChrome(nsHttpChannel* aChannel,
                            nsINetworkInterceptController* aController,
                            nsICacheEntry* aEntry);
 
   NS_IMETHOD ResetInterception() override;
-  NS_IMETHOD FinishSynthesizedResponse() override;
+  NS_IMETHOD FinishSynthesizedResponse(const nsACString& aFinalURLSpec) override;
   NS_IMETHOD GetChannel(nsIChannel** aChannel) override;
   NS_IMETHOD SynthesizeStatus(uint16_t aStatus, const nsACString& aReason) override;
   NS_IMETHOD SynthesizeHeader(const nsACString& aName, const nsACString& aValue) override;
   NS_IMETHOD Cancel(nsresult aStatus) override;
   NS_IMETHOD SetChannelInfo(mozilla::dom::ChannelInfo* aChannelInfo) override;
   NS_IMETHOD GetInternalContentPolicyType(nsContentPolicyType *aInternalContentPolicyType) override;
 
   virtual void NotifyController() override;
@@ -95,17 +95,17 @@ class InterceptedChannelContent : public
   // the actual channel.
   nsCOMPtr<nsIStreamListener> mStreamListener;
 public:
   InterceptedChannelContent(HttpChannelChild* aChannel,
                             nsINetworkInterceptController* aController,
                             nsIStreamListener* aListener);
 
   NS_IMETHOD ResetInterception() override;
-  NS_IMETHOD FinishSynthesizedResponse() override;
+  NS_IMETHOD FinishSynthesizedResponse(const nsACString& aFinalURLSpec) override;
   NS_IMETHOD GetChannel(nsIChannel** aChannel) override;
   NS_IMETHOD SynthesizeStatus(uint16_t aStatus, const nsACString& aReason) override;
   NS_IMETHOD SynthesizeHeader(const nsACString& aName, const nsACString& aValue) override;
   NS_IMETHOD Cancel(nsresult aStatus) override;
   NS_IMETHOD SetChannelInfo(mozilla::dom::ChannelInfo* aChannelInfo) override;
   NS_IMETHOD GetInternalContentPolicyType(nsContentPolicyType *aInternalContentPolicyType) override;
 
   virtual void NotifyController() override;
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -2005,22 +2005,29 @@ nsHttpChannel::StartRedirectChannelToURI
     NS_ENSURE_SUCCESS(rv, rv);
 
     // Inform consumers about this fake redirect
     mRedirectChannel = newChannel;
 
     if (!(flags & nsIChannelEventSink::REDIRECT_STS_UPGRADE)) {
         // Ensure that internally-redirected channels cannot be intercepted, which would look
         // like two separate requests to the nsINetworkInterceptController.
-        nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL;
-        rv = mRedirectChannel->GetLoadFlags(&loadFlags);
-        NS_ENSURE_SUCCESS(rv, rv);
-        loadFlags |= nsIChannel::LOAD_BYPASS_SERVICE_WORKER;
-        rv = mRedirectChannel->SetLoadFlags(loadFlags);
-        NS_ENSURE_SUCCESS(rv, rv);
+        if (mInterceptCache == INTERCEPTED) {
+            nsCOMPtr<nsIHttpChannelInternal> httpRedirect = do_QueryInterface(mRedirectChannel);
+            if (httpRedirect) {
+                httpRedirect->ForceIntercepted(mInterceptionID);
+            }
+        } else {
+            nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL;
+            rv = mRedirectChannel->GetLoadFlags(&loadFlags);
+            NS_ENSURE_SUCCESS(rv, rv);
+            loadFlags |= nsIChannel::LOAD_BYPASS_SERVICE_WORKER;
+            rv = mRedirectChannel->SetLoadFlags(loadFlags);
+            NS_ENSURE_SUCCESS(rv, rv);
+        }
     }
 
     PushRedirectAsyncFunc(
         &nsHttpChannel::ContinueAsyncRedirectChannelToURI);
     rv = gHttpHandler->AsyncOnChannelRedirect(this, newChannel, flags);
 
     if (NS_SUCCEEDED(rv))
         rv = WaitForRedirectCallback();
@@ -2988,21 +2995,20 @@ nsHttpChannel::OpenCacheEntry(bool isHtt
         (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI))
         cacheEntryOpenFlags |= nsICacheStorage::OPEN_PRIORITY;
 
     // Only for backward compatibility with the old cache back end.
     // When removed, remove the flags and related code snippets.
     if (mLoadFlags & LOAD_BYPASS_LOCAL_CACHE_IF_BUSY)
         cacheEntryOpenFlags |= nsICacheStorage::OPEN_BYPASS_IF_BUSY;
 
-    if (mPostID) {
-        extension.Append(nsPrintfCString("%d", mPostID));
-    }
     if (PossiblyIntercepted()) {
         extension.Append(nsPrintfCString("u%lld", mInterceptionID));
+    } else if (mPostID) {
+        extension.Append(nsPrintfCString("%d", mPostID));
     }
 
     // If this channel should be intercepted, we do not open a cache entry for this channel
     // until the interception process is complete and the consumer decides what to do with it.
     if (mInterceptCache == MAYBE_INTERCEPT) {
         DebugOnly<bool> exists;
         MOZ_ASSERT(NS_FAILED(cacheStorage->Exists(openURI, extension, &exists)) || !exists,
                    "The entry must not exist in the cache before we create it here");
@@ -4979,17 +4985,17 @@ nsHttpChannel::AsyncOpen(nsIStreamListen
     }
 
     rv = NS_CheckPortSafety(mURI);
     if (NS_FAILED(rv)) {
         ReleaseListeners();
         return rv;
     }
 
-    if (ShouldIntercept()) {
+    if (mInterceptCache != INTERCEPTED && ShouldIntercept()) {
         mInterceptCache = MAYBE_INTERCEPT;
         SetCouldBeSynthesized();
     }
 
     // Remember the cookie header that was set, if any
     const char *cookieHeader = mRequestHead.PeekHeader(nsHttp::Cookie);
     if (cookieHeader) {
         mUserSetCookieHeader = cookieHeader;
@@ -5339,16 +5345,31 @@ nsHttpChannel::SetupFallbackChannel(cons
     LOG(("nsHttpChannel::SetupFallbackChannel [this=%p, key=%s]\n",
          this, aFallbackKey));
     mFallbackChannel = true;
     mFallbackKey = aFallbackKey;
 
     return NS_OK;
 }
 
+NS_IMETHODIMP
+nsHttpChannel::ForceIntercepted(uint64_t aInterceptionID)
+{
+    ENSURE_CALLED_BEFORE_ASYNC_OPEN();
+
+    if (NS_WARN_IF(mLoadFlags & LOAD_BYPASS_SERVICE_WORKER)) {
+        return NS_ERROR_NOT_AVAILABLE;
+    }
+
+    MarkIntercepted();
+    mResponseCouldBeSynthesized = true;
+    mInterceptionID = aInterceptionID;
+    return NS_OK;
+}
+
 //-----------------------------------------------------------------------------
 // nsHttpChannel::nsISupportsPriority
 //-----------------------------------------------------------------------------
 
 NS_IMETHODIMP
 nsHttpChannel::SetPriority(int32_t value)
 {
     int16_t newValue = clamped<int32_t>(value, INT16_MIN, INT16_MAX);
--- a/netwerk/protocol/http/nsHttpChannel.h
+++ b/netwerk/protocol/http/nsHttpChannel.h
@@ -129,16 +129,17 @@ public:
     NS_IMETHOD Suspend() override;
     NS_IMETHOD Resume() override;
     // nsIChannel
     NS_IMETHOD GetSecurityInfo(nsISupports **aSecurityInfo) override;
     NS_IMETHOD AsyncOpen(nsIStreamListener *listener, nsISupports *aContext) override;
     NS_IMETHOD AsyncOpen2(nsIStreamListener *aListener) override;
     // nsIHttpChannelInternal
     NS_IMETHOD SetupFallbackChannel(const char *aFallbackKey) override;
+    NS_IMETHOD ForceIntercepted(uint64_t aInterceptionID) override;
     // nsISupportsPriority
     NS_IMETHOD SetPriority(int32_t value) override;
     // nsIClassOfService
     NS_IMETHOD SetClassFlags(uint32_t inFlags) override;
     NS_IMETHOD AddClassFlags(uint32_t inFlags) override;
     NS_IMETHOD ClearClassFlags(uint32_t inFlags) override;
 
     // nsIResumableChannel
@@ -428,18 +429,19 @@ private:
     nsCOMPtr<nsIHttpChannelAuthProvider> mAuthProvider;
 
     // States of channel interception
     enum {
         DO_NOT_INTERCEPT,  // no interception will occur
         MAYBE_INTERCEPT,   // interception in progress, but can be cancelled
         INTERCEPTED,       // a synthesized response has been provided
     } mInterceptCache;
-    // Unique ID of this channel for the interception purposes.
-    const uint64_t mInterceptionID;
+    // ID of this channel for the interception purposes. Unique unless this
+    // channel is replacing an intercepted one via an redirection.
+    uint64_t mInterceptionID;
 
     bool PossiblyIntercepted() {
         return mInterceptCache != DO_NOT_INTERCEPT;
     }
 
     // If the channel is associated with a cache, and the URI matched
     // a fallback namespace, this will hold the key for the fallback
     // cache entry.
--- a/netwerk/protocol/http/nsIHttpChannelInternal.idl
+++ b/netwerk/protocol/http/nsIHttpChannelInternal.idl
@@ -34,17 +34,17 @@ interface nsIHttpUpgradeListener : nsISu
                               in nsIAsyncOutputStream aSocketOut);
 };
 
 /**
  * Dumping ground for http.  This interface will never be frozen.  If you are
  * using any feature exposed by this interface, be aware that this interface
  * will change and you will be broken.  You have been warned.
  */
-[scriptable, uuid(99767aaf-937d-4f2f-8990-bc79bd7c0ece)]
+[scriptable, uuid(9eabaac6-cc7c-4ca1-9430-65f2daaa578f)]
 interface nsIHttpChannelInternal : nsISupports
 {
     /**
      * An http channel can own a reference to the document URI
      */
     attribute nsIURI documentURI;
 
     /**
@@ -217,16 +217,23 @@ interface nsIHttpChannelInternal : nsISu
     /**
      * Enable/Disable use of Alternate Services with this channel.
      * The network.http.altsvc.enabled preference is still a pre-requisite.
      */
     attribute boolean allowAltSvc;
 
     readonly attribute PRTime lastModifiedTime;
 
+    /**
+     * Force a channel that has not been AsyncOpen'ed to skip any check for possible
+     * interception and proceed immediately to open a previously-synthesized cache
+     * entry using the provided ID.
+     */
+    void forceIntercepted(in uint64_t aInterceptionID);
+
     readonly attribute boolean responseSynthesized;
 
     /**
      * Set by nsCORSListenerProxy if credentials should be included in
      * cross-origin requests. false indicates "same-origin", users should still
      * check flag LOAD_ANONYMOUS!
      */
     attribute boolean corsIncludeCredentials;
--- a/netwerk/test/unit/test_synthesized_response.js
+++ b/netwerk/test/unit/test_synthesized_response.js
@@ -58,17 +58,17 @@ function make_channel(url, body, cb) {
     channelIntercepted: function(channel) {
       channel.QueryInterface(Ci.nsIInterceptedChannel);
       if (body) {
         var synthesized = Cc["@mozilla.org/io/string-input-stream;1"]
                             .createInstance(Ci.nsIStringInputStream);
         synthesized.data = body;
 
         NetUtil.asyncCopy(synthesized, channel.responseBody, function() {
-          channel.finishSynthesizedResponse();
+          channel.finishSynthesizedResponse('');
         });
       }
       if (cb) {
         cb(channel);
       }
     },
   };
   return chan;
@@ -145,17 +145,17 @@ add_test(function() {
 add_test(function() {
   var chan = make_channel(URL + '/body', null, function(channel) {
     do_timeout(100, function() {
       var synthesized = Cc["@mozilla.org/io/string-input-stream;1"]
                           .createInstance(Ci.nsIStringInputStream);
       synthesized.data = NON_REMOTE_BODY;
       NetUtil.asyncCopy(synthesized, channel.responseBody, function() {
         channel.synthesizeHeader("Content-Length", NON_REMOTE_BODY.length);
-        channel.finishSynthesizedResponse();
+        channel.finishSynthesizedResponse('');
       });
     });
   });
   chan.asyncOpen(new ChannelListener(handle_synthesized_response, null), null);
 });
 
 // ensure that the channel waits for a decision
 add_test(function() {
@@ -173,17 +173,17 @@ add_test(function() {
     var synthesized = Cc["@mozilla.org/io/string-input-stream;1"]
                         .createInstance(Ci.nsIStringInputStream);
     synthesized.data = NON_REMOTE_BODY;
 
     NetUtil.asyncCopy(synthesized, intercepted.responseBody, function() {
       // set the content-type to ensure that the stream converter doesn't hold up notifications
       // and cause the test to fail
       intercepted.synthesizeHeader("Content-Type", "text/plain");
-      intercepted.finishSynthesizedResponse();
+      intercepted.finishSynthesizedResponse('');
     });
   });
   chan.asyncOpen(new ChannelListener(handle_synthesized_response, null,
 				     CL_ALLOW_UNKNOWN_CL | CL_SUSPEND | CL_EXPECT_3S_DELAY), null);
 });
 
 // ensure that the intercepted channel can be cancelled
 add_test(function() {
@@ -215,34 +215,34 @@ add_test(function() {
 add_test(function() {
   var chan = make_channel(URL + '/body', null, function(intercepted) {
     var synthesized = Cc["@mozilla.org/io/string-input-stream;1"]
                         .createInstance(Ci.nsIStringInputStream);
     synthesized.data = NON_REMOTE_BODY;
 
     NetUtil.asyncCopy(synthesized, intercepted.responseBody, function() {
       let channel = intercepted.channel;
-      intercepted.finishSynthesizedResponse();
+      intercepted.finishSynthesizedResponse('');
       channel.cancel(Cr.NS_BINDING_ABORTED);
     });
   });
   chan.asyncOpen(new ChannelListener(run_next_test, null,
                                      CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL), null);
 });
 
 // ensure that the intercepted channel can be canceled before the response
 add_test(function() {
   var chan = make_channel(URL + '/body', null, function(intercepted) {
     var synthesized = Cc["@mozilla.org/io/string-input-stream;1"]
                         .createInstance(Ci.nsIStringInputStream);
     synthesized.data = NON_REMOTE_BODY;
 
     NetUtil.asyncCopy(synthesized, intercepted.responseBody, function() {
       intercepted.channel.cancel(Cr.NS_BINDING_ABORTED);
-      intercepted.finishSynthesizedResponse();
+      intercepted.finishSynthesizedResponse('');
     });
   });
   chan.asyncOpen(new ChannelListener(run_next_test, null,
                                      CL_EXPECT_FAILURE | CL_ALLOW_UNKNOWN_CL), null);
 });
 
 add_test(function() {
   httpServer.stop(run_next_test);
--- a/testing/web-platform/mozilla/meta/MANIFEST.json
+++ b/testing/web-platform/mozilla/meta/MANIFEST.json
@@ -101,16 +101,22 @@
           }
         ],
         "service-workers/service-worker/extendable-event-waituntil.https.html": [
           {
             "path": "service-workers/service-worker/extendable-event-waituntil.https.html",
             "url": "/_mozilla/service-workers/service-worker/extendable-event-waituntil.https.html"
           }
         ],
+        "service-workers/service-worker/fetch-canvas-tainting-cache.https.html": [
+          {
+            "path": "service-workers/service-worker/fetch-canvas-tainting-cache.https.html",
+            "url": "/_mozilla/service-workers/service-worker/fetch-canvas-tainting-cache.https.html"
+          }
+        ],
         "service-workers/service-worker/fetch-canvas-tainting.https.html": [
           {
             "path": "service-workers/service-worker/fetch-canvas-tainting.https.html",
             "url": "/_mozilla/service-workers/service-worker/fetch-canvas-tainting.https.html"
           }
         ],
         "service-workers/service-worker/fetch-cors-xhr.https.html": [
           {
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/mozilla/meta/service-workers/service-worker/fetch-request-css-base-url.https.html.ini
@@ -0,0 +1,5 @@
+[fetch-request-css-base-url.https.html]
+  type: testharness
+  bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1201160
+  [CSS's base URL must be the request URL even when fetched from other URL.]
+    expected: FAIL
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/mozilla/meta/service-workers/service-worker/resources/worker-interception-iframe.https.html.ini
@@ -0,0 +1,3 @@
+[worker-interception-iframe.https.html]
+  type: testharness
+  disabled: not a test
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/mozilla/tests/service-workers/service-worker/fetch-canvas-tainting-cache.https.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<title>Service Worker: canvas tainting of the fetched image using cached responses</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/get-host-info.sub.js"></script>
+<script src="resources/test-helpers.sub.js?pipe=sub"></script>
+<body>
+<script>
+async_test(function(t) {
+    var SCOPE = 'resources/fetch-canvas-tainting-iframe.html?cache';
+    var SCRIPT = 'resources/fetch-rewrite-worker.js';
+    var host_info = get_host_info();
+
+    login_https(t)
+      .then(function() {
+          return service_worker_unregister_and_register(t, SCRIPT, SCOPE);
+        })
+      .then(function(registration) {
+          return wait_for_state(t, registration.installing, 'activated');
+        })
+      .then(function() { return with_iframe(SCOPE); })
+      .then(function(frame) {
+          return new Promise(function(resolve, reject) {
+              var channel = new MessageChannel();
+              channel.port1.onmessage = t.step_func(function(e) {
+                  assert_equals(e.data.results, 'finish');
+                  frame.remove();
+                  service_worker_unregister_and_done(t, SCOPE);
+                });
+              frame.contentWindow.postMessage({},
+                                              host_info['HTTPS_ORIGIN'],
+                                              [channel.port2]);
+            });
+        })
+      .catch(unreached_rejection(t));
+  }, 'Verify canvas tainting of fetched image in a Service Worker');
+</script>
+</body>
--- a/testing/web-platform/mozilla/tests/service-workers/service-worker/fetch-event-redirect.https.html
+++ b/testing/web-platform/mozilla/tests/service-workers/service-worker/fetch-event-redirect.https.html
@@ -60,17 +60,18 @@ function redirect_fetch_test(t, test) {
           resolve(e.data.result);
         } else {
           frame.remove();
           resolve(e.data.detail);
         }
       };
       frame.contentWindow.postMessage({
         url: url,
-        request_init: test.request_init
+        request_init: test.request_init,
+        redirect_dest: test.redirect_dest,
       }, '*', [channel.port2]);
     });
 
     if (test.should_reject) {
       return assert_promise_rejects(p);
     }
 
     return p.then(function(result) {
--- a/testing/web-platform/mozilla/tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html
+++ b/testing/web-platform/mozilla/tests/service-workers/service-worker/resources/fetch-canvas-tainting-iframe.html
@@ -1,18 +1,33 @@
 <script src="../resources/get-host-info.sub.js"></script>
 <script src="test-helpers.sub.js?pipe=sub"></script>
 <script>
 var image_path = base_path() + 'fetch-access-control.py?PNGIMAGE';
 var host_info = get_host_info();
+var params = get_query_params(location.href);
 
 var NOT_TAINTED = 'NOT_TAINTED';
 var TAINTED = 'TAINTED';
 var LOAD_ERROR = 'LOAD_ERROR';
 
+function get_query_params(url) {
+  var search = (new URL(url)).search;
+  if (!search) {
+    return {};
+  }
+  var ret = {};
+  var params = search.substring(1).split('&');
+  params.forEach(function(param) {
+      var element = param.split('=');
+      ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]);
+    });
+  return ret;
+}
+
 function create_test_case_promise(url, cross_origin) {
   return new Promise(function(resolve) {
       var img = new Image();
       if (cross_origin != '') {
         img.crossOrigin = cross_origin;
       }
       img.onload = function() {
         try {
@@ -30,16 +45,20 @@ function create_test_case_promise(url, c
       img.onerror = function() {
         resolve(LOAD_ERROR);
       }
       img.src = url;
     });
 }
 
 function create_test_promise(url, cross_origin, expected_result) {
+  if (params['cache']) {
+    url += "&cache";
+  }
+
   return new Promise(function(resolve, reject) {
       create_test_case_promise(url, cross_origin)
         .then(function(result) {
           if (result == expected_result) {
             resolve();
           } else {
             reject('Result of url:' + url + ' ' +
                    ' cross_origin: ' + cross_origin + ' must be ' +
@@ -135,17 +154,17 @@ window.addEventListener('message', funct
             image_url +
             '&mode=same-origin&url=' + encodeURIComponent(image_url),
             'use-credentials',
             NOT_TAINTED),
         create_test_promise(
             remote_image_url +
             '&mode=same-origin&url=' + encodeURIComponent(image_url),
             '',
-            NOT_TAINTED),
+            TAINTED),
         create_test_promise(
             remote_image_url +
             '&mode=same-origin&url=' + encodeURIComponent(image_url),
             'anonymous',
             NOT_TAINTED),
         create_test_promise(
             remote_image_url +
             '&mode=same-origin&url=' + encodeURIComponent(image_url),
@@ -186,19 +205,17 @@ window.addEventListener('message', funct
         
         // CORS response
         create_test_promise(
             image_url +
             '&mode=cors&url=' +
             encodeURIComponent(remote_image_url +
                                '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
             '',
-            TAINTED), // We expect TAINTED since the default origin behavior here
-                      // is taint, and it doesn't matter what kind of fetch the
-                      // SW performs.
+            NOT_TAINTED),
         create_test_promise(
             image_url +
             '&mode=cors&url=' +
             encodeURIComponent(remote_image_url +
                                '&ACAOrigin=' + host_info['HTTPS_ORIGIN']),
             'anonymous',
             NOT_TAINTED),
         create_test_promise(
--- a/testing/web-platform/mozilla/tests/service-workers/service-worker/resources/fetch-event-redirect-iframe.html
+++ b/testing/web-platform/mozilla/tests/service-workers/service-worker/resources/fetch-event-redirect-iframe.html
@@ -1,13 +1,20 @@
 <script>
 window.addEventListener('message', function(evt) {
   var port = evt.ports[0];
   var data = evt.data;
   fetch(new Request(data.url, data.request_init)).then(function(response) {
+    if (data.request_init.mode === 'no-cors' && data.redirect_dest != 'same-origin') {
+      if (response.type === 'opaque') {
+        return {result: 'success', detail: ''};
+      } else {
+        return {result: 'failure', detail: 'expected opaque response'};
+      }
+    }
     return response.json();
   }).then(function(body) {
     port.postMessage({result: body.result, detail: body.detail});
   }).catch(function(e) {
     port.postMessage({result: 'reject', detail: e.toString()});
   });
 });
 </script>
--- a/testing/web-platform/mozilla/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js
+++ b/testing/web-platform/mozilla/tests/service-workers/service-worker/resources/fetch-rewrite-worker.js
@@ -75,12 +75,30 @@ self.addEventListener('fetch', function(
             // in order to distinguish this from a NetworkError, which
             // may be expected even if the type is correct.
             resolve(new Response(JSON.stringify({
               result: 'failure',
               detail: 'got ' + response.type + ' Response.type instead of ' +
                       expectedType
             })));
           }
-          resolve(response);
+
+          if (params['cache']) {
+            var cacheName = "cached-fetches-" + Date.now();
+            var cache;
+            var cachedResponse;
+            return self.caches.open(cacheName).then(function(opened) {
+              cache = opened;
+              return cache.put(request, response);
+            }).then(function() {
+              return cache.match(request);
+            }).then(function(cached) {
+              cachedResponse = cached;
+              return self.caches.delete(cacheName);
+            }).then(function() {
+               resolve(cachedResponse);
+            });
+          } else {
+            resolve(response);
+          }
         }, reject)
       }));
   });