diff options
Diffstat (limited to 'netwerk/protocol/http/nsCORSListenerProxy.cpp')
-rw-r--r-- | netwerk/protocol/http/nsCORSListenerProxy.cpp | 1526 |
1 files changed, 1526 insertions, 0 deletions
diff --git a/netwerk/protocol/http/nsCORSListenerProxy.cpp b/netwerk/protocol/http/nsCORSListenerProxy.cpp new file mode 100644 index 000000000..c2a624330 --- /dev/null +++ b/netwerk/protocol/http/nsCORSListenerProxy.cpp @@ -0,0 +1,1526 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "mozilla/Assertions.h" +#include "mozilla/LinkedList.h" + +#include "nsCORSListenerProxy.h" +#include "nsIChannel.h" +#include "nsIHttpChannel.h" +#include "HttpChannelChild.h" +#include "nsIHttpChannelInternal.h" +#include "nsError.h" +#include "nsContentUtils.h" +#include "nsIScriptSecurityManager.h" +#include "nsNetUtil.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsMimeTypes.h" +#include "nsIStreamConverterService.h" +#include "nsStringStream.h" +#include "nsGkAtoms.h" +#include "nsWhitespaceTokenizer.h" +#include "nsIChannelEventSink.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsCharSeparatedTokenizer.h" +#include "nsAsyncRedirectVerifyHelper.h" +#include "nsClassHashtable.h" +#include "nsHashKeys.h" +#include "nsStreamUtils.h" +#include "mozilla/Preferences.h" +#include "nsIScriptError.h" +#include "nsILoadGroup.h" +#include "nsILoadContext.h" +#include "nsIConsoleService.h" +#include "nsIDOMNode.h" +#include "nsIDOMWindowUtils.h" +#include "nsIDOMWindow.h" +#include "nsINetworkInterceptController.h" +#include "nsNullPrincipal.h" +#include "nsICorsPreflightCallback.h" +#include "nsISupportsImpl.h" +#include "mozilla/LoadInfo.h" +#include "nsIHttpHeaderVisitor.h" +#include <algorithm> + +using namespace mozilla; + +#define PREFLIGHT_CACHE_SIZE 100 + +static bool gDisableCORS = false; +static bool gDisableCORSPrivateData = false; + +static void +LogBlockedRequest(nsIRequest* aRequest, + const char* aProperty, + const char16_t* aParam) +{ + nsresult rv = NS_OK; + + // Build the error object and log it to the console + nsCOMPtr<nsIConsoleService> console(do_GetService(NS_CONSOLESERVICE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to log blocked cross-site request (no console)"); + return; + } + + nsCOMPtr<nsIScriptError> scriptError = + do_CreateInstance(NS_SCRIPTERROR_CONTRACTID, &rv); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to log blocked cross-site request (no scriptError)"); + return; + } + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + nsCOMPtr<nsIURI> aUri; + channel->GetURI(getter_AddRefs(aUri)); + nsAutoCString spec; + if (aUri) { + spec = aUri->GetSpecOrDefault(); + } + + // Generate the error message + nsXPIDLString blockedMessage; + NS_ConvertUTF8toUTF16 specUTF16(spec); + const char16_t* params[] = { specUTF16.get(), aParam }; + rv = nsContentUtils::FormatLocalizedString(nsContentUtils::eSECURITY_PROPERTIES, + aProperty, + params, + blockedMessage); + + if (NS_FAILED(rv)) { + NS_WARNING("Failed to log blocked cross-site request (no formalizedStr"); + return; + } + + nsAutoString msg(blockedMessage.get()); + + // query innerWindowID and log to web console, otherwise log to + // the error to the browser console. + uint64_t innerWindowID = nsContentUtils::GetInnerWindowID(aRequest); + + if (innerWindowID > 0) { + rv = scriptError->InitWithWindowID(msg, + EmptyString(), // sourceName + EmptyString(), // sourceLine + 0, // lineNumber + 0, // columnNumber + nsIScriptError::warningFlag, + "CORS", + innerWindowID); + } + else { + rv = scriptError->Init(msg, + EmptyString(), // sourceName + EmptyString(), // sourceLine + 0, // lineNumber + 0, // columnNumber + nsIScriptError::warningFlag, + "CORS"); + } + if (NS_FAILED(rv)) { + NS_WARNING("Failed to log blocked cross-site request (scriptError init failed)"); + return; + } + console->LogMessage(scriptError); +} + +////////////////////////////////////////////////////////////////////////// +// Preflight cache + +class nsPreflightCache +{ +public: + struct TokenTime + { + nsCString token; + TimeStamp expirationTime; + }; + + struct CacheEntry : public LinkedListElement<CacheEntry> + { + explicit CacheEntry(nsCString& aKey) + : mKey(aKey) + { + MOZ_COUNT_CTOR(nsPreflightCache::CacheEntry); + } + + ~CacheEntry() + { + MOZ_COUNT_DTOR(nsPreflightCache::CacheEntry); + } + + void PurgeExpired(TimeStamp now); + bool CheckRequest(const nsCString& aMethod, + const nsTArray<nsCString>& aCustomHeaders); + + nsCString mKey; + nsTArray<TokenTime> mMethods; + nsTArray<TokenTime> mHeaders; + }; + + nsPreflightCache() + { + MOZ_COUNT_CTOR(nsPreflightCache); + } + + ~nsPreflightCache() + { + Clear(); + MOZ_COUNT_DTOR(nsPreflightCache); + } + + bool Initialize() + { + return true; + } + + CacheEntry* GetEntry(nsIURI* aURI, nsIPrincipal* aPrincipal, + bool aWithCredentials, bool aCreate); + void RemoveEntries(nsIURI* aURI, nsIPrincipal* aPrincipal); + + void Clear(); + +private: + static bool GetCacheKey(nsIURI* aURI, nsIPrincipal* aPrincipal, + bool aWithCredentials, nsACString& _retval); + + nsClassHashtable<nsCStringHashKey, CacheEntry> mTable; + LinkedList<CacheEntry> mList; +}; + +// Will be initialized in EnsurePreflightCache. +static nsPreflightCache* sPreflightCache = nullptr; + +static bool EnsurePreflightCache() +{ + if (sPreflightCache) + return true; + + nsAutoPtr<nsPreflightCache> newCache(new nsPreflightCache()); + + if (newCache->Initialize()) { + sPreflightCache = newCache.forget(); + return true; + } + + return false; +} + +void +nsPreflightCache::CacheEntry::PurgeExpired(TimeStamp now) +{ + uint32_t i; + for (i = 0; i < mMethods.Length(); ++i) { + if (now >= mMethods[i].expirationTime) { + mMethods.RemoveElementAt(i--); + } + } + for (i = 0; i < mHeaders.Length(); ++i) { + if (now >= mHeaders[i].expirationTime) { + mHeaders.RemoveElementAt(i--); + } + } +} + +bool +nsPreflightCache::CacheEntry::CheckRequest(const nsCString& aMethod, + const nsTArray<nsCString>& aHeaders) +{ + PurgeExpired(TimeStamp::NowLoRes()); + + if (!aMethod.EqualsLiteral("GET") && !aMethod.EqualsLiteral("POST")) { + uint32_t i; + for (i = 0; i < mMethods.Length(); ++i) { + if (aMethod.Equals(mMethods[i].token)) + break; + } + if (i == mMethods.Length()) { + return false; + } + } + + for (uint32_t i = 0; i < aHeaders.Length(); ++i) { + uint32_t j; + for (j = 0; j < mHeaders.Length(); ++j) { + if (aHeaders[i].Equals(mHeaders[j].token, + nsCaseInsensitiveCStringComparator())) { + break; + } + } + if (j == mHeaders.Length()) { + return false; + } + } + + return true; +} + +nsPreflightCache::CacheEntry* +nsPreflightCache::GetEntry(nsIURI* aURI, + nsIPrincipal* aPrincipal, + bool aWithCredentials, + bool aCreate) +{ + nsCString key; + if (!GetCacheKey(aURI, aPrincipal, aWithCredentials, key)) { + NS_WARNING("Invalid cache key!"); + return nullptr; + } + + CacheEntry* existingEntry = nullptr; + + if (mTable.Get(key, &existingEntry)) { + // Entry already existed so just return it. Also update the LRU list. + + // Move to the head of the list. + existingEntry->removeFrom(mList); + mList.insertFront(existingEntry); + + return existingEntry; + } + + if (!aCreate) { + return nullptr; + } + + // This is a new entry, allocate and insert into the table now so that any + // failures don't cause items to be removed from a full cache. + CacheEntry* newEntry = new CacheEntry(key); + if (!newEntry) { + NS_WARNING("Failed to allocate new cache entry!"); + return nullptr; + } + + NS_ASSERTION(mTable.Count() <= PREFLIGHT_CACHE_SIZE, + "Something is borked, too many entries in the cache!"); + + // Now enforce the max count. + if (mTable.Count() == PREFLIGHT_CACHE_SIZE) { + // Try to kick out all the expired entries. + TimeStamp now = TimeStamp::NowLoRes(); + for (auto iter = mTable.Iter(); !iter.Done(); iter.Next()) { + nsAutoPtr<CacheEntry>& entry = iter.Data(); + entry->PurgeExpired(now); + + if (entry->mHeaders.IsEmpty() && + entry->mMethods.IsEmpty()) { + // Expired, remove from the list as well as the hash table. + entry->removeFrom(sPreflightCache->mList); + iter.Remove(); + } + } + + // If that didn't remove anything then kick out the least recently used + // entry. + if (mTable.Count() == PREFLIGHT_CACHE_SIZE) { + CacheEntry* lruEntry = static_cast<CacheEntry*>(mList.popLast()); + MOZ_ASSERT(lruEntry); + + // This will delete 'lruEntry'. + mTable.Remove(lruEntry->mKey); + + NS_ASSERTION(mTable.Count() == PREFLIGHT_CACHE_SIZE - 1, + "Somehow tried to remove an entry that was never added!"); + } + } + + mTable.Put(key, newEntry); + mList.insertFront(newEntry); + + return newEntry; +} + +void +nsPreflightCache::RemoveEntries(nsIURI* aURI, nsIPrincipal* aPrincipal) +{ + CacheEntry* entry; + nsCString key; + if (GetCacheKey(aURI, aPrincipal, true, key) && + mTable.Get(key, &entry)) { + entry->removeFrom(mList); + mTable.Remove(key); + } + + if (GetCacheKey(aURI, aPrincipal, false, key) && + mTable.Get(key, &entry)) { + entry->removeFrom(mList); + mTable.Remove(key); + } +} + +void +nsPreflightCache::Clear() +{ + mList.clear(); + mTable.Clear(); +} + +/* static */ bool +nsPreflightCache::GetCacheKey(nsIURI* aURI, + nsIPrincipal* aPrincipal, + bool aWithCredentials, + nsACString& _retval) +{ + NS_ASSERTION(aURI, "Null uri!"); + NS_ASSERTION(aPrincipal, "Null principal!"); + + NS_NAMED_LITERAL_CSTRING(space, " "); + + nsCOMPtr<nsIURI> uri; + nsresult rv = aPrincipal->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, false); + + nsAutoCString scheme, host, port; + if (uri) { + uri->GetScheme(scheme); + uri->GetHost(host); + port.AppendInt(NS_GetRealPort(uri)); + } + + if (aWithCredentials) { + _retval.AssignLiteral("cred"); + } + else { + _retval.AssignLiteral("nocred"); + } + + nsAutoCString spec; + rv = aURI->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, false); + + _retval.Append(space + scheme + space + host + space + port + space + + spec); + + return true; +} + +////////////////////////////////////////////////////////////////////////// +// nsCORSListenerProxy + +NS_IMPL_ISUPPORTS(nsCORSListenerProxy, nsIStreamListener, + nsIRequestObserver, nsIChannelEventSink, + nsIInterfaceRequestor, nsIThreadRetargetableStreamListener) + +/* static */ +void +nsCORSListenerProxy::Startup() +{ + Preferences::AddBoolVarCache(&gDisableCORS, + "content.cors.disable"); + Preferences::AddBoolVarCache(&gDisableCORSPrivateData, + "content.cors.no_private_data"); +} + +/* static */ +void +nsCORSListenerProxy::Shutdown() +{ + delete sPreflightCache; + sPreflightCache = nullptr; +} + +nsCORSListenerProxy::nsCORSListenerProxy(nsIStreamListener* aOuter, + nsIPrincipal* aRequestingPrincipal, + bool aWithCredentials) + : mOuterListener(aOuter), + mRequestingPrincipal(aRequestingPrincipal), + mOriginHeaderPrincipal(aRequestingPrincipal), + mWithCredentials(aWithCredentials && !gDisableCORSPrivateData), + mRequestApproved(false), + mHasBeenCrossSite(false) +{ +} + +nsCORSListenerProxy::~nsCORSListenerProxy() +{ +} + +nsresult +nsCORSListenerProxy::Init(nsIChannel* aChannel, DataURIHandling aAllowDataURI) +{ + aChannel->GetNotificationCallbacks(getter_AddRefs(mOuterNotificationCallbacks)); + aChannel->SetNotificationCallbacks(this); + + nsresult rv = UpdateChannel(aChannel, aAllowDataURI, UpdateType::Default); + if (NS_FAILED(rv)) { + mOuterListener = nullptr; + mRequestingPrincipal = nullptr; + mOriginHeaderPrincipal = nullptr; + mOuterNotificationCallbacks = nullptr; + } +#ifdef DEBUG + mInited = true; +#endif + return rv; +} + +NS_IMETHODIMP +nsCORSListenerProxy::OnStartRequest(nsIRequest* aRequest, + nsISupports* aContext) +{ + MOZ_ASSERT(mInited, "nsCORSListenerProxy has not been initialized properly"); + nsresult rv = CheckRequestApproved(aRequest); + mRequestApproved = NS_SUCCEEDED(rv); + if (!mRequestApproved) { + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + if (channel) { + nsCOMPtr<nsIURI> uri; + NS_GetFinalChannelURI(channel, getter_AddRefs(uri)); + if (uri) { + if (sPreflightCache) { + // OK to use mRequestingPrincipal since preflights never get + // redirected. + sPreflightCache->RemoveEntries(uri, mRequestingPrincipal); + } else { + nsCOMPtr<nsIHttpChannelChild> httpChannelChild = + do_QueryInterface(channel); + if (httpChannelChild) { + rv = httpChannelChild->RemoveCorsPreflightCacheEntry(uri, mRequestingPrincipal); + if (NS_FAILED(rv)) { + // Only warn here to ensure we fall through the request Cancel() + // and outer listener OnStartRequest() calls. + NS_WARNING("Failed to remove CORS preflight cache entry!"); + } + } + } + } + } + + aRequest->Cancel(NS_ERROR_DOM_BAD_URI); + mOuterListener->OnStartRequest(aRequest, aContext); + + return NS_ERROR_DOM_BAD_URI; + } + + return mOuterListener->OnStartRequest(aRequest, aContext); +} + +namespace { +class CheckOriginHeader final : public nsIHttpHeaderVisitor { + +public: + NS_DECL_ISUPPORTS + + CheckOriginHeader() + : mHeaderCount(0) + {} + + NS_IMETHOD + VisitHeader(const nsACString & aHeader, const nsACString & aValue) override + { + if (aHeader.EqualsLiteral("Access-Control-Allow-Origin")) { + mHeaderCount++; + } + + if (mHeaderCount > 1) { + return NS_ERROR_DOM_BAD_URI; + } + return NS_OK; + } + +private: + uint32_t mHeaderCount; + + ~CheckOriginHeader() + {} + +}; + +NS_IMPL_ISUPPORTS(CheckOriginHeader, nsIHttpHeaderVisitor) +} + +nsresult +nsCORSListenerProxy::CheckRequestApproved(nsIRequest* aRequest) +{ + // Check if this was actually a cross domain request + if (!mHasBeenCrossSite) { + return NS_OK; + } + + if (gDisableCORS) { + LogBlockedRequest(aRequest, "CORSDisabled", nullptr); + return NS_ERROR_DOM_BAD_URI; + } + + // Check if the request failed + nsresult status; + nsresult rv = aRequest->GetStatus(&status); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_SUCCESS(status, status); + + // Test that things worked on a HTTP level + nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aRequest); + if (!http) { + LogBlockedRequest(aRequest, "CORSRequestNotHttp", nullptr); + return NS_ERROR_DOM_BAD_URI; + } + + nsCOMPtr<nsIHttpChannelInternal> internal = do_QueryInterface(aRequest); + NS_ENSURE_STATE(internal); + bool responseSynthesized = false; + if (NS_SUCCEEDED(internal->GetResponseSynthesized(&responseSynthesized)) && + responseSynthesized) { + // For synthesized responses, we don't need to perform any checks. + // Note: This would be unsafe if we ever changed our behavior to allow + // service workers to intercept CORS preflights. + return NS_OK; + } + + // Check the Access-Control-Allow-Origin header + RefPtr<CheckOriginHeader> visitor = new CheckOriginHeader(); + nsAutoCString allowedOriginHeader; + + // check for duplicate headers + rv = http->VisitOriginalResponseHeaders(visitor); + if (NS_FAILED(rv)) { + LogBlockedRequest(aRequest, "CORSAllowOriginNotMatchingOrigin", nullptr); + return rv; + } + + rv = http->GetResponseHeader( + NS_LITERAL_CSTRING("Access-Control-Allow-Origin"), allowedOriginHeader); + if (NS_FAILED(rv)) { + LogBlockedRequest(aRequest, "CORSMissingAllowOrigin", nullptr); + return rv; + } + + // Bug 1210985 - Explicitly point out the error that the credential is + // not supported if the allowing origin is '*'. Note that this check + // has to be done before the condition + // + // >> if (mWithCredentials || !allowedOriginHeader.EqualsLiteral("*")) + // + // below since "if (A && B)" is included in "if (A || !B)". + // + if (mWithCredentials && allowedOriginHeader.EqualsLiteral("*")) { + LogBlockedRequest(aRequest, "CORSNotSupportingCredentials", nullptr); + return NS_ERROR_DOM_BAD_URI; + } + + if (mWithCredentials || !allowedOriginHeader.EqualsLiteral("*")) { + MOZ_ASSERT(!nsContentUtils::IsExpandedPrincipal(mOriginHeaderPrincipal)); + nsAutoCString origin; + nsContentUtils::GetASCIIOrigin(mOriginHeaderPrincipal, origin); + + if (!allowedOriginHeader.Equals(origin)) { + LogBlockedRequest(aRequest, "CORSAllowOriginNotMatchingOrigin", + NS_ConvertUTF8toUTF16(allowedOriginHeader).get()); + return NS_ERROR_DOM_BAD_URI; + } + } + + // Check Access-Control-Allow-Credentials header + if (mWithCredentials) { + nsAutoCString allowCredentialsHeader; + rv = http->GetResponseHeader( + NS_LITERAL_CSTRING("Access-Control-Allow-Credentials"), allowCredentialsHeader); + + if (!allowCredentialsHeader.EqualsLiteral("true")) { + LogBlockedRequest(aRequest, "CORSMissingAllowCredentials", nullptr); + return NS_ERROR_DOM_BAD_URI; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsCORSListenerProxy::OnStopRequest(nsIRequest* aRequest, + nsISupports* aContext, + nsresult aStatusCode) +{ + MOZ_ASSERT(mInited, "nsCORSListenerProxy has not been initialized properly"); + nsresult rv = mOuterListener->OnStopRequest(aRequest, aContext, aStatusCode); + mOuterListener = nullptr; + mOuterNotificationCallbacks = nullptr; + return rv; +} + +NS_IMETHODIMP +nsCORSListenerProxy::OnDataAvailable(nsIRequest* aRequest, + nsISupports* aContext, + nsIInputStream* aInputStream, + uint64_t aOffset, + uint32_t aCount) +{ + // NB: This can be called on any thread! But we're guaranteed that it is + // called between OnStartRequest and OnStopRequest, so we don't need to worry + // about races. + + MOZ_ASSERT(mInited, "nsCORSListenerProxy has not been initialized properly"); + if (!mRequestApproved) { + return NS_ERROR_DOM_BAD_URI; + } + return mOuterListener->OnDataAvailable(aRequest, aContext, aInputStream, + aOffset, aCount); +} + +void +nsCORSListenerProxy::SetInterceptController(nsINetworkInterceptController* aInterceptController) +{ + mInterceptController = aInterceptController; +} + +NS_IMETHODIMP +nsCORSListenerProxy::GetInterface(const nsIID & aIID, void **aResult) +{ + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + *aResult = static_cast<nsIChannelEventSink*>(this); + NS_ADDREF_THIS(); + + return NS_OK; + } + + if (aIID.Equals(NS_GET_IID(nsINetworkInterceptController)) && + mInterceptController) { + nsCOMPtr<nsINetworkInterceptController> copy(mInterceptController); + *aResult = copy.forget().take(); + + return NS_OK; + } + + return mOuterNotificationCallbacks ? + mOuterNotificationCallbacks->GetInterface(aIID, aResult) : + NS_ERROR_NO_INTERFACE; +} + +NS_IMETHODIMP +nsCORSListenerProxy::AsyncOnChannelRedirect(nsIChannel *aOldChannel, + nsIChannel *aNewChannel, + uint32_t aFlags, + nsIAsyncVerifyRedirectCallback *aCb) +{ + nsresult rv; + if (NS_IsInternalSameURIRedirect(aOldChannel, aNewChannel, aFlags) || + NS_IsHSTSUpgradeRedirect(aOldChannel, aNewChannel, aFlags)) { + // Internal redirects still need to be updated in order to maintain + // the correct headers. We use DataURIHandling::Allow, since unallowed + // data URIs should have been blocked before we got to the internal + // redirect. + rv = UpdateChannel(aNewChannel, DataURIHandling::Allow, + UpdateType::InternalOrHSTSRedirect); + if (NS_FAILED(rv)) { + NS_WARNING("nsCORSListenerProxy::AsyncOnChannelRedirect: " + "internal redirect UpdateChannel() returned failure"); + aOldChannel->Cancel(rv); + return rv; + } + } else { + // A real, external redirect. Perform CORS checking on new URL. + rv = CheckRequestApproved(aOldChannel); + if (NS_FAILED(rv)) { + nsCOMPtr<nsIURI> oldURI; + NS_GetFinalChannelURI(aOldChannel, getter_AddRefs(oldURI)); + if (oldURI) { + if (sPreflightCache) { + // OK to use mRequestingPrincipal since preflights never get + // redirected. + sPreflightCache->RemoveEntries(oldURI, mRequestingPrincipal); + } else { + nsCOMPtr<nsIHttpChannelChild> httpChannelChild = + do_QueryInterface(aOldChannel); + if (httpChannelChild) { + rv = httpChannelChild->RemoveCorsPreflightCacheEntry(oldURI, mRequestingPrincipal); + if (NS_FAILED(rv)) { + // Only warn here to ensure we call the channel Cancel() below + NS_WARNING("Failed to remove CORS preflight cache entry!"); + } + } + } + } + aOldChannel->Cancel(NS_ERROR_DOM_BAD_URI); + return NS_ERROR_DOM_BAD_URI; + } + + if (mHasBeenCrossSite) { + // Once we've been cross-site, cross-origin redirects reset our source + // origin. Note that we need to call GetChannelURIPrincipal() because + // we are looking for the principal that is actually being loaded and not + // the principal that initiated the load. + nsCOMPtr<nsIPrincipal> oldChannelPrincipal; + nsContentUtils::GetSecurityManager()-> + GetChannelURIPrincipal(aOldChannel, getter_AddRefs(oldChannelPrincipal)); + nsCOMPtr<nsIPrincipal> newChannelPrincipal; + nsContentUtils::GetSecurityManager()-> + GetChannelURIPrincipal(aNewChannel, getter_AddRefs(newChannelPrincipal)); + if (!oldChannelPrincipal || !newChannelPrincipal) { + rv = NS_ERROR_OUT_OF_MEMORY; + } + + if (NS_SUCCEEDED(rv)) { + bool equal; + rv = oldChannelPrincipal->Equals(newChannelPrincipal, &equal); + if (NS_SUCCEEDED(rv) && !equal) { + // Spec says to set our source origin to a unique origin. + mOriginHeaderPrincipal = + nsNullPrincipal::CreateWithInheritedAttributes(oldChannelPrincipal); + } + } + + if (NS_FAILED(rv)) { + aOldChannel->Cancel(rv); + return rv; + } + } + + rv = UpdateChannel(aNewChannel, DataURIHandling::Disallow, + UpdateType::Default); + if (NS_FAILED(rv)) { + NS_WARNING("nsCORSListenerProxy::AsyncOnChannelRedirect: " + "UpdateChannel() returned failure"); + aOldChannel->Cancel(rv); + return rv; + } + } + + nsCOMPtr<nsIChannelEventSink> outer = + do_GetInterface(mOuterNotificationCallbacks); + if (outer) { + return outer->AsyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCb); + } + + aCb->OnRedirectVerifyCallback(NS_OK); + + return NS_OK; +} + +NS_IMETHODIMP +nsCORSListenerProxy::CheckListenerChain() +{ + MOZ_ASSERT(NS_IsMainThread()); + + if (nsCOMPtr<nsIThreadRetargetableStreamListener> retargetableListener = + do_QueryInterface(mOuterListener)) { + return retargetableListener->CheckListenerChain(); + } + + return NS_ERROR_NO_INTERFACE; +} + +// Please note that the CSP directive 'upgrade-insecure-requests' relies +// on the promise that channels get updated from http: to https: before +// the channel fetches any data from the netwerk. Such channels should +// not be blocked by CORS and marked as cross origin requests. E.g.: +// toplevel page: https://www.example.com loads +// xhr: http://www.example.com/foo which gets updated to +// https://www.example.com/foo +// In such a case we should bail out of CORS and rely on the promise that +// nsHttpChannel::Connect() upgrades the request from http to https. +bool +CheckUpgradeInsecureRequestsPreventsCORS(nsIPrincipal* aRequestingPrincipal, + nsIChannel* aChannel) +{ + nsCOMPtr<nsIURI> channelURI; + nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(channelURI)); + NS_ENSURE_SUCCESS(rv, false); + bool isHttpScheme = false; + rv = channelURI->SchemeIs("http", &isHttpScheme); + NS_ENSURE_SUCCESS(rv, false); + + // upgrade insecure requests is only applicable to http requests + if (!isHttpScheme) { + return false; + } + + nsCOMPtr<nsIURI> principalURI; + rv = aRequestingPrincipal->GetURI(getter_AddRefs(principalURI)); + NS_ENSURE_SUCCESS(rv, false); + + // if the requestingPrincipal does not have a uri, there is nothing to do + if (!principalURI) { + return false; + } + + nsCOMPtr<nsIURI>originalURI; + rv = aChannel->GetOriginalURI(getter_AddRefs(originalURI)); + NS_ENSURE_SUCCESS(rv, false); + + nsAutoCString principalHost, channelHost, origChannelHost; + + // if we can not query a host from the uri, there is nothing to do + if (NS_FAILED(principalURI->GetAsciiHost(principalHost)) || + NS_FAILED(channelURI->GetAsciiHost(channelHost)) || + NS_FAILED(originalURI->GetAsciiHost(origChannelHost))) { + return false; + } + + // if the hosts do not match, there is nothing to do + if (!principalHost.EqualsIgnoreCase(channelHost.get())) { + return false; + } + + // also check that uri matches the one of the originalURI + if (!channelHost.EqualsIgnoreCase(origChannelHost.get())) { + return false; + } + + nsCOMPtr<nsILoadInfo> loadInfo; + rv = aChannel->GetLoadInfo(getter_AddRefs(loadInfo)); + NS_ENSURE_SUCCESS(rv, false); + + // lets see if the loadInfo indicates that the request will + // be upgraded before fetching any data from the netwerk. + return loadInfo->GetUpgradeInsecureRequests(); +} + + +nsresult +nsCORSListenerProxy::UpdateChannel(nsIChannel* aChannel, + DataURIHandling aAllowDataURI, + UpdateType aUpdateType) +{ + nsCOMPtr<nsIURI> uri, originalURI; + nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + rv = aChannel->GetOriginalURI(getter_AddRefs(originalURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->GetLoadInfo(); + + // exempt data URIs from the same origin check. + if (aAllowDataURI == DataURIHandling::Allow && originalURI == uri) { + bool dataScheme = false; + rv = uri->SchemeIs("data", &dataScheme); + NS_ENSURE_SUCCESS(rv, rv); + if (dataScheme) { + return NS_OK; + } + if (loadInfo && loadInfo->GetAboutBlankInherits() && + NS_IsAboutBlank(uri)) { + return NS_OK; + } + } + + // Set CORS attributes on channel so that intercepted requests get correct + // values. We have to do this here because the CheckMayLoad checks may lead + // to early return. We can't be sure this is an http channel though, so we + // can't return early on failure. + nsCOMPtr<nsIHttpChannelInternal> internal = do_QueryInterface(aChannel); + if (internal) { + rv = internal->SetCorsMode(nsIHttpChannelInternal::CORS_MODE_CORS); + NS_ENSURE_SUCCESS(rv, rv); + rv = internal->SetCorsIncludeCredentials(mWithCredentials); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Check that the uri is ok to load + rv = nsContentUtils::GetSecurityManager()-> + CheckLoadURIWithPrincipal(mRequestingPrincipal, uri, + nsIScriptSecurityManager::STANDARD); + NS_ENSURE_SUCCESS(rv, rv); + + if (originalURI != uri) { + rv = nsContentUtils::GetSecurityManager()-> + CheckLoadURIWithPrincipal(mRequestingPrincipal, originalURI, + nsIScriptSecurityManager::STANDARD); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (!mHasBeenCrossSite && + NS_SUCCEEDED(mRequestingPrincipal->CheckMayLoad(uri, false, false)) && + (originalURI == uri || + NS_SUCCEEDED(mRequestingPrincipal->CheckMayLoad(originalURI, + false, false)))) { + return NS_OK; + } + + // if the CSP directive 'upgrade-insecure-requests' is used then we should + // not incorrectly require CORS if the only difference of a subresource + // request and the main page is the scheme. + // e.g. toplevel page: https://www.example.com loads + // xhr: http://www.example.com/somefoo, + // then the xhr request will be upgraded to https before it fetches any data + // from the netwerk, hence we shouldn't require CORS in that specific case. + if (CheckUpgradeInsecureRequestsPreventsCORS(mRequestingPrincipal, aChannel)) { + return NS_OK; + } + + // Check if we need to do a preflight, and if so set one up. This must be + // called once we know that the request is going, or has gone, cross-origin. + rv = CheckPreflightNeeded(aChannel, aUpdateType); + NS_ENSURE_SUCCESS(rv, rv); + + // It's a cross site load + mHasBeenCrossSite = true; + + nsCString userpass; + uri->GetUserPass(userpass); + NS_ENSURE_TRUE(userpass.IsEmpty(), NS_ERROR_DOM_BAD_URI); + + // If we have an expanded principal here, we'll reject the CORS request, + // because we can't send a useful Origin header which is required for CORS. + if (nsContentUtils::IsExpandedPrincipal(mOriginHeaderPrincipal)) { + return NS_ERROR_DOM_BAD_URI; + } + + // Add the Origin header + nsAutoCString origin; + rv = nsContentUtils::GetASCIIOrigin(mOriginHeaderPrincipal, origin); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aChannel); + NS_ENSURE_TRUE(http, NS_ERROR_FAILURE); + + rv = http->SetRequestHeader(NS_LITERAL_CSTRING("Origin"), origin, false); + NS_ENSURE_SUCCESS(rv, rv); + + // Make cookie-less if needed. We don't need to do anything here if the + // channel was opened with AsyncOpen2, since then AsyncOpen2 will take + // care of the cookie policy for us. + if (!mWithCredentials && + (!loadInfo || !loadInfo->GetEnforceSecurity())) { + nsLoadFlags flags; + rv = http->GetLoadFlags(&flags); + NS_ENSURE_SUCCESS(rv, rv); + + flags |= nsIRequest::LOAD_ANONYMOUS; + rv = http->SetLoadFlags(flags); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult +nsCORSListenerProxy::CheckPreflightNeeded(nsIChannel* aChannel, UpdateType aUpdateType) +{ + // If this caller isn't using AsyncOpen2, or if this *is* a preflight channel, + // then we shouldn't initiate preflight for this channel. + nsCOMPtr<nsILoadInfo> loadInfo = aChannel->GetLoadInfo(); + if (!loadInfo || + loadInfo->GetSecurityMode() != + nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS || + loadInfo->GetIsPreflight()) { + return NS_OK; + } + + bool doPreflight = loadInfo->GetForcePreflight(); + + nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aChannel); + NS_ENSURE_TRUE(http, NS_ERROR_DOM_BAD_URI); + nsAutoCString method; + http->GetRequestMethod(method); + if (!method.LowerCaseEqualsLiteral("get") && + !method.LowerCaseEqualsLiteral("post") && + !method.LowerCaseEqualsLiteral("head")) { + doPreflight = true; + } + + // Avoid copying the array here + const nsTArray<nsCString>& loadInfoHeaders = loadInfo->CorsUnsafeHeaders(); + if (!loadInfoHeaders.IsEmpty()) { + doPreflight = true; + } + + // Add Content-Type header if needed + nsTArray<nsCString> headers; + nsAutoCString contentTypeHeader; + nsresult rv = http->GetRequestHeader(NS_LITERAL_CSTRING("Content-Type"), + contentTypeHeader); + // GetRequestHeader return an error if the header is not set. Don't add + // "content-type" to the list if that's the case. + if (NS_SUCCEEDED(rv) && + !nsContentUtils::IsAllowedNonCorsContentType(contentTypeHeader) && + !loadInfoHeaders.Contains(NS_LITERAL_CSTRING("content-type"), + nsCaseInsensitiveCStringArrayComparator())) { + headers.AppendElements(loadInfoHeaders); + headers.AppendElement(NS_LITERAL_CSTRING("content-type")); + doPreflight = true; + } + + if (!doPreflight) { + return NS_OK; + } + + // A preflight is needed. But if we've already been cross-site, then + // we already did a preflight when that happened, and so we're not allowed + // to do another preflight again. + if (aUpdateType != UpdateType::InternalOrHSTSRedirect) { + NS_ENSURE_FALSE(mHasBeenCrossSite, NS_ERROR_DOM_BAD_URI); + } + + nsCOMPtr<nsIHttpChannelInternal> internal = do_QueryInterface(http); + NS_ENSURE_TRUE(internal, NS_ERROR_DOM_BAD_URI); + + internal->SetCorsPreflightParameters( + headers.IsEmpty() ? loadInfoHeaders : headers); + + return NS_OK; +} + +////////////////////////////////////////////////////////////////////////// +// Preflight proxy + +// Class used as streamlistener and notification callback when +// doing the initial OPTIONS request for a CORS check +class nsCORSPreflightListener final : public nsIStreamListener, + public nsIInterfaceRequestor, + public nsIChannelEventSink +{ +public: + nsCORSPreflightListener(nsIPrincipal* aReferrerPrincipal, + nsICorsPreflightCallback* aCallback, + nsILoadContext* aLoadContext, + bool aWithCredentials, + const nsCString& aPreflightMethod, + const nsTArray<nsCString>& aPreflightHeaders) + : mPreflightMethod(aPreflightMethod), + mPreflightHeaders(aPreflightHeaders), + mReferrerPrincipal(aReferrerPrincipal), + mCallback(aCallback), + mLoadContext(aLoadContext), + mWithCredentials(aWithCredentials) + { + } + + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + + nsresult CheckPreflightRequestApproved(nsIRequest* aRequest); + +private: + ~nsCORSPreflightListener() {} + + void AddResultToCache(nsIRequest* aRequest); + + nsCString mPreflightMethod; + nsTArray<nsCString> mPreflightHeaders; + nsCOMPtr<nsIPrincipal> mReferrerPrincipal; + nsCOMPtr<nsICorsPreflightCallback> mCallback; + nsCOMPtr<nsILoadContext> mLoadContext; + bool mWithCredentials; +}; + +NS_IMPL_ISUPPORTS(nsCORSPreflightListener, nsIStreamListener, + nsIRequestObserver, nsIInterfaceRequestor, + nsIChannelEventSink) + +void +nsCORSPreflightListener::AddResultToCache(nsIRequest *aRequest) +{ + nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aRequest); + NS_ASSERTION(http, "Request was not http"); + + // The "Access-Control-Max-Age" header should return an age in seconds. + nsAutoCString headerVal; + http->GetResponseHeader(NS_LITERAL_CSTRING("Access-Control-Max-Age"), + headerVal); + if (headerVal.IsEmpty()) { + return; + } + + // Sanitize the string. We only allow 'delta-seconds' as specified by + // http://dev.w3.org/2006/waf/access-control (digits 0-9 with no leading or + // trailing non-whitespace characters). + uint32_t age = 0; + nsCSubstring::const_char_iterator iter, end; + headerVal.BeginReading(iter); + headerVal.EndReading(end); + while (iter != end) { + if (*iter < '0' || *iter > '9') { + return; + } + age = age * 10 + (*iter - '0'); + // Cap at 24 hours. This also avoids overflow + age = std::min(age, 86400U); + ++iter; + } + + if (!age || !EnsurePreflightCache()) { + return; + } + + + // String seems fine, go ahead and cache. + // Note that we have already checked that these headers follow the correct + // syntax. + + nsCOMPtr<nsIURI> uri; + NS_GetFinalChannelURI(http, getter_AddRefs(uri)); + + TimeStamp expirationTime = TimeStamp::NowLoRes() + TimeDuration::FromSeconds(age); + + nsPreflightCache::CacheEntry* entry = + sPreflightCache->GetEntry(uri, mReferrerPrincipal, mWithCredentials, + true); + if (!entry) { + return; + } + + // The "Access-Control-Allow-Methods" header contains a comma separated + // list of method names. + http->GetResponseHeader(NS_LITERAL_CSTRING("Access-Control-Allow-Methods"), + headerVal); + + nsCCharSeparatedTokenizer methods(headerVal, ','); + while(methods.hasMoreTokens()) { + const nsDependentCSubstring& method = methods.nextToken(); + if (method.IsEmpty()) { + continue; + } + uint32_t i; + for (i = 0; i < entry->mMethods.Length(); ++i) { + if (entry->mMethods[i].token.Equals(method)) { + entry->mMethods[i].expirationTime = expirationTime; + break; + } + } + if (i == entry->mMethods.Length()) { + nsPreflightCache::TokenTime* newMethod = + entry->mMethods.AppendElement(); + if (!newMethod) { + return; + } + + newMethod->token = method; + newMethod->expirationTime = expirationTime; + } + } + + // The "Access-Control-Allow-Headers" header contains a comma separated + // list of method names. + http->GetResponseHeader(NS_LITERAL_CSTRING("Access-Control-Allow-Headers"), + headerVal); + + nsCCharSeparatedTokenizer headers(headerVal, ','); + while(headers.hasMoreTokens()) { + const nsDependentCSubstring& header = headers.nextToken(); + if (header.IsEmpty()) { + continue; + } + uint32_t i; + for (i = 0; i < entry->mHeaders.Length(); ++i) { + if (entry->mHeaders[i].token.Equals(header)) { + entry->mHeaders[i].expirationTime = expirationTime; + break; + } + } + if (i == entry->mHeaders.Length()) { + nsPreflightCache::TokenTime* newHeader = + entry->mHeaders.AppendElement(); + if (!newHeader) { + return; + } + + newHeader->token = header; + newHeader->expirationTime = expirationTime; + } + } +} + +NS_IMETHODIMP +nsCORSPreflightListener::OnStartRequest(nsIRequest *aRequest, + nsISupports *aContext) +{ +#ifdef DEBUG + { + nsCOMPtr<nsIHttpChannelInternal> internal = do_QueryInterface(aRequest); + bool responseSynthesized = false; + if (internal && + NS_SUCCEEDED(internal->GetResponseSynthesized(&responseSynthesized))) { + // For synthesized responses, we don't need to perform any checks. + // This would be unsafe if we ever changed our behavior to allow + // service workers to intercept CORS preflights. + MOZ_ASSERT(!responseSynthesized); + } + } +#endif + + nsresult rv = CheckPreflightRequestApproved(aRequest); + + if (NS_SUCCEEDED(rv)) { + // Everything worked, try to cache and then fire off the actual request. + AddResultToCache(aRequest); + + mCallback->OnPreflightSucceeded(); + } else { + mCallback->OnPreflightFailed(rv); + } + + return rv; +} + +NS_IMETHODIMP +nsCORSPreflightListener::OnStopRequest(nsIRequest *aRequest, + nsISupports *aContext, + nsresult aStatus) +{ + mCallback = nullptr; + return NS_OK; +} + +/** nsIStreamListener methods **/ + +NS_IMETHODIMP +nsCORSPreflightListener::OnDataAvailable(nsIRequest *aRequest, + nsISupports *ctxt, + nsIInputStream *inStr, + uint64_t sourceOffset, + uint32_t count) +{ + uint32_t totalRead; + return inStr->ReadSegments(NS_DiscardSegment, nullptr, count, &totalRead); +} + +NS_IMETHODIMP +nsCORSPreflightListener::AsyncOnChannelRedirect(nsIChannel *aOldChannel, + nsIChannel *aNewChannel, + uint32_t aFlags, + nsIAsyncVerifyRedirectCallback *callback) +{ + // Only internal redirects allowed for now. + if (!NS_IsInternalSameURIRedirect(aOldChannel, aNewChannel, aFlags) && + !NS_IsHSTSUpgradeRedirect(aOldChannel, aNewChannel, aFlags)) + return NS_ERROR_DOM_BAD_URI; + + callback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +nsresult +nsCORSPreflightListener::CheckPreflightRequestApproved(nsIRequest* aRequest) +{ + nsresult status; + nsresult rv = aRequest->GetStatus(&status); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_SUCCESS(status, status); + + // Test that things worked on a HTTP level + nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aRequest); + nsCOMPtr<nsIHttpChannelInternal> internal = do_QueryInterface(aRequest); + NS_ENSURE_STATE(internal); + + bool succeedded; + rv = http->GetRequestSucceeded(&succeedded); + if (NS_FAILED(rv) || !succeedded) { + LogBlockedRequest(aRequest, "CORSPreflightDidNotSucceed", nullptr); + return NS_ERROR_DOM_BAD_URI; + } + + nsAutoCString headerVal; + // The "Access-Control-Allow-Methods" header contains a comma separated + // list of method names. + http->GetResponseHeader(NS_LITERAL_CSTRING("Access-Control-Allow-Methods"), + headerVal); + bool foundMethod = mPreflightMethod.EqualsLiteral("GET") || + mPreflightMethod.EqualsLiteral("HEAD") || + mPreflightMethod.EqualsLiteral("POST"); + nsCCharSeparatedTokenizer methodTokens(headerVal, ','); + while(methodTokens.hasMoreTokens()) { + const nsDependentCSubstring& method = methodTokens.nextToken(); + if (method.IsEmpty()) { + continue; + } + if (!NS_IsValidHTTPToken(method)) { + LogBlockedRequest(aRequest, "CORSInvalidAllowMethod", + NS_ConvertUTF8toUTF16(method).get()); + return NS_ERROR_DOM_BAD_URI; + } + foundMethod |= mPreflightMethod.Equals(method); + } + if (!foundMethod) { + LogBlockedRequest(aRequest, "CORSMethodNotFound", nullptr); + return NS_ERROR_DOM_BAD_URI; + } + + // The "Access-Control-Allow-Headers" header contains a comma separated + // list of header names. + http->GetResponseHeader(NS_LITERAL_CSTRING("Access-Control-Allow-Headers"), + headerVal); + nsTArray<nsCString> headers; + nsCCharSeparatedTokenizer headerTokens(headerVal, ','); + while(headerTokens.hasMoreTokens()) { + const nsDependentCSubstring& header = headerTokens.nextToken(); + if (header.IsEmpty()) { + continue; + } + if (!NS_IsValidHTTPToken(header)) { + LogBlockedRequest(aRequest, "CORSInvalidAllowHeader", + NS_ConvertUTF8toUTF16(header).get()); + return NS_ERROR_DOM_BAD_URI; + } + headers.AppendElement(header); + } + for (uint32_t i = 0; i < mPreflightHeaders.Length(); ++i) { + if (!headers.Contains(mPreflightHeaders[i], + nsCaseInsensitiveCStringArrayComparator())) { + LogBlockedRequest(aRequest, "CORSMissingAllowHeaderFromPreflight", + NS_ConvertUTF8toUTF16(mPreflightHeaders[i]).get()); + return NS_ERROR_DOM_BAD_URI; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsCORSPreflightListener::GetInterface(const nsIID & aIID, void **aResult) +{ + if (aIID.Equals(NS_GET_IID(nsILoadContext)) && mLoadContext) { + nsCOMPtr<nsILoadContext> copy = mLoadContext; + copy.forget(aResult); + return NS_OK; + } + + return QueryInterface(aIID, aResult); +} + +void +nsCORSListenerProxy::RemoveFromCorsPreflightCache(nsIURI* aURI, + nsIPrincipal* aRequestingPrincipal) +{ + MOZ_ASSERT(XRE_IsParentProcess()); + if (sPreflightCache) { + sPreflightCache->RemoveEntries(aURI, aRequestingPrincipal); + } +} + +nsresult +nsCORSListenerProxy::StartCORSPreflight(nsIChannel* aRequestChannel, + nsICorsPreflightCallback* aCallback, + nsTArray<nsCString>& aUnsafeHeaders, + nsIChannel** aPreflightChannel) +{ + *aPreflightChannel = nullptr; + + if (gDisableCORS) { + LogBlockedRequest(aRequestChannel, "CORSDisabled", nullptr); + return NS_ERROR_DOM_BAD_URI; + } + + nsAutoCString method; + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(aRequestChannel)); + NS_ENSURE_TRUE(httpChannel, NS_ERROR_UNEXPECTED); + httpChannel->GetRequestMethod(method); + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_GetFinalChannelURI(aRequestChannel, getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsILoadInfo> originalLoadInfo = aRequestChannel->GetLoadInfo(); + MOZ_ASSERT(originalLoadInfo, "can not perform CORS preflight without a loadInfo"); + if (!originalLoadInfo) { + return NS_ERROR_FAILURE; + } + + MOZ_ASSERT(originalLoadInfo->GetSecurityMode() == + nsILoadInfo::SEC_REQUIRE_CORS_DATA_INHERITS, + "how did we end up here?"); + + nsCOMPtr<nsIPrincipal> principal = originalLoadInfo->LoadingPrincipal(); + MOZ_ASSERT(principal && + originalLoadInfo->GetExternalContentPolicyType() != + nsIContentPolicy::TYPE_DOCUMENT, + "Should not do CORS loads for top-level loads, so a loadingPrincipal should always exist."); + bool withCredentials = originalLoadInfo->GetCookiePolicy() == + nsILoadInfo::SEC_COOKIES_INCLUDE; + + nsPreflightCache::CacheEntry* entry = + sPreflightCache ? + sPreflightCache->GetEntry(uri, principal, withCredentials, false) : + nullptr; + + if (entry && entry->CheckRequest(method, aUnsafeHeaders)) { + aCallback->OnPreflightSucceeded(); + return NS_OK; + } + + // Either it wasn't cached or the cached result has expired. Build a + // channel for the OPTIONS request. + + nsCOMPtr<nsILoadInfo> loadInfo = static_cast<mozilla::LoadInfo*> + (originalLoadInfo.get())->CloneForNewRequest(); + static_cast<mozilla::LoadInfo*>(loadInfo.get())->SetIsPreflight(); + + nsCOMPtr<nsILoadGroup> loadGroup; + rv = aRequestChannel->GetLoadGroup(getter_AddRefs(loadGroup)); + NS_ENSURE_SUCCESS(rv, rv); + + // We want to give the preflight channel's notification callbacks the same + // load context as the original channel's notification callbacks had. We + // don't worry about a load context provided via the loadgroup here, since + // they have the same loadgroup. + nsCOMPtr<nsIInterfaceRequestor> callbacks; + rv = aRequestChannel->GetNotificationCallbacks(getter_AddRefs(callbacks)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsILoadContext> loadContext = do_GetInterface(callbacks); + + nsLoadFlags loadFlags; + rv = aRequestChannel->GetLoadFlags(&loadFlags); + NS_ENSURE_SUCCESS(rv, rv); + + // Preflight requests should never be intercepted by service workers and + // are always anonymous. + // NOTE: We ignore CORS checks on synthesized responses (see the CORS + // preflights, then we need to extend the GetResponseSynthesized() check in + // nsCORSListenerProxy::CheckRequestApproved()). If we change our behavior + // here and allow service workers to intercept CORS preflights, then that + // check won't be safe any more. + loadFlags |= nsIChannel::LOAD_BYPASS_SERVICE_WORKER | + nsIRequest::LOAD_ANONYMOUS; + + nsCOMPtr<nsIChannel> preflightChannel; + rv = NS_NewChannelInternal(getter_AddRefs(preflightChannel), + uri, + loadInfo, + loadGroup, + nullptr, // aCallbacks + loadFlags); + NS_ENSURE_SUCCESS(rv, rv); + + // Set method and headers + nsCOMPtr<nsIHttpChannel> preHttp = do_QueryInterface(preflightChannel); + NS_ASSERTION(preHttp, "Failed to QI to nsIHttpChannel!"); + + rv = preHttp->SetRequestMethod(NS_LITERAL_CSTRING("OPTIONS")); + NS_ENSURE_SUCCESS(rv, rv); + + rv = preHttp-> + SetRequestHeader(NS_LITERAL_CSTRING("Access-Control-Request-Method"), + method, false); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray<nsCString> preflightHeaders; + if (!aUnsafeHeaders.IsEmpty()) { + for (uint32_t i = 0; i < aUnsafeHeaders.Length(); ++i) { + preflightHeaders.AppendElement(); + ToLowerCase(aUnsafeHeaders[i], preflightHeaders[i]); + } + preflightHeaders.Sort(); + nsAutoCString headers; + for (uint32_t i = 0; i < preflightHeaders.Length(); ++i) { + if (i != 0) { + headers += ','; + } + headers += preflightHeaders[i]; + } + rv = preHttp-> + SetRequestHeader(NS_LITERAL_CSTRING("Access-Control-Request-Headers"), + headers, false); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Set up listener which will start the original channel + RefPtr<nsCORSPreflightListener> preflightListener = + new nsCORSPreflightListener(principal, aCallback, loadContext, + withCredentials, method, preflightHeaders); + + rv = preflightChannel->SetNotificationCallbacks(preflightListener); + NS_ENSURE_SUCCESS(rv, rv); + + // Start preflight + rv = preflightChannel->AsyncOpen2(preflightListener); + NS_ENSURE_SUCCESS(rv, rv); + + // Return newly created preflight channel + preflightChannel.forget(aPreflightChannel); + + return NS_OK; +} |