Bug 898524 - Part 1: Permit certain HTTP channels to be intercepted before initiating a network connection. r=mayhemer
authorJosh Matthews <josh@joshmatthews.net>
Wed, 09 Jul 2014 16:35:02 -0400
changeset 211038 35824fa211761c855531eed78408b04ee4366999
parent 211037 6e59ea43d686e66cb4595a9943f2987509ac35b4
child 211039 b02c0d6ff380864655156fafe45c9545c19dd1f6
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersmayhemer
bugs898524
milestone36.0a1
Bug 898524 - Part 1: Permit certain HTTP channels to be intercepted before initiating a network connection. r=mayhemer
netwerk/base/public/moz.build
netwerk/base/public/nsINetworkInterceptController.idl
netwerk/protocol/http/HttpBaseChannel.cpp
netwerk/protocol/http/HttpBaseChannel.h
netwerk/protocol/http/nsHttpChannel.cpp
netwerk/protocol/http/nsHttpChannel.h
netwerk/protocol/http/nsIHttpChannelInternal.idl
netwerk/test/unit/test_synthesized_response.js
netwerk/test/unit/xpcshell.ini
--- a/netwerk/base/public/moz.build
+++ b/netwerk/base/public/moz.build
@@ -51,16 +51,17 @@ XPIDL_SOURCES += [
     'nsILoadContextInfo.idl',
     'nsILoadGroup.idl',
     'nsILoadGroupChild.idl',
     'nsIMIMEInputStream.idl',
     'nsIMultiPartChannel.idl',
     'nsINestedURI.idl',
     'nsINetAddr.idl',
     'nsINetUtil.idl',
+    'nsINetworkInterceptController.idl',
     'nsINetworkLinkService.idl',
     'nsINetworkPredictor.idl',
     'nsINetworkPredictorVerifier.idl',
     'nsINetworkProperties.idl',
     'nsINSSErrorsService.idl',
     'nsIParentChannel.idl',
     'nsIParentRedirectingChannel.idl',
     'nsIPermission.idl',
new file mode 100644
--- /dev/null
+++ b/netwerk/base/public/nsINetworkInterceptController.idl
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIHttpChannelInternal;
+interface nsIOutputStream;
+interface nsIURI;
+
+/**
+ * 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(0b5f82a7-5824-4a0d-bf5c-8a8a7684c0c8)]
+interface nsIInterceptedChannel : nsISupports
+{
+    /**
+     * Instruct a channel that has been intercepted to continue with the original
+     * network request.
+     */
+    void resetInterception();
+
+    /**
+     * 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.
+     */
+    void finishSynthesizedResponse();
+};
+
+/**
+ * Interface to allow consumers to attach themselves to a channel's
+ * notification callbacks/loadgroup and determine if a given channel
+ * request should be intercepted before any network request is initiated.
+ */
+
+[scriptable, uuid(b3ad3e9b-91d8-44d0-a0c5-dc2e9374f599)]
+interface nsINetworkInterceptController : nsISupports
+{
+    /**
+     * Returns true if a channel should avoid initiating any network
+     * requests until specifically instructed to do so.
+     *
+     * @param aURI the URI being requested by a channel
+     */
+    bool shouldPrepareForIntercept(in nsIURI aURI);
+
+    /**
+     * Notification when a given intercepted channel is prepared to accept a synthesized
+     * response via the provided stream.
+     *
+     * @param aChannel the controlling interface for a channel that has been intercepted
+     * @param aStream a stream directly into the channel's synthesized response body
+     */
+    void channelIntercepted(in nsIInterceptedChannel aChannel, in nsIOutputStream aStream);
+};
--- a/netwerk/protocol/http/HttpBaseChannel.cpp
+++ b/netwerk/protocol/http/HttpBaseChannel.cpp
@@ -29,16 +29,17 @@
 #include "nsIStreamConverterService.h"
 #include "nsCRT.h"
 #include "nsContentUtils.h"
 #include "nsIScriptSecurityManager.h"
 #include "nsIObserverService.h"
 #include "nsProxyRelease.h"
 #include "nsPIDOMWindow.h"
 #include "nsPerformance.h"
+#include "nsINetworkInterceptController.h"
 
 #include <algorithm>
 
 namespace mozilla {
 namespace net {
 
 HttpBaseChannel::HttpBaseChannel()
   : mStartPos(UINT64_MAX)
@@ -64,16 +65,17 @@ HttpBaseChannel::HttpBaseChannel()
   , mTracingEnabled(true)
   , mTimingEnabled(false)
   , mAllowSpdy(true)
   , mLoadAsBlocking(false)
   , mLoadUnblocked(false)
   , mResponseTimeoutEnabled(true)
   , mAllRedirectsSameOrigin(true)
   , mAllRedirectsPassTimingAllowCheck(true)
+  , mForceNoIntercept(false)
   , mSuspendCount(0)
   , mProxyResolveFlags(0)
   , mContentDispositionHint(UINT32_MAX)
   , mHttpHandler(gHttpHandler)
   , mRedirectCount(0)
   , mForcePending(false)
 {
   LOG(("Creating HttpBaseChannel @%x\n", this));
@@ -1659,16 +1661,22 @@ HttpBaseChannel::GetLastModifiedTime(PRT
   if (!mResponseHead)
     return NS_ERROR_NOT_AVAILABLE;
   uint32_t lastMod;
   mResponseHead->GetLastModifiedValue(&lastMod);
   *lastModifiedTime = lastMod;
   return NS_OK;
 }
 
+NS_IMETHODIMP
+HttpBaseChannel::ForceNoIntercept()
+{
+  mForceNoIntercept = true;
+  return NS_OK;
+}
 
 //-----------------------------------------------------------------------------
 // HttpBaseChannel::nsISupportsPriority
 //-----------------------------------------------------------------------------
 
 NS_IMETHODIMP
 HttpBaseChannel::GetPriority(int32_t *value)
 {
@@ -1762,16 +1770,29 @@ HttpBaseChannel::GetPrincipal(bool requi
   if (requireAppId && mPrincipal->GetUnknownAppId()) {
       LOG(("HttpBaseChannel::GetPrincipal: No app id [this=%p]", this));
       return nullptr;
   }
 
   return mPrincipal;
 }
 
+bool
+HttpBaseChannel::ShouldIntercept()
+{
+  nsCOMPtr<nsINetworkInterceptController> controller;
+  GetCallback(controller);
+  bool shouldIntercept = false;
+  if (controller && !mForceNoIntercept) {
+    nsresult rv = controller->ShouldPrepareForIntercept(mURI, &shouldIntercept);
+    NS_ENSURE_SUCCESS(rv, false);
+  }
+  return shouldIntercept;
+}
+
 // nsIRedirectHistory
 NS_IMETHODIMP
 HttpBaseChannel::GetRedirects(nsIArray * *aRedirects)
 {
   nsresult rv;
   nsCOMPtr<nsIMutableArray> redirects =
     do_CreateInstance(NS_ARRAY_CONTRACTID, &rv);
   NS_ENSURE_SUCCESS(rv, rv);
--- a/netwerk/protocol/http/HttpBaseChannel.h
+++ b/netwerk/protocol/http/HttpBaseChannel.h
@@ -172,16 +172,17 @@ public:
   NS_IMETHOD GetApiRedirectToURI(nsIURI * *aApiRedirectToURI);
   NS_IMETHOD AddSecurityMessage(const nsAString &aMessageTag, const nsAString &aMessageCategory);
   NS_IMETHOD TakeAllSecurityMessages(nsCOMArray<nsISecurityConsoleMessage> &aMessages);
   NS_IMETHOD GetResponseTimeoutEnabled(bool *aEnable);
   NS_IMETHOD SetResponseTimeoutEnabled(bool aEnable);
   NS_IMETHOD AddRedirect(nsIPrincipal *aRedirect);
   NS_IMETHOD ForcePending(bool aForcePending);
   NS_IMETHOD GetLastModifiedTime(PRTime* lastModifiedTime);
+  NS_IMETHOD ForceNoIntercept();
 
   inline void CleanRedirectCacheChainIfNecessary()
   {
       mRedirectedCachekeys = nullptr;
   }
   NS_IMETHOD HTTPUpgrade(const nsACString & aProtocolName,
                          nsIHttpUpgradeListener *aListener);
 
@@ -271,16 +272,20 @@ protected:
   // Checks whether or not aURI and mOriginalURI share the same domain.
   bool SameOriginWithOriginalUri(nsIURI *aURI);
 
   // GetPrincipal
   // Returns the channel principal. If requireAppId is true, then returns
   // null if the principal has unknown appId.
   nsIPrincipal *GetPrincipal(bool requireAppId);
 
+  // Returns true if this channel should intercept the network request and prepare
+  // for a possible synthesized response instead.
+  bool ShouldIntercept();
+
   friend class PrivateBrowsingChannel<HttpBaseChannel>;
 
   nsCOMPtr<nsIURI>                  mURI;
   nsCOMPtr<nsIURI>                  mOriginalURI;
   nsCOMPtr<nsIURI>                  mDocumentURI;
   nsCOMPtr<nsIStreamListener>       mListener;
   nsCOMPtr<nsISupports>             mListenerContext;
   nsCOMPtr<nsILoadGroup>            mLoadGroup;
@@ -343,16 +348,19 @@ protected:
   uint32_t                          mResponseTimeoutEnabled     : 1;
   // A flag that should be false only if a cross-domain redirect occurred
   uint32_t                          mAllRedirectsSameOrigin     : 1;
 
   // Is 1 if no redirects have occured or if all redirects
   // pass the Resource Timing timing-allow-check
   uint32_t                          mAllRedirectsPassTimingAllowCheck : 1;
 
+  // True if this channel should skip any interception checks
+  uint32_t                          mForceNoIntercept           : 1;
+
   // Current suspension depth for this channel object
   uint32_t                          mSuspendCount;
 
   nsCOMPtr<nsIURI>                  mAPIRedirectToURI;
   nsAutoPtr<nsTArray<nsCString> >   mRedirectedCachekeys;
   // Redirects added by previous channels.
   nsCOMArray<nsIPrincipal>          mRedirects;
 
--- a/netwerk/protocol/http/nsHttpChannel.cpp
+++ b/netwerk/protocol/http/nsHttpChannel.cpp
@@ -11,16 +11,17 @@
 #include "nsHttpChannel.h"
 #include "nsHttpHandler.h"
 #include "nsIApplicationCacheService.h"
 #include "nsIApplicationCacheContainer.h"
 #include "nsICacheStorageService.h"
 #include "nsICacheStorage.h"
 #include "nsICacheEntry.h"
 #include "nsICryptoHash.h"
+#include "nsINetworkInterceptController.h"
 #include "nsIStringBundle.h"
 #include "nsIStreamListenerTee.h"
 #include "nsISeekableStream.h"
 #include "nsILoadGroupChild.h"
 #include "nsIProtocolProxyService2.h"
 #include "nsMimeTypes.h"
 #include "nsNetUtil.h"
 #include "prprf.h"
@@ -145,16 +146,19 @@ bool
 WillRedirect(const nsHttpResponseHead * response)
 {
     return IsRedirectStatus(response->Status()) &&
            response->PeekHeader(nsHttp::Location);
 }
 
 } // unnamed namespace
 
+nsresult
+StoreAuthorizationMetaData(nsICacheEntry *entry, nsHttpRequestHead *requestHead);
+
 class AutoRedirectVetoNotifier
 {
 public:
     explicit AutoRedirectVetoNotifier(nsHttpChannel* channel) : mChannel(channel)
     {
       if (mChannel->mHasAutoRedirectVetoNotifier) {
         MOZ_CRASH("Nested AutoRedirectVetoNotifier on the stack");
         mChannel = nullptr;
@@ -191,26 +195,64 @@ AutoRedirectVetoNotifier::ReportRedirect
         vetoHook->OnRedirectResult(succeeded);
 
     // Drop after the notification
     channel->mHasAutoRedirectVetoNotifier = false;
 
     MOZ_EVENT_TRACER_DONE(channel, "net::http::redirect-callbacks");
 }
 
+// An object representing a channel that has been intercepted. This avoids complicating
+// the actual channel implementation with the details of synthesizing responses.
+class InterceptedChannel : public nsIInterceptedChannel
+{
+    // The actual channel being intercepted
+    nsRefPtr<nsHttpChannel> mChannel;
+
+    // The interception controller to notify about the successful channel interception
+    nsCOMPtr<nsINetworkInterceptController> mController;
+
+    // Writeable cache entry for use when synthesizing a response
+    nsCOMPtr<nsICacheEntry>           mSynthesizedCacheEntry;
+
+    // Response head for use when synthesizing
+    Maybe<nsHttpResponseHead>         mSynthesizedResponseHead;
+
+    void EnsureSynthesizedResponse();
+
+    virtual ~InterceptedChannel() {}
+public:
+    InterceptedChannel(nsHttpChannel* aChannel,
+                       nsINetworkInterceptController* aController,
+                       nsICacheEntry* aEntry)
+    : mChannel(aChannel)
+    , mController(aController)
+    , mSynthesizedCacheEntry(aEntry)
+    {
+    }
+
+    // Notify the interception controller that the channel has been intercepted
+    // and prepare the response body output stream.
+    void NotifyController();
+
+    NS_DECL_ISUPPORTS
+    NS_DECL_NSIINTERCEPTEDCHANNEL
+};
+
 //-----------------------------------------------------------------------------
 // nsHttpChannel <public>
 //-----------------------------------------------------------------------------
 
 nsHttpChannel::nsHttpChannel()
     : HttpAsyncAborter<nsHttpChannel>(MOZ_THIS_IN_INITIALIZER_LIST())
     , mLogicalOffset(0)
     , mPostID(0)
     , mRequestTime(0)
     , mOfflineCacheLastModifiedTime(0)
+    , mInterceptCache(DO_NOT_INTERCEPT)
     , mCachedContentIsValid(false)
     , mCachedContentIsPartial(false)
     , mCacheOnlyMetadata(false)
     , mTransactionReplaced(false)
     , mAuthRetryPending(false)
     , mProxyAuthPending(false)
     , mResuming(false)
     , mInitedCacheEntry(false)
@@ -324,17 +366,17 @@ nsHttpChannel::Connect()
     if (!gHttpHandler->UseCache()) {
         return ContinueConnect();
     }
 
     // open a cache entry for this channel...
     rv = OpenCacheEntry(isHttps);
 
     // do not continue if asyncOpenCacheEntry is in progress
-    if (mCacheEntriesToWaitFor) {
+    if (AwaitingCacheCallbacks()) {
         MOZ_ASSERT(NS_SUCCEEDED(rv), "Unexpected state");
         return NS_OK;
     }
 
     if (NS_FAILED(rv)) {
         LOG(("OpenCacheEntry failed [rv=%x]\n", rv));
         // if this channel is only allowed to pull from the cache, then
         // we must fail if we were unable to open a cache entry.
@@ -1817,16 +1859,23 @@ nsHttpChannel::StartRedirectChannelToURI
     NS_ENSURE_SUCCESS(rv, rv);
 
     rv = SetupReplacementChannel(upgradedURI, newChannel, true);
     NS_ENSURE_SUCCESS(rv, rv);
 
     // Inform consumers about this fake redirect
     mRedirectChannel = newChannel;
 
+    // Ensure that internally-redirected channels cannot be intercepted, which would look
+    // like two separate requests to the nsINetworkInterceptController.
+    nsCOMPtr<nsIHttpChannelInternal> httpRedirect = do_QueryInterface(mRedirectChannel);
+    if (httpRedirect) {
+        httpRedirect->ForceNoIntercept();
+    }
+
     PushRedirectAsyncFunc(
         &nsHttpChannel::ContinueAsyncRedirectChannelToURI);
     rv = gHttpHandler->AsyncOnChannelRedirect(this, newChannel, flags);
 
     if (NS_SUCCEEDED(rv))
         rv = WaitForRedirectCallback();
 
     if (NS_FAILED(rv)) {
@@ -2654,16 +2703,17 @@ nsHttpChannel::OpenCacheEntry(bool isHtt
     mHasQueryString = HasQueryString(mRequestHead.ParsedMethod(), mURI);
 
     LOG(("nsHttpChannel::OpenCacheEntry [this=%p]", this));
 
     // make sure we're not abusing this function
     NS_PRECONDITION(!mCacheEntry, "cache entry already open");
 
     nsAutoCString cacheKey;
+    nsAutoCString extension;
 
     if (mRequestHead.IsPost()) {
         // If the post id is already set then this is an attempt to replay
         // a post transaction via the cache.  Otherwise, we need a unique
         // post id for this transaction.
         if (mPostID == 0)
             mPostID = gHttpHandler->GenerateUniqueID();
     }
@@ -2735,17 +2785,17 @@ nsHttpChannel::OpenCacheEntry(bool isHtt
                             | nsICacheStorage::CHECK_MULTITHREADED;
     }
 
     if (!mPostID && mApplicationCache) {
         rv = cacheStorageService->AppCacheStorage(info, 
             mApplicationCache,
             getter_AddRefs(cacheStorage));
     }
-    else if (mLoadFlags & INHIBIT_PERSISTENT_CACHING) {
+    else if (PossiblyIntercepted() || mLoadFlags & INHIBIT_PERSISTENT_CACHING) {
         rv = cacheStorageService->MemoryCacheStorage(info, // ? choose app cache as well...
             getter_AddRefs(cacheStorage));
     }
     else {
         rv = cacheStorageService->DiskCacheStorage(info,
             !mPostID && (mChooseApplicationCache || (mLoadFlags & LOAD_CHECK_OFFLINE_CACHE)),
             getter_AddRefs(cacheStorage));
     }
@@ -2757,20 +2807,39 @@ nsHttpChannel::OpenCacheEntry(bool isHtt
     if (mLoadAsBlocking || (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;
 
-    rv = cacheStorage->AsyncOpenURI(
-        openURI, mPostID ? nsPrintfCString("%d", mPostID) : EmptyCString(),
-        cacheEntryOpenFlags, this);
-    NS_ENSURE_SUCCESS(rv, rv);
+    if (mPostID) {
+        extension.Append(nsPrintfCString("%d", mPostID));
+    }
+    if (PossiblyIntercepted()) {
+        extension.Append('u');
+    }
+
+    // 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) {
+        nsCOMPtr<nsICacheEntry> entry;
+        rv = cacheStorage->OpenTruncate(openURI, extension, getter_AddRefs(entry));
+        NS_ENSURE_SUCCESS(rv, rv);
+
+        nsCOMPtr<nsINetworkInterceptController> controller;
+        GetCallback(controller);
+
+        nsRefPtr<InterceptedChannel> intercepted = new InterceptedChannel(this, controller, entry);
+        intercepted->NotifyController();
+    } else {
+        rv = cacheStorage->AsyncOpenURI(openURI, extension, cacheEntryOpenFlags, this);
+        NS_ENSURE_SUCCESS(rv, rv);
+    }
 
     waitFlags.Keep(WAIT_FOR_CACHE_ENTRY);
 
 bypassCacheEntryOpen:
     if (!mApplicationCacheForWrite)
         return NS_OK;
 
     // If there is an app cache to write to, open the entry right now in parallel.
@@ -3267,17 +3336,17 @@ nsHttpChannel::OnCacheEntryAvailableInte
         }
         return NS_ERROR_DOCUMENT_NOT_CACHED;
     }
 
     if (NS_FAILED(rv))
       return rv;
 
     // We may be waiting for more callbacks...
-    if (mCacheEntriesToWaitFor)
+    if (AwaitingCacheCallbacks())
       return NS_OK;
 
     return ContinueConnect();
 }
 
 nsresult
 nsHttpChannel::OnNormalCacheEntryAvailable(nsICacheEntry *aEntry,
                                            bool aNew,
@@ -3967,123 +4036,133 @@ nsHttpChannel::InitOfflineCacheEntry()
         mOfflineCacheEntry->SetExpirationTime(expirationTime);
     }
 
     return AddCacheEntryHeaders(mOfflineCacheEntry);
 }
 
 
 nsresult
-nsHttpChannel::AddCacheEntryHeaders(nsICacheEntry *entry)
+DoAddCacheEntryHeaders(nsHttpChannel *self,
+                       nsICacheEntry *entry,
+                       nsHttpRequestHead *requestHead,
+                       nsHttpResponseHead *responseHead,
+                       nsISupports *securityInfo)
 {
     nsresult rv;
 
-    LOG(("nsHttpChannel::AddCacheEntryHeaders [this=%p] begin", this));
+    LOG(("nsHttpChannel::AddCacheEntryHeaders [this=%p] begin", self));
     // Store secure data in memory only
-    if (mSecurityInfo)
-        entry->SetSecurityInfo(mSecurityInfo);
+    if (securityInfo)
+        entry->SetSecurityInfo(securityInfo);
 
     // Store the HTTP request method with the cache entry so we can distinguish
     // for example GET and HEAD responses.
     rv = entry->SetMetaDataElement("request-method",
-                                   mRequestHead.Method().get());
+                                   requestHead->Method().get());
     if (NS_FAILED(rv)) return rv;
 
     // Store the HTTP authorization scheme used if any...
-    rv = StoreAuthorizationMetaData(entry);
+    rv = StoreAuthorizationMetaData(entry, requestHead);
     if (NS_FAILED(rv)) return rv;
 
     // Iterate over the headers listed in the Vary response header, and
     // store the value of the corresponding request header so we can verify
     // that it has not varied when we try to re-use the cached response at
     // a later time.  Take care to store "Cookie" headers only as hashes
     // due to security considerations and the fact that they can be pretty
     // large (bug 468426). We take care of "Vary: cookie" in ResponseWouldVary.
     //
     // NOTE: if "Vary: accept, cookie", then we will store the "accept" header
     // in the cache.  we could try to avoid needlessly storing the "accept"
     // header in this case, but it doesn't seem worth the extra code to perform
     // the check.
     {
         nsAutoCString buf, metaKey;
-        mResponseHead->GetHeader(nsHttp::Vary, buf);
+        responseHead->GetHeader(nsHttp::Vary, buf);
         if (!buf.IsEmpty()) {
             NS_NAMED_LITERAL_CSTRING(prefix, "request-");
 
             char *val = buf.BeginWriting(); // going to munge buf
             char *token = nsCRT::strtok(val, NS_HTTP_HEADER_SEPS, &val);
             while (token) {
                 LOG(("nsHttpChannel::AddCacheEntryHeaders [this=%p] " \
-                        "processing %s", this, token));
+                        "processing %s", self, token));
                 if (*token != '*') {
                     nsHttpAtom atom = nsHttp::ResolveAtom(token);
-                    const char *val = mRequestHead.PeekHeader(atom);
+                    const char *val = requestHead->PeekHeader(atom);
                     nsAutoCString hash;
                     if (val) {
                         // If cookie-header, store a hash of the value
                         if (atom == nsHttp::Cookie) {
                             LOG(("nsHttpChannel::AddCacheEntryHeaders [this=%p] " \
-                                    "cookie-value %s", this, val));
+                                    "cookie-value %s", self, val));
                             rv = Hash(val, hash);
                             // If hash failed, store a string not very likely
                             // to be the result of subsequent hashes
                             if (NS_FAILED(rv))
                                 val = "<hash failed>";
                             else
                                 val = hash.get();
 
                             LOG(("   hashed to %s\n", val));
                         }
 
                         // build cache meta data key and set meta data element...
                         metaKey = prefix + nsDependentCString(token);
                         entry->SetMetaDataElement(metaKey.get(), val);
                     } else {
                         LOG(("nsHttpChannel::AddCacheEntryHeaders [this=%p] " \
-                                "clearing metadata for %s", this, token));
+                                "clearing metadata for %s", self, token));
                         metaKey = prefix + nsDependentCString(token);
                         entry->SetMetaDataElement(metaKey.get(), nullptr);
                     }
                 }
                 token = nsCRT::strtok(val, NS_HTTP_HEADER_SEPS, &val);
             }
         }
     }
 
 
     // Store the received HTTP head with the cache entry as an element of
     // the meta data.
     nsAutoCString head;
-    mResponseHead->Flatten(head, true);
+    responseHead->Flatten(head, true);
     rv = entry->SetMetaDataElement("response-head", head.get());
     if (NS_FAILED(rv)) return rv;
 
     // Indicate we have successfully finished setting metadata on the cache entry.
     rv = entry->MetaDataReady();
 
     return rv;
 }
 
+nsresult
+nsHttpChannel::AddCacheEntryHeaders(nsICacheEntry *entry)
+{
+    return DoAddCacheEntryHeaders(this, entry, &mRequestHead, mResponseHead, mSecurityInfo);
+}
+
 inline void
 GetAuthType(const char *challenge, nsCString &authType)
 {
     const char *p;
 
     // get the challenge type
     if ((p = strchr(challenge, ' ')) != nullptr)
         authType.Assign(challenge, p - challenge);
     else
         authType.Assign(challenge);
 }
 
 nsresult
-nsHttpChannel::StoreAuthorizationMetaData(nsICacheEntry *entry)
+StoreAuthorizationMetaData(nsICacheEntry *entry, nsHttpRequestHead *requestHead)
 {
     // Not applicable to proxy authorization...
-    const char *val = mRequestHead.PeekHeader(nsHttp::Authorization);
+    const char *val = requestHead->PeekHeader(nsHttp::Authorization);
     if (!val)
         return NS_OK;
 
     // eg. [Basic realm="wally world"]
     nsAutoCString buf;
     GetAuthType(val, buf);
     return entry->SetMetaDataElement("auth", buf.get());
 }
@@ -4646,16 +4725,20 @@ nsHttpChannel::AsyncOpen(nsIStreamListen
     nsresult rv;
 
     rv = NS_CheckPortSafety(mURI);
     if (NS_FAILED(rv)) {
         ReleaseListeners();
         return rv;
     }
 
+    if (ShouldIntercept()) {
+        mInterceptCache = MAYBE_INTERCEPT;
+    }
+
     // Remember the cookie header that was set, if any
     const char *cookieHeader = mRequestHead.PeekHeader(nsHttp::Cookie);
     if (cookieHeader) {
         mUserSetCookieHeader = cookieHeader;
     }
 
     AddCookiesToRequest();
 
@@ -5123,17 +5206,16 @@ nsHttpChannel::GetLoadGroup(nsILoadGroup
 }
 
 NS_IMETHODIMP
 nsHttpChannel::GetRequestMethod(nsACString& aMethod)
 {
     return HttpBaseChannel::GetRequestMethod(aMethod);
 }
 
-
 //-----------------------------------------------------------------------------
 // nsHttpChannel::nsIRequestObserver
 //-----------------------------------------------------------------------------
 
 NS_IMETHODIMP
 nsHttpChannel::OnStartRequest(nsIRequest *request, nsISupports *ctxt)
 {
     PROFILER_LABEL("nsHttpChannel", "OnStartRequest",
@@ -6415,9 +6497,119 @@ nsHttpChannel::SetNotificationCallbacks(
 
     nsresult rv = HttpBaseChannel::SetNotificationCallbacks(aCallbacks);
     if (NS_SUCCEEDED(rv)) {
         UpdateAggregateCallbacks();
     }
     return rv;
 }
 
+void
+nsHttpChannel::MarkIntercepted()
+{
+    mInterceptCache = INTERCEPTED;
+}
+
+bool
+nsHttpChannel::AwaitingCacheCallbacks()
+{
+    return mCacheEntriesToWaitFor != 0;
+}
+
+NS_IMPL_ISUPPORTS(InterceptedChannel, nsIInterceptedChannel)
+
+void
+InterceptedChannel::NotifyController()
+{
+    nsCOMPtr<nsIOutputStream> out;
+    nsresult rv = mSynthesizedCacheEntry->OpenOutputStream(0, getter_AddRefs(out));
+    NS_ENSURE_SUCCESS_VOID(rv);
+
+    rv = mController->ChannelIntercepted(this, out);
+    NS_ENSURE_SUCCESS_VOID(rv);
+}
+
+void
+InterceptedChannel::EnsureSynthesizedResponse()
+{
+    if (mSynthesizedResponseHead.isNothing()) {
+        mSynthesizedResponseHead.emplace();
+    }
+}
+
+NS_IMETHODIMP
+InterceptedChannel::ResetInterception()
+{
+    if (!mChannel) {
+        return NS_ERROR_NOT_AVAILABLE;
+    }
+
+    mSynthesizedCacheEntry->AsyncDoom(nullptr);
+    mSynthesizedCacheEntry = nullptr;
+
+    nsCOMPtr<nsIURI> uri;
+    mChannel->GetURI(getter_AddRefs(uri));
+
+    nsresult rv = mChannel->StartRedirectChannelToURI(uri, nsIChannelEventSink::REDIRECT_INTERNAL);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    mChannel = nullptr;
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+InterceptedChannel::SynthesizeHeader(const nsACString& aName, const nsACString& aValue)
+{
+    if (!mSynthesizedCacheEntry) {
+        return NS_ERROR_NOT_AVAILABLE;
+    }
+
+    EnsureSynthesizedResponse();
+    nsAutoCString header = aName + NS_LITERAL_CSTRING(": ") + aValue;
+    // Overwrite any existing header.
+    nsresult rv = mSynthesizedResponseHead->ParseHeaderLine(header.get());
+    NS_ENSURE_SUCCESS(rv, rv);
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+InterceptedChannel::FinishSynthesizedResponse()
+{
+    if (!mChannel) {
+        return NS_ERROR_NOT_AVAILABLE;
+    }
+
+    mChannel->MarkIntercepted();
+
+    // First we ensure the appropriate metadata is set on the synthesized cache entry
+    // (i.e. the flattened response head)
+
+    nsCOMPtr<nsISupports> securityInfo;
+    nsresult rv = mChannel->GetSecurityInfo(getter_AddRefs(securityInfo));
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    EnsureSynthesizedResponse();
+    rv = DoAddCacheEntryHeaders(mChannel, mSynthesizedCacheEntry, mChannel->GetRequestHead(),
+                                mSynthesizedResponseHead.ptr(), securityInfo);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsCOMPtr<nsIURI> uri;
+    mChannel->GetURI(getter_AddRefs(uri));
+
+    bool usingSSL = false;
+    uri->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;
+}
+
 } } // namespace mozilla::net
--- a/netwerk/protocol/http/nsHttpChannel.h
+++ b/netwerk/protocol/http/nsHttpChannel.h
@@ -133,16 +133,21 @@ public: /* internal necko use only */
         nsAutoCString spec;
         nsresult rv = referrer->GetAsciiSpec(spec);
         if (NS_FAILED(rv)) return rv;
         mReferrer = referrer;
         mRequestHead.SetHeader(nsHttp::Referer, spec);
         return NS_OK;
     }
 
+    nsresult OpenCacheEntry(bool usingSSL);
+    nsresult ContinueConnect();
+
+    nsresult StartRedirectChannelToURI(nsIURI *, uint32_t);
+
     // This allows cache entry to be marked as foreign even after channel itself
     // is gone.  Needed for e10s (see HttpChannelParent::RecvDocumentChannelCleanup)
     class OfflineCacheEntryAsForeignMarker {
         nsCOMPtr<nsIApplicationCache> mApplicationCache;
         nsCOMPtr<nsIURI> mCacheURI;
     public:
         OfflineCacheEntryAsForeignMarker(nsIApplicationCache* appCache,
                                          nsIURI* aURI)
@@ -181,26 +186,28 @@ public: /* internal necko use only */
         mChannel->mCacheEntriesToWaitFor &= mKeep;
       }
 
     private:
       nsHttpChannel* mChannel;
       uint32_t mKeep : 2;
     };
 
+    void MarkIntercepted();
+    bool AwaitingCacheCallbacks();
+
 protected:
     virtual ~nsHttpChannel();
 
 private:
     typedef nsresult (nsHttpChannel::*nsContinueRedirectionFunc)(nsresult result);
 
     bool     RequestIsConditional();
     nsresult BeginConnect();
     nsresult Connect();
-    nsresult ContinueConnect();
     void     SpeculativeConnect();
     nsresult SetupTransaction();
     void     SetupTransactionLoadGroupInfo();
     nsresult CallOnStartRequest();
     nsresult ProcessResponse();
     nsresult ContinueProcessResponse(nsresult);
     nsresult ProcessNormal();
     nsresult ContinueProcessNormal(nsresult);
@@ -225,27 +232,25 @@ private:
     // redirection specific methods
     void     HandleAsyncRedirect();
     void     HandleAsyncAPIRedirect();
     nsresult ContinueHandleAsyncRedirect(nsresult);
     void     HandleAsyncNotModified();
     void     HandleAsyncFallback();
     nsresult ContinueHandleAsyncFallback(nsresult);
     nsresult PromptTempRedirect();
-    nsresult StartRedirectChannelToURI(nsIURI *, uint32_t);
     virtual  nsresult SetupReplacementChannel(nsIURI *, nsIChannel *, bool preserveMethod);
 
     // proxy specific methods
     nsresult ProxyFailover();
     nsresult AsyncDoReplaceWithProxy(nsIProxyInfo *);
     nsresult ContinueDoReplaceWithProxy(nsresult);
     nsresult ResolveProxy();
 
     // cache specific methods
-    nsresult OpenCacheEntry(bool usingSSL);
     nsresult OnOfflineCacheEntryAvailable(nsICacheEntry *aEntry,
                                           bool aNew,
                                           nsIApplicationCache* aAppCache,
                                           nsresult aResult);
     nsresult OnNormalCacheEntryAvailable(nsICacheEntry *aEntry,
                                          bool aNew,
                                          nsresult aResult);
     nsresult OpenOfflineCacheEntryForWriting();
@@ -262,17 +267,16 @@ private:
     bool ShouldUpdateOfflineCacheEntry();
     nsresult ReadFromCache(bool alreadyMarkedValid);
     void     CloseCacheEntry(bool doomOnFailure);
     void     CloseOfflineCacheEntry();
     nsresult InitCacheEntry();
     void     UpdateInhibitPersistentCachingFlag();
     nsresult InitOfflineCacheEntry();
     nsresult AddCacheEntryHeaders(nsICacheEntry *entry);
-    nsresult StoreAuthorizationMetaData(nsICacheEntry *entry);
     nsresult FinalizeCacheEntry();
     nsresult InstallCacheListener(int64_t offset = 0);
     nsresult InstallOfflineCacheListener(int64_t offset = 0);
     void     MaybeInvalidateCacheEntryForSubsequentGet();
     void     AsyncOnExamineCachedResponse();
 
     // Handle the bogus Content-Encoding Apache sometimes sends
     void ClearBogusContentEncodingIfNeeded();
@@ -362,16 +366,27 @@ private:
 
     nsCOMPtr<nsICacheEntry> mOfflineCacheEntry;
     uint32_t                          mOfflineCacheLastModifiedTime;
     nsCOMPtr<nsIApplicationCache>     mApplicationCacheForWrite;
 
     // auth specific data
     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;
+
+    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.
     nsCString                         mFallbackKey;
 
     friend class AutoRedirectVetoNotifier;
     friend class HttpAsyncAborter<nsHttpChannel>;
 
--- a/netwerk/protocol/http/nsIHttpChannelInternal.idl
+++ b/netwerk/protocol/http/nsIHttpChannelInternal.idl
@@ -33,17 +33,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(a4bf4fc5-b5a9-4098-bd20-409d71bf18e6)]
+[scriptable, uuid(a95e45c1-b145-487c-b2a9-4e96e814a1b5)]
 interface nsIHttpChannelInternal : nsISupports
 {
     /**
      * An http channel can own a reference to the document URI
      */
     attribute nsIURI documentURI;
 
     /**
@@ -196,9 +196,15 @@ interface nsIHttpChannelInternal : nsISu
 
     /**
      * Add a new nsIPrincipal to the redirect chain. This is the only way to
      * write to nsIRedirectHistory.redirects.
      */
     void addRedirect(in nsIPrincipal aPrincipal);
 
     readonly attribute PRTime lastModifiedTime;
+
+    /**
+     * Force a channel that has not been AsyncOpen'ed to skip any check for possible
+     * interception and proceed immediately to the network/cache.
+     */
+    void forceNoIntercept();
 };
new file mode 100644
--- /dev/null
+++ b/netwerk/test/unit/test_synthesized_response.js
@@ -0,0 +1,145 @@
+"use strict";
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "URL", function() {
+  return "http://localhost:" + httpServer.identity.primaryPort;
+});
+
+var httpServer = null;
+
+function make_uri(url) {
+  var ios = Cc["@mozilla.org/network/io-service;1"].
+            getService(Ci.nsIIOService);
+  return ios.newURI(url, null, null);
+}
+
+// ensure the cache service is prepped when running the test
+Cc["@mozilla.org/netwerk/cache-storage-service;1"].getService(Ci.nsICacheStorageService);
+
+function make_channel(url, body, cb) {
+  var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
+  var chan = ios.newChannel(url, null, null).QueryInterface(Ci.nsIHttpChannel);
+  chan.notificationCallbacks = {
+    numChecks: 0,
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsINetworkInterceptController,
+                                           Ci.nsIInterfaceRequestor]),
+    getInterface: function(iid) {
+      return this.QueryInterface(iid);
+    },
+    shouldPrepareForIntercept: function() {
+      do_check_eq(this.numChecks, 0);
+      this.numChecks++;
+      return true;
+    },
+    channelIntercepted: function(channel, stream) {
+      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, stream, function() {
+          channel.finishSynthesizedResponse();
+        });
+      }
+      if (cb) {
+        cb(channel, stream);
+      }
+    },
+  };
+  return chan;
+}
+
+const REMOTE_BODY = "http handler body";
+const NON_REMOTE_BODY = "synthesized body";
+const NON_REMOTE_BODY_2 = "synthesized body #2";
+
+function bodyHandler(metadata, response) {
+  response.setHeader('Content-Type', 'text/plain');
+  response.write(REMOTE_BODY);
+}
+
+function run_test() {
+  httpServer = new HttpServer();
+  httpServer.registerPathHandler('/body', bodyHandler);
+  httpServer.start(-1);
+
+  run_next_test();
+}
+
+function handle_synthesized_response(request, buffer) {
+  do_check_eq(buffer, NON_REMOTE_BODY);
+  run_next_test();
+}
+
+function handle_synthesized_response_2(request, buffer) {
+  do_check_eq(buffer, NON_REMOTE_BODY_2);
+  run_next_test();
+}
+
+function handle_remote_response(request, buffer) {
+  do_check_eq(buffer, REMOTE_BODY);
+  run_next_test();
+}
+
+// hit the network instead of synthesizing
+add_test(function() {
+  var chan = make_channel(URL + '/body', null, function(chan) {
+    chan.resetInterception();
+  });
+  chan.asyncOpen(new ChannelListener(handle_remote_response, null), null);
+});
+
+// synthesize a response
+add_test(function() {
+  var chan = make_channel(URL + '/body', NON_REMOTE_BODY);
+  chan.asyncOpen(new ChannelListener(handle_synthesized_response, null, CL_ALLOW_UNKNOWN_CL), null);
+});
+
+// hit the network instead of synthesizing, to test that no previous synthesized
+// cache entry is used.
+add_test(function() {
+  var chan = make_channel(URL + '/body', null, function(chan) {
+    chan.resetInterception();
+  });
+  chan.asyncOpen(new ChannelListener(handle_remote_response, null), null);
+});
+
+// synthesize a different response to ensure no previous response is cached
+add_test(function() {
+  var chan = make_channel(URL + '/body', NON_REMOTE_BODY_2);
+  chan.asyncOpen(new ChannelListener(handle_synthesized_response_2, null, CL_ALLOW_UNKNOWN_CL), null);
+});
+
+// ensure that the channel waits for a decision and synthesizes headers correctly
+add_test(function() {
+  var chan = make_channel(URL + '/body', null, function(channel, stream) {
+    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, stream, function() {
+        channel.synthesizeHeader("Content-Length", NON_REMOTE_BODY.length);
+        channel.finishSynthesizedResponse();
+      });
+    });
+  });
+  chan.asyncOpen(new ChannelListener(handle_synthesized_response, null), null);
+});
+
+// ensure that the channel waits for a decision
+add_test(function() {
+  var chan = make_channel(URL + '/body', null, function(chan) {
+    do_timeout(100, function() {
+      chan.resetInterception();
+    });
+  });
+  chan.asyncOpen(new ChannelListener(handle_remote_response, null), null);
+});
+
+add_test(function() {
+  httpServer.stop(run_next_test);
+});
--- a/netwerk/test/unit/xpcshell.ini
+++ b/netwerk/test/unit/xpcshell.ini
@@ -292,16 +292,17 @@ skip-if = os == "android"
 [test_referrer.js]
 [test_predictor.js]
 # Android version detection w/in gecko does not work right on infra, so we just
 # disable this test on all android versions, even though it's enabled on 2.3+ in
 # the wild.
 skip-if = os == "android"
 [test_signature_extraction.js]
 run-if = os == "win"
+[test_synthesized_response.js]
 [test_udp_multicast.js]
 [test_redirect_history.js]
 [test_reply_without_content_type.js]
 [test_websocket_offline.js]
 [test_tls_server.js]
 # The local cert service used by this test is not currently shipped on Android
 skip-if = os == "android"
 [test_1073747.js]