Bug 1212669 P1 Return the correct Response type from redirected fetch() requests. r=nsm r=sicking r=ehsan
authorBen Kelly <ben@wanderview.com>
Mon, 31 Aug 2015 14:26:29 -0700
changeset 289533 63092955e4b7
parent 289532 8974b3c15604
child 289534 e8bb3ab47bdd
push id5172
push userkwierso@gmail.com
push date2015-10-14 01:18 +0000
treeherdermozilla-beta@7c95e499110e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnsm, sicking, ehsan
bugs1212669
milestone42.0
Bug 1212669 P1 Return the correct Response type from redirected fetch() requests. r=nsm r=sicking r=ehsan
dom/fetch/FetchDriver.cpp
dom/fetch/FetchDriver.h
--- a/dom/fetch/FetchDriver.cpp
+++ b/dom/fetch/FetchDriver.cpp
@@ -25,16 +25,17 @@
 #include "nsHostObjectProtocolHandler.h"
 #include "nsNetUtil.h"
 #include "nsPrintfCString.h"
 #include "nsStreamUtils.h"
 #include "nsStringStream.h"
 
 #include "mozilla/dom/File.h"
 #include "mozilla/dom/workers/Workers.h"
+#include "mozilla/unused.h"
 
 #include "Fetch.h"
 #include "InternalRequest.h"
 #include "InternalResponse.h"
 
 namespace mozilla {
 namespace dom {
 
@@ -43,16 +44,17 @@ NS_IMPL_ISUPPORTS(FetchDriver,
                   nsIAsyncVerifyRedirectCallback)
 
 FetchDriver::FetchDriver(InternalRequest* aRequest, nsIPrincipal* aPrincipal,
                          nsILoadGroup* aLoadGroup)
   : mPrincipal(aPrincipal)
   , mLoadGroup(aLoadGroup)
   , mRequest(aRequest)
   , mFetchRecursionCount(0)
+  , mCORSFlagEverSet(false)
   , mResponseAvailableCalled(false)
 {
 }
 
 FetchDriver::~FetchDriver()
 {
   // We assert this since even on failures, we should call
   // FailWithNetworkError().
@@ -88,28 +90,28 @@ FetchDriver::Fetch(bool aCORSFlag)
       FailWithNetworkError();
     }
     return rv;
   }
 
   MOZ_CRASH("Synchronous fetch not supported");
 }
 
-nsresult
-FetchDriver::ContinueFetch(bool aCORSFlag)
+FetchDriver::MainFetchOp
+FetchDriver::SetTaintingAndGetNextOp(bool aCORSFlag)
 {
   workers::AssertIsOnMainThread();
 
   nsAutoCString url;
   mRequest->GetURL(url);
   nsCOMPtr<nsIURI> requestURI;
   nsresult rv = NS_NewURI(getter_AddRefs(requestURI), url,
                           nullptr, nullptr);
   if (NS_WARN_IF(NS_FAILED(rv))) {
-    return FailWithNetworkError();
+    return MainFetchOp(NETWORK_ERROR);
   }
 
   // CSP/mixed content checks.
   int16_t shouldLoad;
   rv = NS_CheckContentLoadPolicy(mRequest->ContentPolicyType(),
                                  requestURI,
                                  mPrincipal,
                                  mDocument,
@@ -117,56 +119,102 @@ FetchDriver::ContinueFetch(bool aCORSFla
                                  // Content-Type header?
                                  EmptyCString(), /* mime guess */
                                  nullptr, /* extra */
                                  &shouldLoad,
                                  nsContentUtils::GetContentPolicy(),
                                  nsContentUtils::GetSecurityManager());
   if (NS_WARN_IF(NS_FAILED(rv) || NS_CP_REJECTED(shouldLoad))) {
     // Disallowed by content policy.
-    return FailWithNetworkError();
+    return MainFetchOp(NETWORK_ERROR);
   }
 
-  // Begin Step 4 of the Fetch algorithm
+  // Begin Step 8 of the Main Fetch algorithm
   // https://fetch.spec.whatwg.org/#fetching
 
   nsAutoCString scheme;
   rv = requestURI->GetScheme(scheme);
   if (NS_WARN_IF(NS_FAILED(rv))) {
-    return FailWithNetworkError();
+    return MainFetchOp(NETWORK_ERROR);
   }
 
-  rv = mPrincipal->CheckMayLoad(requestURI, false /* report */, false /* allowIfInheritsPrincipal */);
+  // request's current url's origin is request's origin and the CORS flag is unset
+  // request's current url's scheme is "data" and request's same-origin data-URL flag is set
+  // request's current url's scheme is "about"
+  rv = mPrincipal->CheckMayLoad(requestURI, false /* report */,
+                                false /* allowIfInheritsPrincipal */);
   if ((!aCORSFlag && NS_SUCCEEDED(rv)) ||
       (scheme.EqualsLiteral("data") && mRequest->SameOriginDataURL()) ||
       scheme.EqualsLiteral("about")) {
-    return BasicFetch();
+    return MainFetchOp(BASIC_FETCH);
+  }
+
+  // request's mode is "same-origin"
+  if (mRequest->Mode() == RequestMode::Same_origin) {
+    return MainFetchOp(NETWORK_ERROR);
+  }
+
+  // request's mode is "no-cors"
+  if (mRequest->Mode() == RequestMode::No_cors) {
+    mRequest->SetResponseTainting(InternalRequest::RESPONSETAINT_OPAQUE);
+    return MainFetchOp(BASIC_FETCH);
+  }
+
+  // request's current url's scheme is not one of "http" and "https"
+  if (!scheme.EqualsLiteral("http") && !scheme.EqualsLiteral("https")) {
+    return MainFetchOp(NETWORK_ERROR);
   }
 
-  if (mRequest->Mode() == RequestMode::Same_origin) {
+  // request's mode is "cors-with-forced-preflight"
+  // request's unsafe-request flag is set and either request's method is not
+  // a simple method or a header in request's header list is not a simple header
+  if (mRequest->Mode() == RequestMode::Cors_with_forced_preflight ||
+      (mRequest->UnsafeRequest() && (!mRequest->HasSimpleMethod() ||
+                                     !mRequest->Headers()->HasOnlySimpleHeaders()))) {
+    mRequest->SetResponseTainting(InternalRequest::RESPONSETAINT_CORS);
+
+    // We block cross-origin redirects that require preflight in
+    // AsyncOnChannelRedirect() instead of using "error" redirect mode.
+
+    // Note, the following text from Main Fetch step 8 is handled in
+    // nsCORSListenerProxy when CheckRequestApproved() fails:
+    //
+    //  The result of performing an HTTP fetch using request with the CORS
+    //  flag and CORS-preflight flag set. If the result is a network error,
+    //  clear cache entries using request.
+
+    return MainFetchOp(HTTP_FETCH, true /* cors */, true /* preflight */);
+  }
+
+  // Otherwise
+  mRequest->SetResponseTainting(InternalRequest::RESPONSETAINT_CORS);
+  return MainFetchOp(HTTP_FETCH, true /* cors */, false /* preflight */);
+}
+
+nsresult
+FetchDriver::ContinueFetch(bool aCORSFlag)
+{
+  workers::AssertIsOnMainThread();
+
+  MainFetchOp nextOp = SetTaintingAndGetNextOp(aCORSFlag);
+
+  if (nextOp.mType == NETWORK_ERROR) {
     return FailWithNetworkError();
   }
 
-  if (mRequest->Mode() == RequestMode::No_cors) {
-    mRequest->SetResponseTainting(InternalRequest::RESPONSETAINT_OPAQUE);
+  if (nextOp.mType == BASIC_FETCH) {
     return BasicFetch();
   }
 
-  if (!scheme.EqualsLiteral("http") && !scheme.EqualsLiteral("https")) {
-    return FailWithNetworkError();
+  if (nextOp.mType == HTTP_FETCH) {
+    return HttpFetch(nextOp.mCORSFlag, nextOp.mCORSPreflightFlag);
   }
 
-  bool corsPreflight = false;
-  if (mRequest->Mode() == RequestMode::Cors_with_forced_preflight ||
-      (mRequest->UnsafeRequest() && (!mRequest->HasSimpleMethod() || !mRequest->Headers()->HasOnlySimpleHeaders()))) {
-    corsPreflight = true;
-  }
-
-  mRequest->SetResponseTainting(InternalRequest::RESPONSETAINT_CORS);
-  return HttpFetch(true /* aCORSFlag */, corsPreflight);
+  MOZ_ASSERT_UNREACHABLE("Unexpected main fetch operation!");
+  return FailWithNetworkError();
 }
 
 nsresult
 FetchDriver::BasicFetch()
 {
   nsAutoCString url;
   mRequest->GetURL(url);
   nsCOMPtr<nsIURI> uri;
@@ -308,16 +356,21 @@ FetchDriver::BasicFetch()
 // Necko HTTP implementation.
 nsresult
 FetchDriver::HttpFetch(bool aCORSFlag, bool aCORSPreflightFlag, bool aAuthenticationFlag)
 {
   // Step 1. "Let response be null."
   mResponse = nullptr;
   nsresult rv;
 
+  // We need to track the CORS flag through redirects.  Since there is no way
+  // for us to go from CORS mode to non-CORS mode, we just need to remember
+  // if it has ever been set.
+  mCORSFlagEverSet = mCORSFlagEverSet || aCORSFlag;
+
   nsCOMPtr<nsIIOService> ios = do_GetIOService(&rv);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     FailWithNetworkError();
     return rv;
   }
 
   nsAutoCString url;
   mRequest->GetURL(url);
@@ -464,16 +517,22 @@ FetchDriver::HttpFetch(bool aCORSFlag, b
       httpChan->SetRequestHeader(NS_LITERAL_CSTRING("origin"),
                                  NS_ConvertUTF16toUTF8(origin),
                                  false /* merge */);
     }
     // Bug 1120722 - Authorization will be handled later.
     // Auth may require prompting, we don't support it yet.
     // The next patch in this same bug prevents this from aborting the request.
     // Credentials checks for CORS are handled by nsCORSListenerProxy,
+
+    nsCOMPtr<nsIHttpChannelInternal> internalChan = do_QueryInterface(httpChan);
+
+    // Conversion between enumerations is safe due to static asserts in
+    // dom/workers/ServiceWorkerManager.cpp
+    internalChan->SetCorsMode(static_cast<uint32_t>(mRequest->Mode()));
   }
 
   // Step 5. Proxy authentication will be handled by Necko.
   // FIXME(nsm): Bug 1120715.
   // Step 7-10. "If request's cache mode is neither no-store nor reload..."
 
   // Continue setting up 'HTTPRequest'. Content-Type and body data.
   nsCOMPtr<nsIUploadChannel2> uploadChan = do_QueryInterface(chan);
@@ -511,20 +570,20 @@ FetchDriver::HttpFetch(bool aCORSFlag, b
       // If it is not an http channel, it has to be a jar one.
       MOZ_ASSERT(jarChannel);
       jarChannel->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) {
+  // Only use nsCORSListenerProxy if we are in CORS mode.  Otherwise it
+  // will overwrite the CorsMode flag unconditionally to "cors" or
+  // "cors-with-forced-preflight".
+  if (mRequest->Mode() == RequestMode::Cors) {
     // Set up a CORS proxy that will handle the various requirements of the CORS
     // protocol. It handles the preflight cache and CORS response headers.
     // If the request is allowed, it will start our original request
     // and our observer will be notified. On failure, our observer is notified
     // directly.
     nsRefPtr<nsCORSListenerProxy> corsListener =
       new nsCORSListenerProxy(this, mPrincipal, useCredentials);
     rv = corsListener->Init(chan, DataURIHandling::Allow);
@@ -670,25 +729,32 @@ public:
 NS_IMPL_ISUPPORTS(FillResponseHeaders, nsIHttpHeaderVisitor)
 } // namespace
 
 NS_IMETHODIMP
 FetchDriver::OnStartRequest(nsIRequest* aRequest,
                             nsISupports* aContext)
 {
   workers::AssertIsOnMainThread();
-  MOZ_ASSERT(!mPipeOutputStream);
-  MOZ_ASSERT(mObserver);
+
+  // Note, this can be called multiple times if we are doing an opaqueredirect.
+  // In that case we will get a simulated OnStartRequest() and then the real
+  // channel will call in with an errored OnStartRequest().
+
   nsresult rv;
   aRequest->GetStatus(&rv);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     FailWithNetworkError();
     return rv;
   }
 
+  // We should only get to the following code once.
+  MOZ_ASSERT(!mPipeOutputStream);
+  MOZ_ASSERT(mObserver);
+
   nsRefPtr<InternalResponse> response;
   nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest);
   if (httpChannel) {
     uint32_t responseStatus;
     httpChannel->GetResponseStatus(&responseStatus);
 
     nsAutoCString statusText;
     httpChannel->GetResponseStatusText(statusText);
@@ -810,22 +876,27 @@ FetchDriver::AsyncOnChannelRedirect(nsIC
                                     nsIChannel* aNewChannel,
                                     uint32_t aFlags,
                                     nsIAsyncVerifyRedirectCallback *aCallback)
 {
   NS_PRECONDITION(aNewChannel, "Redirect without a channel?");
 
   nsresult rv;
 
-  // Section 4.2, Step 4.6-4.7, enforcing a redirect count is done by Necko.
-  // The pref used is "network.http.redirection-limit" which is set to 20 by
-  // default.
-  //
-  // Step 4.8. We only unset this for spec compatibility. Any actions we take
-  // on mRequest here do not affect what the channel does.
+  // HTTP Fetch step 5, "redirect status", steps 2 through 6 are automatically
+  // handled by necko before calling AsyncOnChannelRedirect() with the new
+  // nsIChannel.
+
+  // HTTP Fetch step 5, "redirect status", steps 7 and 8 enforcing a redirect
+  // count are done by Necko.  The pref used is "network.http.redirection-limit"
+  // which is set to 20 by default.
+
+  // HTTP Fetch Step 9, "redirect status". We only unset this for spec
+  // compatibility. Any actions we take on mRequest here do not affect what the
+  //channel does.
   mRequest->UnsetSameOriginDataURL();
 
   //
   // Requests that require preflight are not permitted to redirect.
   // Fetch spec section 4.2 "HTTP Fetch", step 4.9 just uses the manual
   // redirect flag to decide whether to execute step 4.10 or not. We do not
   // represent it in our implementation.
   // The only thing we do is to check if the request requires a preflight (part
@@ -837,16 +908,20 @@ FetchDriver::AsyncOnChannelRedirect(nsIC
     rv = DoesNotRequirePreflight(aNewChannel);
     if (NS_FAILED(rv)) {
       NS_WARNING("FetchDriver::OnChannelRedirect: "
                  "DoesNotRequirePreflight returned failure");
       return rv;
     }
   }
 
+  // HTTP Fetch step 5, "redirect status", steps 11.1 and 11.2 block redirecting
+  // to a URL with credentials in CORS mode.  This is implemented in
+  // nsCORSListenerProxy.
+
   mRedirectCallback = aCallback;
   mOldRedirectChannel = aOldChannel;
   mNewRedirectChannel = aNewChannel;
 
   nsCOMPtr<nsIChannelEventSink> outer =
     do_GetInterface(mNotificationCallbacks);
   if (outer) {
     // The callee is supposed to call OnRedirectVerifyCallback() on success,
@@ -923,38 +998,54 @@ FetchDriver::GetInterface(const nsIID& a
   }
 
   return QueryInterface(aIID, aResult);
 }
 
 NS_IMETHODIMP
 FetchDriver::OnRedirectVerifyCallback(nsresult aResult)
 {
-  // On a successful redirect we perform the following substeps of Section 4.2,
-  // step 4.10.
+  // On a successful redirect we perform the following substeps of HTTP Fetch,
+  // step 5, "redirect status", step 11.
   if (NS_SUCCEEDED(aResult)) {
-    // Step 4.10.3 "Set request's url to locationURL." so that when we set the
-    // Response's URL from the Request's URL in Section 4, step 6, we get the
-    // final value.
+    // Step 11.5 "Append locationURL to request's url list." so that when we set the
+    // Response's URL from the Request's URL in Main Fetch, step 15, we get the
+    // final value. Note, we still use a single URL value instead of a list.
     nsCOMPtr<nsIURI> newURI;
     nsresult rv = NS_GetFinalChannelURI(mNewRedirectChannel, getter_AddRefs(newURI));
     if (NS_WARN_IF(NS_FAILED(rv))) {
       aResult = rv;
     } else {
       // We need to update our request's URL.
       nsAutoCString newUrl;
       newURI->GetSpec(newUrl);
       mRequest->SetURL(newUrl);
     }
   }
 
   if (NS_FAILED(aResult)) {
     mOldRedirectChannel->Cancel(aResult);
   }
 
+  // Implement Main Fetch step 8 again on redirect.
+  MainFetchOp nextOp = SetTaintingAndGetNextOp(mCORSFlagEverSet);
+
+  if (nextOp.mType == NETWORK_ERROR) {
+    // Cancel the channel if Main Fetch blocks the redirect from continuing.
+    aResult = NS_ERROR_DOM_BAD_URI;
+    mOldRedirectChannel->Cancel(aResult);
+  } else {
+    // Otherwise, we rely on necko and the CORS proxy to do the right thing
+    // as the redirect is followed.  In general this means basic or http
+    // fetch.  If we've ever been CORS, we need to stay CORS.
+    MOZ_ASSERT(nextOp.mType == BASIC_FETCH || nextOp.mType == HTTP_FETCH);
+    MOZ_ASSERT_IF(mCORSFlagEverSet, nextOp.mType == HTTP_FETCH);
+    MOZ_ASSERT_IF(mCORSFlagEverSet, nextOp.mCORSFlag);
+  }
+
   mOldRedirectChannel = nullptr;
   mNewRedirectChannel = nullptr;
   mRedirectCallback->OnRedirectVerifyCallback(aResult);
   mRedirectCallback = nullptr;
   return NS_OK;
 }
 
 void
--- a/dom/fetch/FetchDriver.h
+++ b/dom/fetch/FetchDriver.h
@@ -69,25 +69,48 @@ private:
   nsCOMPtr<nsIOutputStream> mPipeOutputStream;
   nsRefPtr<FetchDriverObserver> mObserver;
   nsCOMPtr<nsIInterfaceRequestor> mNotificationCallbacks;
   nsCOMPtr<nsIAsyncVerifyRedirectCallback> mRedirectCallback;
   nsCOMPtr<nsIChannel> mOldRedirectChannel;
   nsCOMPtr<nsIChannel> mNewRedirectChannel;
   nsCOMPtr<nsIDocument> mDocument;
   uint32_t mFetchRecursionCount;
+  bool mCORSFlagEverSet;
 
   DebugOnly<bool> mResponseAvailableCalled;
 
   FetchDriver() = delete;
   FetchDriver(const FetchDriver&) = delete;
   FetchDriver& operator=(const FetchDriver&) = delete;
   ~FetchDriver();
 
+  enum MainFetchOpType
+  {
+    NETWORK_ERROR,
+    BASIC_FETCH,
+    HTTP_FETCH,
+    NUM_MAIN_FETCH_OPS
+  };
+
+  struct MainFetchOp
+  {
+    explicit MainFetchOp(MainFetchOpType aType, bool aCORSFlag = false,
+                         bool aCORSPreflightFlag = false)
+      : mType(aType), mCORSFlag(aCORSFlag),
+        mCORSPreflightFlag(aCORSPreflightFlag)
+    { }
+
+    MainFetchOpType mType;
+    bool mCORSFlag;
+    bool mCORSPreflightFlag;
+  };
+
   nsresult Fetch(bool aCORSFlag);
+  MainFetchOp SetTaintingAndGetNextOp(bool aCORSFlag);
   nsresult ContinueFetch(bool aCORSFlag);
   nsresult BasicFetch();
   nsresult HttpFetch(bool aCORSFlag = false, bool aCORSPreflightFlag = false, bool aAuthenticationFlag = false);
   nsresult ContinueHttpFetchAfterNetworkFetch();
   // Returns the filtered response sent to the observer.
   // Callers who don't have access to a channel can pass null for aFinalURI.
   already_AddRefed<InternalResponse>
   BeginAndGetFilteredResponse(InternalResponse* aResponse, nsIURI* aFinalURI);