diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /dom/fetch/FetchDriver.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/fetch/FetchDriver.cpp')
-rw-r--r-- | dom/fetch/FetchDriver.cpp | 1755 |
1 files changed, 1755 insertions, 0 deletions
diff --git a/dom/fetch/FetchDriver.cpp b/dom/fetch/FetchDriver.cpp new file mode 100644 index 0000000000..8c527ffeae --- /dev/null +++ b/dom/fetch/FetchDriver.cpp @@ -0,0 +1,1755 @@ +/* -*- 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 "js/Value.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/TaskQueue.h" +#include "mozilla/dom/FetchDriver.h" + +#include "mozilla/dom/ReferrerInfo.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "mozilla/dom/Document.h" +#include "nsICookieJarSettings.h" +#include "nsIFile.h" +#include "nsIInputStream.h" +#include "nsIInterceptionInfo.h" +#include "nsIOutputStream.h" +#include "nsIFileChannel.h" +#include "nsIHttpChannel.h" +#include "nsIHttpChannelInternal.h" +#include "nsISupportsPriority.h" +#include "nsIThreadRetargetableRequest.h" +#include "nsIUploadChannel2.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIPipe.h" +#include "nsIRedirectHistoryEntry.h" + +#include "nsContentPolicyUtils.h" +#include "nsDataHandler.h" +#include "nsNetUtil.h" +#include "nsPrintfCString.h" +#include "nsProxyRelease.h" +#include "nsStreamUtils.h" +#include "nsStringStream.h" +#include "nsHttpChannel.h" + +#include "mozilla/dom/BlobURLProtocolHandler.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/PerformanceStorage.h" +#include "mozilla/dom/PerformanceTiming.h" +#include "mozilla/dom/ServiceWorkerInterceptController.h" +#include "mozilla/dom/UserActivation.h" +#include "mozilla/dom/WorkerCommon.h" +#include "mozilla/PreloaderBase.h" +#include "mozilla/net/InterceptionInfo.h" +#include "mozilla/net/NeckoChannelParams.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/StaticPrefs_javascript.h" +#include "mozilla/Unused.h" + +#include "Fetch.h" +#include "FetchUtil.h" +#include "InternalRequest.h" +#include "InternalResponse.h" + +namespace mozilla::dom { + +namespace { + +void GetBlobURISpecFromChannel(nsIRequest* aRequest, nsCString& aBlobURISpec) { + MOZ_ASSERT(aRequest); + + aBlobURISpec.SetIsVoid(true); + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + if (!channel) { + return; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = NS_GetFinalChannelURI(channel, getter_AddRefs(uri)); + if (NS_FAILED(rv)) { + return; + } + + if (!dom::IsBlobURI(uri)) { + return; + } + + uri->GetSpec(aBlobURISpec); +} + +bool ShouldCheckSRI(const InternalRequest& aRequest, + const InternalResponse& aResponse) { + return !aRequest.GetIntegrity().IsEmpty() && + aResponse.Type() != ResponseType::Error; +} + +} // anonymous namespace + +//----------------------------------------------------------------------------- +// AlternativeDataStreamListener +//----------------------------------------------------------------------------- +class AlternativeDataStreamListener final + : public nsIStreamListener, + public nsIThreadRetargetableStreamListener { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + + // The status of AlternativeDataStreamListener + // LOADING: is the initial status, loading the alternative data + // COMPLETED: Alternative data loading is completed + // CANCELED: Alternative data loading is canceled, this would make + // AlternativeDataStreamListener ignore all channel callbacks + // FALLBACK: fallback the channel callbacks to FetchDriver + // Depends on different situaions, the status transition could be followings + // 1. LOADING->COMPLETED + // This is the normal status transition for alternative data loading + // + // 2. LOADING->CANCELED + // LOADING->COMPLETED->CANCELED + // Alternative data loading could be canceled when cacheId from alternative + // data channel does not match with from main data channel(The cacheID + // checking is in FetchDriver::OnStartRequest). + // Notice the alternative data loading could finish before the cacheID + // checking, so the statust transition could be + // LOADING->COMPLETED->CANCELED + // + // 3. LOADING->FALLBACK + // For the case that alternative data loading could not be initialized, + // i.e. alternative data does not exist or no preferred alternative data + // type is requested. Once the status becomes FALLBACK, + // AlternativeDataStreamListener transits the channel callback request to + // FetchDriver, and the status should not go back to LOADING, COMPLETED, or + // CANCELED anymore. + enum eStatus { LOADING = 0, COMPLETED, CANCELED, FALLBACK }; + + AlternativeDataStreamListener(FetchDriver* aFetchDriver, nsIChannel* aChannel, + const nsACString& aAlternativeDataType); + eStatus Status(); + void Cancel(); + uint64_t GetAlternativeDataCacheEntryId(); + const nsACString& GetAlternativeDataType() const; + already_AddRefed<nsICacheInfoChannel> GetCacheInfoChannel(); + already_AddRefed<nsIInputStream> GetAlternativeInputStream(); + + private: + ~AlternativeDataStreamListener() = default; + + // This creates a strong reference cycle with FetchDriver and its + // mAltDataListener. We need to clear at least one reference of them once the + // data loading finishes. + RefPtr<FetchDriver> mFetchDriver; + nsCString mAlternativeDataType; + nsCOMPtr<nsIInputStream> mPipeAlternativeInputStream; + nsCOMPtr<nsIOutputStream> mPipeAlternativeOutputStream; + uint64_t mAlternativeDataCacheEntryId; + nsCOMPtr<nsICacheInfoChannel> mCacheInfoChannel; + nsCOMPtr<nsIChannel> mChannel; + Atomic<eStatus> mStatus; +}; + +NS_IMPL_ISUPPORTS(AlternativeDataStreamListener, nsIStreamListener, + nsIThreadRetargetableStreamListener) + +AlternativeDataStreamListener::AlternativeDataStreamListener( + FetchDriver* aFetchDriver, nsIChannel* aChannel, + const nsACString& aAlternativeDataType) + : mFetchDriver(aFetchDriver), + mAlternativeDataType(aAlternativeDataType), + mAlternativeDataCacheEntryId(0), + mChannel(aChannel), + mStatus(AlternativeDataStreamListener::LOADING) { + MOZ_DIAGNOSTIC_ASSERT(mFetchDriver); + MOZ_DIAGNOSTIC_ASSERT(mChannel); +} + +AlternativeDataStreamListener::eStatus AlternativeDataStreamListener::Status() { + return mStatus; +} + +void AlternativeDataStreamListener::Cancel() { + mAlternativeDataCacheEntryId = 0; + mCacheInfoChannel = nullptr; + mPipeAlternativeOutputStream = nullptr; + mPipeAlternativeInputStream = nullptr; + if (mChannel && mStatus != AlternativeDataStreamListener::FALLBACK) { + // if mStatus is fallback, we need to keep channel to forward request back + // to FetchDriver + mChannel->CancelWithReason(NS_BINDING_ABORTED, + "AlternativeDataStreamListener::Cancel"_ns); + mChannel = nullptr; + } + mStatus = AlternativeDataStreamListener::CANCELED; +} + +uint64_t AlternativeDataStreamListener::GetAlternativeDataCacheEntryId() { + return mAlternativeDataCacheEntryId; +} + +const nsACString& AlternativeDataStreamListener::GetAlternativeDataType() + const { + return mAlternativeDataType; +} + +already_AddRefed<nsIInputStream> +AlternativeDataStreamListener::GetAlternativeInputStream() { + nsCOMPtr<nsIInputStream> inputStream = mPipeAlternativeInputStream; + return inputStream.forget(); +} + +already_AddRefed<nsICacheInfoChannel> +AlternativeDataStreamListener::GetCacheInfoChannel() { + nsCOMPtr<nsICacheInfoChannel> channel = mCacheInfoChannel; + return channel.forget(); +} + +NS_IMETHODIMP +AlternativeDataStreamListener::OnStartRequest(nsIRequest* aRequest) { + AssertIsOnMainThread(); + MOZ_ASSERT(!mAlternativeDataType.IsEmpty()); + // Checking the alternative data type is the same between we asked and the + // saved in the channel. + nsAutoCString alternativeDataType; + nsCOMPtr<nsICacheInfoChannel> cic = do_QueryInterface(aRequest); + mStatus = AlternativeDataStreamListener::LOADING; + if (cic && NS_SUCCEEDED(cic->GetAlternativeDataType(alternativeDataType)) && + mAlternativeDataType.Equals(alternativeDataType) && + NS_SUCCEEDED(cic->GetCacheEntryId(&mAlternativeDataCacheEntryId))) { + MOZ_DIAGNOSTIC_ASSERT(!mPipeAlternativeInputStream); + MOZ_DIAGNOSTIC_ASSERT(!mPipeAlternativeOutputStream); + NS_NewPipe(getter_AddRefs(mPipeAlternativeInputStream), + getter_AddRefs(mPipeAlternativeOutputStream), + 0 /* default segment size */, UINT32_MAX /* infinite pipe */, + true /* non-blocking input, otherwise you deadlock */, + false /* blocking output, since the pipe is 'in'finite */); + + MOZ_DIAGNOSTIC_ASSERT(!mCacheInfoChannel); + mCacheInfoChannel = cic; + + // call FetchDriver::HttpFetch to load main body + MOZ_ASSERT(mFetchDriver); + return mFetchDriver->HttpFetch(); + } + // Needn't load alternative data, since alternative data does not exist. + // Set status to FALLBACK to reuse the opened channel to load main body, + // then call FetchDriver::OnStartRequest to continue the work. Unfortunately + // can't change the stream listener to mFetchDriver, need to keep + // AlternativeDataStreamListener alive to redirect OnDataAvailable and + // OnStopRequest to mFetchDriver. + MOZ_ASSERT(alternativeDataType.IsEmpty()); + mStatus = AlternativeDataStreamListener::FALLBACK; + mAlternativeDataCacheEntryId = 0; + MOZ_ASSERT(mFetchDriver); + return mFetchDriver->OnStartRequest(aRequest); +} + +NS_IMETHODIMP +AlternativeDataStreamListener::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInputStream, + uint64_t aOffset, + uint32_t aCount) { + if (mStatus == AlternativeDataStreamListener::LOADING) { + MOZ_ASSERT(mPipeAlternativeOutputStream); + uint32_t read = 0; + return aInputStream->ReadSegments( + NS_CopySegmentToStream, mPipeAlternativeOutputStream, aCount, &read); + } + if (mStatus == AlternativeDataStreamListener::FALLBACK) { + MOZ_ASSERT(mFetchDriver); + return mFetchDriver->OnDataAvailable(aRequest, aInputStream, aOffset, + aCount); + } + return NS_OK; +} + +NS_IMETHODIMP +AlternativeDataStreamListener::OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) { + AssertIsOnMainThread(); + + // Alternative data loading is going to finish, breaking the reference cycle + // here by taking the ownership to a loacl variable. + RefPtr<FetchDriver> fetchDriver = std::move(mFetchDriver); + + if (mStatus == AlternativeDataStreamListener::CANCELED) { + // do nothing + return NS_OK; + } + + if (mStatus == AlternativeDataStreamListener::FALLBACK) { + MOZ_ASSERT(fetchDriver); + return fetchDriver->OnStopRequest(aRequest, aStatusCode); + } + + MOZ_DIAGNOSTIC_ASSERT(mStatus == AlternativeDataStreamListener::LOADING); + + MOZ_ASSERT(!mAlternativeDataType.IsEmpty() && mPipeAlternativeOutputStream && + mPipeAlternativeInputStream); + + mPipeAlternativeOutputStream->Close(); + mPipeAlternativeOutputStream = nullptr; + + // Cleanup the states for alternative data if needed. + if (NS_FAILED(aStatusCode)) { + mAlternativeDataCacheEntryId = 0; + mCacheInfoChannel = nullptr; + mPipeAlternativeInputStream = nullptr; + } + mStatus = AlternativeDataStreamListener::COMPLETED; + // alternative data loading finish, call FetchDriver::FinishOnStopRequest to + // continue the final step for the case FetchDriver::OnStopRequest is called + // earlier than AlternativeDataStreamListener::OnStopRequest + MOZ_ASSERT(fetchDriver); + fetchDriver->FinishOnStopRequest(this); + return NS_OK; +} + +NS_IMETHODIMP +AlternativeDataStreamListener::CheckListenerChain() { return NS_OK; } +//----------------------------------------------------------------------------- +// FetchDriver +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(FetchDriver, nsIStreamListener, nsIChannelEventSink, + nsIInterfaceRequestor, nsIThreadRetargetableStreamListener, + nsINetworkInterceptController) + +FetchDriver::FetchDriver(SafeRefPtr<InternalRequest> aRequest, + nsIPrincipal* aPrincipal, nsILoadGroup* aLoadGroup, + nsIEventTarget* aMainThreadEventTarget, + nsICookieJarSettings* aCookieJarSettings, + PerformanceStorage* aPerformanceStorage, + bool aIsTrackingFetch) + : mPrincipal(aPrincipal), + mLoadGroup(aLoadGroup), + mRequest(std::move(aRequest)), + mMainThreadEventTarget(aMainThreadEventTarget), + mCookieJarSettings(aCookieJarSettings), + mPerformanceStorage(aPerformanceStorage), + mNeedToObserveOnDataAvailable(false), + mIsTrackingFetch(aIsTrackingFetch), + mOnStopRequestCalled(false) +#ifdef DEBUG + , + mResponseAvailableCalled(false), + mFetchCalled(false) +#endif +{ + AssertIsOnMainThread(); + + MOZ_ASSERT(mRequest); + MOZ_ASSERT(aPrincipal); + MOZ_ASSERT(aMainThreadEventTarget); +} + +FetchDriver::~FetchDriver() { + AssertIsOnMainThread(); + + // We assert this since even on failures, we should call + // FailWithNetworkError(). + MOZ_ASSERT(mResponseAvailableCalled); + if (mObserver) { + mObserver = nullptr; + } +} + +already_AddRefed<PreloaderBase> FetchDriver::FindPreload(nsIURI* aURI) { + // Decide if we allow reuse of an existing <link rel=preload as=fetch> + // response for this request. First examine this fetch requets itself if it + // is 'pure' enough to use the response and then try to find a preload. + + if (!mDocument) { + // Preloads are mapped on the document, no document, no preload. + return nullptr; + } + CORSMode cors; + switch (mRequest->Mode()) { + case RequestMode::No_cors: + cors = CORSMode::CORS_NONE; + break; + case RequestMode::Cors: + cors = mRequest->GetCredentialsMode() == RequestCredentials::Include + ? CORSMode::CORS_USE_CREDENTIALS + : CORSMode::CORS_ANONYMOUS; + break; + default: + // Can't be satisfied by a preload because preload cannot define any of + // remaining modes. + return nullptr; + } + if (!mRequest->Headers()->HasOnlySimpleHeaders()) { + // Preload can't set any headers. + return nullptr; + } + if (!mRequest->GetIntegrity().IsEmpty()) { + // There is currently no support for SRI checking in the fetch preloader. + return nullptr; + } + if (mRequest->GetCacheMode() != RequestCache::Default) { + // Preload can only go with the default caching mode. + return nullptr; + } + if (mRequest->SkipServiceWorker()) { + // Preload can't be forbidden interception. + return nullptr; + } + if (mRequest->GetRedirectMode() != RequestRedirect::Follow) { + // Preload always follows redirects. + return nullptr; + } + nsAutoCString method; + mRequest->GetMethod(method); + if (!method.EqualsLiteral("GET")) { + // Preload can only do GET, this also eliminates the case we do upload, so + // no need to check if the request has any body to send out. + return nullptr; + } + + // OK, this request can be satisfied by a preloaded response, try to find one. + + auto preloadKey = PreloadHashKey::CreateAsFetch(aURI, cors); + return mDocument->Preloads().LookupPreload(preloadKey); +} + +void FetchDriver::UpdateReferrerInfoFromNewChannel(nsIChannel* aChannel) { + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aChannel); + if (!httpChannel) { + return; + } + + nsCOMPtr<nsIReferrerInfo> referrerInfo = httpChannel->GetReferrerInfo(); + if (!referrerInfo) { + return; + } + + nsAutoString computedReferrerSpec; + mRequest->SetReferrerPolicy(referrerInfo->ReferrerPolicy()); + Unused << referrerInfo->GetComputedReferrerSpec(computedReferrerSpec); + mRequest->SetReferrer(computedReferrerSpec); +} + +nsresult FetchDriver::Fetch(AbortSignalImpl* aSignalImpl, + FetchDriverObserver* aObserver) { + AssertIsOnMainThread(); +#ifdef DEBUG + MOZ_ASSERT(!mFetchCalled); + mFetchCalled = true; +#endif + + mObserver = aObserver; + + // FIXME(nsm): Deal with HSTS. + + MOZ_RELEASE_ASSERT(!mRequest->IsSynchronous(), + "Synchronous fetch not supported"); + + UniquePtr<mozilla::ipc::PrincipalInfo> principalInfo( + new mozilla::ipc::PrincipalInfo()); + nsresult rv = PrincipalToPrincipalInfo(mPrincipal, principalInfo.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + mRequest->SetPrincipalInfo(std::move(principalInfo)); + + // If the signal is aborted, it's time to inform the observer and terminate + // the operation. + if (aSignalImpl) { + if (aSignalImpl->Aborted()) { + FetchDriverAbortActions(aSignalImpl); + return NS_OK; + } + + Follow(aSignalImpl); + } + + rv = HttpFetch(mRequest->GetPreferredAlternativeDataType()); + if (NS_FAILED(rv)) { + FailWithNetworkError(rv); + } + + // Any failure is handled by FailWithNetworkError notifying the aObserver. + return NS_OK; +} + +// This function implements the "HTTP Fetch" algorithm from the Fetch spec. +// Functionality is often split between here, the CORS listener proxy and the +// Necko HTTP implementation. +nsresult FetchDriver::HttpFetch( + const nsACString& aPreferredAlternativeDataType) { + MOZ_ASSERT(NS_IsMainThread()); + + // Step 1. "Let response be null." + mResponse = nullptr; + mOnStopRequestCalled = false; + nsresult rv; + + nsCOMPtr<nsIIOService> ios = do_GetIOService(&rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString url; + mRequest->GetURL(url); + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), url); + NS_ENSURE_SUCCESS(rv, rv); + + // Unsafe requests aren't allowed with when using no-core mode. + if (mRequest->Mode() == RequestMode::No_cors && mRequest->UnsafeRequest() && + (!mRequest->HasSimpleMethod() || + !mRequest->Headers()->HasOnlySimpleHeaders())) { + MOZ_ASSERT(false, "The API should have caught this"); + return NS_ERROR_DOM_BAD_URI; + } + + // non-GET requests aren't allowed for blob. + if (IsBlobURI(uri)) { + nsAutoCString method; + mRequest->GetMethod(method); + if (!method.EqualsLiteral("GET")) { + return NS_ERROR_DOM_NETWORK_ERR; + } + } + + RefPtr<PreloaderBase> fetchPreload = FindPreload(uri); + if (fetchPreload) { + fetchPreload->RemoveSelf(mDocument); + fetchPreload->NotifyUsage(mDocument, PreloaderBase::LoadBackground::Keep); + + rv = fetchPreload->AsyncConsume(this); + if (NS_SUCCEEDED(rv)) { + mFromPreload = true; + + mChannel = fetchPreload->Channel(); + MOZ_ASSERT(mChannel); + mChannel->SetNotificationCallbacks(this); + + // Copied from AsyncOnChannelRedirect. + for (const auto& redirect : fetchPreload->Redirects()) { + if (redirect.Flags() & nsIChannelEventSink::REDIRECT_INTERNAL) { + mRequest->SetURLForInternalRedirect(redirect.Flags(), redirect.Spec(), + redirect.Fragment()); + } else { + mRequest->AddURL(redirect.Spec(), redirect.Fragment()); + } + } + + return NS_OK; + } + + // The preload failed to be consumed. Behave like there were no preload. + fetchPreload = nullptr; + } + + // Step 2 deals with letting ServiceWorkers intercept requests. This is + // handled by Necko after the channel is opened. + // FIXME(nsm): Bug 1119026: The channel's skip service worker flag should be + // set based on the Request's flag. + + // Step 3.1 "If the CORS preflight flag is set and one of these conditions is + // true..." is handled by the CORS proxy. + // + // Step 3.2 "Set request's skip service worker flag." This isn't required + // since Necko will fall back to the network if the ServiceWorker does not + // respond with a valid Response. + // + // NS_StartCORSPreflight() will automatically kick off the original request + // if it succeeds, so we need to have everything setup for the original + // request too. + + // Step 3.3 "Let credentials flag be set if one of + // - request's credentials mode is "include" + // - request's credentials mode is "same-origin" and either the CORS flag + // is unset or response tainting is "opaque" + // is true, and unset otherwise." + + // Set skip serviceworker flag. + // While the spec also gates on the client being a ServiceWorker, we can't + // infer that here. Instead we rely on callers to set the flag correctly. + const nsLoadFlags bypassFlag = mRequest->SkipServiceWorker() + ? nsIChannel::LOAD_BYPASS_SERVICE_WORKER + : 0; + + nsSecurityFlags secFlags = 0; + if (mRequest->Mode() == RequestMode::Cors) { + secFlags |= nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT; + } else if (mRequest->Mode() == RequestMode::Same_origin || + mRequest->Mode() == RequestMode::Navigate) { + secFlags |= nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT; + } else if (mRequest->Mode() == RequestMode::No_cors) { + secFlags |= nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT; + } else { + MOZ_ASSERT_UNREACHABLE("Unexpected request mode!"); + return NS_ERROR_UNEXPECTED; + } + + if (mRequest->GetRedirectMode() != RequestRedirect::Follow) { + secFlags |= nsILoadInfo::SEC_DONT_FOLLOW_REDIRECTS; + } + + // This handles the use credentials flag in "HTTP + // network or cache fetch" in the spec and decides whether to transmit + // cookies and other identifying information. + if (mRequest->GetCredentialsMode() == RequestCredentials::Include) { + secFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE; + } else if (mRequest->GetCredentialsMode() == RequestCredentials::Omit) { + secFlags |= nsILoadInfo::SEC_COOKIES_OMIT; + } else if (mRequest->GetCredentialsMode() == + RequestCredentials::Same_origin) { + secFlags |= nsILoadInfo::SEC_COOKIES_SAME_ORIGIN; + } else { + MOZ_ASSERT_UNREACHABLE("Unexpected credentials mode!"); + return NS_ERROR_UNEXPECTED; + } + + // From here on we create a channel and set its properties with the + // information from the InternalRequest. This is an implementation detail. + MOZ_ASSERT(mLoadGroup); + nsCOMPtr<nsIChannel> chan; + + nsLoadFlags loadFlags = nsIRequest::LOAD_BACKGROUND | bypassFlag; + if (mDocument) { + MOZ_ASSERT(mDocument->NodePrincipal() == mPrincipal); + MOZ_ASSERT(mDocument->CookieJarSettings() == mCookieJarSettings); + rv = NS_NewChannel(getter_AddRefs(chan), uri, mDocument, secFlags, + mRequest->ContentPolicyType(), + nullptr, /* aPerformanceStorage */ + mLoadGroup, nullptr, /* aCallbacks */ + loadFlags, ios); + } else if (mClientInfo.isSome()) { + rv = NS_NewChannel(getter_AddRefs(chan), uri, mPrincipal, mClientInfo.ref(), + mController, secFlags, mRequest->ContentPolicyType(), + mCookieJarSettings, mPerformanceStorage, mLoadGroup, + nullptr, /* aCallbacks */ + loadFlags, ios); + } else { + rv = + NS_NewChannel(getter_AddRefs(chan), uri, mPrincipal, secFlags, + mRequest->ContentPolicyType(), mCookieJarSettings, + mPerformanceStorage, mLoadGroup, nullptr, /* aCallbacks */ + loadFlags, ios); + } + NS_ENSURE_SUCCESS(rv, rv); + + if (mCSPEventListener) { + nsCOMPtr<nsILoadInfo> loadInfo = chan->LoadInfo(); + rv = loadInfo->SetCspEventListener(mCSPEventListener); + NS_ENSURE_SUCCESS(rv, rv); + } + + { + nsCOMPtr<nsILoadInfo> loadInfo = chan->LoadInfo(); + rv = loadInfo->SetLoadingEmbedderPolicy(mRequest->GetEmbedderPolicy()); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mAssociatedBrowsingContextID) { + nsCOMPtr<nsILoadInfo> loadInfo = chan->LoadInfo(); + rv = loadInfo->SetWorkerAssociatedBrowsingContextID( + mAssociatedBrowsingContextID); + } + + // If the fetch is created by FetchEvent.request or NavigationPreload request, + // corresponding InterceptedHttpChannel information need to propagte to the + // channel of the fetch. + if (mRequest->GetInterceptionTriggeringPrincipalInfo()) { + auto principalOrErr = mozilla::ipc::PrincipalInfoToPrincipal( + *(mRequest->GetInterceptionTriggeringPrincipalInfo().get())); + if (!principalOrErr.isErr()) { + nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap(); + + nsTArray<nsCOMPtr<nsIRedirectHistoryEntry>> redirectChain; + if (!mRequest->InterceptionRedirectChain().IsEmpty()) { + for (const RedirectHistoryEntryInfo& entryInfo : + mRequest->InterceptionRedirectChain()) { + nsCOMPtr<nsIRedirectHistoryEntry> entry = + mozilla::ipc::RHEntryInfoToRHEntry(entryInfo); + redirectChain.AppendElement(entry); + } + } + + nsCOMPtr<nsILoadInfo> loadInfo = chan->LoadInfo(); + MOZ_ASSERT(loadInfo); + loadInfo->SetInterceptionInfo(new mozilla::net::InterceptionInfo( + principal, mRequest->InterceptionContentPolicyType(), redirectChain, + mRequest->InterceptionFromThirdParty())); + } + } + + if (mDocument && mDocument->GetEmbedderElement() && + mDocument->GetEmbedderElement()->IsAnyOfHTMLElements(nsGkAtoms::object, + nsGkAtoms::embed)) { + nsCOMPtr<nsILoadInfo> loadInfo = chan->LoadInfo(); + rv = loadInfo->SetIsFromObjectOrEmbed(true); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Insert ourselves into the notification callbacks chain so we can set + // headers on redirects. +#ifdef DEBUG + { + nsCOMPtr<nsIInterfaceRequestor> notificationCallbacks; + chan->GetNotificationCallbacks(getter_AddRefs(notificationCallbacks)); + MOZ_ASSERT(!notificationCallbacks); + } +#endif + chan->SetNotificationCallbacks(this); + + nsCOMPtr<nsIClassOfService> cos(do_QueryInterface(chan)); + // Mark channel as urgent-start if the Fetch is triggered by user input + // events. + if (cos && UserActivation::IsHandlingUserInput()) { + cos->AddClassFlags(nsIClassOfService::UrgentStart); + } + + // Step 3.5 begins "HTTP network or cache fetch". + // HTTP network or cache fetch + // --------------------------- + // Step 1 "Let HTTPRequest..." The channel is the HTTPRequest. + nsCOMPtr<nsIHttpChannel> httpChan = do_QueryInterface(chan); + if (httpChan) { + // Copy the method. + nsAutoCString method; + mRequest->GetMethod(method); + rv = httpChan->SetRequestMethod(method); + NS_ENSURE_SUCCESS(rv, rv); + + // Set the same headers. + SetRequestHeaders(httpChan, false, false); + + // Step 5 of https://fetch.spec.whatwg.org/#main-fetch + // If request's referrer policy is the empty string and request's client is + // non-null, then set request's referrer policy to request's client's + // associated referrer policy. + // Basically, "client" is not in our implementation, we use + // EnvironmentReferrerPolicy of the worker or document context + ReferrerPolicy referrerPolicy = mRequest->GetEnvironmentReferrerPolicy(); + if (mRequest->ReferrerPolicy_() == ReferrerPolicy::_empty) { + mRequest->SetReferrerPolicy(referrerPolicy); + } + // Step 6 of https://fetch.spec.whatwg.org/#main-fetch + // If request’s referrer policy is the empty string, + // then set request’s referrer policy to the user-set default policy. + if (mRequest->ReferrerPolicy_() == ReferrerPolicy::_empty) { + nsCOMPtr<nsILoadInfo> loadInfo = httpChan->LoadInfo(); + bool isPrivate = loadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; + referrerPolicy = + ReferrerInfo::GetDefaultReferrerPolicy(httpChan, uri, isPrivate); + mRequest->SetReferrerPolicy(referrerPolicy); + } + + rv = FetchUtil::SetRequestReferrer(mPrincipal, mDocument, httpChan, + *mRequest); + NS_ENSURE_SUCCESS(rv, rv); + + // 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); + + rv = internalChan->SetRequestMode(mRequest->Mode()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + // Conversion between enumerations is safe due to static asserts in + // dom/workers/ServiceWorkerManager.cpp + rv = internalChan->SetRedirectMode( + static_cast<uint32_t>(mRequest->GetRedirectMode())); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + mRequest->MaybeSkipCacheIfPerformingRevalidation(); + rv = internalChan->SetFetchCacheMode( + static_cast<uint32_t>(mRequest->GetCacheMode())); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + rv = internalChan->SetIntegrityMetadata(mRequest->GetIntegrity()); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Set the initiator type + nsCOMPtr<nsITimedChannel> timedChannel(do_QueryInterface(httpChan)); + if (timedChannel) { + timedChannel->SetInitiatorType(u"fetch"_ns); + } + } + + // Step 5. Proxy authentication will be handled by Necko. + + // Continue setting up 'HTTPRequest'. Content-Type and body data. + nsCOMPtr<nsIUploadChannel2> uploadChan = do_QueryInterface(chan); + if (uploadChan) { + nsAutoCString contentType; + ErrorResult result; + mRequest->Headers()->GetFirst("content-type"_ns, contentType, result); + // We don't actually expect "result" to have failed here: that only happens + // for invalid header names. But if for some reason it did, just propagate + // it out. + if (result.Failed()) { + return result.StealNSResult(); + } + + // Now contentType is the header that was set in mRequest->Headers(), or a + // void string if no header was set. +#ifdef DEBUG + bool hasContentTypeHeader = + mRequest->Headers()->Has("content-type"_ns, result); + MOZ_ASSERT(!result.Failed()); + MOZ_ASSERT_IF(!hasContentTypeHeader, contentType.IsVoid()); +#endif // DEBUG + + int64_t bodyLength; + nsCOMPtr<nsIInputStream> bodyStream; + mRequest->GetBody(getter_AddRefs(bodyStream), &bodyLength); + if (bodyStream) { + nsAutoCString method; + mRequest->GetMethod(method); + rv = uploadChan->ExplicitSetUploadStream(bodyStream, contentType, + bodyLength, method, + false /* aStreamHasHeaders */); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // If preflight is required, start a "CORS preflight fetch" + // https://fetch.spec.whatwg.org/#cors-preflight-fetch-0. All the + // implementation is handled by the http channel calling into + // nsCORSListenerProxy. We just inform it which unsafe headers are included + // in the request. + if (mRequest->Mode() == RequestMode::Cors) { + AutoTArray<nsCString, 5> unsafeHeaders; + mRequest->Headers()->GetUnsafeHeaders(unsafeHeaders); + nsCOMPtr<nsILoadInfo> loadInfo = chan->LoadInfo(); + loadInfo->SetCorsPreflightInfo(unsafeHeaders, false); + } + + if (mIsTrackingFetch && StaticPrefs::network_http_tailing_enabled() && cos) { + cos->AddClassFlags(nsIClassOfService::Throttleable | + nsIClassOfService::Tail); + } + + if (mIsTrackingFetch && + StaticPrefs::privacy_trackingprotection_lower_network_priority()) { + nsCOMPtr<nsISupportsPriority> p = do_QueryInterface(chan); + if (p) { + p->SetPriority(nsISupportsPriority::PRIORITY_LOWEST); + } + } + + NotifyNetworkMonitorAlternateStack(chan, std::move(mOriginStack)); + if (mObserver && httpChan) { + mObserver->OnNotifyNetworkMonitorAlternateStack(httpChan->ChannelId()); + } + + // if the preferred alternative data type in InternalRequest is not empty, set + // the data type on the created channel and also create a + // AlternativeDataStreamListener to be the stream listener of the channel. + if (!aPreferredAlternativeDataType.IsEmpty()) { + nsCOMPtr<nsICacheInfoChannel> cic = do_QueryInterface(chan); + if (cic) { + cic->PreferAlternativeDataType( + aPreferredAlternativeDataType, ""_ns, + nsICacheInfoChannel::PreferredAlternativeDataDeliveryType::ASYNC); + MOZ_ASSERT(!mAltDataListener); + mAltDataListener = new AlternativeDataStreamListener( + this, chan, aPreferredAlternativeDataType); + rv = chan->AsyncOpen(mAltDataListener); + } else { + rv = chan->AsyncOpen(this); + } + } else { + // Integrity check cannot be done on alt-data yet. + if (mRequest->GetIntegrity().IsEmpty()) { + MOZ_ASSERT(!FetchUtil::WasmAltDataType.IsEmpty()); + nsCOMPtr<nsICacheInfoChannel> cic = do_QueryInterface(chan); + if (cic && StaticPrefs::javascript_options_wasm_caching() && + !mRequest->SkipWasmCaching()) { + cic->PreferAlternativeDataType( + FetchUtil::WasmAltDataType, nsLiteralCString(WASM_CONTENT_TYPE), + nsICacheInfoChannel::PreferredAlternativeDataDeliveryType:: + SERIALIZE); + } + } + + rv = chan->AsyncOpen(this); + } + + if (NS_FAILED(rv)) { + return rv; + } + + // Step 4 onwards of "HTTP Fetch" is handled internally by Necko. + + mChannel = chan; + return NS_OK; +} + +SafeRefPtr<InternalResponse> FetchDriver::BeginAndGetFilteredResponse( + SafeRefPtr<InternalResponse> aResponse, bool aFoundOpaqueRedirect) { + MOZ_ASSERT(aResponse); + AutoTArray<nsCString, 4> reqURLList; + mRequest->GetURLListWithoutFragment(reqURLList); + MOZ_ASSERT(!reqURLList.IsEmpty()); + aResponse->SetURLList(reqURLList); + SafeRefPtr<InternalResponse> filteredResponse; + if (aFoundOpaqueRedirect) { + filteredResponse = aResponse->OpaqueRedirectResponse(); + } else { + switch (mRequest->GetResponseTainting()) { + case LoadTainting::Basic: + filteredResponse = aResponse->BasicResponse(); + break; + case LoadTainting::CORS: + filteredResponse = aResponse->CORSResponse(); + break; + case LoadTainting::Opaque: { + filteredResponse = aResponse->OpaqueResponse(); + nsresult rv = filteredResponse->GeneratePaddingInfo(); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + break; + } + default: + MOZ_CRASH("Unexpected case"); + } + } + + MOZ_ASSERT(filteredResponse); + MOZ_ASSERT(mObserver); + MOZ_ASSERT(filteredResponse); + if (!ShouldCheckSRI(*mRequest, *filteredResponse)) { + // Need to keep mObserver alive. + RefPtr<FetchDriverObserver> observer = mObserver; + observer->OnResponseAvailable(filteredResponse.clonePtr()); +#ifdef DEBUG + mResponseAvailableCalled = true; +#endif + } + + return filteredResponse; +} + +void FetchDriver::FailWithNetworkError(nsresult rv) { + AssertIsOnMainThread(); + if (mObserver) { + // Need to keep mObserver alive. + RefPtr<FetchDriverObserver> observer = mObserver; + observer->OnResponseAvailable(InternalResponse::NetworkError(rv)); +#ifdef DEBUG + mResponseAvailableCalled = true; +#endif + } + + // mObserver could be null after OnResponseAvailable(). + if (mObserver) { + mObserver->OnResponseEnd(FetchDriverObserver::eByNetworking, + JS::UndefinedHandleValue); + mObserver = nullptr; + } + + mChannel = nullptr; + Unfollow(); +} + +NS_IMETHODIMP +FetchDriver::OnStartRequest(nsIRequest* aRequest) { + AssertIsOnMainThread(); + + // 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(). + + if (mFromPreload && mAborted) { + aRequest->CancelWithReason(NS_BINDING_ABORTED, + "FetchDriver::OnStartRequest aborted"_ns); + return NS_BINDING_ABORTED; + } + + if (!mChannel) { + // if the request is aborted, we remove the mObserver reference in + // OnStopRequest or ~FetchDriver() + MOZ_ASSERT_IF(!mAborted, !mObserver); + return NS_BINDING_ABORTED; + } + + nsresult rv; + aRequest->GetStatus(&rv); + if (NS_FAILED(rv)) { + FailWithNetworkError(rv); + return rv; + } + + // We should only get to the following code once. + MOZ_ASSERT(!mPipeOutputStream); + + if (!mObserver) { + MOZ_ASSERT(false, "We should have mObserver here."); + FailWithNetworkError(NS_ERROR_UNEXPECTED); + return NS_ERROR_UNEXPECTED; + } + + mNeedToObserveOnDataAvailable = mObserver->NeedOnDataAvailable(); + + SafeRefPtr<InternalResponse> response; + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(aRequest); + + // On a successful redirect we perform the following substeps of HTTP Fetch, + // step 5, "redirect status", step 11. + + bool foundOpaqueRedirect = false; + + nsAutoCString contentType; + channel->GetContentType(contentType); + + int64_t contentLength = InternalResponse::UNKNOWN_BODY_SIZE; + rv = channel->GetContentLength(&contentLength); + MOZ_ASSERT_IF(NS_FAILED(rv), + contentLength == InternalResponse::UNKNOWN_BODY_SIZE); + + if (httpChannel) { + uint32_t responseStatus = 0; + rv = httpChannel->GetResponseStatus(&responseStatus); + if (NS_FAILED(rv)) { + FailWithNetworkError(rv); + return rv; + } + + if (mozilla::net::nsHttpChannel::IsRedirectStatus(responseStatus)) { + if (mRequest->GetRedirectMode() == RequestRedirect::Error) { + FailWithNetworkError(NS_BINDING_ABORTED); + return NS_BINDING_FAILED; + } + if (mRequest->GetRedirectMode() == RequestRedirect::Manual) { + foundOpaqueRedirect = true; + } + } + + nsAutoCString statusText; + rv = httpChannel->GetResponseStatusText(statusText); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + response = MakeSafeRefPtr<InternalResponse>(responseStatus, statusText, + mRequest->GetCredentialsMode()); + + UniquePtr<mozilla::ipc::PrincipalInfo> principalInfo( + new mozilla::ipc::PrincipalInfo()); + nsresult rv = PrincipalToPrincipalInfo(mPrincipal, principalInfo.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + response->SetPrincipalInfo(std::move(principalInfo)); + + response->Headers()->FillResponseHeaders(httpChannel); + + // If Content-Encoding or Transfer-Encoding headers are set, then the actual + // Content-Length (which refer to the decoded data) is obscured behind the + // encodings. + ErrorResult result; + if (response->Headers()->Has("content-encoding"_ns, result) || + response->Headers()->Has("transfer-encoding"_ns, result)) { + // We cannot trust the content-length when content-encoding or + // transfer-encoding are set. There are many servers which just + // get this wrong. + contentLength = InternalResponse::UNKNOWN_BODY_SIZE; + } + MOZ_ASSERT(!result.Failed()); + } else { + response = MakeSafeRefPtr<InternalResponse>(200, "OK"_ns, + mRequest->GetCredentialsMode()); + + if (!contentType.IsEmpty()) { + nsAutoCString contentCharset; + channel->GetContentCharset(contentCharset); + if (NS_SUCCEEDED(rv) && !contentCharset.IsEmpty()) { + contentType += ";charset="_ns + contentCharset; + } + } + + IgnoredErrorResult result; + response->Headers()->Append("Content-Type"_ns, contentType, result); + MOZ_ASSERT(!result.Failed()); + + if (contentLength >= 0) { + nsAutoCString contentLenStr; + contentLenStr.AppendInt(contentLength); + + IgnoredErrorResult result; + response->Headers()->Append("Content-Length"_ns, contentLenStr, result); + MOZ_ASSERT(!result.Failed()); + } + } + + nsCOMPtr<nsICacheInfoChannel> cic = do_QueryInterface(aRequest); + if (cic) { + if (mAltDataListener) { + // Skip the case that mAltDataListener->Status() equals to FALLBACK, that + // means the opened channel for alternative data loading is reused for + // loading the main data. + if (mAltDataListener->Status() != + AlternativeDataStreamListener::FALLBACK) { + // Verify the cache ID is the same with from alternative data cache. + // If the cache ID is different, droping the alternative data loading, + // otherwise setup the response's alternative body and cacheInfoChannel. + uint64_t cacheEntryId = 0; + if (NS_SUCCEEDED(cic->GetCacheEntryId(&cacheEntryId)) && + cacheEntryId != + mAltDataListener->GetAlternativeDataCacheEntryId()) { + mAltDataListener->Cancel(); + } else { + // AlternativeDataStreamListener::OnStartRequest had already been + // called, the alternative data input stream and cacheInfo channel + // must be created. + nsCOMPtr<nsICacheInfoChannel> cacheInfo = + mAltDataListener->GetCacheInfoChannel(); + nsCOMPtr<nsIInputStream> altInputStream = + mAltDataListener->GetAlternativeInputStream(); + MOZ_ASSERT(altInputStream && cacheInfo); + response->SetAlternativeBody(altInputStream); + nsMainThreadPtrHandle<nsICacheInfoChannel> handle( + new nsMainThreadPtrHolder<nsICacheInfoChannel>( + "nsICacheInfoChannel", cacheInfo, false)); + response->SetCacheInfoChannel(handle); + } + } else if (!mAltDataListener->GetAlternativeDataType().IsEmpty()) { + // If the status is FALLBACK and the + // mAltDataListener::mAlternativeDataType is not empty, that means the + // data need to be saved into cache, setup the response's + // nsICacheInfoChannel for caching the data after loading. + nsMainThreadPtrHandle<nsICacheInfoChannel> handle( + new nsMainThreadPtrHolder<nsICacheInfoChannel>( + "nsICacheInfoChannel", cic, false)); + response->SetCacheInfoChannel(handle); + } + } else if (!cic->PreferredAlternativeDataTypes().IsEmpty()) { + MOZ_ASSERT(cic->PreferredAlternativeDataTypes().Length() == 1); + MOZ_ASSERT(cic->PreferredAlternativeDataTypes()[0].type().Equals( + FetchUtil::WasmAltDataType)); + MOZ_ASSERT( + cic->PreferredAlternativeDataTypes()[0].contentType().EqualsLiteral( + WASM_CONTENT_TYPE)); + + if (contentType.EqualsLiteral(WASM_CONTENT_TYPE)) { + // We want to attach the CacheInfoChannel to the response object such + // that we can track its origin when the Response object is manipulated + // by JavaScript code. This is important for WebAssembly, which uses + // fetch to query its sources in JavaScript and transfer the Response + // object to other function responsible for storing the alternate data + // using the CacheInfoChannel. + nsMainThreadPtrHandle<nsICacheInfoChannel> handle( + new nsMainThreadPtrHolder<nsICacheInfoChannel>( + "nsICacheInfoChannel", cic, false)); + response->SetCacheInfoChannel(handle); + } + } + } + + // We open a pipe so that we can immediately set the pipe's read end as the + // response's body. Setting the segment size to UINT32_MAX means that the + // pipe has infinite space. The nsIChannel will continue to buffer data in + // xpcom events even if we block on a fixed size pipe. It might be possible + // to suspend the channel and then resume when there is space available, but + // for now use an infinite pipe to avoid blocking. + nsCOMPtr<nsIInputStream> pipeInputStream; + NS_NewPipe(getter_AddRefs(pipeInputStream), getter_AddRefs(mPipeOutputStream), + 0, /* default segment size */ + UINT32_MAX /* infinite pipe */, + true /* non-blocking input, otherwise you deadlock */, + false /* blocking output, since the pipe is 'in'finite */); + response->SetBody(pipeInputStream, contentLength); + + // If the request is a file channel, then remember the local path to + // that file so we can later create File blobs rather than plain ones. + nsCOMPtr<nsIFileChannel> fc = do_QueryInterface(aRequest); + if (fc) { + nsCOMPtr<nsIFile> file; + rv = fc->GetFile(getter_AddRefs(file)); + if (!NS_WARN_IF(NS_FAILED(rv))) { + nsAutoString path; + file->GetPath(path); + response->SetBodyLocalPath(path); + } + } else { + // If the request is a blob URI, then remember that URI so that we + // can later just use that blob instance instead of cloning it. + nsCString blobURISpec; + GetBlobURISpecFromChannel(aRequest, blobURISpec); + if (!blobURISpec.IsVoid()) { + response->SetBodyBlobURISpec(blobURISpec); + } + } + + response->InitChannelInfo(channel); + + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + // Propagate any tainting from the channel back to our response here. This + // step is not reflected in the spec because the spec is written such that + // FetchEvent.respondWith() just passes the already-tainted Response back to + // the outer fetch(). In gecko, however, we serialize the Response through + // the channel and must regenerate the tainting from the channel in the + // interception case. + mRequest->MaybeIncreaseResponseTainting(loadInfo->GetTainting()); + + // Resolves fetch() promise which may trigger code running in a worker. Make + // sure the Response is fully initialized before calling this. + mResponse = + BeginAndGetFilteredResponse(std::move(response), foundOpaqueRedirect); + if (NS_WARN_IF(!mResponse)) { + // Fail to generate a paddingInfo for opaque response. + MOZ_DIAGNOSTIC_ASSERT(mRequest->GetResponseTainting() == + LoadTainting::Opaque && + !foundOpaqueRedirect); + FailWithNetworkError(NS_ERROR_UNEXPECTED); + return NS_ERROR_UNEXPECTED; + } + + // From "Main Fetch" step 19: SRI-part1. + if (ShouldCheckSRI(*mRequest, *mResponse) && mSRIMetadata.IsEmpty()) { + nsIConsoleReportCollector* reporter = nullptr; + if (mObserver) { + reporter = mObserver->GetReporter(); + } + + nsAutoCString sourceUri; + if (mDocument && mDocument->GetDocumentURI()) { + mDocument->GetDocumentURI()->GetAsciiSpec(sourceUri); + } else if (!mWorkerScript.IsEmpty()) { + sourceUri.Assign(mWorkerScript); + } + SRICheck::IntegrityMetadata(mRequest->GetIntegrity(), sourceUri, reporter, + &mSRIMetadata); + mSRIDataVerifier = + MakeUnique<SRICheckDataVerifier>(mSRIMetadata, sourceUri, reporter); + + // Do not retarget off main thread when using SRI API. + return NS_OK; + } + + nsCOMPtr<nsIEventTarget> sts = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + FailWithNetworkError(rv); + // Cancel request. + return rv; + } + + // Try to retarget off main thread. + if (nsCOMPtr<nsIThreadRetargetableRequest> rr = do_QueryInterface(aRequest)) { + RefPtr<TaskQueue> queue = + TaskQueue::Create(sts.forget(), "FetchDriver STS Delivery Queue"); + Unused << NS_WARN_IF(NS_FAILED(rr->RetargetDeliveryTo(queue))); + } + return NS_OK; +} + +namespace { + +// Runnable to call the observer OnDataAvailable on the main-thread. +class DataAvailableRunnable final : public Runnable { + RefPtr<FetchDriverObserver> mObserver; + + public: + explicit DataAvailableRunnable(FetchDriverObserver* aObserver) + : Runnable("dom::DataAvailableRunnable"), mObserver(aObserver) { + MOZ_ASSERT(aObserver); + } + + NS_IMETHOD + Run() override { + mObserver->OnDataAvailable(); + mObserver = nullptr; + return NS_OK; + } +}; + +struct SRIVerifierAndOutputHolder { + SRIVerifierAndOutputHolder(SRICheckDataVerifier* aVerifier, + nsIOutputStream* aOutputStream) + : mVerifier(aVerifier), mOutputStream(aOutputStream) {} + + SRICheckDataVerifier* mVerifier; + nsIOutputStream* mOutputStream; + + private: + SRIVerifierAndOutputHolder() = delete; +}; + +// Just like NS_CopySegmentToStream, but also sends the data into an +// SRICheckDataVerifier. +nsresult CopySegmentToStreamAndSRI(nsIInputStream* aInStr, void* aClosure, + const char* aBuffer, uint32_t aOffset, + uint32_t aCount, uint32_t* aCountWritten) { + auto holder = static_cast<SRIVerifierAndOutputHolder*>(aClosure); + MOZ_DIAGNOSTIC_ASSERT(holder && holder->mVerifier && holder->mOutputStream, + "Bogus holder"); + nsresult rv = holder->mVerifier->Update( + aCount, reinterpret_cast<const uint8_t*>(aBuffer)); + NS_ENSURE_SUCCESS(rv, rv); + + // The rest is just like NS_CopySegmentToStream. + *aCountWritten = 0; + while (aCount) { + uint32_t n = 0; + rv = holder->mOutputStream->Write(aBuffer, aCount, &n); + if (NS_FAILED(rv)) { + return rv; + } + aBuffer += n; + aCount -= n; + *aCountWritten += n; + } + return NS_OK; +} + +} // anonymous namespace + +NS_IMETHODIMP +FetchDriver::OnDataAvailable(nsIRequest* aRequest, 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 for accesses in OnStartRequest, OnStopRequest and + // member functions accessed before opening the channel. + // However, we have a possibility of a race from FetchDriverAbortActions. + // Hence, we need to ensure that we are not modifying any members accessed by + // FetchDriver::FetchDriverAbortActions + + if (mNeedToObserveOnDataAvailable) { + mNeedToObserveOnDataAvailable = false; + if (mObserver) { + // Need to keep mObserver alive. + RefPtr<FetchDriverObserver> observer = mObserver; + if (NS_IsMainThread()) { + observer->OnDataAvailable(); + } else { + RefPtr<Runnable> runnable = new DataAvailableRunnable(observer); + nsresult rv = mMainThreadEventTarget->Dispatch(runnable.forget(), + NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + } + + if (!mResponse) { + MOZ_ASSERT(false); + return NS_ERROR_UNEXPECTED; + } + + // Needs to be initialized to 0 because in some cases nsStringInputStream may + // not write to aRead. + uint32_t aRead = 0; + MOZ_ASSERT(mPipeOutputStream); + + // From "Main Fetch" step 19: SRI-part2. + // Note: Avoid checking the hidden opaque body. + nsresult rv; + if (mResponse->Type() != ResponseType::Opaque && + ShouldCheckSRI(*mRequest, *mResponse)) { + MOZ_ASSERT(mSRIDataVerifier); + + SRIVerifierAndOutputHolder holder(mSRIDataVerifier.get(), + mPipeOutputStream); + rv = aInputStream->ReadSegments(CopySegmentToStreamAndSRI, &holder, aCount, + &aRead); + } else { + rv = aInputStream->ReadSegments(NS_CopySegmentToStream, mPipeOutputStream, + aCount, &aRead); + } + + // If no data was read, it's possible the output stream is closed but the + // ReadSegments call followed its contract of returning NS_OK despite write + // errors. Unfortunately, nsIOutputStream has an ill-conceived contract when + // taken together with ReadSegments' contract, because the pipe will just + // NS_OK if we try and invoke its Write* functions ourselves with a 0 count. + // So we must just assume the pipe is broken. + if (aRead == 0 && aCount != 0) { + return NS_BASE_STREAM_CLOSED; + } + return rv; +} + +NS_IMETHODIMP +FetchDriver::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) { + AssertIsOnMainThread(); + + MOZ_DIAGNOSTIC_ASSERT(!mOnStopRequestCalled); + mOnStopRequestCalled = true; + + if (mObserver && mAborted) { + // fetch request was aborted. + // We have already sent the observer + // notification that request has been aborted in FetchDriverAbortActions. + // Remove the observer reference and don't push anymore notifications. + mObserver = nullptr; + } + + // main data loading is going to finish, breaking the reference cycle. + RefPtr<AlternativeDataStreamListener> altDataListener = + std::move(mAltDataListener); + + // For PFetch and ServiceWorker navigationPreload, resource timing should be + // reported before the body stream closing. + if (mObserver) { + mObserver->OnReportPerformanceTiming(); + } + + // We need to check mObserver, which is nulled by FailWithNetworkError(), + // because in the case of "error" redirect mode, aStatusCode may be NS_OK but + // mResponse will definitely be null so we must not take the else branch. + if (NS_FAILED(aStatusCode) || !mObserver) { + nsCOMPtr<nsIAsyncOutputStream> outputStream = + do_QueryInterface(mPipeOutputStream); + if (outputStream) { + outputStream->CloseWithStatus(NS_FAILED(aStatusCode) ? aStatusCode + : NS_BINDING_FAILED); + } + if (altDataListener) { + altDataListener->Cancel(); + } + + // We proceed as usual here, since we've already created a successful + // response from OnStartRequest. + } else { + MOZ_ASSERT(mResponse); + MOZ_ASSERT(!mResponse->IsError()); + + // From "Main Fetch" step 19: SRI-part3. + if (ShouldCheckSRI(*mRequest, *mResponse)) { + MOZ_ASSERT(mSRIDataVerifier); + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + + nsIConsoleReportCollector* reporter = nullptr; + if (mObserver) { + reporter = mObserver->GetReporter(); + } + + nsAutoCString sourceUri; + if (mDocument && mDocument->GetDocumentURI()) { + mDocument->GetDocumentURI()->GetAsciiSpec(sourceUri); + } else if (!mWorkerScript.IsEmpty()) { + sourceUri.Assign(mWorkerScript); + } + nsresult rv = + mSRIDataVerifier->Verify(mSRIMetadata, channel, sourceUri, reporter); + if (NS_FAILED(rv)) { + if (altDataListener) { + altDataListener->Cancel(); + } + FailWithNetworkError(rv); + // Cancel request. + return rv; + } + } + + if (mPipeOutputStream) { + mPipeOutputStream->Close(); + } + } + + FinishOnStopRequest(altDataListener); + return NS_OK; +} + +void FetchDriver::FinishOnStopRequest( + AlternativeDataStreamListener* aAltDataListener) { + AssertIsOnMainThread(); + // OnStopRequest is not called from channel, that means the main data loading + // does not finish yet. Reaching here since alternative data loading finishes. + if (!mOnStopRequestCalled) { + return; + } + + MOZ_DIAGNOSTIC_ASSERT(!mAltDataListener); + // Wait for alternative data loading finish if we needed it. + if (aAltDataListener && + aAltDataListener->Status() == AlternativeDataStreamListener::LOADING) { + // For LOADING case, channel holds the reference of altDataListener, no need + // to restore it to mAltDataListener. + return; + } + + if (mObserver) { + // From "Main Fetch" step 19.1, 19.2: Process response. + if (ShouldCheckSRI(*mRequest, *mResponse)) { + MOZ_ASSERT(mResponse); + // Need to keep mObserver alive. + RefPtr<FetchDriverObserver> observer = mObserver; + observer->OnResponseAvailable(mResponse.clonePtr()); +#ifdef DEBUG + mResponseAvailableCalled = true; +#endif + } + } + + if (mObserver) { + mObserver->OnResponseEnd(FetchDriverObserver::eByNetworking, + JS::UndefinedHandleValue); + mObserver = nullptr; + } + + mChannel = nullptr; + Unfollow(); +} + +NS_IMETHODIMP +FetchDriver::ShouldPrepareForIntercept(nsIURI* aURI, nsIChannel* aChannel, + bool* aShouldIntercept) { + MOZ_ASSERT(aChannel); + + if (mInterceptController) { + MOZ_ASSERT(XRE_IsParentProcess()); + return mInterceptController->ShouldPrepareForIntercept(aURI, aChannel, + aShouldIntercept); + } + + nsCOMPtr<nsINetworkInterceptController> controller; + NS_QueryNotificationCallbacks(nullptr, mLoadGroup, + NS_GET_IID(nsINetworkInterceptController), + getter_AddRefs(controller)); + if (controller) { + return controller->ShouldPrepareForIntercept(aURI, aChannel, + aShouldIntercept); + } + + *aShouldIntercept = false; + return NS_OK; +} + +NS_IMETHODIMP +FetchDriver::ChannelIntercepted(nsIInterceptedChannel* aChannel) { + if (mInterceptController) { + MOZ_ASSERT(XRE_IsParentProcess()); + return mInterceptController->ChannelIntercepted(aChannel); + } + + nsCOMPtr<nsINetworkInterceptController> controller; + NS_QueryNotificationCallbacks(nullptr, mLoadGroup, + NS_GET_IID(nsINetworkInterceptController), + getter_AddRefs(controller)); + if (controller) { + return controller->ChannelIntercepted(aChannel); + } + + return NS_OK; +} + +void FetchDriver::EnableNetworkInterceptControl() { + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mInterceptController); + mInterceptController = new ServiceWorkerInterceptController(); +} + +NS_IMETHODIMP +FetchDriver::AsyncOnChannelRedirect(nsIChannel* aOldChannel, + nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* aCallback) { + nsCOMPtr<nsIHttpChannel> oldHttpChannel = do_QueryInterface(aOldChannel); + nsCOMPtr<nsIHttpChannel> newHttpChannel = do_QueryInterface(aNewChannel); + if (oldHttpChannel && newHttpChannel) { + nsAutoCString method; + mRequest->GetMethod(method); + + // Fetch 4.4.11 + bool rewriteToGET = false; + Unused << oldHttpChannel->ShouldStripRequestBodyHeader(method, + &rewriteToGET); + + // we need to strip Authentication headers for cross-origin requests + // Ref: https://fetch.spec.whatwg.org/#http-redirect-fetch + bool skipAuthHeader = + (StaticPrefs::network_fetch_redirect_stripAuthHeader() && + NS_ShouldRemoveAuthHeaderOnRedirect(aOldChannel, aNewChannel, aFlags)); + + SetRequestHeaders(newHttpChannel, rewriteToGET, skipAuthHeader); + } + + // "HTTP-redirect fetch": step 14 "Append locationURL to request's URL list." + // However, ignore internal redirects here. We don't want to flip + // Response.redirected to true if an internal redirect occurs. These + // should be transparent to script. + nsCOMPtr<nsIURI> uri; + MOZ_ALWAYS_SUCCEEDS(NS_GetFinalChannelURI(aNewChannel, getter_AddRefs(uri))); + + nsCOMPtr<nsIURI> uriClone; + nsresult rv = NS_GetURIWithoutRef(uri, getter_AddRefs(uriClone)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + nsCString spec; + rv = uriClone->GetSpec(spec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + nsCString fragment; + rv = uri->GetRef(fragment); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!(aFlags & nsIChannelEventSink::REDIRECT_INTERNAL)) { + mRequest->AddURL(spec, fragment); + } else { + // Overwrite the URL only when the request is redirected by a service + // worker. + mRequest->SetURLForInternalRedirect(aFlags, spec, fragment); + } + + // In redirect, httpChannel already took referrer-policy into account, so + // updates request’s associated referrer policy from channel. + UpdateReferrerInfoFromNewChannel(aNewChannel); + + aCallback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +NS_IMETHODIMP +FetchDriver::CheckListenerChain() { return NS_OK; } + +NS_IMETHODIMP +FetchDriver::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(nsIStreamListener))) { + *aResult = static_cast<nsIStreamListener*>(this); + NS_ADDREF_THIS(); + return NS_OK; + } + if (aIID.Equals(NS_GET_IID(nsIRequestObserver))) { + *aResult = static_cast<nsIRequestObserver*>(this); + NS_ADDREF_THIS(); + return NS_OK; + } + + return QueryInterface(aIID, aResult); +} + +void FetchDriver::SetDocument(Document* aDocument) { + // Cannot set document after Fetch() has been called. + MOZ_ASSERT(!mFetchCalled); + mDocument = aDocument; +} + +void FetchDriver::SetCSPEventListener(nsICSPEventListener* aCSPEventListener) { + MOZ_ASSERT(!mFetchCalled); + mCSPEventListener = aCSPEventListener; +} + +void FetchDriver::SetClientInfo(const ClientInfo& aClientInfo) { + MOZ_ASSERT(!mFetchCalled); + mClientInfo.emplace(aClientInfo); +} + +void FetchDriver::SetController( + const Maybe<ServiceWorkerDescriptor>& aController) { + MOZ_ASSERT(!mFetchCalled); + mController = aController; +} + +PerformanceTimingData* FetchDriver::GetPerformanceTimingData( + nsAString& aInitiatorType, nsAString& aEntryName) { + MOZ_ASSERT(XRE_IsParentProcess()); + if (!mChannel) { + return nullptr; + } + + nsCOMPtr<nsITimedChannel> timedChannel = do_QueryInterface(mChannel); + if (!timedChannel) { + return nullptr; + } + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(mChannel); + if (!httpChannel) { + return nullptr; + } + return dom::PerformanceTimingData::Create(timedChannel, httpChannel, 0, + aInitiatorType, aEntryName); +} + +void FetchDriver::SetRequestHeaders(nsIHttpChannel* aChannel, + bool aStripRequestBodyHeader, + bool aStripAuthHeader) const { + MOZ_ASSERT(aChannel); + + // nsIHttpChannel has a set of pre-configured headers (Accept, + // Accept-Languages, ...) and we don't want to merge the Request's headers + // with them. This array is used to know if the current header has been aleady + // set, if yes, we ask necko to merge it with the previous one, otherwise, we + // don't want the merge. + nsTArray<nsCString> headersSet; + + AutoTArray<InternalHeaders::Entry, 5> headers; + mRequest->Headers()->GetEntries(headers); + for (uint32_t i = 0; i < headers.Length(); ++i) { + if (aStripRequestBodyHeader && + (headers[i].mName.LowerCaseEqualsASCII("content-type") || + headers[i].mName.LowerCaseEqualsASCII("content-encoding") || + headers[i].mName.LowerCaseEqualsASCII("content-language") || + headers[i].mName.LowerCaseEqualsASCII("content-location"))) { + continue; + } + + if (aStripAuthHeader && + headers[i].mName.LowerCaseEqualsASCII("authorization")) { + continue; + } + + bool alreadySet = headersSet.Contains(headers[i].mName); + if (!alreadySet) { + headersSet.AppendElement(headers[i].mName); + } + + if (headers[i].mValue.IsEmpty()) { + DebugOnly<nsresult> rv = + aChannel->SetEmptyRequestHeader(headers[i].mName); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } else { + DebugOnly<nsresult> rv = aChannel->SetRequestHeader( + headers[i].mName, headers[i].mValue, alreadySet /* merge */); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + } +} + +void FetchDriver::RunAbortAlgorithm() { FetchDriverAbortActions(Signal()); } + +void FetchDriver::FetchDriverAbortActions(AbortSignalImpl* aSignalImpl) { + MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread()); + + if (mObserver) { +#ifdef DEBUG + mResponseAvailableCalled = true; +#endif + JS::Rooted<JS::Value> reason(RootingCx()); + if (aSignalImpl) { + reason.set(aSignalImpl->RawReason()); + } + mObserver->OnResponseEnd(FetchDriverObserver::eAborted, reason); + // As a part of cleanup, we are not removing the mObserver reference as it + // could race with mObserver access in OnDataAvailable when it runs OMT. + // We will be removing the reference in the OnStopRequest which guaranteed + // to run after cancelling the channel. + } + + if (mChannel) { + mChannel->CancelWithReason(NS_BINDING_ABORTED, + "FetchDriver::RunAbortAlgorithm"_ns); + mChannel = nullptr; + } + + mAborted = true; +} + +} // namespace mozilla::dom |