/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set expandtab ts=4 sw=2 sts=2 cin: */ /* 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/. */ // HttpLog.h should generally be included first #include "HttpLog.h" #include #include "mozilla/ScopeExit.h" #include "mozilla/Sprintf.h" #include "mozilla/dom/nsCSPContext.h" #include "mozilla/glean/GleanMetrics.h" #include "mozilla/StoragePrincipalHelper.h" #include "nsCOMPtr.h" #include "nsContentSecurityUtils.h" #include "nsHttp.h" #include "nsHttpChannel.h" #include "nsHttpChannelAuthProvider.h" #include "nsHttpHandler.h" #include "nsIStreamConverter.h" #include "nsString.h" #include "nsICacheStorageService.h" #include "nsICacheStorage.h" #include "nsICacheEntry.h" #include "nsICryptoHash.h" #include "nsIEffectiveTLDService.h" #include "nsIHttpHeaderVisitor.h" #include "nsINetworkInterceptController.h" #include "nsIStringBundle.h" #include "nsIStreamListenerTee.h" #include "nsISeekableStream.h" #include "nsIProtocolProxyService2.h" #include "nsIURLQueryStringStripper.h" #include "nsIWebTransport.h" #include "nsCRT.h" #include "nsMimeTypes.h" #include "nsNetCID.h" #include "nsNetUtil.h" #include "nsIStreamTransportService.h" #include "prnetdb.h" #include "nsEscape.h" #include "nsComponentManagerUtils.h" #include "nsStreamUtils.h" #include "nsIOService.h" #include "nsDNSPrefetch.h" #include "nsChannelClassifier.h" #include "nsIRedirectResultListener.h" #include "mozilla/TimeStamp.h" #include "nsError.h" #include "nsPrintfCString.h" #include "nsAlgorithm.h" #include "nsQueryObject.h" #include "nsThreadUtils.h" #include "nsIConsoleService.h" #include "mozilla/AntiTrackingRedirectHeuristic.h" #include "mozilla/AntiTrackingUtils.h" #include "mozilla/Attributes.h" #include "mozilla/BasePrincipal.h" #include "mozilla/DebugOnly.h" #include "mozilla/PerfStats.h" #include "mozilla/ProfilerLabels.h" #include "mozilla/Components.h" #include "mozilla/StaticPrefs_network.h" #include "mozilla/StaticPrefs_privacy.h" #include "mozilla/StaticPrefs_security.h" #include "sslt.h" #include "nsCharSeparatedTokenizer.h" #include "nsContentUtils.h" #include "nsContentSecurityManager.h" #include "nsIClassOfService.h" #include "nsIPrincipal.h" #include "nsIScriptError.h" #include "nsIScriptSecurityManager.h" #include "nsITransportSecurityInfo.h" #include "nsIWebProgressListener.h" #include "LoadContextInfo.h" #include "netCore.h" #include "nsHttpTransaction.h" #include "nsICancelable.h" #include "nsIHttpChannelInternal.h" #include "nsIPrompt.h" #include "nsInputStreamPump.h" #include "nsURLHelper.h" #include "nsISocketTransport.h" #include "nsIStreamConverterService.h" #include "nsISiteSecurityService.h" #include "nsString.h" #include "mozilla/dom/PerformanceStorage.h" #include "mozilla/dom/ReferrerInfo.h" #include "mozilla/Telemetry.h" #include "AlternateServices.h" #include "NetworkMarker.h" #include "nsIHttpPushListener.h" #include "nsIDNSRecord.h" #include "mozilla/dom/Document.h" #include "nsICompressConvStats.h" #include "nsCORSListenerProxy.h" #include "nsISocketProvider.h" #include "mozilla/extensions/StreamFilterParent.h" #include "mozilla/net/Predictor.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/NullPrincipal.h" #include "CacheControlParser.h" #include "nsMixedContentBlocker.h" #include "CacheStorageService.h" #include "HttpChannelParent.h" #include "HttpTransactionParent.h" #include "ThirdPartyUtil.h" #include "InterceptedHttpChannel.h" #include "../../cache2/CacheFileUtils.h" #include "nsINetworkLinkService.h" #include "mozilla/ContentBlockingAllowList.h" #include "mozilla/dom/ServiceWorkerUtils.h" #include "mozilla/dom/nsHTTPSOnlyStreamListener.h" #include "mozilla/dom/nsHTTPSOnlyUtils.h" #include "mozilla/net/AsyncUrlChannelClassifier.h" #include "mozilla/net/CookieJarSettings.h" #include "mozilla/net/NeckoChannelParams.h" #include "mozilla/net/OpaqueResponseUtils.h" #include "mozilla/net/UrlClassifierFeatureFactory.h" #include "HttpTrafficAnalyzer.h" #include "mozilla/net/SocketProcessParent.h" #include "mozilla/dom/SecFetch.h" #include "mozilla/net/TRRService.h" #include "nsUnknownDecoder.h" #ifdef XP_WIN # include "HttpWinUtils.h" #endif #ifdef FUZZING # include "mozilla/StaticPrefs_fuzzing.h" #endif namespace mozilla { using namespace dom; namespace net { namespace { // True if the local cache should be bypassed when processing a request. #define BYPASS_LOCAL_CACHE(loadFlags, isPreferCacheLoadOverBypass) \ ((loadFlags) & (nsIRequest::LOAD_BYPASS_CACHE | \ nsICachingChannel::LOAD_BYPASS_LOCAL_CACHE) && \ !(((loadFlags) & nsIRequest::LOAD_FROM_CACHE) && \ (isPreferCacheLoadOverBypass))) #define RECOVER_FROM_CACHE_FILE_ERROR(result) \ ((result) == NS_ERROR_FILE_NOT_FOUND || \ (result) == NS_ERROR_FILE_CORRUPTED || (result) == NS_ERROR_OUT_OF_MEMORY) #define WRONG_RACING_RESPONSE_SOURCE(req) \ (mRaceCacheWithNetwork && \ (((mFirstResponseSource == RESPONSE_FROM_CACHE) && \ ((req) != mCachePump)) || \ ((mFirstResponseSource == RESPONSE_FROM_NETWORK) && \ ((req) != mTransactionPump)))) static NS_DEFINE_CID(kStreamListenerTeeCID, NS_STREAMLISTENERTEE_CID); void AccumulateCacheHitTelemetry(CacheDisposition hitOrMiss, nsIChannel* aChannel) { nsCString key("UNKNOWN"); nsCOMPtr loadInfo = aChannel->LoadInfo(); nsAutoCString contentType; if (NS_SUCCEEDED(aChannel->GetContentType(contentType))) { if (nsContentUtils::IsJavascriptMIMEType( NS_ConvertUTF8toUTF16(contentType))) { key.AssignLiteral("JAVASCRIPT"); } else if (StringBeginsWith(contentType, "text/css"_ns) || (loadInfo && loadInfo->GetExternalContentPolicyType() == ExtContentPolicy::TYPE_STYLESHEET)) { key.AssignLiteral("STYLESHEET"); } else if (StringBeginsWith(contentType, "application/wasm"_ns)) { key.AssignLiteral("WASM"); } else if (StringBeginsWith(contentType, "image/"_ns)) { key.AssignLiteral("IMAGE"); } else if (StringBeginsWith(contentType, "video/"_ns)) { key.AssignLiteral("MEDIA"); } else if (StringBeginsWith(contentType, "audio/"_ns)) { key.AssignLiteral("MEDIA"); } else if (!StringBeginsWith(contentType, nsLiteralCString(UNKNOWN_CONTENT_TYPE))) { key.AssignLiteral("OTHER"); } } Telemetry::LABELS_HTTP_CACHE_DISPOSITION_3 label = Telemetry::LABELS_HTTP_CACHE_DISPOSITION_3::Unresolved; switch (hitOrMiss) { case kCacheUnresolved: label = Telemetry::LABELS_HTTP_CACHE_DISPOSITION_3::Unresolved; break; case kCacheHit: label = Telemetry::LABELS_HTTP_CACHE_DISPOSITION_3::Hit; break; case kCacheHitViaReval: label = Telemetry::LABELS_HTTP_CACHE_DISPOSITION_3::HitViaReval; break; case kCacheMissedViaReval: label = Telemetry::LABELS_HTTP_CACHE_DISPOSITION_3::MissedViaReval; break; case kCacheMissed: label = Telemetry::LABELS_HTTP_CACHE_DISPOSITION_3::Missed; break; case kCacheUnknown: label = Telemetry::LABELS_HTTP_CACHE_DISPOSITION_3::Unknown; break; } Telemetry::AccumulateCategoricalKeyed(key, label); Telemetry::AccumulateCategoricalKeyed("ALL"_ns, label); } // Computes and returns a SHA1 hash of the input buffer. The input buffer // must be a null-terminated string. nsresult Hash(const char* buf, nsACString& hash) { nsresult rv; nsCOMPtr hasher = do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); rv = hasher->Init(nsICryptoHash::SHA1); NS_ENSURE_SUCCESS(rv, rv); rv = hasher->Update(reinterpret_cast(buf), strlen(buf)); NS_ENSURE_SUCCESS(rv, rv); rv = hasher->Finish(true, hash); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } } // unnamed namespace // We only treat 3xx responses as redirects if they have a Location header and // the status code is in a whitelist. bool nsHttpChannel::WillRedirect(const nsHttpResponseHead& response) { return IsRedirectStatus(response.Status()) && response.HasHeader(nsHttp::Location); } nsresult StoreAuthorizationMetaData(nsICacheEntry* entry, nsHttpRequestHead* requestHead); class MOZ_STACK_CLASS AutoRedirectVetoNotifier { public: explicit AutoRedirectVetoNotifier(nsHttpChannel* channel, nsresult& aRv) : mChannel(channel), mRv(aRv) { if (mChannel->LoadHasAutoRedirectVetoNotifier()) { MOZ_CRASH("Nested AutoRedirectVetoNotifier on the stack"); mChannel = nullptr; return; } mChannel->StoreHasAutoRedirectVetoNotifier(true); } ~AutoRedirectVetoNotifier() { ReportRedirectResult(mRv); } void RedirectSucceeded() { ReportRedirectResult(NS_OK); } private: nsHttpChannel* mChannel; bool mCalledReport = false; nsresult& mRv; void ReportRedirectResult(nsresult aRv); }; void AutoRedirectVetoNotifier::ReportRedirectResult(nsresult aRv) { if (!mChannel) return; if (mCalledReport) { return; } mCalledReport = true; mChannel->mRedirectChannel = nullptr; if (NS_SUCCEEDED(aRv)) { mChannel->RemoveAsNonTailRequest(); } nsCOMPtr vetoHook; NS_QueryNotificationCallbacks(mChannel, NS_GET_IID(nsIRedirectResultListener), getter_AddRefs(vetoHook)); nsHttpChannel* channel = mChannel; mChannel = nullptr; if (vetoHook) vetoHook->OnRedirectResult(aRv); // Drop after the notification channel->StoreHasAutoRedirectVetoNotifier(false); } //----------------------------------------------------------------------------- // nsHttpChannel //----------------------------------------------------------------------------- nsHttpChannel::nsHttpChannel() : HttpAsyncAborter(this) { LOG(("Creating nsHttpChannel [this=%p, nsIChannel=%p]\n", this, static_cast(this))); mChannelCreationTime = PR_Now(); mChannelCreationTimestamp = TimeStamp::Now(); } nsHttpChannel::~nsHttpChannel() { LOG(("Destroying nsHttpChannel [this=%p, nsIChannel=%p]\n", this, static_cast(this))); if (LOG_ENABLED()) { nsCString webExtension; this->GetPropertyAsACString(u"cancelledByExtension"_ns, webExtension); if (!webExtension.IsEmpty()) { LOG(("channel [%p] cancelled by extension [id=%s]", this, webExtension.get())); } } if (mAuthProvider) { DebugOnly rv = mAuthProvider->Disconnect(NS_ERROR_ABORT); MOZ_ASSERT(NS_SUCCEEDED(rv)); } ReleaseMainThreadOnlyReferences(); if (gHttpHandler) { gHttpHandler->RemoveHttpChannel(mChannelId); } } void nsHttpChannel::ReleaseMainThreadOnlyReferences() { if (NS_IsMainThread()) { // Already on main thread, let dtor to // take care of releasing references return; } nsTArray> arrayToRelease; arrayToRelease.AppendElement(mAuthProvider.forget()); arrayToRelease.AppendElement(mRedirectChannel.forget()); arrayToRelease.AppendElement(mPreflightChannel.forget()); arrayToRelease.AppendElement(mDNSPrefetch.forget()); MOZ_DIAGNOSTIC_ASSERT( !mEarlyHintObserver, "Early hint observer should have been released in ReleaseListeners()"); arrayToRelease.AppendElement(mEarlyHintObserver.forget()); MOZ_DIAGNOSTIC_ASSERT( !mChannelClassifier, "Channel classifier should have been released in ReleaseListeners()"); arrayToRelease.AppendElement( mChannelClassifier.forget().downcast()); MOZ_DIAGNOSTIC_ASSERT( !mWarningReporter, "Warning reporter should have been released in ReleaseListeners()"); arrayToRelease.AppendElement(mWarningReporter.forget()); NS_DispatchToMainThread(new ProxyReleaseRunnable(std::move(arrayToRelease))); } nsresult nsHttpChannel::Init(nsIURI* uri, uint32_t caps, nsProxyInfo* proxyInfo, uint32_t proxyResolveFlags, nsIURI* proxyURI, uint64_t channelId, ExtContentPolicyType aContentPolicyType, nsILoadInfo* aLoadInfo) { nsresult rv = HttpBaseChannel::Init(uri, caps, proxyInfo, proxyResolveFlags, proxyURI, channelId, aContentPolicyType, aLoadInfo); if (NS_FAILED(rv)) return rv; LOG1(("nsHttpChannel::Init [this=%p]\n", this)); return rv; } nsresult nsHttpChannel::AddSecurityMessage(const nsAString& aMessageTag, const nsAString& aMessageCategory) { if (mWarningReporter) { return mWarningReporter->ReportSecurityMessage(aMessageTag, aMessageCategory); } return HttpBaseChannel::AddSecurityMessage(aMessageTag, aMessageCategory); } NS_IMETHODIMP nsHttpChannel::LogBlockedCORSRequest(const nsAString& aMessage, const nsACString& aCategory, bool aIsWarning) { if (mWarningReporter) { return mWarningReporter->LogBlockedCORSRequest(aMessage, aCategory, aIsWarning); } return NS_ERROR_UNEXPECTED; } NS_IMETHODIMP nsHttpChannel::LogMimeTypeMismatch(const nsACString& aMessageName, bool aWarning, const nsAString& aURL, const nsAString& aContentType) { if (mWarningReporter) { return mWarningReporter->LogMimeTypeMismatch(aMessageName, aWarning, aURL, aContentType); } return NS_ERROR_UNEXPECTED; } //----------------------------------------------------------------------------- // nsHttpChannel //----------------------------------------------------------------------------- nsresult nsHttpChannel::PrepareToConnect() { LOG(("nsHttpChannel::PrepareToConnect [this=%p]\n", this)); // notify "http-on-modify-request-before-cookies" observers gHttpHandler->OnModifyRequestBeforeCookies(this); AddCookiesToRequest(); #ifdef XP_WIN auto prefEnabledForCurrentContainer = [&]() { uint32_t containerId = mLoadInfo->GetOriginAttributes().mUserContextId; // Make sure that the default container ID is 0 static_assert(nsIScriptSecurityManager::DEFAULT_USER_CONTEXT_ID == 0); nsPrintfCString prefName("network.http.windows-sso.container-enabled.%u", containerId); bool enabled = false; Preferences::GetBool(prefName.get(), &enabled); LOG(("Pref for %s is %d\n", prefName.get(), enabled)); return enabled; }; // If Windows 10 SSO is enabled, we potentially add auth information to // secure top level loads (DOCUMENTs) and iframes (SUBDOCUMENTs) that // aren't anonymous or private browsing. if (StaticPrefs::network_http_windows_sso_enabled() && mURI->SchemeIs("https") && !(mLoadFlags & LOAD_ANONYMOUS) && !mPrivateBrowsing) { ExtContentPolicyType type = mLoadInfo->GetExternalContentPolicyType(); if ((type == ExtContentPolicy::TYPE_DOCUMENT || type == ExtContentPolicy::TYPE_SUBDOCUMENT) && prefEnabledForCurrentContainer()) { AddWindowsSSO(this); } } #endif // notify "http-on-modify-request" observers CallOnModifyRequestObservers(); return CallOrWaitForResume( [](auto* self) { return self->OnBeforeConnect(); }); } void nsHttpChannel::HandleContinueCancellingByURLClassifier( nsresult aErrorCode) { MOZ_ASSERT( UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode(aErrorCode)); MOZ_ASSERT(!mCallOnResume, "How did that happen?"); if (mSuspendCount) { LOG( ("Waiting until resume HandleContinueCancellingByURLClassifier " "[this=%p]\n", this)); mCallOnResume = [aErrorCode](nsHttpChannel* self) { self->HandleContinueCancellingByURLClassifier(aErrorCode); return NS_OK; }; return; } LOG(("nsHttpChannel::HandleContinueCancellingByURLClassifier [this=%p]\n", this)); ContinueCancellingByURLClassifier(aErrorCode); } void nsHttpChannel::SetPriorityHeader() { uint8_t urgency = nsHttpHandler::UrgencyFromCoSFlags(mClassOfService.Flags()); bool incremental = mClassOfService.Incremental(); nsPrintfCString value( "%s", urgency != 3 ? nsPrintfCString("u=%d", urgency).get() : ""); if (incremental) { if (!value.IsEmpty()) { value.Append(", "); } value.Append("i"); } if (!value.IsEmpty()) { SetRequestHeader("Priority"_ns, value, false); } } nsresult nsHttpChannel::OnBeforeConnect() { nsresult rv = NS_OK; // Check if request was cancelled during suspend AFTER on-modify-request if (mCanceled) { return mStatus; } // Check to see if we should redirect this channel elsewhere by // nsIHttpChannel.redirectTo API request if (mAPIRedirectToURI) { return AsyncCall(&nsHttpChannel::HandleAsyncAPIRedirect); } // Note that we are only setting the "Upgrade-Insecure-Requests" request // header for *all* navigational requests instead of all requests as // defined in the spec, see: // https://www.w3.org/TR/upgrade-insecure-requests/#preference ExtContentPolicyType type = mLoadInfo->GetExternalContentPolicyType(); if (type == ExtContentPolicy::TYPE_DOCUMENT || type == ExtContentPolicy::TYPE_SUBDOCUMENT) { rv = SetRequestHeader("Upgrade-Insecure-Requests"_ns, "1"_ns, false); NS_ENSURE_SUCCESS(rv, rv); } if (LoadAuthRedirectedChannel()) { // This channel is a result of a redirect due to auth retry // We have already checked for HSTS upgarde in the redirecting channel. // We can safely skip those checks return ContinueOnBeforeConnect(false, rv); } SecFetch::AddSecFetchHeader(this); // Check to see if we should redirect this channel to the unstripped URI. To // revert the query stripping if the loading channel is in the content // blocking allow list. if (ContentBlockingAllowList::Check(this)) { nsCOMPtr unstrippedURI; mLoadInfo->GetUnstrippedURI(getter_AddRefs(unstrippedURI)); if (unstrippedURI) { return AsyncCall(&nsHttpChannel::HandleAsyncRedirectToUnstrippedURI); } } nsCOMPtr resultPrincipal; if (!mURI->SchemeIs("https")) { nsContentUtils::GetSecurityManager()->GetChannelResultPrincipal( this, getter_AddRefs(resultPrincipal)); } // Check if we already know about the HSTS status of the host nsISiteSecurityService* sss = gHttpHandler->GetSSService(); NS_ENSURE_TRUE(sss, NS_ERROR_OUT_OF_MEMORY); bool isSecureURI; OriginAttributes originAttributes; if (!StoragePrincipalHelper::GetOriginAttributesForHSTS(this, originAttributes)) { return NS_ERROR_FAILURE; } rv = sss->IsSecureURI(mURI, originAttributes, &isSecureURI); NS_ENSURE_SUCCESS(rv, rv); // Save that on the loadInfo so it can later be consumed by // SecurityInfo.sys.mjs mLoadInfo->SetHstsStatus(isSecureURI); RefPtr bc; mLoadInfo->GetBrowsingContext(getter_AddRefs(bc)); if (bc && bc->Top()->GetForceOffline()) { return NS_ERROR_OFFLINE; } // At this point it is no longer possible to call // HttpBaseChannel::UpgradeToSecure. StoreUpgradableToSecure(false); bool shouldUpgrade = LoadUpgradeToSecure(); if (mURI->SchemeIs("http")) { OriginAttributes originAttributes; if (!StoragePrincipalHelper::GetOriginAttributesForHSTS(this, originAttributes)) { return NS_ERROR_FAILURE; } if (!shouldUpgrade) { // Make sure http channel is released on main thread. // See bug 1539148 for details. nsMainThreadPtrHandle self( new nsMainThreadPtrHolder( "nsHttpChannel::OnBeforeConnect::self", this)); auto resultCallback = [self(self)](bool aResult, nsresult aStatus) { MOZ_ASSERT(NS_IsMainThread()); nsresult rv = self->MaybeUseHTTPSRRForUpgrade(aResult, aStatus); if (NS_FAILED(rv)) { self->CloseCacheEntry(false); Unused << self->AsyncAbort(rv); } }; bool willCallback = false; rv = NS_ShouldSecureUpgrade( mURI, mLoadInfo, resultPrincipal, LoadAllowSTS(), originAttributes, shouldUpgrade, std::move(resultCallback), willCallback); // If the request gets upgraded because of the HTTPS-Only mode, but no // event listener has been registered so far, we want to do that here. uint32_t httpOnlyStatus = mLoadInfo->GetHttpsOnlyStatus(); if (httpOnlyStatus & nsILoadInfo::HTTPS_ONLY_UPGRADED_LISTENER_NOT_REGISTERED) { RefPtr httpsOnlyListener = new nsHTTPSOnlyStreamListener(mListener, mLoadInfo); mListener = httpsOnlyListener; httpOnlyStatus ^= nsILoadInfo::HTTPS_ONLY_UPGRADED_LISTENER_NOT_REGISTERED; httpOnlyStatus |= nsILoadInfo::HTTPS_ONLY_UPGRADED_LISTENER_REGISTERED; mLoadInfo->SetHttpsOnlyStatus(httpOnlyStatus); } LOG( ("nsHttpChannel::OnBeforeConnect " "[this=%p willCallback=%d rv=%" PRIx32 "]\n", this, willCallback, static_cast(rv))); if (NS_FAILED(rv) || MOZ_UNLIKELY(willCallback)) { return rv; } } } return MaybeUseHTTPSRRForUpgrade(shouldUpgrade, NS_OK); } nsresult nsHttpChannel::MaybeUseHTTPSRRForUpgrade(bool aShouldUpgrade, nsresult aStatus) { if (NS_FAILED(aStatus)) { return aStatus; } if (mURI->SchemeIs("https") || aShouldUpgrade || !LoadUseHTTPSSVC()) { return ContinueOnBeforeConnect(aShouldUpgrade, aStatus); } auto shouldSkipUpgradeWithHTTPSRR = [&]() -> bool { // Skip using HTTPS RR to upgrade when this is not a top-level load and the // loading principal is http. if ((mLoadInfo->GetExternalContentPolicyType() != ExtContentPolicy::TYPE_DOCUMENT) && (mLoadInfo->GetLoadingPrincipal() && mLoadInfo->GetLoadingPrincipal()->SchemeIs("http"))) { return true; } nsAutoCString uriHost; mURI->GetAsciiHost(uriHost); if (gHttpHandler->IsHostExcludedForHTTPSRR(uriHost)) { return true; } if (nsHTTPSOnlyUtils::IsUpgradeDowngradeEndlessLoop( mURI, mLoadInfo, {nsHTTPSOnlyUtils::UpgradeDowngradeEndlessLoopOptions:: EnforceForHTTPSRR})) { // Add the host to a excluded list because: // 1. We don't need to do the same check again. // 2. Other subresources in the same host will be also excluded. gHttpHandler->ExcludeHTTPSRRHost(uriHost); LOG(("[%p] skip HTTPS upgrade for host [%s]", this, uriHost.get())); return true; } return false; }; if (shouldSkipUpgradeWithHTTPSRR()) { StoreUseHTTPSSVC(false); // If the website does not want to use HTTPS RR, we should set // NS_HTTP_DISALLOW_HTTPS_RR. This is for avoiding HTTPS RR being used by // the transaction. mCaps |= NS_HTTP_DISALLOW_HTTPS_RR; return ContinueOnBeforeConnect(aShouldUpgrade, aStatus); } if (mHTTPSSVCRecord.isSome()) { LOG(( "nsHttpChannel::MaybeUseHTTPSRRForUpgrade [%p] mHTTPSSVCRecord is some", this)); StoreWaitHTTPSSVCRecord(false); bool hasHTTPSRR = (mHTTPSSVCRecord.ref() != nullptr); return ContinueOnBeforeConnect(hasHTTPSRR, aStatus, hasHTTPSRR); } auto dnsStrategy = GetProxyDNSStrategy(); if (!(dnsStrategy & DNS_PREFETCH_ORIGIN)) { return ContinueOnBeforeConnect(aShouldUpgrade, aStatus); } LOG(("nsHttpChannel::MaybeUseHTTPSRRForUpgrade [%p] wait for HTTPS RR", this)); OriginAttributes originAttributes; StoragePrincipalHelper::GetOriginAttributesForHTTPSRR(this, originAttributes); RefPtr resolver = new nsDNSPrefetch(mURI, originAttributes, nsIRequest::GetTRRMode()); nsWeakPtr weakPtrThis( do_GetWeakReference(static_cast(this))); nsresult rv = resolver->FetchHTTPSSVC( mCaps & NS_HTTP_REFRESH_DNS, !LoadUseHTTPSSVC(), [weakPtrThis](nsIDNSHTTPSSVCRecord* aRecord) { nsCOMPtr channel = do_QueryReferent(weakPtrThis); RefPtr httpChannelImpl = do_QueryObject(channel); if (httpChannelImpl) { httpChannelImpl->OnHTTPSRRAvailable(aRecord); } }); if (NS_FAILED(rv)) { LOG((" FetchHTTPSSVC failed with 0x%08" PRIx32, static_cast(rv))); return ContinueOnBeforeConnect(aShouldUpgrade, aStatus); } StoreWaitHTTPSSVCRecord(true); return NS_OK; } nsresult nsHttpChannel::ContinueOnBeforeConnect(bool aShouldUpgrade, nsresult aStatus, bool aUpgradeWithHTTPSRR) { LOG( ("nsHttpChannel::ContinueOnBeforeConnect " "[this=%p aShouldUpgrade=%d rv=%" PRIx32 "]\n", this, aShouldUpgrade, static_cast(aStatus))); MOZ_ASSERT(!LoadWaitHTTPSSVCRecord()); if (NS_FAILED(aStatus)) { return aStatus; } if (aShouldUpgrade && !mURI->SchemeIs("https")) { mozilla::glean::networking::https_upgrade_with_https_rr .Get(aUpgradeWithHTTPSRR ? "https_rr"_ns : "others"_ns) .Add(1); return AsyncCall(&nsHttpChannel::HandleAsyncRedirectChannelToHttps); } // ensure that we are using a valid hostname if (!net_IsValidHostName(nsDependentCString(mConnectionInfo->Origin()))) { return NS_ERROR_UNKNOWN_HOST; } if (mUpgradeProtocolCallback) { // Websockets can run over HTTP/2, but other upgrades can't. if (mUpgradeProtocol.EqualsLiteral("websocket") && StaticPrefs::network_http_http2_websockets()) { // Need to tell the conn manager that we're ok with http/2 even with // the allow keepalive bit not set. That bit needs to stay off, // though, in case we end up having to fallback to http/1.1 (where // we absolutely do want to disable keepalive). mCaps |= NS_HTTP_ALLOW_SPDY_WITHOUT_KEEPALIVE; } else { mCaps |= NS_HTTP_DISALLOW_SPDY; } // Upgrades cannot use HTTP/3. mCaps |= NS_HTTP_DISALLOW_HTTP3; // Because NS_HTTP_STICKY_CONNECTION breaks HTTPS RR fallabck mecnahism, we // can not use HTTPS RR for upgrade requests. mCaps |= NS_HTTP_DISALLOW_HTTPS_RR; } if (LoadIsTRRServiceChannel()) { mCaps |= NS_HTTP_LARGE_KEEPALIVE; mCaps |= NS_HTTP_DISALLOW_HTTPS_RR; } if (mTransactionSticky) { MOZ_ASSERT(LoadAuthRedirectedChannel()); // this means this is a redirected channel channel due to auth retry and a // connection based auth scheme was used // we have a reference to the old-transaction with sticky connection which // we need to use mCaps |= NS_HTTP_STICKY_CONNECTION; } mCaps |= NS_HTTP_TRR_FLAGS_FROM_MODE(nsIRequest::GetTRRMode()); // Finalize ConnectionInfo flags before SpeculativeConnect mConnectionInfo->SetAnonymous((mLoadFlags & LOAD_ANONYMOUS) != 0); mConnectionInfo->SetPrivate(mPrivateBrowsing); mConnectionInfo->SetNoSpdy(mCaps & NS_HTTP_DISALLOW_SPDY); mConnectionInfo->SetBeConservative((mCaps & NS_HTTP_BE_CONSERVATIVE) || LoadBeConservative()); mConnectionInfo->SetTlsFlags(mTlsFlags); mConnectionInfo->SetIsTrrServiceChannel(LoadIsTRRServiceChannel()); mConnectionInfo->SetTRRMode(nsIRequest::GetTRRMode()); mConnectionInfo->SetIPv4Disabled(mCaps & NS_HTTP_DISABLE_IPV4); mConnectionInfo->SetIPv6Disabled(mCaps & NS_HTTP_DISABLE_IPV6); mConnectionInfo->SetAnonymousAllowClientCert( (mLoadFlags & LOAD_ANONYMOUS_ALLOW_CLIENT_CERT) != 0); if (mWebTransportSessionEventListener) { nsTArray> aServerCertHashes; nsresult rv; nsCOMPtr wtconSettings = do_QueryInterface(mWebTransportSessionEventListener, &rv); NS_ENSURE_SUCCESS(rv, rv); wtconSettings->GetServerCertificateHashes(aServerCertHashes); gHttpHandler->ConnMgr()->StoreServerCertHashes( mConnectionInfo, gHttpHandler->IsHttp2Excluded(mConnectionInfo), !Http3Allowed(), std::move(aServerCertHashes)); } // notify "http-on-before-connect" observers gHttpHandler->OnBeforeConnect(this); return CallOrWaitForResume([](auto* self) { return self->Connect(); }); } nsresult nsHttpChannel::Connect() { LOG(("nsHttpChannel::Connect [this=%p]\n", this)); // Don't allow resuming when cache must be used if (LoadResuming() && (mLoadFlags & LOAD_ONLY_FROM_CACHE)) { LOG(("Resuming from cache is not supported yet")); return NS_ERROR_DOCUMENT_NOT_CACHED; } if (ShouldIntercept()) { return RedirectToInterceptedChannel(); } // Step 8.18 of HTTP-network-or-cache fetch // https://fetch.spec.whatwg.org/#http-network-or-cache-fetch nsAutoCString rangeVal; if (NS_SUCCEEDED(GetRequestHeader("Range"_ns, rangeVal))) { SetRequestHeader("Accept-Encoding"_ns, "identity"_ns, true); } bool isTrackingResource = IsThirdPartyTrackingResource(); LOG(("nsHttpChannel %p tracking resource=%d, cos=%lu, inc=%d", this, isTrackingResource, mClassOfService.Flags(), mClassOfService.Incremental())); if (isTrackingResource) { AddClassFlags(nsIClassOfService::Tail); } if (WaitingForTailUnblock()) { MOZ_DIAGNOSTIC_ASSERT(!mOnTailUnblock); mOnTailUnblock = &nsHttpChannel::ConnectOnTailUnblock; return NS_OK; } return ConnectOnTailUnblock(); } nsresult nsHttpChannel::ConnectOnTailUnblock() { nsresult rv; LOG(("nsHttpChannel::ConnectOnTailUnblock [this=%p]\n", this)); // Consider opening a TCP connection right away. SpeculativeConnect(); // open a cache entry for this channel... rv = OpenCacheEntry(mURI->SchemeIs("https")); // do not continue if asyncOpenCacheEntry is in progress if (AwaitingCacheCallbacks()) { LOG(("nsHttpChannel::Connect %p AwaitingCacheCallbacks forces async\n", this)); MOZ_ASSERT(NS_SUCCEEDED(rv), "Unexpected state"); if (mNetworkTriggered && mWaitingForProxy) { // Someone has called TriggerNetwork(), meaning we are racing the // network with the cache. mWaitingForProxy = false; return ContinueConnect(); } return NS_OK; } if (NS_FAILED(rv)) { LOG(("OpenCacheEntry failed [rv=%" PRIx32 "]\n", static_cast(rv))); // if this channel is only allowed to pull from the cache, then // we must fail if we were unable to open a cache entry. if (mLoadFlags & LOAD_ONLY_FROM_CACHE) { return NS_ERROR_DOCUMENT_NOT_CACHED; } // otherwise, let's just proceed without using the cache. } if (mRaceCacheWithNetwork && ((mCacheEntry && !mCachedContentIsValid && (mDidReval || LoadCachedContentIsPartial())) || mIgnoreCacheEntry)) { // We won't send the conditional request because the unconditional // request was already sent (see bug 1377223). AccumulateCategorical( Telemetry::LABELS_NETWORK_RACE_CACHE_VALIDATION::NotSent); } // When racing, if OnCacheEntryAvailable is called before AsyncOpenURI // returns, then we may not have started reading from the cache. // If the content is valid, we should attempt to do so, as technically the // cache has won the race. if (mRaceCacheWithNetwork && mCachedContentIsValid) { Unused << ReadFromCache(true); } return TriggerNetwork(); } nsresult nsHttpChannel::ContinueConnect() { // If we need to start a CORS preflight, do it now! // Note that it is important to do this before the early returns below. if (!LoadIsCorsPreflightDone() && LoadRequireCORSPreflight()) { MOZ_ASSERT(!mPreflightChannel); nsresult rv = nsCORSListenerProxy::StartCORSPreflight( this, this, mUnsafeHeaders, getter_AddRefs(mPreflightChannel)); return rv; } MOZ_RELEASE_ASSERT(!LoadRequireCORSPreflight() || LoadIsCorsPreflightDone(), "CORS preflight must have been finished by the time we " "do the rest of ContinueConnect"); // we may or may not have a cache entry at this point if (mCacheEntry) { // read straight from the cache if possible... if (mCachedContentIsValid) { nsRunnableMethod* event = nullptr; nsresult rv; if (!LoadCachedContentIsPartial()) { rv = AsyncCall(&nsHttpChannel::AsyncOnExamineCachedResponse, &event); if (NS_FAILED(rv)) { LOG((" AsyncCall failed (%08x)", static_cast(rv))); } } rv = ReadFromCache(true); if (NS_FAILED(rv) && event) { event->Revoke(); } AccumulateCacheHitTelemetry(kCacheHit, this); mCacheDisposition = kCacheHit; return rv; } if (mLoadFlags & LOAD_ONLY_FROM_CACHE) { // the cache contains the requested resource, but it must be // validated before we can reuse it. since we are not allowed // to hit the net, there's nothing more to do. the document // is effectively not in the cache. LOG((" !mCachedContentIsValid && mLoadFlags & LOAD_ONLY_FROM_CACHE")); return NS_ERROR_DOCUMENT_NOT_CACHED; } } else if (mLoadFlags & LOAD_ONLY_FROM_CACHE) { LOG((" !mCacheEntry && mLoadFlags & LOAD_ONLY_FROM_CACHE")); return NS_ERROR_DOCUMENT_NOT_CACHED; } if (mLoadFlags & LOAD_NO_NETWORK_IO) { LOG((" mLoadFlags & LOAD_NO_NETWORK_IO")); return NS_ERROR_DOCUMENT_NOT_CACHED; } // hit the net... nsresult rv = DoConnect(mTransactionSticky); mTransactionSticky = nullptr; return rv; } nsresult nsHttpChannel::DoConnect(HttpTransactionShell* aTransWithStickyConn) { LOG(("nsHttpChannel::DoConnect [this=%p]\n", this)); if (!mDNSBlockingPromise.IsEmpty()) { LOG((" waiting for DNS prefetch")); // Transaction is passed only from auth retry for which we will definitely // not block on DNS to alter the origin server name for IP; it has already // been done. MOZ_ASSERT(!aTransWithStickyConn); MOZ_ASSERT(mDNSBlockingThenable); nsCOMPtr target(do_GetMainThread()); RefPtr self(this); mDNSBlockingThenable->Then( target, __func__, [self](const nsCOMPtr& aRec) { nsresult rv = self->DoConnectActual(nullptr); if (NS_FAILED(rv)) { self->CloseCacheEntry(false); Unused << self->AsyncAbort(rv); } }, [self](nsresult err) { self->CloseCacheEntry(false); Unused << self->AsyncAbort(err); }); // The connection will continue when the promise is resolved in // OnLookupComplete. return NS_OK; } return DoConnectActual(aTransWithStickyConn); } nsresult nsHttpChannel::DoConnectActual( HttpTransactionShell* aTransWithStickyConn) { LOG(("nsHttpChannel::DoConnectActual [this=%p, aTransWithStickyConn=%p]\n", this, aTransWithStickyConn)); nsresult rv = SetupTransaction(); if (NS_FAILED(rv)) { return rv; } if (aTransWithStickyConn) { rv = gHttpHandler->InitiateTransactionWithStickyConn( mTransaction, mPriority, aTransWithStickyConn); } else { rv = gHttpHandler->InitiateTransaction(mTransaction, mPriority); } if (NS_FAILED(rv)) { return rv; } rv = mTransaction->AsyncRead(this, getter_AddRefs(mTransactionPump)); if (NS_FAILED(rv)) { return rv; } uint32_t suspendCount = mSuspendCount; if (LoadAsyncResumePending()) { LOG( (" Suspend()'ing transaction pump once because of async resume pending" ", sc=%u, pump=%p, this=%p", suspendCount, mTransactionPump.get(), this)); ++suspendCount; } while (suspendCount--) { mTransactionPump->Suspend(); } return NS_OK; } void nsHttpChannel::SpeculativeConnect() { // Before we take the latency hit of dealing with the cache, try and // get the TCP (and SSL) handshakes going so they can overlap. // don't speculate if we are offline, when doing http upgrade (i.e. // websockets bootstrap), or if we can't do keep-alive (because then we // couldn't reuse the speculative connection anyhow). RefPtr bc; mLoadInfo->GetBrowsingContext(getter_AddRefs(bc)); if (gIOService->IsOffline() || mUpgradeProtocolCallback || !(mCaps & NS_HTTP_ALLOW_KEEPALIVE) || (bc && bc->Top()->GetForceOffline())) { return; } // LOAD_ONLY_FROM_CACHE and LOAD_NO_NETWORK_IO must not hit network. // LOAD_FROM_CACHE is unlikely to hit network, so skip preconnects for it. if (mLoadFlags & (LOAD_ONLY_FROM_CACHE | LOAD_FROM_CACHE | LOAD_NO_NETWORK_IO)) { return; } if (LoadAllowStaleCacheContent()) { return; } nsCOMPtr callbacks; NS_NewNotificationCallbacksAggregation(mCallbacks, mLoadGroup, getter_AddRefs(callbacks)); if (!callbacks) return; Unused << gHttpHandler->SpeculativeConnect( mConnectionInfo, callbacks, mCaps & (NS_HTTP_DISALLOW_SPDY | NS_HTTP_TRR_MODE_MASK | NS_HTTP_DISABLE_IPV4 | NS_HTTP_DISABLE_IPV6 | NS_HTTP_DISALLOW_HTTP3 | NS_HTTP_REFRESH_DNS), gHttpHandler->EchConfigEnabled()); } void nsHttpChannel::DoNotifyListenerCleanup() { // We don't need this info anymore CleanRedirectCacheChainIfNecessary(); } void nsHttpChannel::ReleaseListeners() { HttpBaseChannel::ReleaseListeners(); mChannelClassifier = nullptr; mWarningReporter = nullptr; mEarlyHintObserver = nullptr; mWebTransportSessionEventListener = nullptr; for (StreamFilterRequest& request : mStreamFilterRequests) { request.mPromise->Reject(false, __func__); } mStreamFilterRequests.Clear(); } void nsHttpChannel::DoAsyncAbort(nsresult aStatus) { Unused << AsyncAbort(aStatus); } void nsHttpChannel::HandleAsyncRedirect() { MOZ_ASSERT(!mCallOnResume, "How did that happen?"); if (mSuspendCount) { LOG(("Waiting until resume to do async redirect [this=%p]\n", this)); mCallOnResume = [](nsHttpChannel* self) { self->HandleAsyncRedirect(); return NS_OK; }; return; } nsresult rv = NS_OK; LOG(("nsHttpChannel::HandleAsyncRedirect [this=%p]\n", this)); // since this event is handled asynchronously, it is possible that this // channel could have been canceled, in which case there would be no point // in processing the redirect. if (NS_SUCCEEDED(mStatus)) { PushRedirectAsyncFunc(&nsHttpChannel::ContinueHandleAsyncRedirect); rv = AsyncProcessRedirection(mResponseHead->Status()); if (NS_FAILED(rv)) { PopRedirectAsyncFunc(&nsHttpChannel::ContinueHandleAsyncRedirect); // TODO: if !DoNotRender3xxBody(), render redirect body instead. // But first we need to cache 3xx bodies (bug 748510) rv = ContinueHandleAsyncRedirect(rv); MOZ_ASSERT(NS_SUCCEEDED(rv)); } } else { rv = ContinueHandleAsyncRedirect(mStatus); MOZ_ASSERT(NS_SUCCEEDED(rv)); } } nsresult nsHttpChannel::ContinueHandleAsyncRedirect(nsresult rv) { if (NS_FAILED(rv)) { // If AsyncProcessRedirection fails, then we have to send out the // OnStart/OnStop notifications. LOG(("ContinueHandleAsyncRedirect got failure result [rv=%" PRIx32 "]\n", static_cast(rv))); bool redirectsEnabled = !mLoadInfo->GetDontFollowRedirects(); if (redirectsEnabled) { // TODO: stop failing original channel if redirect vetoed? mStatus = rv; DoNotifyListener(); // Blow away cache entry if we couldn't process the redirect // for some reason (the cache entry might be corrupt). if (mCacheEntry) { mCacheEntry->AsyncDoom(nullptr); } } else { DoNotifyListener(); } } CloseCacheEntry(true); StoreIsPending(false); if (mLoadGroup) mLoadGroup->RemoveRequest(this, nullptr, mStatus); return NS_OK; } void nsHttpChannel::HandleAsyncNotModified() { MOZ_ASSERT(!mCallOnResume, "How did that happen?"); if (mSuspendCount) { LOG(("Waiting until resume to do async not-modified [this=%p]\n", this)); mCallOnResume = [](nsHttpChannel* self) { self->HandleAsyncNotModified(); return NS_OK; }; return; } LOG(("nsHttpChannel::HandleAsyncNotModified [this=%p]\n", this)); DoNotifyListener(); CloseCacheEntry(false); StoreIsPending(false); if (mLoadGroup) mLoadGroup->RemoveRequest(this, nullptr, mStatus); } nsresult nsHttpChannel::SetupTransaction() { LOG(("nsHttpChannel::SetupTransaction [this=%p, cos=%lu, inc=%d prio=%d]\n", this, mClassOfService.Flags(), mClassOfService.Incremental(), mPriority)); NS_ENSURE_TRUE(!mTransaction, NS_ERROR_ALREADY_INITIALIZED); nsresult rv; mozilla::MutexAutoLock lock(mRCWNLock); if (StaticPrefs::network_http_priority_header_enabled()) { SetPriorityHeader(); } // If we're racing cache with network, conditional or byte range header // could be added in OnCacheEntryCheck. We cannot send conditional request // without having the entry, so we need to remove the headers here and // ignore the cache entry in OnCacheEntryAvailable. if (mRaceCacheWithNetwork && AwaitingCacheCallbacks()) { if (mDidReval) { LOG((" Removing conditional request headers")); UntieValidationRequest(); mDidReval = false; mIgnoreCacheEntry = true; } if (LoadCachedContentIsPartial()) { LOG((" Removing byte range request headers")); UntieByteRangeRequest(); StoreCachedContentIsPartial(false); mIgnoreCacheEntry = true; } if (mIgnoreCacheEntry) { mAvailableCachedAltDataType.Truncate(); StoreDeliveringAltData(false); mAltDataLength = -1; mCacheInputStream.CloseAndRelease(); } } StoreUsedNetwork(1); if (!LoadAllowSpdy()) { mCaps |= NS_HTTP_DISALLOW_SPDY; } if (!LoadAllowHttp3()) { mCaps |= NS_HTTP_DISALLOW_HTTP3; } if (LoadBeConservative()) { mCaps |= NS_HTTP_BE_CONSERVATIVE; } if (mLoadFlags & LOAD_ANONYMOUS_ALLOW_CLIENT_CERT) { mCaps |= NS_HTTP_LOAD_ANONYMOUS_CONNECT_ALLOW_CLIENT_CERT; } if (nsContentUtils::ShouldResistFingerprinting(this, RFPTarget::HttpUserAgent)) { mCaps |= NS_HTTP_USE_RFP; } // Use the URI path if not proxying (transparent proxying such as proxy // CONNECT does not count here). Also figure out what HTTP version to use. nsAutoCString buf, path; nsCString* requestURI; // This is the normal e2e H1 path syntax "/index.html" rv = mURI->GetPathQueryRef(path); if (NS_FAILED(rv)) { return rv; } // path may contain UTF-8 characters, so ensure that they're escaped. if (NS_EscapeURL(path.get(), path.Length(), esc_OnlyNonASCII | esc_Spaces, buf)) { requestURI = &buf; } else { requestURI = &path; } // trim off the #ref portion if any... int32_t ref1 = requestURI->FindChar('#'); if (ref1 != kNotFound) { requestURI->SetLength(ref1); } if (mConnectionInfo->UsingConnect() || !mConnectionInfo->UsingHttpProxy()) { mRequestHead.SetVersion(gHttpHandler->HttpVersion()); } else { mRequestHead.SetPath(*requestURI); // RequestURI should be the absolute uri H1 proxy syntax // "http://foo/index.html" so we will overwrite the relative version in // requestURI rv = mURI->GetUserPass(buf); if (NS_FAILED(rv)) return rv; if (!buf.IsEmpty() && ((strncmp(mSpec.get(), "http:", 5) == 0) || strncmp(mSpec.get(), "https:", 6) == 0)) { nsCOMPtr tempURI = nsIOService::CreateExposableURI(mURI); rv = tempURI->GetAsciiSpec(path); if (NS_FAILED(rv)) return rv; requestURI = &path; } else { requestURI = &mSpec; } // trim off the #ref portion if any... int32_t ref2 = requestURI->FindChar('#'); if (ref2 != kNotFound) { requestURI->SetLength(ref2); } mRequestHead.SetVersion(gHttpHandler->ProxyHttpVersion()); } mRequestHead.SetRequestURI(*requestURI); // set the request time for cache expiration calculations mRequestTime = NowInSeconds(); StoreRequestTimeInitialized(true); // if doing a reload, force end-to-end if (mLoadFlags & LOAD_BYPASS_CACHE) { // We need to send 'Pragma:no-cache' to inhibit proxy caching even if // no proxy is configured since we might be talking with a transparent // proxy, i.e. one that operates at the network level. See bug #14772. rv = mRequestHead.SetHeaderOnce(nsHttp::Pragma, "no-cache", true); MOZ_ASSERT(NS_SUCCEEDED(rv)); // If we're configured to speak HTTP/1.1 then also send 'Cache-control: // no-cache' if (mRequestHead.Version() >= HttpVersion::v1_1) { rv = mRequestHead.SetHeaderOnce(nsHttp::Cache_Control, "no-cache", true); MOZ_ASSERT(NS_SUCCEEDED(rv)); } } else if ((mLoadFlags & VALIDATE_ALWAYS) && !LoadCacheEntryIsWriteOnly()) { // We need to send 'Cache-Control: max-age=0' to force each cache along // the path to the origin server to revalidate its own entry, if any, // with the next cache or server. See bug #84847. // // If we're configured to speak HTTP/1.0 then just send 'Pragma: no-cache' if (mRequestHead.Version() >= HttpVersion::v1_1) { rv = mRequestHead.SetHeaderOnce(nsHttp::Cache_Control, "max-age=0", true); } else { rv = mRequestHead.SetHeaderOnce(nsHttp::Pragma, "no-cache", true); } MOZ_ASSERT(NS_SUCCEEDED(rv)); } if (LoadResuming()) { char byteRange[32]; SprintfLiteral(byteRange, "bytes=%" PRIu64 "-", mStartPos); rv = mRequestHead.SetHeader(nsHttp::Range, nsDependentCString(byteRange)); MOZ_ASSERT(NS_SUCCEEDED(rv)); if (!mEntityID.IsEmpty()) { // Also, we want an error if this resource changed in the meantime // Format of the entity id is: escaped_etag/size/lastmod nsCString::const_iterator start, end, slash; mEntityID.BeginReading(start); mEntityID.EndReading(end); mEntityID.BeginReading(slash); if (FindCharInReadable('/', slash, end)) { nsAutoCString ifMatch; rv = mRequestHead.SetHeader( nsHttp::If_Match, NS_UnescapeURL(Substring(start, slash), 0, ifMatch)); MOZ_ASSERT(NS_SUCCEEDED(rv)); ++slash; // Incrementing, so that searching for '/' won't find // the same slash again } if (FindCharInReadable('/', slash, end)) { rv = mRequestHead.SetHeader(nsHttp::If_Unmodified_Since, Substring(++slash, end)); MOZ_ASSERT(NS_SUCCEEDED(rv)); } } } // create wrapper for this channel's notification callbacks nsCOMPtr callbacks; NS_NewNotificationCallbacksAggregation(mCallbacks, mLoadGroup, getter_AddRefs(callbacks)); // create the transaction object if (nsIOService::UseSocketProcess()) { if (NS_WARN_IF(!gIOService->SocketProcessReady())) { return NS_ERROR_NOT_AVAILABLE; } SocketProcessParent* socketProcess = SocketProcessParent::GetSingleton(); if (!socketProcess->CanSend()) { return NS_ERROR_NOT_AVAILABLE; } nsCOMPtr parentChannel; NS_QueryNotificationCallbacks(this, parentChannel); RefPtr documentChannelParent = do_QueryObject(parentChannel); // See HttpTransactionChild::CanSendODAToContentProcessDirectly() and // nsHttpChannel::CallOnStartRequest() for the reason why we need to know if // this is a document load. We only send ODA directly to child process for // non document loads. RefPtr transParent = new HttpTransactionParent(!!documentChannelParent); LOG1(("nsHttpChannel %p created HttpTransactionParent %p\n", this, transParent.get())); // Since OnStopRequest could be sent to child process from socket process // directly, we need to store these two values in HttpTransactionChild and // forward to child process until HttpTransactionChild::OnStopRequest is // called. transParent->SetRedirectTimestamp(mRedirectStartTimeStamp, mRedirectEndTimeStamp); if (socketProcess) { MOZ_ALWAYS_TRUE( socketProcess->SendPHttpTransactionConstructor(transParent)); } mTransaction = transParent; } else { mTransaction = new nsHttpTransaction(); LOG1(("nsHttpChannel %p created nsHttpTransaction %p\n", this, mTransaction.get())); } // Save the mapping of channel id and the channel. We need this mapping for // nsIHttpActivityObserver. gHttpHandler->AddHttpChannel(mChannelId, ToSupports(this)); // See bug #466080. Transfer LOAD_ANONYMOUS flag to socket-layer. if (mLoadFlags & LOAD_ANONYMOUS) mCaps |= NS_HTTP_LOAD_ANONYMOUS; if (LoadTimingEnabled()) mCaps |= NS_HTTP_TIMING_ENABLED; if (mUpgradeProtocolCallback) { rv = mRequestHead.SetHeader(nsHttp::Upgrade, mUpgradeProtocol, false); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = mRequestHead.SetHeaderOnce(nsHttp::Connection, nsHttp::Upgrade.get(), true); MOZ_ASSERT(NS_SUCCEEDED(rv)); mCaps |= NS_HTTP_STICKY_CONNECTION; mCaps &= ~NS_HTTP_ALLOW_KEEPALIVE; } if (mWebTransportSessionEventListener) { mCaps |= NS_HTTP_STICKY_CONNECTION; } nsCOMPtr pushListener; NS_QueryNotificationCallbacks(mCallbacks, mLoadGroup, NS_GET_IID(nsIHttpPushListener), getter_AddRefs(pushListener)); HttpTransactionShell::OnPushCallback pushCallback = nullptr; if (pushListener) { mCaps |= NS_HTTP_ONPUSH_LISTENER; nsWeakPtr weakPtrThis( do_GetWeakReference(static_cast(this))); pushCallback = [weakPtrThis](uint32_t aPushedStreamId, const nsACString& aUrl, const nsACString& aRequestString, HttpTransactionShell* aTransaction) { if (nsCOMPtr channel = do_QueryReferent(weakPtrThis)) { return static_cast(channel.get()) ->OnPush(aPushedStreamId, aUrl, aRequestString, aTransaction); } return NS_ERROR_NOT_AVAILABLE; }; } EnsureBrowserId(); EnsureRequestContext(); HttpTrafficCategory category = CreateTrafficCategory(); std::function observer; if (mTransactionObserver) { observer = [transactionObserver{std::move(mTransactionObserver)}]( TransactionObserverResult&& aResult) { transactionObserver->Complete(aResult.versionOk(), aResult.authOk(), aResult.closeReason()); }; } mTransaction->SetIsForWebTransport(!!mWebTransportSessionEventListener); rv = mTransaction->Init( mCaps, mConnectionInfo, &mRequestHead, mUploadStream, mReqContentLength, LoadUploadStreamHasHeaders(), GetCurrentSerialEventTarget(), callbacks, this, mBrowserId, category, mRequestContext, mClassOfService, mInitialRwin, LoadResponseTimeoutEnabled(), mChannelId, std::move(observer), std::move(pushCallback), mTransWithPushedStream, mPushedStreamId); if (NS_FAILED(rv)) { mTransaction = nullptr; return rv; } return rv; } HttpTrafficCategory nsHttpChannel::CreateTrafficCategory() { MOZ_ASSERT(!mFirstPartyClassificationFlags || !mThirdPartyClassificationFlags); if (!StaticPrefs::network_traffic_analyzer_enabled()) { return HttpTrafficCategory::eInvalid; } HttpTrafficAnalyzer::ClassOfService cos; { if ((mClassOfService.Flags() & nsIClassOfService::Leader) && mLoadInfo->GetExternalContentPolicyType() == ExtContentPolicy::TYPE_SCRIPT) { cos = HttpTrafficAnalyzer::ClassOfService::eLeader; } else if (mLoadFlags & nsIRequest::LOAD_BACKGROUND) { cos = HttpTrafficAnalyzer::ClassOfService::eBackground; } else { cos = HttpTrafficAnalyzer::ClassOfService::eOther; } } bool isThirdParty = AntiTrackingUtils::IsThirdPartyChannel(this); HttpTrafficAnalyzer::TrackingClassification tc; { uint32_t flags = isThirdParty ? mThirdPartyClassificationFlags : mFirstPartyClassificationFlags; using CF = nsIClassifiedChannel::ClassificationFlags; using TC = HttpTrafficAnalyzer::TrackingClassification; if (flags & CF::CLASSIFIED_TRACKING_CONTENT) { tc = TC::eContent; } else if (flags & CF::CLASSIFIED_FINGERPRINTING_CONTENT) { tc = TC::eFingerprinting; } else if (flags & CF::CLASSIFIED_ANY_BASIC_TRACKING) { tc = TC::eBasic; } else { tc = TC::eNone; } } bool isSystemPrincipal = mLoadInfo->GetLoadingPrincipal() && mLoadInfo->GetLoadingPrincipal()->IsSystemPrincipal(); return HttpTrafficAnalyzer::CreateTrafficCategory( NS_UsePrivateBrowsing(this), isSystemPrincipal, isThirdParty, cos, tc); } void nsHttpChannel::SetCachedContentType() { if (!mResponseHead) { return; } nsAutoCString contentTypeStr; mResponseHead->ContentType(contentTypeStr); uint8_t contentType = nsICacheEntry::CONTENT_TYPE_OTHER; if (nsContentUtils::IsJavascriptMIMEType( NS_ConvertUTF8toUTF16(contentTypeStr))) { contentType = nsICacheEntry::CONTENT_TYPE_JAVASCRIPT; } else if (StringBeginsWith(contentTypeStr, "text/css"_ns) || (mLoadInfo->GetExternalContentPolicyType() == ExtContentPolicy::TYPE_STYLESHEET)) { contentType = nsICacheEntry::CONTENT_TYPE_STYLESHEET; } else if (StringBeginsWith(contentTypeStr, "application/wasm"_ns)) { contentType = nsICacheEntry::CONTENT_TYPE_WASM; } else if (StringBeginsWith(contentTypeStr, "image/"_ns)) { contentType = nsICacheEntry::CONTENT_TYPE_IMAGE; } else if (StringBeginsWith(contentTypeStr, "video/"_ns)) { contentType = nsICacheEntry::CONTENT_TYPE_MEDIA; } else if (StringBeginsWith(contentTypeStr, "audio/"_ns)) { contentType = nsICacheEntry::CONTENT_TYPE_MEDIA; } mCacheEntry->SetContentType(contentType); } nsresult nsHttpChannel::CallOnStartRequest() { LOG(("nsHttpChannel::CallOnStartRequest [this=%p]", this)); MOZ_RELEASE_ASSERT(!LoadRequireCORSPreflight() || LoadIsCorsPreflightDone(), "CORS preflight must have been finished by the time we " "call OnStartRequest"); MOZ_RELEASE_ASSERT(mCanceled || LoadProcessCrossOriginSecurityHeadersCalled(), "Security headers need to have been processed before " "calling CallOnStartRequest"); mEarlyHintObserver = nullptr; if (LoadOnStartRequestCalled()) { // This can only happen when a range request loading rest of the data // after interrupted concurrent cache read asynchronously failed, e.g. // the response range bytes are not as expected or this channel has // been externally canceled. // // It's legal to bypass CallOnStartRequest for that case since we've // already called OnStartRequest on our listener and also added all // content converters before. MOZ_ASSERT(LoadConcurrentCacheAccess()); LOG(("CallOnStartRequest already invoked before")); return mStatus; } // Ensure mListener->OnStartRequest will be invoked before exiting // this function. auto onStartGuard = MakeScopeExit([&] { LOG( (" calling mListener->OnStartRequest by ScopeExit [this=%p, " "listener=%p]\n", this, mListener.get())); MOZ_ASSERT(!LoadOnStartRequestCalled()); if (mListener) { nsCOMPtr deleteProtector(mListener); StoreOnStartRequestCalled(true); deleteProtector->OnStartRequest(this); } StoreOnStartRequestCalled(true); }); nsresult rv = ValidateMIMEType(); // Since ODA and OnStopRequest could be sent from socket process directly, we // need to update the channel status before calling mListener->OnStartRequest. // This is the only way to let child process discard the already received ODA // messages. if (NS_FAILED(rv)) { mStatus = rv; return mStatus; } // EnsureOpaqueResponseIsAllowed and EnsureOpauqeResponseIsAllowedAfterSniff // are the checks for Opaque Response Blocking to ensure that we block as many // cross-origin responses with CORS headers as possible that are not either // Javascript or media to avoid leaking their contents through side channels. OpaqueResponse opaqueResponse = PerformOpaqueResponseSafelistCheckBeforeSniff(); if (opaqueResponse == OpaqueResponse::Block) { SetChannelBlockedByOpaqueResponse(); CancelWithReason(NS_ERROR_FAILURE, "OpaqueResponseBlocker::BlockResponse"_ns); return NS_ERROR_FAILURE; } // Allow consumers to override our content type if (mLoadFlags & LOAD_CALL_CONTENT_SNIFFERS) { // NOTE: We can have both a txn pump and a cache pump when the cache // content is partial. In that case, we need to read from the cache, // because that's the one that has the initial contents. If that fails // then give the transaction pump a shot. nsIChannel* thisChannel = static_cast(this); bool typeSniffersCalled = false; if (mCachePump) { typeSniffersCalled = NS_SUCCEEDED(mCachePump->PeekStream(CallTypeSniffers, thisChannel)); } if (!typeSniffersCalled && mTransactionPump) { RefPtr pump = do_QueryObject(mTransactionPump); if (pump) { pump->PeekStream(CallTypeSniffers, thisChannel); } else { MOZ_ASSERT(nsIOService::UseSocketProcess()); RefPtr trans = do_QueryObject(mTransactionPump); MOZ_ASSERT(trans); trans->SetSniffedTypeToChannel(CallTypeSniffers, thisChannel); } } } // Note that the code below should be synced with the code in // HttpTransactionChild::CanSendODAToContentProcessDirectly(). We MUST make // sure HttpTransactionChild::CanSendODAToContentProcessDirectly() returns // false when a stream converter is applied. bool unknownDecoderStarted = false; if (mResponseHead && !mResponseHead->HasContentType()) { MOZ_ASSERT(mConnectionInfo, "Should have connection info here"); if (!mContentTypeHint.IsEmpty()) { mResponseHead->SetContentType(mContentTypeHint); } else if (mResponseHead->Version() == HttpVersion::v0_9 && mConnectionInfo->OriginPort() != mConnectionInfo->DefaultPort()) { mResponseHead->SetContentType(nsLiteralCString(TEXT_PLAIN)); } else { // Uh-oh. We had better find out what type we are! mListener = new nsUnknownDecoder(mListener); unknownDecoderStarted = true; } } // If unknownDecoder is not going to be launched, call // EnsureOpaqueResponseIsAllowedAfterSniff immediately. if (!unknownDecoderStarted) { if (opaqueResponse == OpaqueResponse::SniffCompressed) { mListener = new nsCompressedAudioVideoImageDetector( mListener, &HttpBaseChannel::CallTypeSniffers); } else if (opaqueResponse == OpaqueResponse::Sniff) { MOZ_DIAGNOSTIC_ASSERT(mORB); nsresult rv = mORB->EnsureOpaqueResponseIsAllowedAfterSniff(this); if (NS_FAILED(rv)) { return rv; } } } // If the content is multipart/x-mixed-replace, we'll insert a MIME decoder // in the pipeline to handle the content and pass it along to our // original listener. nsUnknownDecoder doesn't support detecting this type, // so we only need to insert this using the response header's mime type. // // We only do this for unwrapped document loads, since we might want to send // parts to the external protocol handler without leaving the parent process. bool mustRunStreamFilterInParent = false; nsCOMPtr parentChannel; NS_QueryNotificationCallbacks(this, parentChannel); RefPtr docListener = do_QueryObject(parentChannel); if (mResponseHead && docListener && docListener->GetChannel() == this) { nsAutoCString contentType; mResponseHead->ContentType(contentType); if (contentType.Equals("multipart/x-mixed-replace"_ns)) { nsCOMPtr convServ( do_GetService("@mozilla.org/streamConverters;1", &rv)); if (NS_SUCCEEDED(rv)) { nsCOMPtr toListener(mListener); nsCOMPtr fromListener; rv = convServ->AsyncConvertData("multipart/x-mixed-replace", "*/*", toListener, nullptr, getter_AddRefs(fromListener)); if (NS_SUCCEEDED(rv)) { mListener = fromListener; mustRunStreamFilterInParent = true; } } } } // If we installed a multipart converter, then we need to add StreamFilter // object before it, so that extensions see the un-parsed original stream. // We may want to add an option for extensions to opt-in to proper multipart // handling. // If not, then pass the StreamFilter promise on to DocumentLoadListener, // where it'll be added in the content process. for (StreamFilterRequest& request : mStreamFilterRequests) { if (mustRunStreamFilterInParent) { mozilla::ipc::Endpoint parent; mozilla::ipc::Endpoint child; nsresult rv = extensions::PStreamFilter::CreateEndpoints(&parent, &child); if (NS_FAILED(rv)) { request.mPromise->Reject(false, __func__); } else { extensions::StreamFilterParent::Attach(this, std::move(parent)); request.mPromise->Resolve(std::move(child), __func__); } } else { if (docListener) { docListener->AttachStreamFilter()->ChainTo(request.mPromise.forget(), __func__); } else { request.mPromise->Reject(false, __func__); } } request.mPromise = nullptr; } mStreamFilterRequests.Clear(); StoreTracingEnabled(false); if (mResponseHead && !mResponseHead->HasContentCharset()) { mResponseHead->SetContentCharset(mContentCharsetHint); } if (mCacheEntry && LoadCacheEntryIsWriteOnly()) { SetCachedContentType(); } LOG((" calling mListener->OnStartRequest [this=%p, listener=%p]\n", this, mListener.get())); // About to call OnStartRequest, dismiss the guard object. onStartGuard.release(); if (mListener) { MOZ_ASSERT(!LoadOnStartRequestCalled(), "We should not call OsStartRequest twice"); nsCOMPtr deleteProtector(mListener); StoreOnStartRequestCalled(true); rv = deleteProtector->OnStartRequest(this); if (NS_FAILED(rv)) return rv; } else { NS_WARNING("OnStartRequest skipped because of null listener"); StoreOnStartRequestCalled(true); } // Install stream converter if required. // Normally, we expect the listener to disable content conversion during // OnStartRequest if it wants to handle it itself (which is common case with // HttpChannelParent, disabling so that it can be done in the content // process). If we've installed an nsUnknownDecoder, then we won't yet have // called OnStartRequest on the final listener (that happens after we send // OnDataAvailable to the nsUnknownDecoder), so it can't yet have disabled // content conversion. // In that case, assume that the listener will disable content conversion, // unless it's specifically told us that it won't. if (!unknownDecoderStarted || LoadListenerRequiresContentConversion()) { nsCOMPtr listener; rv = DoApplyContentConversions(mListener, getter_AddRefs(listener), nullptr); if (NS_FAILED(rv)) { return rv; } if (listener) { MOZ_ASSERT(!LoadDataSentToChildProcess(), "DataSentToChildProcess being true means ODAs are sent to " "the child process directly. We MUST NOT apply content " "converter in this case."); mListener = listener; mCompressListener = listener; StoreHasAppliedConversion(true); } } // if this channel is for a download, close off access to the cache. if (mCacheEntry && LoadChannelIsForDownload()) { mCacheEntry->AsyncDoom(nullptr); // We must keep the cache entry in case of partial request. // Concurrent access is the same, we need the entry in // OnStopRequest. // We also need the cache entry when racing cache with network to find // out what is the source of the data. if (!LoadCachedContentIsPartial() && !LoadConcurrentCacheAccess() && !(mRaceCacheWithNetwork && mFirstResponseSource == RESPONSE_FROM_CACHE)) { CloseCacheEntry(false); } } return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetHttpProxyConnectResponseCode( int32_t* aResponseCode) { NS_ENSURE_ARG_POINTER(aResponseCode); if (mConnectionInfo && mConnectionInfo->UsingConnect()) { *aResponseCode = mProxyConnectResponseCode; } else { *aResponseCode = -1; } return NS_OK; } nsresult nsHttpChannel::ProcessFailedProxyConnect(uint32_t httpStatus) { // Failure to set up a proxy tunnel via CONNECT means one of the following: // 1) Proxy wants authorization, or forbids. // 2) DNS at proxy couldn't resolve target URL. // 3) Proxy connection to target failed or timed out. // 4) Eve intercepted our CONNECT, and is replying with malicious HTML. // // Our current architecture would parse the proxy's response content with // the permission of the target URL. Given #4, we must avoid rendering the // body of the reply, and instead give the user a (hopefully helpful) // boilerplate error page, based on just the HTTP status of the reply. MOZ_ASSERT(mConnectionInfo->UsingConnect(), "proxy connect failed but not using CONNECT?"); nsresult rv = HttpProxyResponseToErrorCode(httpStatus); LOG(("Cancelling failed proxy CONNECT [this=%p httpStatus=%u]\n", this, httpStatus)); // Make sure the connection is thrown away as it can be in a bad state // and the proxy may just hang on the next request. MOZ_ASSERT(mTransaction); mTransaction->DontReuseConnection(); Cancel(rv); { nsresult rv = CallOnStartRequest(); if (NS_FAILED(rv)) { LOG(("CallOnStartRequest failed [this=%p httpStatus=%u rv=%08x]\n", this, httpStatus, static_cast(rv))); } } return rv; } static void GetSTSConsoleErrorTag(uint32_t failureResult, nsAString& consoleErrorTag) { switch (failureResult) { case nsISiteSecurityService::ERROR_COULD_NOT_PARSE_HEADER: consoleErrorTag = u"STSCouldNotParseHeader"_ns; break; case nsISiteSecurityService::ERROR_NO_MAX_AGE: consoleErrorTag = u"STSNoMaxAge"_ns; break; case nsISiteSecurityService::ERROR_MULTIPLE_MAX_AGES: consoleErrorTag = u"STSMultipleMaxAges"_ns; break; case nsISiteSecurityService::ERROR_INVALID_MAX_AGE: consoleErrorTag = u"STSInvalidMaxAge"_ns; break; case nsISiteSecurityService::ERROR_MULTIPLE_INCLUDE_SUBDOMAINS: consoleErrorTag = u"STSMultipleIncludeSubdomains"_ns; break; case nsISiteSecurityService::ERROR_INVALID_INCLUDE_SUBDOMAINS: consoleErrorTag = u"STSInvalidIncludeSubdomains"_ns; break; case nsISiteSecurityService::ERROR_COULD_NOT_SAVE_STATE: consoleErrorTag = u"STSCouldNotSaveState"_ns; break; default: consoleErrorTag = u"STSUnknownError"_ns; break; } } /** * Process an HTTP Strict Transport Security (HSTS) header. */ nsresult nsHttpChannel::ProcessHSTSHeader(nsITransportSecurityInfo* aSecInfo) { nsHttpAtom atom(nsHttp::ResolveAtom("Strict-Transport-Security"_ns)); nsAutoCString securityHeader; nsresult rv = mResponseHead->GetHeader(atom, securityHeader); if (rv == NS_ERROR_NOT_AVAILABLE) { LOG(("nsHttpChannel: No %s header, continuing load.\n", atom.get())); return NS_OK; } if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (!aSecInfo) { LOG(("nsHttpChannel::ProcessHSTSHeader: no securityInfo?")); return NS_ERROR_INVALID_ARG; } nsITransportSecurityInfo::OverridableErrorCategory overridableErrorCategory; rv = aSecInfo->GetOverridableErrorCategory(&overridableErrorCategory); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (overridableErrorCategory != nsITransportSecurityInfo::OverridableErrorCategory::ERROR_UNSET) { LOG( ("nsHttpChannel::ProcessHSTSHeader: untrustworthy connection - not " "processing header")); return NS_ERROR_FAILURE; } nsISiteSecurityService* sss = gHttpHandler->GetSSService(); NS_ENSURE_TRUE(sss, NS_ERROR_OUT_OF_MEMORY); OriginAttributes originAttributes; if (NS_WARN_IF(!StoragePrincipalHelper::GetOriginAttributesForHSTS( this, originAttributes))) { return NS_ERROR_FAILURE; } uint32_t failureResult; rv = sss->ProcessHeader(mURI, securityHeader, originAttributes, nullptr, nullptr, &failureResult); if (NS_FAILED(rv)) { nsAutoString consoleErrorCategory(u"Invalid HSTS Headers"_ns); nsAutoString consoleErrorTag; GetSTSConsoleErrorTag(failureResult, consoleErrorTag); Unused << AddSecurityMessage(consoleErrorTag, consoleErrorCategory); LOG(("nsHttpChannel: Failed to parse %s header, continuing load.\n", atom.get())); } return NS_OK; } /** * Decide whether or not to remember Strict-Transport-Security, and whether * or not to enforce channel integrity. * * @return NS_ERROR_FAILURE if there's security information missing even though * it's an HTTPS connection. */ nsresult nsHttpChannel::ProcessSecurityHeaders() { // If this channel is not loading securely, STS or PKP doesn't do anything. // In the case of HSTS, the upgrade to HTTPS takes place earlier in the // channel load process. if (!mURI->SchemeIs("https")) { return NS_OK; } if (IsBrowsingContextDiscarded()) { return NS_OK; } nsAutoCString asciiHost; nsresult rv = mURI->GetAsciiHost(asciiHost); NS_ENSURE_SUCCESS(rv, NS_OK); // If the channel is not a hostname, but rather an IP, do not process STS // or PKP headers if (HostIsIPLiteral(asciiHost)) { return NS_OK; } // mSecurityInfo may not always be present, and if it's not then it is okay // to just disregard any security headers since we know nothing about the // security of the connection. NS_ENSURE_TRUE(mSecurityInfo, NS_OK); // Only process HSTS headers for first-party loads. This prevents a // proliferation of useless HSTS state for partitioned third parties. if (!mLoadInfo->GetIsThirdPartyContextToTopWindow()) { rv = ProcessHSTSHeader(mSecurityInfo); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } bool nsHttpChannel::IsHTTPS() { return mURI->SchemeIs("https"); } void nsHttpChannel::ProcessSSLInformation() { // If this is HTTPS, record any use of RSA so that Key Exchange Algorithm // can be whitelisted for TLS False Start in future sessions. We could // do the same for DH but its rarity doesn't justify the lookup. if (mCanceled || NS_FAILED(mStatus) || !mSecurityInfo || !IsHTTPS() || mPrivateBrowsing) { return; } if (!mSecurityInfo) { return; } uint32_t state; if (NS_SUCCEEDED(mSecurityInfo->GetSecurityState(&state)) && (state & nsIWebProgressListener::STATE_IS_BROKEN)) { // Send weak crypto warnings to the web console if (state & nsIWebProgressListener::STATE_USES_WEAK_CRYPTO) { nsString consoleErrorTag = u"WeakCipherSuiteWarning"_ns; nsString consoleErrorCategory = u"SSL"_ns; Unused << AddSecurityMessage(consoleErrorTag, consoleErrorCategory); } } uint16_t tlsVersion; nsresult rv = mSecurityInfo->GetProtocolVersion(&tlsVersion); if (NS_SUCCEEDED(rv) && tlsVersion != nsITransportSecurityInfo::TLS_VERSION_1_2 && tlsVersion != nsITransportSecurityInfo::TLS_VERSION_1_3) { nsString consoleErrorTag = u"DeprecatedTLSVersion2"_ns; nsString consoleErrorCategory = u"TLS"_ns; Unused << AddSecurityMessage(consoleErrorTag, consoleErrorCategory); } } void nsHttpChannel::ProcessAltService() { // e.g. Alt-Svc: h2=":443"; ma=60 // e.g. Alt-Svc: h2="otherhost:443" // Alt-Svc = 1#( alternative *( OWS ";" OWS parameter ) ) // alternative = protocol-id "=" alt-authority // protocol-id = token ; percent-encoded ALPN protocol identifier // alt-authority = quoted-string ; containing [ uri-host ] ":" port if (!LoadAllowAltSvc()) { // per channel opt out return; } if (mWebTransportSessionEventListener) { return; } if (!gHttpHandler->AllowAltSvc() || (mCaps & NS_HTTP_DISALLOW_SPDY)) { return; } if (IsBrowsingContextDiscarded()) { return; } nsAutoCString scheme; mURI->GetScheme(scheme); bool isHttp = scheme.EqualsLiteral("http"); if (!isHttp && !scheme.EqualsLiteral("https")) { return; } nsAutoCString altSvc; Unused << mResponseHead->GetHeader(nsHttp::Alternate_Service, altSvc); if (altSvc.IsEmpty()) { return; } if (!nsHttp::IsReasonableHeaderValue(altSvc)) { LOG(("Alt-Svc Response Header seems unreasonable - skipping\n")); return; } nsAutoCString originHost; int32_t originPort = 80; mURI->GetPort(&originPort); if (NS_FAILED(mURI->GetAsciiHost(originHost))) { return; } nsCOMPtr callbacks; nsCOMPtr proxyInfo; NS_NewNotificationCallbacksAggregation(mCallbacks, mLoadGroup, getter_AddRefs(callbacks)); if (mProxyInfo) { proxyInfo = do_QueryInterface(mProxyInfo); } OriginAttributes originAttributes; // Regular principal in case we have a proxy. if (proxyInfo && !StaticPrefs::privacy_partition_network_state_connection_with_proxy()) { StoragePrincipalHelper::GetOriginAttributes( this, originAttributes, StoragePrincipalHelper::eRegularPrincipal); } else { StoragePrincipalHelper::GetOriginAttributesForNetworkState( this, originAttributes); } AltSvcMapping::ProcessHeader( altSvc, scheme, originHost, originPort, mUsername, mPrivateBrowsing, callbacks, proxyInfo, mCaps & NS_HTTP_DISALLOW_SPDY, originAttributes); } nsresult nsHttpChannel::ProcessResponse() { uint32_t httpStatus = mResponseHead->Status(); LOG(("nsHttpChannel::ProcessResponse [this=%p httpStatus=%u]\n", this, httpStatus)); // Gather data on whether the transaction and page (if this is // the initial page load) is being loaded with SSL. Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_IS_SSL, mConnectionInfo->EndToEndSSL()); if (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI) { Telemetry::Accumulate(Telemetry::HTTP_PAGELOAD_IS_SSL, mConnectionInfo->EndToEndSSL()); } if (Telemetry::CanRecordPrereleaseData()) { // how often do we see something like Alt-Svc: "443:quic,p=1" // and Alt-Svc: "h3-****" nsAutoCString alt_service; Unused << mResponseHead->GetHeader(nsHttp::Alternate_Service, alt_service); uint32_t saw_quic = 0; if (!alt_service.IsEmpty()) { if (strstr(alt_service.get(), "h3-")) { saw_quic = 1; } else if (strstr(alt_service.get(), "quic")) { saw_quic = 2; } } Telemetry::Accumulate(Telemetry::HTTP_SAW_QUIC_ALT_PROTOCOL_2, saw_quic); // Gather data on how many URLS get redirected switch (httpStatus) { case 200: Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 0); break; case 301: Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 1); break; case 302: Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 2); break; case 304: Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 3); break; case 307: Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 4); break; case 308: Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 5); break; case 400: Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 6); break; case 401: Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 7); break; case 403: Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 8); break; case 404: Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 9); break; case 500: Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 10); break; default: Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 11); break; } } // Let the predictor know whether this was a cacheable response or not so // that it knows whether or not to possibly prefetch this resource in the // future. // We use GetReferringPage because mReferrerInfo may not be set at all(this is // especially useful in xpcshell tests, where we don't have an actual pageload // to get a referrer from). nsCOMPtr referrer = GetReferringPage(); if (!referrer && mReferrerInfo) { referrer = mReferrerInfo->GetOriginalReferrer(); } if (referrer) { nsCOMPtr lci = GetLoadContextInfo(this); mozilla::net::Predictor::UpdateCacheability( referrer, mURI, httpStatus, mRequestHead, mResponseHead.get(), lci, IsThirdPartyTrackingResource()); } // Only allow 407 (authentication required) to continue if (mTransaction && mTransaction->ProxyConnectFailed() && httpStatus != 407) { return ProcessFailedProxyConnect(httpStatus); } MOZ_ASSERT(!mCachedContentIsValid || mRaceCacheWithNetwork, "We should not be hitting the network if we have valid cached " "content unless we are racing the network and cache"); ProcessSSLInformation(); // notify "http-on-examine-response" observers gHttpHandler->OnExamineResponse(this); return ContinueProcessResponse1(); } void nsHttpChannel::AsyncContinueProcessResponse() { nsresult rv; rv = ContinueProcessResponse1(); if (NS_FAILED(rv)) { // A synchronous failure here would normally be passed as the return // value from OnStartRequest, which would in turn cancel the request. // If we're continuing asynchronously, we need to cancel the request // ourselves. Unused << Cancel(rv); } } nsresult nsHttpChannel::ContinueProcessResponse1() { MOZ_ASSERT(!mCallOnResume, "How did that happen?"); nsresult rv = NS_OK; if (mSuspendCount) { LOG(("Waiting until resume to finish processing response [this=%p]\n", this)); mCallOnResume = [](nsHttpChannel* self) { self->AsyncContinueProcessResponse(); return NS_OK; }; return NS_OK; } // Check if request was cancelled during http-on-examine-response. if (mCanceled) { return CallOnStartRequest(); } uint32_t httpStatus = mResponseHead->Status(); // STS, Cookies and Alt-Service should not be handled on proxy failure. // If proxy CONNECT response needs to complete, wait to process connection // for Strict-Transport-Security. if (!(mTransaction && mTransaction->ProxyConnectFailed()) && (httpStatus != 407)) { if (nsAutoCString cookie; NS_SUCCEEDED(mResponseHead->GetHeader(nsHttp::Set_Cookie, cookie))) { SetCookie(cookie); nsCOMPtr parentChannel; NS_QueryNotificationCallbacks(this, parentChannel); if (RefPtr httpParent = do_QueryObject(parentChannel)) { httpParent->SetCookie(std::move(cookie)); } } // Given a successful connection, process any STS or PKP data that's // relevant. nsresult rv = ProcessSecurityHeaders(); if (NS_FAILED(rv)) { NS_WARNING("ProcessSTSHeader failed, continuing load."); } if ((httpStatus < 500) && (httpStatus != 421)) { ProcessAltService(); } } if (LoadConcurrentCacheAccess() && LoadCachedContentIsPartial() && httpStatus != 206) { LOG( (" only expecting 206 when doing partial request during " "interrupted cache concurrent read")); return NS_ERROR_CORRUPTED_CONTENT; } // handle unused username and password in url (see bug 232567) if (httpStatus != 401 && httpStatus != 407) { if (!mAuthRetryPending) { rv = mAuthProvider->CheckForSuperfluousAuth(); if (NS_FAILED(rv)) { mStatus = rv; LOG((" CheckForSuperfluousAuth failed (%08x)", static_cast(rv))); } } if (mCanceled) return CallOnStartRequest(); // reset the authentication's current continuation state because ourvr // last authentication attempt has been completed successfully rv = mAuthProvider->Disconnect(NS_ERROR_ABORT); if (NS_FAILED(rv)) { LOG((" Disconnect failed (%08x)", static_cast(rv))); } mAuthProvider = nullptr; LOG((" continuation state has been reset")); } // No process switch needed, continue as normal. return ContinueProcessResponse2(rv); } nsresult nsHttpChannel::ContinueProcessResponse2(nsresult rv) { if (NS_FAILED(rv) && !mCanceled) { // The process switch failed, cancel this channel. Cancel(rv); return CallOnStartRequest(); } if (mAPIRedirectToURI && !mCanceled) { MOZ_ASSERT(!LoadOnStartRequestCalled()); nsCOMPtr redirectTo; mAPIRedirectToURI.swap(redirectTo); PushRedirectAsyncFunc(&nsHttpChannel::ContinueProcessResponse3); rv = StartRedirectChannelToURI(redirectTo, nsIChannelEventSink::REDIRECT_TEMPORARY); if (NS_SUCCEEDED(rv)) { return NS_OK; } PopRedirectAsyncFunc(&nsHttpChannel::ContinueProcessResponse3); } // Hack: ContinueProcessResponse3 uses NS_OK to detect successful // redirects, so we distinguish this codepath (a non-redirect that's // processing normally) by passing in a bogus error code. return ContinueProcessResponse3(NS_BINDING_FAILED); } nsresult nsHttpChannel::ContinueProcessResponse3(nsresult rv) { LOG(("nsHttpChannel::ContinueProcessResponse3 [this=%p, rv=%" PRIx32 "]", this, static_cast(rv))); if (NS_SUCCEEDED(rv)) { // redirectTo() has passed through, we don't want to go on with // this channel. It will now be canceled by the redirect handling // code that called this function. return NS_OK; } rv = NS_OK; uint32_t httpStatus = mResponseHead->Status(); bool transactionRestarted = mTransaction->TakeRestartedState(); // handle different server response categories. Note that we handle // caching or not caching of error pages in // nsHttpResponseHead::MustValidate; if you change this switch, update that // one switch (httpStatus) { case 200: case 203: // Per RFC 2616, 14.35.2, "A server MAY ignore the Range header". // So if a server does that and sends 200 instead of 206 that we // expect, notify our caller. // However, if we wanted to start from the beginning, let it go through if (LoadResuming() && mStartPos != 0) { LOG(("Server ignored our Range header, cancelling [this=%p]\n", this)); Cancel(NS_ERROR_NOT_RESUMABLE); rv = CallOnStartRequest(); break; } // these can normally be cached rv = ProcessNormal(); MaybeInvalidateCacheEntryForSubsequentGet(); break; case 206: if (LoadCachedContentIsPartial()) { // an internal byte range request... auto func = [](auto* self, nsresult aRv) { return self->ContinueProcessResponseAfterPartialContent(aRv); }; rv = ProcessPartialContent(func); // Directly call ContinueProcessResponseAfterPartialContent if channel // is not suspended or ProcessPartialContent throws. if (!mSuspendCount || NS_FAILED(rv)) { return ContinueProcessResponseAfterPartialContent(rv); } return NS_OK; } else { mCacheInputStream.CloseAndRelease(); rv = ProcessNormal(); } break; case 301: case 302: case 307: case 308: case 303: #if 0 case 305: // disabled as a security measure (see bug 187996). #endif // don't store the response body for redirects MaybeInvalidateCacheEntryForSubsequentGet(); PushRedirectAsyncFunc(&nsHttpChannel::ContinueProcessResponse4); rv = AsyncProcessRedirection(httpStatus); if (NS_FAILED(rv)) { PopRedirectAsyncFunc(&nsHttpChannel::ContinueProcessResponse4); LOG(("AsyncProcessRedirection failed [rv=%" PRIx32 "]\n", static_cast(rv))); // don't cache failed redirect responses. if (mCacheEntry) mCacheEntry->AsyncDoom(nullptr); if (DoNotRender3xxBody(rv)) { mStatus = rv; DoNotifyListener(); } else { rv = ContinueProcessResponse4(rv); } } break; case 304: if (!ShouldBypassProcessNotModified()) { auto func = [](auto* self, nsresult aRv) { return self->ContinueProcessResponseAfterNotModified(aRv); }; rv = ProcessNotModified(func); // Directly call ContinueProcessResponseAfterNotModified if channel // is not suspended or ProcessNotModified throws. if (!mSuspendCount || NS_FAILED(rv)) { return ContinueProcessResponseAfterNotModified(rv); } return NS_OK; } // Don't cache uninformative 304 if (LoadCustomConditionalRequest()) { CloseCacheEntry(false); } if (ShouldBypassProcessNotModified() || NS_FAILED(rv)) { rv = ProcessNormal(); } break; case 401: case 407: if (MOZ_UNLIKELY(httpStatus == 407 && transactionRestarted)) { // The transaction has been internally restarted. We want to // authenticate to the proxy again, so reuse either cached credentials // or use default credentials for NTLM/Negotiate. This prevents // considering the previously used credentials as invalid. mAuthProvider->ClearProxyIdent(); } if (!LoadAuthRedirectedChannel() && MOZ_UNLIKELY(LoadCustomAuthHeader()) && httpStatus == 401) { // When a custom auth header fails, we don't want to try // any cached credentials, nor we want to ask the user. // It's up to the consumer to re-try w/o setting a custom // auth header if cached credentials should be attempted. rv = NS_ERROR_FAILURE; } else if (httpStatus == 401 && StaticPrefs:: network_auth_supress_auth_prompt_for_XFO_failures() && !nsContentSecurityUtils::CheckCSPFrameAncestorAndXFO(this)) { // CSP Frame Ancestor and X-Frame-Options check has failed // Do not prompt http auth - Bug 1629307 rv = NS_ERROR_FAILURE; } else { rv = mAuthProvider->ProcessAuthentication( httpStatus, mConnectionInfo->EndToEndSSL() && mTransaction && mTransaction->ProxyConnectFailed()); } if (rv == NS_ERROR_IN_PROGRESS) { // authentication prompt has been invoked and result // is expected asynchronously mIsAuthChannel = true; mAuthRetryPending = true; if (httpStatus == 407 || (mTransaction && mTransaction->ProxyConnectFailed())) { StoreProxyAuthPending(true); } // suspend the transaction pump to stop receiving the // unauthenticated content data. We will throw that data // away when user provides credentials or resume the pump // when user refuses to authenticate. LOG( ("Suspending the transaction, asynchronously prompting for " "credentials")); mTransactionPump->Suspend(); #ifdef DEBUG // This is for test purposes only. See bug 1683176 for details. gHttpHandler->OnTransactionSuspendedDueToAuthentication(this); #endif rv = NS_OK; } else if (NS_FAILED(rv)) { LOG(("ProcessAuthentication failed [rv=%" PRIx32 "]\n", static_cast(rv))); if (mTransaction && mTransaction->ProxyConnectFailed()) { return ProcessFailedProxyConnect(httpStatus); } if (!mAuthRetryPending) { rv = mAuthProvider->CheckForSuperfluousAuth(); if (NS_FAILED(rv)) { mStatus = rv; LOG(("CheckForSuperfluousAuth failed [rv=%x]\n", static_cast(rv))); } } rv = ProcessNormal(); } else { mIsAuthChannel = true; mAuthRetryPending = true; if (StaticPrefs::network_auth_use_redirect_for_retries()) { if (NS_SUCCEEDED(RedirectToNewChannelForAuthRetry())) { return NS_OK; } mAuthRetryPending = false; rv = ProcessNormal(); } } break; case 408: case 425: case 429: // Do not cache 408, 425 and 429. CloseCacheEntry(false); [[fallthrough]]; // process normally default: rv = ProcessNormal(); MaybeInvalidateCacheEntryForSubsequentGet(); break; } UpdateCacheDisposition(false, false); return rv; } nsresult nsHttpChannel::ContinueProcessResponseAfterPartialContent( nsresult aRv) { LOG( ("nsHttpChannel::ContinueProcessResponseAfterPartialContent " "[this=%p, rv=%" PRIx32 "]", this, static_cast(aRv))); UpdateCacheDisposition(false, NS_SUCCEEDED(aRv)); return aRv; } nsresult nsHttpChannel::ContinueProcessResponseAfterNotModified(nsresult aRv) { LOG( ("nsHttpChannel::ContinueProcessResponseAfterNotModified " "[this=%p, rv=%" PRIx32 "]", this, static_cast(aRv))); if (NS_SUCCEEDED(aRv)) { StoreTransactionReplaced(true); UpdateCacheDisposition(true, false); return NS_OK; } LOG(("ProcessNotModified failed [rv=%" PRIx32 "]\n", static_cast(aRv))); // We cannot read from the cache entry, it might be in an // incosistent state. Doom it and redirect the channel // to the same URI to reload from the network. mCacheInputStream.CloseAndRelease(); if (mCacheEntry) { mCacheEntry->AsyncDoom(nullptr); mCacheEntry = nullptr; } nsresult rv = StartRedirectChannelToURI(mURI, nsIChannelEventSink::REDIRECT_INTERNAL); if (NS_SUCCEEDED(rv)) { return NS_OK; } // Don't cache uninformative 304 if (LoadCustomConditionalRequest()) { CloseCacheEntry(false); } if (ShouldBypassProcessNotModified() || NS_FAILED(rv)) { rv = ProcessNormal(); } UpdateCacheDisposition(false, false); return rv; } static void ReportHttpResponseVersion(HttpVersion version) { if (Telemetry::CanRecordPrereleaseData()) { Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_VERSION, static_cast(version)); } nsAutoCString versionLabel; switch (version) { case HttpVersion::v0_9: case HttpVersion::v1_0: case HttpVersion::v1_1: versionLabel = "http_1"_ns; break; case HttpVersion::v2_0: versionLabel = "http_2"_ns; break; case HttpVersion::v3_0: versionLabel = "http_3"_ns; break; default: versionLabel = "unknown"_ns; break; } mozilla::glean::networking::http_response_version.Get(versionLabel).Add(1); } void nsHttpChannel::UpdateCacheDisposition(bool aSuccessfulReval, bool aPartialContentUsed) { if (mRaceDelay && !mRaceCacheWithNetwork && (LoadCachedContentIsPartial() || mDidReval)) { if (aSuccessfulReval || aPartialContentUsed) { AccumulateCategorical( Telemetry::LABELS_NETWORK_RACE_CACHE_VALIDATION::CachedContentUsed); } else { AccumulateCategorical(Telemetry::LABELS_NETWORK_RACE_CACHE_VALIDATION:: CachedContentNotUsed); } } if (Telemetry::CanRecordPrereleaseData()) { CacheDisposition cacheDisposition; if (!mDidReval) { cacheDisposition = kCacheMissed; } else if (aSuccessfulReval) { cacheDisposition = kCacheHitViaReval; } else { cacheDisposition = kCacheMissedViaReval; } AccumulateCacheHitTelemetry(cacheDisposition, this); mCacheDisposition = cacheDisposition; if (mResponseHead->Version() == HttpVersion::v0_9) { // DefaultPortTopLevel = 0, DefaultPortSubResource = 1, // NonDefaultPortTopLevel = 2, NonDefaultPortSubResource = 3 uint32_t v09Info = 0; if (!(mLoadFlags & LOAD_INITIAL_DOCUMENT_URI)) { v09Info += 1; } if (mConnectionInfo->OriginPort() != mConnectionInfo->DefaultPort()) { v09Info += 2; } Telemetry::Accumulate(Telemetry::HTTP_09_INFO, v09Info); } } ReportHttpResponseVersion(mResponseHead->Version()); } nsresult nsHttpChannel::ContinueProcessResponse4(nsresult rv) { bool doNotRender = DoNotRender3xxBody(rv); if (rv == NS_ERROR_DOM_BAD_URI && mRedirectURI) { bool isHTTP = mRedirectURI->SchemeIs("http") || mRedirectURI->SchemeIs("https"); if (!isHTTP) { // This was a blocked attempt to redirect and subvert the system by // redirecting to another protocol (perhaps javascript:) // In that case we want to throw an error instead of displaying the // non-redirected response body. LOG(("ContinueProcessResponse4 detected rejected Non-HTTP Redirection")); doNotRender = true; rv = NS_ERROR_CORRUPTED_CONTENT; } } if (doNotRender) { Cancel(rv); DoNotifyListener(); return rv; } if (NS_SUCCEEDED(rv)) { UpdateInhibitPersistentCachingFlag(); MaybeCreateCacheEntryWhenRCWN(); rv = InitCacheEntry(); if (NS_FAILED(rv)) { LOG( ("ContinueProcessResponse4 " "failed to init cache entry [rv=%x]\n", static_cast(rv))); } CloseCacheEntry(false); return NS_OK; } LOG(("ContinueProcessResponse4 got failure result [rv=%" PRIx32 "]\n", static_cast(rv))); if (mTransaction && mTransaction->ProxyConnectFailed()) { return ProcessFailedProxyConnect(mRedirectType); } return ProcessNormal(); } nsresult nsHttpChannel::ProcessNormal() { LOG(("nsHttpChannel::ProcessNormal [this=%p]\n", this)); return ContinueProcessNormal(NS_OK); } nsresult nsHttpChannel::ContinueProcessNormal(nsresult rv) { LOG(("nsHttpChannel::ContinueProcessNormal [this=%p]", this)); if (NS_FAILED(rv)) { // Fill the failure status here, we have failed to fall back, thus we // have to report our status as failed. mStatus = rv; DoNotifyListener(); return rv; } rv = ProcessCrossOriginSecurityHeaders(); if (NS_FAILED(rv)) { mStatus = rv; HandleAsyncAbort(); return rv; } // if we're here, then any byte-range requests failed to result in a partial // response. we must clear this flag to prevent BufferPartialContent from // being called inside our OnDataAvailable (see bug 136678). StoreCachedContentIsPartial(false); UpdateInhibitPersistentCachingFlag(); MaybeCreateCacheEntryWhenRCWN(); // this must be called before firing OnStartRequest, since http clients, // such as imagelib, expect our cache entry to already have the correct // expiration time (bug 87710). if (mCacheEntry) { rv = InitCacheEntry(); if (NS_FAILED(rv)) CloseCacheEntry(true); } // Check that the server sent us what we were asking for if (LoadResuming()) { // Create an entity id from the response nsAutoCString id; rv = GetEntityID(id); if (NS_FAILED(rv)) { // If creating an entity id is not possible -> error Cancel(NS_ERROR_NOT_RESUMABLE); } else if (mResponseHead->Status() != 206 && mResponseHead->Status() != 200) { // Probably 404 Not Found, 412 Precondition Failed or // 416 Invalid Range -> error LOG(("Unexpected response status while resuming, aborting [this=%p]\n", this)); Cancel(NS_ERROR_ENTITY_CHANGED); } // If we were passed an entity id, verify it's equal to the server's else if (!mEntityID.IsEmpty()) { if (!mEntityID.Equals(id)) { LOG(("Entity mismatch, expected '%s', got '%s', aborting [this=%p]", mEntityID.get(), id.get(), this)); Cancel(NS_ERROR_ENTITY_CHANGED); } } } rv = CallOnStartRequest(); if (NS_FAILED(rv)) return rv; // install cache listener if we still have a cache entry open if (mCacheEntry && !LoadCacheEntryIsReadOnly()) { rv = InstallCacheListener(); if (NS_FAILED(rv)) return rv; } return NS_OK; } nsresult nsHttpChannel::PromptTempRedirect() { if (!gHttpHandler->PromptTempRedirect()) { return NS_OK; } nsresult rv; nsCOMPtr bundleService = do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv); if (NS_FAILED(rv)) return rv; nsCOMPtr stringBundle; rv = bundleService->CreateBundle(NECKO_MSGS_URL, getter_AddRefs(stringBundle)); if (NS_FAILED(rv)) return rv; nsAutoString messageString; rv = stringBundle->GetStringFromName("RepostFormData", messageString); if (NS_SUCCEEDED(rv)) { bool repost = false; nsCOMPtr prompt; GetCallback(prompt); if (!prompt) return NS_ERROR_NO_INTERFACE; prompt->Confirm(nullptr, messageString.get(), &repost); if (!repost) return NS_ERROR_FAILURE; } return rv; } nsresult nsHttpChannel::ProxyFailover() { LOG(("nsHttpChannel::ProxyFailover [this=%p]\n", this)); nsresult rv; nsCOMPtr pps = do_GetService(NS_PROTOCOLPROXYSERVICE_CONTRACTID, &rv); if (NS_FAILED(rv)) return rv; nsCOMPtr pi; rv = pps->GetFailoverForProxy(mConnectionInfo->ProxyInfo(), mURI, mStatus, getter_AddRefs(pi)); #ifdef MOZ_PROXY_DIRECT_FAILOVER if (NS_FAILED(rv)) { if (!StaticPrefs::network_proxy_failover_direct()) { return rv; } // If this request used a failed proxy and there is no failover available, // fallback to DIRECT connections for conservative requests. if (LoadBeConservative()) { rv = pps->NewProxyInfo("direct"_ns, ""_ns, 0, ""_ns, ""_ns, 0, UINT32_MAX, nullptr, getter_AddRefs(pi)); } #endif if (NS_FAILED(rv)) { return rv; } #ifdef MOZ_PROXY_DIRECT_FAILOVER } #endif // XXXbz so where does this codepath remove us from the loadgroup, // exactly? return AsyncDoReplaceWithProxy(pi); } void nsHttpChannel::SetHTTPSSVCRecord( already_AddRefed&& aRecord) { LOG(("nsHttpChannel::SetHTTPSSVCRecord [this=%p]\n", this)); nsCOMPtr record = aRecord; MOZ_ASSERT(!mHTTPSSVCRecord); mHTTPSSVCRecord.emplace(std::move(record)); } void nsHttpChannel::HandleAsyncRedirectChannelToHttps() { MOZ_ASSERT(!mCallOnResume, "How did that happen?"); if (mSuspendCount) { LOG(("Waiting until resume to do async redirect to https [this=%p]\n", this)); mCallOnResume = [](nsHttpChannel* self) { self->HandleAsyncRedirectChannelToHttps(); return NS_OK; }; return; } nsresult rv = StartRedirectChannelToHttps(); if (NS_FAILED(rv)) { rv = ContinueAsyncRedirectChannelToURI(rv); if (NS_FAILED(rv)) { LOG(("ContinueAsyncRedirectChannelToURI failed (%08x) [this=%p]\n", static_cast(rv), this)); } } } nsresult nsHttpChannel::StartRedirectChannelToHttps() { LOG(("nsHttpChannel::HandleAsyncRedirectChannelToHttps() [STS]\n")); nsCOMPtr upgradedURI; nsresult rv = NS_GetSecureUpgradedURI(mURI, getter_AddRefs(upgradedURI)); NS_ENSURE_SUCCESS(rv, rv); return StartRedirectChannelToURI( upgradedURI, nsIChannelEventSink::REDIRECT_PERMANENT | nsIChannelEventSink::REDIRECT_STS_UPGRADE); } void nsHttpChannel::HandleAsyncAPIRedirect() { MOZ_ASSERT(!mCallOnResume, "How did that happen?"); MOZ_ASSERT(mAPIRedirectToURI, "How did that happen?"); if (mSuspendCount) { LOG(("Waiting until resume to do async API redirect [this=%p]\n", this)); mCallOnResume = [](nsHttpChannel* self) { self->HandleAsyncAPIRedirect(); return NS_OK; }; return; } nsresult rv = StartRedirectChannelToURI( mAPIRedirectToURI, nsIChannelEventSink::REDIRECT_PERMANENT); if (NS_FAILED(rv)) { rv = ContinueAsyncRedirectChannelToURI(rv); if (NS_FAILED(rv)) { LOG(("ContinueAsyncRedirectChannelToURI failed (%08x) [this=%p]\n", static_cast(rv), this)); } } } void nsHttpChannel::HandleAsyncRedirectToUnstrippedURI() { MOZ_ASSERT(!mCallOnResume, "How did that happen?"); if (mSuspendCount) { LOG( ("Waiting until resume to do async redirect to unstripped URI " "[this=%p]\n", this)); mCallOnResume = [](nsHttpChannel* self) { self->HandleAsyncRedirectToUnstrippedURI(); return NS_OK; }; return; } nsCOMPtr unstrippedURI; mLoadInfo->GetUnstrippedURI(getter_AddRefs(unstrippedURI)); // Clear the unstripped URI from the loadInfo before starting redirect in case // endless redirect. mLoadInfo->SetUnstrippedURI(nullptr); nsresult rv = StartRedirectChannelToURI( unstrippedURI, nsIChannelEventSink::REDIRECT_PERMANENT); if (NS_FAILED(rv)) { rv = ContinueAsyncRedirectChannelToURI(rv); if (NS_FAILED(rv)) { LOG(("ContinueAsyncRedirectChannelToURI failed (%08x) [this=%p]\n", static_cast(rv), this)); } } } nsresult nsHttpChannel::RedirectToNewChannelForAuthRetry() { LOG(("nsHttpChannel::RedirectToNewChannelForAuthRetry %p", this)); nsresult rv = NS_OK; nsCOMPtr redirectLoadInfo = CloneLoadInfoForRedirect( mURI, nsIChannelEventSink::REDIRECT_INTERNAL | nsIChannelEventSink::REDIRECT_AUTH_RETRY); nsCOMPtr ioService; rv = gHttpHandler->GetIOService(getter_AddRefs(ioService)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr newChannel; rv = gHttpHandler->NewProxiedChannel(mURI, mProxyInfo, mProxyResolveFlags, mProxyURI, mLoadInfo, getter_AddRefs(newChannel)); NS_ENSURE_SUCCESS(rv, rv); rv = SetupReplacementChannel(mURI, newChannel, true, nsIChannelEventSink::REDIRECT_INTERNAL | nsIChannelEventSink::REDIRECT_AUTH_RETRY); NS_ENSURE_SUCCESS(rv, rv); // rewind the upload stream if (mUploadStream) { nsCOMPtr seekable = do_QueryInterface(mUploadStream); nsresult rv = NS_ERROR_NO_INTERFACE; if (seekable) { rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0); } // This should not normally happen, but it's possible that big memory // blobs originating in the other process can't be rewinded. // In that case we just fail the request, otherwise the content length // will not match and this load will never complete. NS_ENSURE_SUCCESS(rv, rv); } RefPtr httpChannelImpl = do_QueryObject(newChannel); MOZ_ASSERT(mAuthProvider); httpChannelImpl->mAuthProvider = std::move(mAuthProvider); httpChannelImpl->mProxyInfo = mProxyInfo; if ((mCaps & NS_HTTP_STICKY_CONNECTION) || mTransaction->HasStickyConnection()) { mConnectionInfo = mTransaction->GetConnInfo(); httpChannelImpl->mTransactionSticky = mTransaction; if (mTransaction->Http2Disabled()) { httpChannelImpl->mCaps |= NS_HTTP_DISALLOW_SPDY; } if (mTransaction->Http3Disabled()) { httpChannelImpl->mCaps |= NS_HTTP_DISALLOW_HTTP3; } } httpChannelImpl->mCaps |= NS_HTTP_STICKY_CONNECTION; if (LoadAuthConnectionRestartable()) { httpChannelImpl->mCaps |= NS_HTTP_CONNECTION_RESTARTABLE; } else { httpChannelImpl->mCaps &= ~NS_HTTP_CONNECTION_RESTARTABLE; } MOZ_ASSERT(mConnectionInfo); httpChannelImpl->mConnectionInfo = mConnectionInfo->Clone(); // we need to store the state to skip unnecessary checks in the new channel httpChannelImpl->StoreAuthRedirectedChannel(true); // We must copy proxy and auth header to the new channel. // Although the new channel can populate auth headers from auth cache, we // would still like to use the auth headers generated in this channel. The // main reason for doing this is that certain connection-based/stateful auth // schemes like NTLM will fail when we try generate the credentials more than // the number of times the server has presented us the challenge due to the // usage of nonce in generating the credentials Copying the auth header will // bypass generation of the credentials nsAutoCString authVal; if (NS_SUCCEEDED(GetRequestHeader("Proxy-Authorization"_ns, authVal))) { httpChannelImpl->SetRequestHeader("Proxy-Authorization"_ns, authVal, false); } if (NS_SUCCEEDED(GetRequestHeader("Authorization"_ns, authVal))) { httpChannelImpl->SetRequestHeader("Authorization"_ns, authVal, false); } httpChannelImpl->SetBlockAuthPrompt(LoadBlockAuthPrompt()); mRedirectChannel = newChannel; rv = gHttpHandler->AsyncOnChannelRedirect( this, newChannel, nsIChannelEventSink::REDIRECT_INTERNAL | nsIChannelEventSink::REDIRECT_AUTH_RETRY); if (NS_SUCCEEDED(rv)) rv = WaitForRedirectCallback(); // redirected channel will be opened after we receive the OnStopRequest if (NS_FAILED(rv)) { AutoRedirectVetoNotifier notifier(this, rv); mRedirectChannel = nullptr; } return rv; } nsresult nsHttpChannel::StartRedirectChannelToURI(nsIURI* upgradedURI, uint32_t flags) { nsresult rv = NS_OK; LOG(("nsHttpChannel::StartRedirectChannelToURI()\n")); nsCOMPtr newChannel; nsCOMPtr redirectLoadInfo = CloneLoadInfoForRedirect(upgradedURI, flags); nsCOMPtr ioService; rv = gHttpHandler->GetIOService(getter_AddRefs(ioService)); NS_ENSURE_SUCCESS(rv, rv); rv = NS_NewChannelInternal(getter_AddRefs(newChannel), upgradedURI, redirectLoadInfo, nullptr, // PerformanceStorage nullptr, // aLoadGroup nullptr, // aCallbacks nsIRequest::LOAD_NORMAL, ioService); NS_ENSURE_SUCCESS(rv, rv); rv = SetupReplacementChannel(upgradedURI, newChannel, true, flags); NS_ENSURE_SUCCESS(rv, rv); if (mHTTPSSVCRecord) { RefPtr httpChan = do_QueryObject(newChannel); nsCOMPtr rec = mHTTPSSVCRecord.ref(); if (httpChan && rec) { httpChan->SetHTTPSSVCRecord(rec.forget()); } } // Inform consumers about this fake redirect mRedirectChannel = newChannel; PushRedirectAsyncFunc(&nsHttpChannel::ContinueAsyncRedirectChannelToURI); rv = gHttpHandler->AsyncOnChannelRedirect(this, newChannel, flags); if (NS_SUCCEEDED(rv)) rv = WaitForRedirectCallback(); if (NS_FAILED(rv)) { AutoRedirectVetoNotifier notifier(this, rv); /* Remove the async call to ContinueAsyncRedirectChannelToURI(). * It is called directly by our callers upon return (to clean up * the failed redirect). */ PopRedirectAsyncFunc(&nsHttpChannel::ContinueAsyncRedirectChannelToURI); } return rv; } nsresult nsHttpChannel::ContinueAsyncRedirectChannelToURI(nsresult rv) { LOG(("nsHttpChannel::ContinueAsyncRedirectChannelToURI [this=%p]", this)); // Since we handle mAPIRedirectToURI also after on-examine-response handler // rather drop it here to avoid any redirect loops, even just hypothetical. mAPIRedirectToURI = nullptr; if (NS_SUCCEEDED(rv)) { rv = OpenRedirectChannel(rv); } if (NS_FAILED(rv)) { // Cancel the channel here, the update to https had been vetoed // but from the security reasons we have to discard the whole channel // load. Cancel(rv); } if (mLoadGroup) { mLoadGroup->RemoveRequest(this, nullptr, mStatus); } if (NS_FAILED(rv) && !mCachePump && !mTransactionPump) { // We have to manually notify the listener because there is not any pump // that would call our OnStart/StopRequest after resume from waiting for // the redirect callback. DoNotifyListener(); } return rv; } nsresult nsHttpChannel::OpenRedirectChannel(nsresult rv) { AutoRedirectVetoNotifier notifier(this, rv); if (NS_FAILED(rv)) return rv; if (!mRedirectChannel) { LOG(( "nsHttpChannel::OpenRedirectChannel unexpected null redirect channel")); return NS_ERROR_FAILURE; } // Make sure to do this after we received redirect veto answer, // i.e. after all sinks had been notified mRedirectChannel->SetOriginalURI(mOriginalURI); // open new channel rv = mRedirectChannel->AsyncOpen(mListener); NS_ENSURE_SUCCESS(rv, rv); mStatus = NS_BINDING_REDIRECTED; notifier.RedirectSucceeded(); ReleaseListeners(); return NS_OK; } nsresult nsHttpChannel::AsyncDoReplaceWithProxy(nsIProxyInfo* pi) { LOG(("nsHttpChannel::AsyncDoReplaceWithProxy [this=%p pi=%p]", this, pi)); nsresult rv; nsCOMPtr newChannel; rv = gHttpHandler->NewProxiedChannel(mURI, pi, mProxyResolveFlags, mProxyURI, mLoadInfo, getter_AddRefs(newChannel)); if (NS_FAILED(rv)) return rv; uint32_t flags = nsIChannelEventSink::REDIRECT_INTERNAL; rv = SetupReplacementChannel(mURI, newChannel, true, flags); if (NS_FAILED(rv)) return rv; // Inform consumers about this fake redirect mRedirectChannel = newChannel; PushRedirectAsyncFunc(&nsHttpChannel::OpenRedirectChannel); rv = gHttpHandler->AsyncOnChannelRedirect(this, newChannel, flags); if (NS_SUCCEEDED(rv)) rv = WaitForRedirectCallback(); if (NS_FAILED(rv)) { AutoRedirectVetoNotifier notifier(this, rv); PopRedirectAsyncFunc(&nsHttpChannel::OpenRedirectChannel); } return rv; } nsresult nsHttpChannel::ResolveProxy() { LOG(("nsHttpChannel::ResolveProxy [this=%p]\n", this)); nsresult rv; nsCOMPtr pps = do_GetService(NS_PROTOCOLPROXYSERVICE_CONTRACTID, &rv); if (NS_FAILED(rv)) return rv; // using the nsIProtocolProxyService2 allows a minor performance // optimization, but if an add-on has only provided the original interface // then it is ok to use that version. nsCOMPtr pps2 = do_QueryInterface(pps); if (pps2) { rv = pps2->AsyncResolve2(this, mProxyResolveFlags, this, nullptr, getter_AddRefs(mProxyRequest)); } else { rv = pps->AsyncResolve(static_cast(this), mProxyResolveFlags, this, nullptr, getter_AddRefs(mProxyRequest)); } return rv; } bool nsHttpChannel::ResponseWouldVary(nsICacheEntry* entry) { nsresult rv; nsAutoCString buf, metaKey; Unused << mCachedResponseHead->GetHeader(nsHttp::Vary, buf); constexpr auto prefix = "request-"_ns; // enumerate the elements of the Vary header... for (const nsACString& token : nsCCharSeparatedTokenizer(buf, NS_HTTP_HEADER_SEP).ToRange()) { LOG( ("nsHttpChannel::ResponseWouldVary [channel=%p] " "processing %s\n", this, nsPromiseFlatCString(token).get())); // // if "*", then assume response would vary. technically speaking, // "Vary: header, *" is not permitted, but we allow it anyways. // // We hash values of cookie-headers for the following reasons: // // 1- cookies can be very large in size // // 2- cookies may contain sensitive information. (for parity with // out policy of not storing Set-cookie headers in the cache // meta data, we likewise do not want to store cookie headers // here.) // if (token.EqualsLiteral("*")) { return true; // if we encounter this, just get out of here } // build cache meta data key... metaKey = prefix + token; // check the last value of the given request header to see if it has // since changed. if so, then indeed the cached response is invalid. nsCString lastVal; entry->GetMetaDataElement(metaKey.get(), getter_Copies(lastVal)); LOG( ("nsHttpChannel::ResponseWouldVary [channel=%p] " "stored value = \"%s\"\n", this, lastVal.get())); // Look for value of "Cookie" in the request headers nsHttpAtom atom = nsHttp::ResolveAtom(token); nsAutoCString newVal; bool hasHeader = NS_SUCCEEDED(mRequestHead.GetHeader(atom, newVal)); if (!lastVal.IsEmpty()) { // value for this header in cache, but no value in request if (!hasHeader) { return true; // yes - response would vary } // If this is a cookie-header, stored metadata is not // the value itself but the hash. So we also hash the // outgoing value here in order to compare the hashes nsAutoCString hash; if (atom == nsHttp::Cookie) { rv = Hash(newVal.get(), hash); // If hash failed, be conservative (the cached hash // exists at this point) and claim response would vary if (NS_FAILED(rv)) return true; newVal = hash; LOG( ("nsHttpChannel::ResponseWouldVary [this=%p] " "set-cookie value hashed to %s\n", this, newVal.get())); } if (!newVal.Equals(lastVal)) { return true; // yes, response would vary } } else if (hasHeader) { // old value is empty, but newVal is set return true; } } return false; } // We need to have an implementation of this function just so that we can keep // all references to mCallOnResume of type nsHttpChannel: it's not OK in C++ // to set a member function ptr to a base class function. void nsHttpChannel::HandleAsyncAbort() { HttpAsyncAborter::HandleAsyncAbort(); } //----------------------------------------------------------------------------- // nsHttpChannel //----------------------------------------------------------------------------- bool nsHttpChannel::IsResumable(int64_t partialLen, int64_t contentLength, bool ignoreMissingPartialLen) const { bool hasContentEncoding = mCachedResponseHead->HasHeader(nsHttp::Content_Encoding); nsAutoCString etag; Unused << mCachedResponseHead->GetHeader(nsHttp::ETag, etag); bool hasWeakEtag = !etag.IsEmpty() && StringBeginsWith(etag, "W/"_ns); return (partialLen < contentLength) && (partialLen > 0 || ignoreMissingPartialLen) && !hasContentEncoding && !hasWeakEtag && mCachedResponseHead->IsResumable() && !LoadCustomConditionalRequest() && !mCachedResponseHead->NoStore(); } nsresult nsHttpChannel::MaybeSetupByteRangeRequest( int64_t partialLen, int64_t contentLength, bool ignoreMissingPartialLen) { // Be pesimistic StoreIsPartialRequest(false); if (!IsResumable(partialLen, contentLength, ignoreMissingPartialLen)) { return NS_ERROR_NOT_RESUMABLE; } // looks like a partial entry we can reuse; add If-Range // and Range headers. nsresult rv = SetupByteRangeRequest(partialLen); if (NS_FAILED(rv)) { // Make the request unconditional again. UntieByteRangeRequest(); } return rv; } nsresult nsHttpChannel::SetupByteRangeRequest(int64_t partialLen) { // cached content has been found to be partial, add necessary request // headers to complete cache entry. // use strongest validator available... nsAutoCString val; Unused << mCachedResponseHead->GetHeader(nsHttp::ETag, val); if (val.IsEmpty()) { Unused << mCachedResponseHead->GetHeader(nsHttp::Last_Modified, val); } if (val.IsEmpty()) { // if we hit this code it means mCachedResponseHead->IsResumable() is // either broken or not being called. MOZ_ASSERT_UNREACHABLE("no cache validator"); StoreIsPartialRequest(false); return NS_ERROR_FAILURE; } char buf[64]; SprintfLiteral(buf, "bytes=%" PRId64 "-", partialLen); DebugOnly rv{}; rv = mRequestHead.SetHeader(nsHttp::Range, nsDependentCString(buf)); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = mRequestHead.SetHeader(nsHttp::If_Range, val); MOZ_ASSERT(NS_SUCCEEDED(rv)); StoreIsPartialRequest(true); return NS_OK; } void nsHttpChannel::UntieByteRangeRequest() { DebugOnly rv{}; rv = mRequestHead.ClearHeader(nsHttp::Range); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = mRequestHead.ClearHeader(nsHttp::If_Range); MOZ_ASSERT(NS_SUCCEEDED(rv)); } nsresult nsHttpChannel::ProcessPartialContent( const std::function& aContinueProcessResponseFunc) { // ok, we've just received a 206 // // we need to stream whatever data is in the cache out first, and then // pick up whatever data is on the wire, writing it into the cache. LOG(("nsHttpChannel::ProcessPartialContent [this=%p]\n", this)); NS_ENSURE_TRUE(mCachedResponseHead, NS_ERROR_NOT_INITIALIZED); NS_ENSURE_TRUE(mCacheEntry, NS_ERROR_NOT_INITIALIZED); // Check if the content-encoding we now got is different from the one we // got before nsAutoCString contentEncoding, cachedContentEncoding; // It is possible that there is not such headers Unused << mResponseHead->GetHeader(nsHttp::Content_Encoding, contentEncoding); Unused << mCachedResponseHead->GetHeader(nsHttp::Content_Encoding, cachedContentEncoding); if (nsCRT::strcasecmp(contentEncoding.get(), cachedContentEncoding.get()) != 0) { Cancel(NS_ERROR_INVALID_CONTENT_ENCODING); return CallOnStartRequest(); } nsresult rv; int64_t cachedContentLength = mCachedResponseHead->ContentLength(); int64_t entitySize = mResponseHead->TotalEntitySize(); nsAutoCString contentRange; Unused << mResponseHead->GetHeader(nsHttp::Content_Range, contentRange); LOG( ("nsHttpChannel::ProcessPartialContent [this=%p trans=%p] " "original content-length %" PRId64 ", entity-size %" PRId64 ", content-range %s\n", this, mTransaction.get(), cachedContentLength, entitySize, contentRange.get())); if ((entitySize >= 0) && (cachedContentLength >= 0) && (entitySize != cachedContentLength)) { LOG( ("nsHttpChannel::ProcessPartialContent [this=%p] " "206 has different total entity size than the content length " "of the original partially cached entity.\n", this)); mCacheEntry->AsyncDoom(nullptr); Cancel(NS_ERROR_CORRUPTED_CONTENT); return CallOnStartRequest(); } if (LoadConcurrentCacheAccess()) { // We started to read cached data sooner than its write has been done. // But the concurrent write has not finished completely, so we had to // do a range request. Now let the content coming from the network // be presented to consumers and also stored to the cache entry. rv = InstallCacheListener(mLogicalOffset); if (NS_FAILED(rv)) return rv; } else { // suspend the current transaction rv = mTransactionPump->Suspend(); if (NS_FAILED(rv)) return rv; } // merge any new headers with the cached response headers mCachedResponseHead->UpdateHeaders(mResponseHead.get()); // update the cached response head nsAutoCString head; mCachedResponseHead->Flatten(head, true); rv = mCacheEntry->SetMetaDataElement("response-head", head.get()); if (NS_FAILED(rv)) return rv; // make the cached response be the current response mResponseHead = std::move(mCachedResponseHead); UpdateInhibitPersistentCachingFlag(); rv = UpdateExpirationTime(); if (NS_FAILED(rv)) return rv; // notify observers interested in looking at a response that has been // merged with any cached headers (http-on-examine-merged-response). gHttpHandler->OnExamineMergedResponse(this); if (LoadConcurrentCacheAccess()) { StoreCachedContentIsPartial(false); // Leave the ConcurrentCacheAccess flag set, we want to use it // to prevent duplicate OnStartRequest call on the target listener // in case this channel is canceled before it gets its OnStartRequest // from the http transaction. return rv; } // Now we continue reading the network response. // the cached content is valid, although incomplete. mCachedContentIsValid = true; return CallOrWaitForResume([aContinueProcessResponseFunc](auto* self) { nsresult rv = self->ReadFromCache(false); return aContinueProcessResponseFunc(self, rv); }); } nsresult nsHttpChannel::OnDoneReadingPartialCacheEntry(bool* streamDone) { nsresult rv; LOG(("nsHttpChannel::OnDoneReadingPartialCacheEntry [this=%p]", this)); // by default, assume we would have streamed all data or failed... *streamDone = true; // setup cache listener to append to cache entry int64_t size; rv = mCacheEntry->GetDataSize(&size); if (NS_FAILED(rv)) return rv; rv = InstallCacheListener(size); if (NS_FAILED(rv)) return rv; // Entry is valid, do it now, after the output stream has been opened, // otherwise when done earlier, pending readers would consider the cache // entry still as partial (CacheEntry::GetDataSize would return the partial // data size) and consumers would do the conditional request again. rv = mCacheEntry->SetValid(); if (NS_FAILED(rv)) return rv; // need to track the logical offset of the data being sent to our listener mLogicalOffset = size; // we're now completing the cached content, so we can clear this flag. // this puts us in the state of a regular download. StoreCachedContentIsPartial(false); // The cache input stream pump is finished, we do not need it any more. // (see bug 1313923) mCachePump = nullptr; // resume the transaction if it exists, otherwise the pipe contained the // remaining part of the document and we've now streamed all of the data. if (mTransactionPump) { rv = mTransactionPump->Resume(); if (NS_SUCCEEDED(rv)) *streamDone = false; } else { MOZ_ASSERT_UNREACHABLE("no transaction"); } return rv; } //----------------------------------------------------------------------------- // nsHttpChannel //----------------------------------------------------------------------------- bool nsHttpChannel::ShouldBypassProcessNotModified() { if (LoadCustomConditionalRequest()) { LOG(("Bypassing ProcessNotModified due to custom conditional headers")); return true; } if (!mDidReval) { LOG( ("Server returned a 304 response even though we did not send a " "conditional request")); return true; } return false; } nsresult nsHttpChannel::ProcessNotModified( const std::function& aContinueProcessResponseFunc) { nsresult rv; LOG(("nsHttpChannel::ProcessNotModified [this=%p]\n", this)); // Assert ShouldBypassProcessNotModified() has been checked before call to // ProcessNotModified(). MOZ_ASSERT(!ShouldBypassProcessNotModified()); MOZ_ASSERT(mCachedResponseHead); MOZ_ASSERT(mCacheEntry); NS_ENSURE_TRUE(mCachedResponseHead && mCacheEntry, NS_ERROR_UNEXPECTED); // If the 304 response contains a Last-Modified different than the // one in our cache that is pretty suspicious and is, in at least the // case of bug 716840, a sign of the server having previously corrupted // our cache with a bad response. Take the minor step here of just dooming // that cache entry so there is a fighting chance of getting things on the // right track. nsAutoCString lastModifiedCached; nsAutoCString lastModified304; rv = mCachedResponseHead->GetHeader(nsHttp::Last_Modified, lastModifiedCached); if (NS_SUCCEEDED(rv)) { rv = mResponseHead->GetHeader(nsHttp::Last_Modified, lastModified304); } if (NS_SUCCEEDED(rv) && !lastModified304.Equals(lastModifiedCached)) { LOG( ("Cache Entry and 304 Last-Modified Headers Do Not Match " "[%s] and [%s]\n", lastModifiedCached.get(), lastModified304.get())); mCacheEntry->AsyncDoom(nullptr); Telemetry::Accumulate(Telemetry::CACHE_LM_INCONSISTENT, true); } // merge any new headers with the cached response headers mCachedResponseHead->UpdateHeaders(mResponseHead.get()); // update the cached response head nsAutoCString head; mCachedResponseHead->Flatten(head, true); rv = mCacheEntry->SetMetaDataElement("response-head", head.get()); if (NS_FAILED(rv)) return rv; // make the cached response be the current response mResponseHead = std::move(mCachedResponseHead); UpdateInhibitPersistentCachingFlag(); rv = UpdateExpirationTime(); if (NS_FAILED(rv)) return rv; rv = AddCacheEntryHeaders(mCacheEntry); if (NS_FAILED(rv)) return rv; // notify observers interested in looking at a reponse that has been // merged with any cached headers gHttpHandler->OnExamineMergedResponse(this); mCachedContentIsValid = true; // Tell other consumers the entry is OK to use rv = mCacheEntry->SetValid(); if (NS_FAILED(rv)) return rv; return CallOrWaitForResume([aContinueProcessResponseFunc](auto* self) { nsresult rv = self->ReadFromCache(false); return aContinueProcessResponseFunc(self, rv); }); } // Determines if a request is a byte range request for a subrange, // i.e. is a byte range request, but not a 0- byte range request. static bool IsSubRangeRequest(nsHttpRequestHead& aRequestHead) { nsAutoCString byteRange; if (NS_FAILED(aRequestHead.GetHeader(nsHttp::Range, byteRange))) { return false; } return !byteRange.EqualsLiteral("bytes=0-"); } nsresult nsHttpChannel::OpenCacheEntry(bool isHttps) { // Drop this flag here StoreConcurrentCacheAccess(0); LOG(("nsHttpChannel::OpenCacheEntry [this=%p]", this)); // make sure we're not abusing this function MOZ_ASSERT(!mCacheEntry, "cache entry already open"); if (mRequestHead.IsPost()) { // If the post id is already set then this is an attempt to replay // a post transaction via the cache. Otherwise, we need a unique // post id for this transaction. if (mPostID == 0) mPostID = gHttpHandler->GenerateUniqueID(); } else if (!mRequestHead.IsGet() && !mRequestHead.IsHead()) { // don't use the cache for other types of requests return NS_OK; } return OpenCacheEntryInternal(isHttps); } nsresult nsHttpChannel::OpenCacheEntryInternal(bool isHttps) { nsresult rv; if (LoadResuming()) { // We don't support caching for requests initiated // via nsIResumableChannel. return NS_OK; } // Don't cache byte range requests which are subranges, only cache 0- // byte range requests. if (IsSubRangeRequest(mRequestHead)) { return NS_OK; } // Handle correctly WaitForCacheEntry AutoCacheWaitFlags waitFlags(this); nsAutoCString cacheKey; nsCOMPtr cacheStorageService( components::CacheStorage::Service()); if (!cacheStorageService) { return NS_ERROR_NOT_AVAILABLE; } nsCOMPtr cacheStorage; mCacheEntryURI = mURI; RefPtr info = GetLoadContextInfo(this); if (!info) { return NS_ERROR_FAILURE; } uint32_t cacheEntryOpenFlags; bool offline = gIOService->IsOffline(); RefPtr bc; mLoadInfo->GetBrowsingContext(getter_AddRefs(bc)); bool maybeRCWN = false; nsAutoCString cacheControlRequestHeader; Unused << mRequestHead.GetHeader(nsHttp::Cache_Control, cacheControlRequestHeader); CacheControlParser cacheControlRequest(cacheControlRequestHeader); if (cacheControlRequest.NoStore()) { return NS_OK; } if (offline || (mLoadFlags & INHIBIT_CACHING) || (bc && bc->Top()->GetForceOffline())) { if (BYPASS_LOCAL_CACHE(mLoadFlags, LoadPreferCacheLoadOverBypass()) && !offline) { return NS_OK; } cacheEntryOpenFlags = nsICacheStorage::OPEN_READONLY; StoreCacheEntryIsReadOnly(true); } else if (BYPASS_LOCAL_CACHE(mLoadFlags, LoadPreferCacheLoadOverBypass())) { cacheEntryOpenFlags = nsICacheStorage::OPEN_TRUNCATE; } else { cacheEntryOpenFlags = nsICacheStorage::OPEN_NORMALLY | nsICacheStorage::CHECK_MULTITHREADED; } // Remember the request is a custom conditional request so that we can // process any 304 response correctly. StoreCustomConditionalRequest( mRequestHead.HasHeader(nsHttp::If_Modified_Since) || mRequestHead.HasHeader(nsHttp::If_None_Match) || mRequestHead.HasHeader(nsHttp::If_Unmodified_Since) || mRequestHead.HasHeader(nsHttp::If_Match) || mRequestHead.HasHeader(nsHttp::If_Range)); if (mLoadFlags & INHIBIT_PERSISTENT_CACHING) { rv = cacheStorageService->MemoryCacheStorage( info, // ? choose app cache as well... getter_AddRefs(cacheStorage)); } else if (LoadPinCacheContent()) { rv = cacheStorageService->PinningCacheStorage(info, getter_AddRefs(cacheStorage)); } else { // Try to race only if we use disk cache storage maybeRCWN = mRequestHead.IsSafeMethod(); rv = cacheStorageService->DiskCacheStorage(info, getter_AddRefs(cacheStorage)); } NS_ENSURE_SUCCESS(rv, rv); if ((mClassOfService.Flags() & nsIClassOfService::Leader) || (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI)) { cacheEntryOpenFlags |= nsICacheStorage::OPEN_PRIORITY; } // Only for backward compatibility with the old cache back end. // When removed, remove the flags and related code snippets. if (mLoadFlags & LOAD_BYPASS_LOCAL_CACHE_IF_BUSY) { cacheEntryOpenFlags |= nsICacheStorage::OPEN_BYPASS_IF_BUSY; } if (mPostID) { mCacheIdExtension.Append(nsPrintfCString("%d", mPostID)); } if (LoadIsTRRServiceChannel()) { mCacheIdExtension.Append("TRR"); } if (mRequestHead.IsHead()) { mCacheIdExtension.Append("HEAD"); } bool isThirdParty = false; if (StaticPrefs::network_fetch_cache_partition_cross_origin() && (NS_FAILED(mLoadInfo->TriggeringPrincipal()->IsThirdPartyChannel( this, &isThirdParty)) || isThirdParty) && (mLoadInfo->InternalContentPolicyType() == nsIContentPolicy::TYPE_FETCH || mLoadInfo->InternalContentPolicyType() == nsIContentPolicy::TYPE_XMLHTTPREQUEST || mLoadInfo->InternalContentPolicyType() == nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST)) { mCacheIdExtension.Append("FETCH"); } mCacheOpenWithPriority = cacheEntryOpenFlags & nsICacheStorage::OPEN_PRIORITY; mCacheQueueSizeWhenOpen = CacheStorageService::CacheQueueSize(mCacheOpenWithPriority); if ((mNetworkTriggerDelay || StaticPrefs::network_http_rcwn_enabled()) && maybeRCWN) { bool hasAltData = false; uint32_t sizeInKb = 0; rv = cacheStorage->GetCacheIndexEntryAttrs( mCacheEntryURI, mCacheIdExtension, &hasAltData, &sizeInKb); // We will attempt to race the network vs the cache if we've found // this entry in the cache index, and it has appropriate attributes // (doesn't have alt-data, and has a small size) if (NS_SUCCEEDED(rv) && !hasAltData && sizeInKb < StaticPrefs::network_http_rcwn_small_resource_size_kb()) { MaybeRaceCacheWithNetwork(); } } if (!mCacheOpenDelay) { MOZ_ASSERT(NS_IsMainThread(), "Should be called on the main thread"); if (mNetworkTriggered) { mRaceCacheWithNetwork = StaticPrefs::network_http_rcwn_enabled(); } rv = cacheStorage->AsyncOpenURI(mCacheEntryURI, mCacheIdExtension, cacheEntryOpenFlags, this); } else { // We pass `this` explicitly as a parameter due to the raw pointer // to refcounted object in lambda analysis. mCacheOpenFunc = [cacheEntryOpenFlags, cacheStorage](nsHttpChannel* self) -> void { MOZ_ASSERT(NS_IsMainThread(), "Should be called on the main thread"); cacheStorage->AsyncOpenURI(self->mCacheEntryURI, self->mCacheIdExtension, cacheEntryOpenFlags, self); }; // calls nsHttpChannel::Notify after `mCacheOpenDelay` milliseconds auto callback = MakeRefPtr(this); NS_NewTimerWithCallback(getter_AddRefs(mCacheOpenTimer), callback, mCacheOpenDelay, nsITimer::TYPE_ONE_SHOT); } NS_ENSURE_SUCCESS(rv, rv); waitFlags.Keep(WAIT_FOR_CACHE_ENTRY); return NS_OK; } nsresult nsHttpChannel::CheckPartial(nsICacheEntry* aEntry, int64_t* aSize, int64_t* aContentLength) { return nsHttp::CheckPartial( aEntry, aSize, aContentLength, mCachedResponseHead ? mCachedResponseHead.get() : mResponseHead.get()); } void nsHttpChannel::UntieValidationRequest() { DebugOnly rv{}; // Make the request unconditional again. rv = mRequestHead.ClearHeader(nsHttp::If_Modified_Since); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = mRequestHead.ClearHeader(nsHttp::If_None_Match); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = mRequestHead.ClearHeader(nsHttp::ETag); MOZ_ASSERT(NS_SUCCEEDED(rv)); } NS_IMETHODIMP nsHttpChannel::OnCacheEntryCheck(nsICacheEntry* entry, uint32_t* aResult) { nsresult rv = NS_OK; LOG(("nsHttpChannel::OnCacheEntryCheck enter [channel=%p entry=%p]", this, entry)); mozilla::MutexAutoLock lock(mRCWNLock); if (mRaceCacheWithNetwork && mFirstResponseSource == RESPONSE_FROM_NETWORK) { LOG( ("Not using cached response because we've already got one from the " "network\n")); *aResult = ENTRY_NOT_WANTED; // Net-win indicates that mOnStartRequestTimestamp is from net. int64_t savedTime = (TimeStamp::Now() - mOnStartRequestTimestamp).ToMilliseconds(); Telemetry::Accumulate(Telemetry::NETWORK_RACE_CACHE_WITH_NETWORK_SAVED_TIME, savedTime); return NS_OK; } if (mRaceCacheWithNetwork && mFirstResponseSource == RESPONSE_PENDING) { mOnCacheEntryCheckTimestamp = TimeStamp::Now(); } nsAutoCString cacheControlRequestHeader; Unused << mRequestHead.GetHeader(nsHttp::Cache_Control, cacheControlRequestHeader); CacheControlParser cacheControlRequest(cacheControlRequestHeader); if (cacheControlRequest.NoStore()) { LOG( ("Not using cached response based on no-store request cache " "directive\n")); *aResult = ENTRY_NOT_WANTED; return NS_OK; } // Be pessimistic: assume the cache entry has no useful data. *aResult = ENTRY_WANTED; mCachedContentIsValid = false; nsCString buf; // Get the method that was used to generate the cached response rv = entry->GetMetaDataElement("request-method", getter_Copies(buf)); NS_ENSURE_SUCCESS(rv, rv); bool methodWasHead = buf.EqualsLiteral("HEAD"); bool methodWasGet = buf.EqualsLiteral("GET"); if (methodWasHead) { // The cached response does not contain an entity. We can only reuse // the response if the current request is also HEAD. if (!mRequestHead.IsHead()) { *aResult = ENTRY_NOT_WANTED; return NS_OK; } } buf.Adopt(nullptr); // We'll need this value in later computations... uint32_t lastModifiedTime; rv = entry->GetLastModified(&lastModifiedTime); NS_ENSURE_SUCCESS(rv, rv); // Determine if this is the first time that this cache entry // has been accessed during this session. bool fromPreviousSession = (gHttpHandler->SessionStartTime() > lastModifiedTime); // Get the cached HTTP response headers mCachedResponseHead = MakeUnique(); rv = nsHttp::GetHttpResponseHeadFromCacheEntry(entry, mCachedResponseHead.get()); NS_ENSURE_SUCCESS(rv, rv); bool isCachedRedirect = WillRedirect(*mCachedResponseHead); // Do not return 304 responses from the cache, and also do not return // any other non-redirect 3xx responses from the cache (see bug 759043). NS_ENSURE_TRUE((mCachedResponseHead->Status() / 100 != 3) || isCachedRedirect, NS_ERROR_ABORT); if (mCachedResponseHead->NoStore() && LoadCacheEntryIsReadOnly()) { // This prevents loading no-store responses when navigating back // while the browser is set to work offline. LOG((" entry loading as read-only but is no-store, set INHIBIT_CACHING")); mLoadFlags |= nsIRequest::INHIBIT_CACHING; } // Don't bother to validate items that are read-only, // unless they are read-only because of INHIBIT_CACHING if ((LoadCacheEntryIsReadOnly() && !(mLoadFlags & nsIRequest::INHIBIT_CACHING))) { int64_t size, contentLength; rv = CheckPartial(entry, &size, &contentLength); NS_ENSURE_SUCCESS(rv, rv); if (contentLength != int64_t(-1) && contentLength != size) { *aResult = ENTRY_NOT_WANTED; return NS_OK; } rv = OpenCacheInputStream(entry, true); if (NS_SUCCEEDED(rv)) { mCachedContentIsValid = true; entry->MaybeMarkValid(); } return rv; } bool wantCompleteEntry = false; if (!methodWasHead && !isCachedRedirect) { // If the cached content-length is set and it does not match the data // size of the cached content, then the cached response is partial... // either we need to issue a byte range request or we need to refetch // the entire document. // // We exclude redirects from this check because we (usually) strip the // entity when we store the cache entry, and even if we didn't, we // always ignore a cached redirect's entity anyway. See bug 759043. int64_t size, contentLength; rv = CheckPartial(entry, &size, &contentLength); NS_ENSURE_SUCCESS(rv, rv); if (size == int64_t(-1)) { LOG((" write is in progress")); if (mLoadFlags & LOAD_BYPASS_LOCAL_CACHE_IF_BUSY) { LOG( (" not interested in the entry, " "LOAD_BYPASS_LOCAL_CACHE_IF_BUSY specified")); *aResult = ENTRY_NOT_WANTED; return NS_OK; } // Ignore !(size > 0) from the resumability condition if (!IsResumable(size, contentLength, true)) { if (IsNavigation()) { LOG( (" bypassing wait for the entry, " "this is a navigational load")); *aResult = ENTRY_NOT_WANTED; return NS_OK; } LOG( (" wait for entry completion, " "response is not resumable")); wantCompleteEntry = true; } else { StoreConcurrentCacheAccess(1); } } else if (contentLength != int64_t(-1) && contentLength != size) { LOG( ("Cached data size does not match the Content-Length header " "[content-length=%" PRId64 " size=%" PRId64 "]\n", contentLength, size)); rv = MaybeSetupByteRangeRequest(size, contentLength); StoreCachedContentIsPartial(NS_SUCCEEDED(rv) && LoadIsPartialRequest()); if (LoadCachedContentIsPartial()) { rv = OpenCacheInputStream(entry, false); if (NS_FAILED(rv)) { UntieByteRangeRequest(); return rv; } *aResult = ENTRY_NEEDS_REVALIDATION; return NS_OK; } if (size == 0 && LoadCacheOnlyMetadata()) { // Don't break cache entry load when the entry's data size // is 0 and CacheOnlyMetadata flag is set. In that case we // want to proceed since the LOAD_ONLY_IF_MODIFIED flag is // also set. MOZ_ASSERT(mLoadFlags & LOAD_ONLY_IF_MODIFIED); } else { return rv; } } } bool isHttps = mURI->SchemeIs("https"); bool doValidation = false; bool doBackgroundValidation = false; bool canAddImsHeader = true; bool isForcedValid = false; entry->GetIsForcedValid(&isForcedValid); auto prefetchStatus = Telemetry::LABELS_PREDICTOR_PREFETCH_USE_STATUS::Used; bool weaklyFramed, isImmutable; nsHttp::DetermineFramingAndImmutability(entry, mCachedResponseHead.get(), isHttps, &weaklyFramed, &isImmutable); // Cached entry is not the entity we request (see bug #633743) if (ResponseWouldVary(entry)) { LOG(("Validating based on Vary headers returning TRUE\n")); canAddImsHeader = false; doValidation = true; prefetchStatus = Telemetry::LABELS_PREDICTOR_PREFETCH_USE_STATUS::WouldVary; } else { if (mCachedResponseHead->ExpiresInPast() || mCachedResponseHead->MustValidateIfExpired()) { prefetchStatus = Telemetry::LABELS_PREDICTOR_PREFETCH_USE_STATUS::Expired; } doValidation = nsHttp::ValidationRequired( isForcedValid, mCachedResponseHead.get(), mLoadFlags, LoadAllowStaleCacheContent(), LoadForceValidateCacheContent(), isImmutable, LoadCustomConditionalRequest(), mRequestHead, entry, cacheControlRequest, fromPreviousSession, &doBackgroundValidation); } nsAutoCString requestedETag; if (!doValidation && NS_SUCCEEDED(mRequestHead.GetHeader(nsHttp::If_Match, requestedETag)) && (methodWasGet || methodWasHead)) { nsAutoCString cachedETag; Unused << mCachedResponseHead->GetHeader(nsHttp::ETag, cachedETag); if (!cachedETag.IsEmpty() && (StringBeginsWith(cachedETag, "W/"_ns) || !requestedETag.Equals(cachedETag))) { // User has defined If-Match header, if the cached entry is not // matching the provided header value or the cached ETag is weak, // force validation. doValidation = true; } } // Previous error should not be propagated. rv = NS_OK; if (!doValidation) { // // Check the authorization headers used to generate the cache entry. // We must validate the cache entry if: // // 1) the cache entry was generated prior to this session w/ // credentials (see bug 103402). // 2) the cache entry was generated w/o credentials, but would now // require credentials (see bug 96705). // // NOTE: this does not apply to proxy authentication. // entry->GetMetaDataElement("auth", getter_Copies(buf)); doValidation = (fromPreviousSession && !buf.IsEmpty()) || (buf.IsEmpty() && mRequestHead.HasHeader(nsHttp::Authorization)); if (doValidation) { prefetchStatus = Telemetry::LABELS_PREDICTOR_PREFETCH_USE_STATUS::Auth; } } // Bug #561276: We maintain a chain of cache-keys which returns cached // 3xx-responses (redirects) in order to detect cycles. If a cycle is // found, ignore the cached response and hit the net. Otherwise, use // the cached response and add the cache-key to the chain. Note that // a limited number of redirects (cached or not) is allowed and is // enforced independently of this mechanism if (!doValidation && isCachedRedirect) { nsAutoCString cacheKey; rv = GenerateCacheKey(mPostID, cacheKey); MOZ_ASSERT(NS_SUCCEEDED(rv)); auto redirectedCachekeys = mRedirectedCachekeys.Lock(); auto& ref = redirectedCachekeys.ref(); if (!ref) { ref = MakeUnique>(); } else if (ref->Contains(cacheKey)) { doValidation = true; } LOG(("Redirection-chain %s key %s\n", doValidation ? "contains" : "does not contain", cacheKey.get())); // Append cacheKey if not in the chain already if (!doValidation) { ref->AppendElement(cacheKey); } else { prefetchStatus = Telemetry::LABELS_PREDICTOR_PREFETCH_USE_STATUS::Redirect; } } mCachedContentIsValid = !doValidation; if (isForcedValid) { // Telemetry value is only useful if this was a prefetched item if (!doValidation) { // Could have gotten to a funky state with some of the if chain above // and in nsHttp::ValidationRequired. Make sure we get it right here. prefetchStatus = Telemetry::LABELS_PREDICTOR_PREFETCH_USE_STATUS::Used; entry->MarkForcedValidUse(); } Telemetry::AccumulateCategorical(prefetchStatus); } if (doValidation) { // // now, we are definitely going to issue a HTTP request to the server. // make it conditional if possible. // // do not attempt to validate no-store content, since servers will not // expect it to be cached. (we only keep it in our cache for the // purposes of back/forward, etc.) // // the request method MUST be either GET or HEAD (see bug 175641) and // the cached response code must be < 400 // // the cached content must not be weakly framed // // do not override conditional headers when consumer has defined its own if (!mCachedResponseHead->NoStore() && (mRequestHead.IsGet() || mRequestHead.IsHead()) && !LoadCustomConditionalRequest() && !weaklyFramed && (mCachedResponseHead->Status() < 400)) { if (LoadConcurrentCacheAccess()) { // In case of concurrent read and also validation request we // must wait for the current writer to close the output stream // first. Otherwise, when the writer's job would have been interrupted // before all the data were downloaded, we'd have to do a range request // which would be a second request in line during this channel's // life-time. nsHttpChannel is not designed to do that, so rather // turn off concurrent read and wait for entry's completion. // Then only re-validation or range-re-validation request will go out. StoreConcurrentCacheAccess(0); // This will cause that OnCacheEntryCheck is called again with the same // entry after the writer is done. wantCompleteEntry = true; } else { nsAutoCString val; // Add If-Modified-Since header if a Last-Modified was given // and we are allowed to do this (see bugs 510359 and 269303) if (canAddImsHeader) { Unused << mCachedResponseHead->GetHeader(nsHttp::Last_Modified, val); if (!val.IsEmpty()) { rv = mRequestHead.SetHeader(nsHttp::If_Modified_Since, val); MOZ_ASSERT(NS_SUCCEEDED(rv)); } } // Add If-None-Match header if an ETag was given in the response Unused << mCachedResponseHead->GetHeader(nsHttp::ETag, val); if (!val.IsEmpty()) { rv = mRequestHead.SetHeader(nsHttp::If_None_Match, val); MOZ_ASSERT(NS_SUCCEEDED(rv)); } mDidReval = true; } } } if (mCachedContentIsValid || mDidReval) { rv = OpenCacheInputStream(entry, mCachedContentIsValid); if (NS_FAILED(rv)) { // If we can't get the entity then we have to act as though we // don't have the cache entry. if (mDidReval) { UntieValidationRequest(); mDidReval = false; } mCachedContentIsValid = false; } } if (mDidReval) { *aResult = ENTRY_NEEDS_REVALIDATION; } else if (wantCompleteEntry) { *aResult = RECHECK_AFTER_WRITE_FINISHED; } else { *aResult = ENTRY_WANTED; if (doBackgroundValidation) { PerformBackgroundCacheRevalidation(); } } if (mCachedContentIsValid) { entry->MaybeMarkValid(); } LOG( ("nsHTTPChannel::OnCacheEntryCheck exit [this=%p doValidation=%d " "result=%d]\n", this, doValidation, *aResult)); return rv; } NS_IMETHODIMP nsHttpChannel::OnCacheEntryAvailable(nsICacheEntry* entry, bool aNew, nsresult status) { MOZ_ASSERT(NS_IsMainThread()); nsresult rv; LOG( ("nsHttpChannel::OnCacheEntryAvailable [this=%p entry=%p " "new=%d status=%" PRIx32 "]\n", this, entry, aNew, static_cast(status))); // if the channel's already fired onStopRequest, then we should ignore // this event. if (!LoadIsPending()) { mCacheInputStream.CloseAndRelease(); return NS_OK; } rv = OnCacheEntryAvailableInternal(entry, aNew, status); if (NS_FAILED(rv)) { CloseCacheEntry(false); if (mRaceCacheWithNetwork && mNetworkTriggered && mFirstResponseSource != RESPONSE_FROM_CACHE) { // Ignore the error if we're racing cache with network and the cache // didn't win, The network part will handle cancelation or any other // error. Otherwise we could end up calling the listener twice, see // bug 1397593. LOG( (" not calling AsyncAbort() because we're racing cache with " "network")); } else { Unused << AsyncAbort(rv); } } return NS_OK; } nsresult nsHttpChannel::OnCacheEntryAvailableInternal(nsICacheEntry* entry, bool aNew, nsresult status) { nsresult rv; if (mCanceled) { LOG(("channel was canceled [this=%p status=%" PRIx32 "]\n", this, static_cast(static_cast(mStatus)))); return mStatus; } if (mIgnoreCacheEntry) { if (!entry || aNew) { // We use this flag later to decide whether to report // LABELS_NETWORK_RACE_CACHE_VALIDATION::NotSent. We didn't have // an usable entry, so drop the flag. mIgnoreCacheEntry = false; } entry = nullptr; status = NS_ERROR_NOT_AVAILABLE; } rv = OnNormalCacheEntryAvailable(entry, aNew, status); if (NS_FAILED(rv) && (mLoadFlags & LOAD_ONLY_FROM_CACHE)) { return NS_ERROR_DOCUMENT_NOT_CACHED; } if (NS_FAILED(rv)) { return rv; } // We may be waiting for more callbacks... if (AwaitingCacheCallbacks()) { return NS_OK; } if (mRaceCacheWithNetwork && ((mCacheEntry && !mCachedContentIsValid && (mDidReval || LoadCachedContentIsPartial())) || mIgnoreCacheEntry)) { // We won't send the conditional request because the unconditional // request was already sent (see bug 1377223). AccumulateCategorical( Telemetry::LABELS_NETWORK_RACE_CACHE_VALIDATION::NotSent); } if (mRaceCacheWithNetwork && mCachedContentIsValid) { Unused << ReadFromCache(true); } return TriggerNetwork(); } nsresult nsHttpChannel::OnNormalCacheEntryAvailable(nsICacheEntry* aEntry, bool aNew, nsresult aEntryStatus) { StoreWaitForCacheEntry(LoadWaitForCacheEntry() & ~WAIT_FOR_CACHE_ENTRY); if (NS_FAILED(aEntryStatus) || aNew) { // Make sure this flag is dropped. It may happen the entry is doomed // between OnCacheEntryCheck and OnCacheEntryAvailable. mCachedContentIsValid = false; // From the same reason remove any conditional headers added // in OnCacheEntryCheck. if (mDidReval) { LOG((" Removing conditional request headers")); UntieValidationRequest(); mDidReval = false; } if (LoadCachedContentIsPartial()) { LOG((" Removing byte range request headers")); UntieByteRangeRequest(); StoreCachedContentIsPartial(false); } if (mLoadFlags & LOAD_ONLY_FROM_CACHE) { // if this channel is only allowed to pull from the cache, then // we must fail if we were unable to open a cache entry for read. return NS_ERROR_DOCUMENT_NOT_CACHED; } } if (NS_SUCCEEDED(aEntryStatus)) { mCacheEntry = aEntry; StoreCacheEntryIsWriteOnly(aNew); if (!aNew && !mAsyncOpenTime.IsNull()) { // We use microseconds for IO operations. For consistency let's use // microseconds here too. uint32_t duration = (TimeStamp::Now() - mAsyncOpenTime).ToMicroseconds(); bool isSlow = false; if ((mCacheOpenWithPriority && mCacheQueueSizeWhenOpen >= StaticPrefs:: network_http_rcwn_cache_queue_priority_threshold()) || (!mCacheOpenWithPriority && mCacheQueueSizeWhenOpen >= StaticPrefs::network_http_rcwn_cache_queue_normal_threshold())) { isSlow = true; } CacheFileUtils::CachePerfStats::AddValue( CacheFileUtils::CachePerfStats::ENTRY_OPEN, duration, isSlow); } } return NS_OK; } // Generates the proper cache-key for this instance of nsHttpChannel nsresult nsHttpChannel::GenerateCacheKey(uint32_t postID, nsACString& cacheKey) { AssembleCacheKey(mSpec.get(), postID, cacheKey); return NS_OK; } // Assembles a cache-key from the given pieces of information and |mLoadFlags| void nsHttpChannel::AssembleCacheKey(const char* spec, uint32_t postID, nsACString& cacheKey) { cacheKey.Truncate(); if (mLoadFlags & LOAD_ANONYMOUS) { cacheKey.AssignLiteral("anon&"); } if (postID) { char buf[32]; SprintfLiteral(buf, "id=%x&", postID); cacheKey.Append(buf); } if (!cacheKey.IsEmpty()) { cacheKey.AppendLiteral("uri="); } // Strip any trailing #ref from the URL before using it as the key const char* p = strchr(spec, '#'); if (p) { cacheKey.Append(spec, p - spec); } else { cacheKey.Append(spec); } } nsresult DoUpdateExpirationTime(nsHttpChannel* aSelf, nsICacheEntry* aCacheEntry, nsHttpResponseHead* aResponseHead, uint32_t& aExpirationTime) { MOZ_ASSERT(aExpirationTime == 0); NS_ENSURE_TRUE(aResponseHead, NS_ERROR_FAILURE); nsresult rv; if (!aResponseHead->MustValidate()) { // For stale-while-revalidate we use expiration time as the absolute base // for calculation of the stale window absolute end time. Hence, when the // entry may be served w/o revalidation, we need a non-zero value for the // expiration time. Let's set it to |now|, which basicly means "expired", // same as when set to 0. uint32_t now = NowInSeconds(); aExpirationTime = now; uint32_t freshnessLifetime = 0; rv = aResponseHead->ComputeFreshnessLifetime(&freshnessLifetime); if (NS_FAILED(rv)) return rv; if (freshnessLifetime > 0) { uint32_t currentAge = 0; rv = aResponseHead->ComputeCurrentAge(now, aSelf->GetRequestTime(), ¤tAge); if (NS_FAILED(rv)) return rv; LOG(("freshnessLifetime = %u, currentAge = %u\n", freshnessLifetime, currentAge)); if (freshnessLifetime > currentAge) { uint32_t timeRemaining = freshnessLifetime - currentAge; // be careful... now + timeRemaining may overflow if (now + timeRemaining < now) { aExpirationTime = uint32_t(-1); } else { aExpirationTime = now + timeRemaining; } } } } rv = aCacheEntry->SetExpirationTime(aExpirationTime); NS_ENSURE_SUCCESS(rv, rv); return rv; } // UpdateExpirationTime is called when a new response comes in from the server. // It updates the stored response-time and sets the expiration time on the // cache entry. // // From section 13.2.4 of RFC2616, we compute expiration time as follows: // // timeRemaining = freshnessLifetime - currentAge // expirationTime = now + timeRemaining // nsresult nsHttpChannel::UpdateExpirationTime() { uint32_t expirationTime = 0; nsresult rv = DoUpdateExpirationTime(this, mCacheEntry, mResponseHead.get(), expirationTime); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult nsHttpChannel::OpenCacheInputStream(nsICacheEntry* cacheEntry, bool startBuffering) { nsresult rv; if (mURI->SchemeIs("https")) { rv = cacheEntry->GetSecurityInfo(getter_AddRefs(mCachedSecurityInfo)); if (NS_FAILED(rv)) { LOG(("failed to parse security-info [channel=%p, entry=%p]", this, cacheEntry)); NS_WARNING("failed to parse security-info"); cacheEntry->AsyncDoom(nullptr); return rv; } MOZ_ASSERT(mCachedSecurityInfo); if (!mCachedSecurityInfo) { LOG( ("mCacheEntry->GetSecurityInfo returned success but did not " "return the security info [channel=%p, entry=%p]", this, cacheEntry)); cacheEntry->AsyncDoom(nullptr); return NS_ERROR_UNEXPECTED; // XXX error code } } // Keep the conditions below in sync with the conditions in ReadFromCache. rv = NS_OK; if (WillRedirect(*mCachedResponseHead)) { // Do not even try to read the entity for a redirect because we do not // return an entity to the application when we process redirects. LOG(("Will skip read of cached redirect entity\n")); return NS_OK; } if ((mLoadFlags & nsICachingChannel::LOAD_ONLY_IF_MODIFIED) && !LoadCachedContentIsPartial()) { // For LOAD_ONLY_IF_MODIFIED, we usually don't have to deal with the // cached entity. LOG( ("Will skip read from cache based on LOAD_ONLY_IF_MODIFIED " "load flag\n")); return NS_OK; } // Open an input stream for the entity, so that the call to OpenInputStream // happens off the main thread. nsCOMPtr stream; // If an alternate representation was requested, try to open the alt // input stream. // If the entry has a "is-from-child" metadata, then only open the altdata // stream if the consumer is also from child. bool altDataFromChild = false; { nsCString value; rv = cacheEntry->GetMetaDataElement("alt-data-from-child", getter_Copies(value)); altDataFromChild = !value.IsEmpty(); } nsAutoCString altDataType; Unused << cacheEntry->GetAltDataType(altDataType); nsAutoCString contentType; mCachedResponseHead->ContentType(contentType); bool foundAltData = false; bool deliverAltData = true; if (!LoadDisableAltDataCache() && !altDataType.IsEmpty() && !mPreferredCachedAltDataTypes.IsEmpty() && altDataFromChild == LoadAltDataForChild()) { for (auto& pref : mPreferredCachedAltDataTypes) { if (pref.type() == altDataType && (pref.contentType().IsEmpty() || pref.contentType() == contentType)) { foundAltData = true; deliverAltData = pref.deliverAltData() == nsICacheInfoChannel::PreferredAlternativeDataDeliveryType::ASYNC; break; } } } nsCOMPtr altData; int64_t altDataSize = -1; if (foundAltData) { rv = cacheEntry->OpenAlternativeInputStream(altDataType, getter_AddRefs(altData)); if (NS_SUCCEEDED(rv)) { // We have succeeded. mAvailableCachedAltDataType = altDataType; StoreDeliveringAltData(deliverAltData); // Set the correct data size on the channel. Unused << cacheEntry->GetAltDataSize(&altDataSize); mAltDataLength = altDataSize; LOG(("Opened alt-data input stream [type=%s, size=%" PRId64 ", deliverAltData=%d]", altDataType.get(), mAltDataLength, deliverAltData)); if (deliverAltData) { stream = altData; } } } if (!stream) { rv = cacheEntry->OpenInputStream(0, getter_AddRefs(stream)); } if (NS_FAILED(rv)) { LOG( ("Failed to open cache input stream [channel=%p, " "mCacheEntry=%p]", this, cacheEntry)); return rv; } if (startBuffering) { bool nonBlocking; rv = stream->IsNonBlocking(&nonBlocking); if (NS_SUCCEEDED(rv) && nonBlocking) startBuffering = false; } if (!startBuffering) { // Bypass wrapping the input stream for the new cache back-end since // nsIStreamTransportService expects a blocking stream. Preloading of // the data must be done on the level of the cache backend, internally. // // We do not connect the stream to the stream transport service if we // have to validate the entry with the server. If we did, we would get // into a race condition between the stream transport service reading // the existing contents and the opening of the cache entry's output // stream to write the new contents in the case where we get a non-304 // response. LOG( ("Opened cache input stream without buffering [channel=%p, " "mCacheEntry=%p, stream=%p]", this, cacheEntry, stream.get())); mCacheInputStream.takeOver(stream); return rv; } // Have the stream transport service start reading the entity on one of its // background threads. nsCOMPtr transport; nsCOMPtr wrapper; nsCOMPtr sts( components::StreamTransport::Service()); rv = sts ? NS_OK : NS_ERROR_NOT_AVAILABLE; if (NS_SUCCEEDED(rv)) { rv = sts->CreateInputTransport(stream, true, getter_AddRefs(transport)); } if (NS_SUCCEEDED(rv)) { rv = transport->OpenInputStream(0, 0, 0, getter_AddRefs(wrapper)); } if (NS_SUCCEEDED(rv)) { LOG( ("Opened cache input stream [channel=%p, wrapper=%p, " "transport=%p, stream=%p]", this, wrapper.get(), transport.get(), stream.get())); } else { LOG( ("Failed to open cache input stream [channel=%p, " "wrapper=%p, transport=%p, stream=%p]", this, wrapper.get(), transport.get(), stream.get())); stream->Close(); return rv; } mCacheInputStream.takeOver(wrapper); return NS_OK; } // Actually process the cached response that we started to handle in CheckCache // and/or StartBufferingCachedEntity. nsresult nsHttpChannel::ReadFromCache(bool alreadyMarkedValid) { NS_ENSURE_TRUE(mCacheEntry, NS_ERROR_FAILURE); NS_ENSURE_TRUE(mCachedContentIsValid, NS_ERROR_FAILURE); NS_ENSURE_TRUE(!mCachePump, NS_OK); // already opened LOG( ("nsHttpChannel::ReadFromCache [this=%p] " "Using cached copy of: %s\n", this, mSpec.get())); // When racing the cache with the network with a timer, and we get data from // the cache, we should prevent the timer from triggering a network request. if (mNetworkTriggerTimer) { mNetworkTriggerTimer->Cancel(); mNetworkTriggerTimer = nullptr; } if (mRaceCacheWithNetwork) { MOZ_ASSERT(mFirstResponseSource != RESPONSE_FROM_CACHE); if (mFirstResponseSource == RESPONSE_PENDING) { LOG(("First response from cache\n")); mFirstResponseSource = RESPONSE_FROM_CACHE; // Cancel the transaction because we will serve the request from the cache CancelNetworkRequest(NS_BINDING_ABORTED); if (mTransactionPump && mSuspendCount) { uint32_t suspendCount = mSuspendCount; while (suspendCount--) { mTransactionPump->Resume(); } } mTransaction = nullptr; mTransactionPump = nullptr; } else { MOZ_ASSERT(mFirstResponseSource == RESPONSE_FROM_NETWORK); LOG( ("Skipping read from cache because first response was from " "network\n")); if (!mOnCacheEntryCheckTimestamp.IsNull()) { TimeStamp currentTime = TimeStamp::Now(); int64_t savedTime = (currentTime - mOnStartRequestTimestamp).ToMilliseconds(); Telemetry::Accumulate( Telemetry::NETWORK_RACE_CACHE_WITH_NETWORK_SAVED_TIME, savedTime); int64_t diffTime = (currentTime - mOnCacheEntryCheckTimestamp).ToMilliseconds(); Telemetry::Accumulate( Telemetry::NETWORK_RACE_CACHE_WITH_NETWORK_OCEC_ON_START_DIFF, diffTime); } return NS_OK; } } if (mCachedResponseHead) mResponseHead = std::move(mCachedResponseHead); UpdateInhibitPersistentCachingFlag(); // if we don't already have security info, try to get it from the cache // entry. there are two cases to consider here: 1) we are just reading // from the cache, or 2) this may be due to a 304 not modified response, // in which case we could have security info from a socket transport. if (!mSecurityInfo) mSecurityInfo = mCachedSecurityInfo; if (!alreadyMarkedValid && !LoadCachedContentIsPartial()) { // We validated the entry, and we have write access to the cache, so // mark the cache entry as valid in order to allow others access to // this cache entry. // // TODO: This should be done asynchronously so we don't take the cache // service lock on the main thread. mCacheEntry->MaybeMarkValid(); } nsresult rv; // Keep the conditions below in sync with the conditions in // StartBufferingCachedEntity. if (WillRedirect(*mResponseHead)) { // TODO: Bug 759040 - We should call HandleAsyncRedirect directly here, // to avoid event dispatching latency. MOZ_ASSERT(!mCacheInputStream); LOG(("Skipping skip read of cached redirect entity\n")); return AsyncCall(&nsHttpChannel::HandleAsyncRedirect); } if ((mLoadFlags & LOAD_ONLY_IF_MODIFIED) && !LoadCachedContentIsPartial()) { LOG( ("Skipping read from cache based on LOAD_ONLY_IF_MODIFIED " "load flag\n")); MOZ_ASSERT(!mCacheInputStream); // TODO: Bug 759040 - We should call HandleAsyncNotModified directly // here, to avoid event dispatching latency. return AsyncCall(&nsHttpChannel::HandleAsyncNotModified); } MOZ_ASSERT(mCacheInputStream); if (!mCacheInputStream) { NS_ERROR( "mCacheInputStream is null but we're expecting to " "be able to read from it."); return NS_ERROR_UNEXPECTED; } nsCOMPtr inputStream = mCacheInputStream.forget(); rv = nsInputStreamPump::Create(getter_AddRefs(mCachePump), inputStream, 0, 0, true); if (NS_FAILED(rv)) { inputStream->Close(); return rv; } rv = mCachePump->AsyncRead(this); if (NS_FAILED(rv)) return rv; if (LoadTimingEnabled()) mCacheReadStart = TimeStamp::Now(); uint32_t suspendCount = mSuspendCount; if (LoadAsyncResumePending()) { LOG( (" Suspend()'ing cache pump once because of async resume pending" ", sc=%u, pump=%p, this=%p", suspendCount, mCachePump.get(), this)); ++suspendCount; } while (suspendCount--) { mCachePump->Suspend(); } return NS_OK; } void nsHttpChannel::CloseCacheEntry(bool doomOnFailure) { mCacheInputStream.CloseAndRelease(); if (!mCacheEntry) return; LOG(("nsHttpChannel::CloseCacheEntry [this=%p] mStatus=%" PRIx32 " CacheEntryIsWriteOnly=%x", this, static_cast(static_cast(mStatus)), LoadCacheEntryIsWriteOnly())); // If we have begun to create or replace a cache entry, and that cache // entry is not complete and not resumable, then it needs to be doomed. // Otherwise, CheckCache will make the mistake of thinking that the // partial cache entry is complete. bool doom = false; if (LoadInitedCacheEntry()) { MOZ_ASSERT(mResponseHead, "oops"); if (NS_FAILED(mStatus) && doomOnFailure && LoadCacheEntryIsWriteOnly() && !mResponseHead->IsResumable()) { doom = true; } } else if (LoadCacheEntryIsWriteOnly()) { doom = true; } if (doom) { LOG((" dooming cache entry!!")); mCacheEntry->AsyncDoom(nullptr); } else { // Store updated security info, makes cached EV status race less likely // (see bug 1040086) if (mSecurityInfo) { mCacheEntry->SetSecurityInfo(mSecurityInfo); } } mCachedResponseHead = nullptr; mCachePump = nullptr; // This releases the entry for other consumers to use. // We call Dismiss() in case someone still keeps a reference // to this entry handle. mCacheEntry->Dismiss(); mCacheEntry = nullptr; StoreCacheEntryIsWriteOnly(false); StoreInitedCacheEntry(false); } void nsHttpChannel::MaybeCreateCacheEntryWhenRCWN() { mozilla::MutexAutoLock lock(mRCWNLock); // Create cache entry for writing only when we're racing cache with network // and we don't have the entry because network won. if (mCacheEntry || !mRaceCacheWithNetwork || mFirstResponseSource != RESPONSE_FROM_NETWORK || LoadCacheEntryIsReadOnly()) { return; } LOG(("nsHttpChannel::MaybeCreateCacheEntryWhenRCWN [this=%p]", this)); nsCOMPtr cacheStorageService( components::CacheStorage::Service()); if (!cacheStorageService) { return; } nsCOMPtr cacheStorage; RefPtr info = GetLoadContextInfo(this); Unused << cacheStorageService->DiskCacheStorage(info, getter_AddRefs(cacheStorage)); if (!cacheStorage) { return; } Unused << cacheStorage->OpenTruncate(mCacheEntryURI, mCacheIdExtension, getter_AddRefs(mCacheEntry)); LOG((" created entry %p", mCacheEntry.get())); if (AwaitingCacheCallbacks()) { // Setting mIgnoreCacheEntry to true ensures that we won't close this // write-only entry in OnCacheEntryAvailable() if this method was called // after OnCacheEntryCheck(). mIgnoreCacheEntry = true; } mAvailableCachedAltDataType.Truncate(); StoreDeliveringAltData(false); mAltDataLength = -1; mCacheInputStream.CloseAndRelease(); mCachedContentIsValid = false; } // Initialize the cache entry for writing. // - finalize storage policy // - store security info // - update expiration time // - store headers and other meta data nsresult nsHttpChannel::InitCacheEntry() { nsresult rv; NS_ENSURE_TRUE(mCacheEntry, NS_ERROR_UNEXPECTED); // if only reading, nothing to be done here. if (LoadCacheEntryIsReadOnly()) return NS_OK; // Don't cache the response again if already cached... if (mCachedContentIsValid) return NS_OK; LOG(("nsHttpChannel::InitCacheEntry [this=%p entry=%p]\n", this, mCacheEntry.get())); bool recreate = !LoadCacheEntryIsWriteOnly(); bool dontPersist = mLoadFlags & INHIBIT_PERSISTENT_CACHING; if (!recreate && dontPersist) { // If the current entry is persistent but we inhibit peristence // then force recreation of the entry as memory/only. rv = mCacheEntry->GetPersistent(&recreate); if (NS_FAILED(rv)) return rv; } if (recreate) { LOG( (" we have a ready entry, but reading it again from the server -> " "recreating cache entry\n")); // clean the altData cache and reset this to avoid wrong content length mAvailableCachedAltDataType.Truncate(); StoreDeliveringAltData(false); nsCOMPtr currentEntry; currentEntry.swap(mCacheEntry); rv = currentEntry->Recreate(dontPersist, getter_AddRefs(mCacheEntry)); if (NS_FAILED(rv)) { LOG((" recreation failed, the response will not be cached")); return NS_OK; } StoreCacheEntryIsWriteOnly(true); } // Set the expiration time for this cache entry rv = UpdateExpirationTime(); if (NS_FAILED(rv)) return rv; // mark this weakly framed until a response body is seen mCacheEntry->SetMetaDataElement("strongly-framed", "0"); rv = AddCacheEntryHeaders(mCacheEntry); if (NS_FAILED(rv)) return rv; StoreInitedCacheEntry(true); // Don't perform the check when writing (doesn't make sense) StoreConcurrentCacheAccess(0); return NS_OK; } void nsHttpChannel::UpdateInhibitPersistentCachingFlag() { // The no-store directive within the 'Cache-Control:' header indicates // that we must not store the response in a persistent cache. if (mResponseHead->NoStore()) mLoadFlags |= INHIBIT_PERSISTENT_CACHING; // Only cache SSL content on disk if the pref is set if (!gHttpHandler->IsPersistentHttpsCachingEnabled() && mURI->SchemeIs("https")) { mLoadFlags |= INHIBIT_PERSISTENT_CACHING; } } nsresult DoAddCacheEntryHeaders(nsHttpChannel* self, nsICacheEntry* entry, nsHttpRequestHead* requestHead, nsHttpResponseHead* responseHead, nsITransportSecurityInfo* securityInfo) { nsresult rv; LOG(("nsHttpChannel::AddCacheEntryHeaders [this=%p] begin", self)); // Store secure data in memory only if (securityInfo) { entry->SetSecurityInfo(securityInfo); } // Store the HTTP request method with the cache entry so we can distinguish // for example GET and HEAD responses. nsAutoCString method; requestHead->Method(method); rv = entry->SetMetaDataElement("request-method", method.get()); if (NS_FAILED(rv)) return rv; // Store the HTTP authorization scheme used if any... rv = StoreAuthorizationMetaData(entry, requestHead); if (NS_FAILED(rv)) return rv; // Iterate over the headers listed in the Vary response header, and // store the value of the corresponding request header so we can verify // that it has not varied when we try to re-use the cached response at // a later time. Take care to store "Cookie" headers only as hashes // due to security considerations and the fact that they can be pretty // large (bug 468426). We take care of "Vary: cookie" in ResponseWouldVary. // // NOTE: if "Vary: accept, cookie", then we will store the "accept" header // in the cache. we could try to avoid needlessly storing the "accept" // header in this case, but it doesn't seem worth the extra code to perform // the check. { nsAutoCString buf, metaKey; Unused << responseHead->GetHeader(nsHttp::Vary, buf); constexpr auto prefix = "request-"_ns; for (const nsACString& token : nsCCharSeparatedTokenizer(buf, NS_HTTP_HEADER_SEP).ToRange()) { LOG( ("nsHttpChannel::AddCacheEntryHeaders [this=%p] " "processing %s", self, nsPromiseFlatCString(token).get())); if (!token.EqualsLiteral("*")) { nsHttpAtom atom = nsHttp::ResolveAtom(token); nsAutoCString val; nsAutoCString hash; if (NS_SUCCEEDED(requestHead->GetHeader(atom, val))) { // If cookie-header, store a hash of the value if (atom == nsHttp::Cookie) { LOG( ("nsHttpChannel::AddCacheEntryHeaders [this=%p] " "cookie-value %s", self, val.get())); rv = Hash(val.get(), hash); // If hash failed, store a string not very likely // to be the result of subsequent hashes if (NS_FAILED(rv)) { val = ""_ns; } else { val = hash; } LOG((" hashed to %s\n", val.get())); } // build cache meta data key and set meta data element... metaKey = prefix + token; entry->SetMetaDataElement(metaKey.get(), val.get()); } else { LOG( ("nsHttpChannel::AddCacheEntryHeaders [this=%p] " "clearing metadata for %s", self, nsPromiseFlatCString(token).get())); metaKey = prefix + token; entry->SetMetaDataElement(metaKey.get(), nullptr); } } } } // Store the received HTTP head with the cache entry as an element of // the meta data. nsAutoCString head; responseHead->Flatten(head, true); rv = entry->SetMetaDataElement("response-head", head.get()); if (NS_FAILED(rv)) return rv; head.Truncate(); responseHead->FlattenNetworkOriginalHeaders(head); rv = entry->SetMetaDataElement("original-response-headers", head.get()); if (NS_FAILED(rv)) return rv; // Indicate we have successfully finished setting metadata on the cache entry. rv = entry->MetaDataReady(); return rv; } nsresult nsHttpChannel::AddCacheEntryHeaders(nsICacheEntry* entry) { return DoAddCacheEntryHeaders(this, entry, &mRequestHead, mResponseHead.get(), mSecurityInfo); } inline void GetAuthType(const char* challenge, nsCString& authType) { const char* p; // get the challenge type if ((p = strchr(challenge, ' ')) != nullptr) { authType.Assign(challenge, p - challenge); } else { authType.Assign(challenge); } } nsresult StoreAuthorizationMetaData(nsICacheEntry* entry, nsHttpRequestHead* requestHead) { // Not applicable to proxy authorization... nsAutoCString val; if (NS_FAILED(requestHead->GetHeader(nsHttp::Authorization, val))) { return NS_OK; } // eg. [Basic realm="wally world"] nsAutoCString buf; GetAuthType(val.get(), buf); return entry->SetMetaDataElement("auth", buf.get()); } // Finalize the cache entry // - may need to rewrite response headers if any headers changed // - may need to recalculate the expiration time if any headers changed // - called only for freshly written cache entries nsresult nsHttpChannel::FinalizeCacheEntry() { LOG(("nsHttpChannel::FinalizeCacheEntry [this=%p]\n", this)); // Don't update this meta-data on 304 if (LoadStronglyFramed() && !mCachedContentIsValid && mCacheEntry) { LOG(("nsHttpChannel::FinalizeCacheEntry [this=%p] Is Strongly Framed\n", this)); mCacheEntry->SetMetaDataElement("strongly-framed", "1"); } if (mResponseHead && LoadResponseHeadersModified()) { // Set the expiration time for this cache entry nsresult rv = UpdateExpirationTime(); if (NS_FAILED(rv)) return rv; } return NS_OK; } // Open an output stream to the cache entry and insert a listener tee into // the chain of response listeners. nsresult nsHttpChannel::InstallCacheListener(int64_t offset) { nsresult rv; LOG(("Preparing to write data into the cache [uri=%s]\n", mSpec.get())); MOZ_ASSERT(mCacheEntry); MOZ_ASSERT(LoadCacheEntryIsWriteOnly() || LoadCachedContentIsPartial() || mRaceCacheWithNetwork); MOZ_ASSERT(mListener); nsAutoCString contentEncoding, contentType; Unused << mResponseHead->GetHeader(nsHttp::Content_Encoding, contentEncoding); mResponseHead->ContentType(contentType); // If the content is compressible and the server has not compressed it, // mark the cache entry for compression. if (contentEncoding.IsEmpty() && (contentType.EqualsLiteral(TEXT_HTML) || contentType.EqualsLiteral(TEXT_PLAIN) || contentType.EqualsLiteral(TEXT_CSS) || contentType.EqualsLiteral(TEXT_JAVASCRIPT) || contentType.EqualsLiteral(TEXT_ECMASCRIPT) || contentType.EqualsLiteral(TEXT_XML) || contentType.EqualsLiteral(APPLICATION_JAVASCRIPT) || contentType.EqualsLiteral(APPLICATION_ECMASCRIPT) || contentType.EqualsLiteral(APPLICATION_XJAVASCRIPT) || contentType.EqualsLiteral(APPLICATION_XHTML_XML))) { rv = mCacheEntry->SetMetaDataElement("uncompressed-len", "0"); if (NS_FAILED(rv)) { LOG(("unable to mark cache entry for compression")); } } LOG(("Trading cache input stream for output stream [channel=%p]", this)); // We must close the input stream first because cache entries do not // correctly handle having an output stream and input streams open at // the same time. mCacheInputStream.CloseAndRelease(); int64_t predictedSize = mResponseHead->TotalEntitySize(); if (predictedSize != -1) { predictedSize -= offset; } nsCOMPtr out; rv = mCacheEntry->OpenOutputStream(offset, predictedSize, getter_AddRefs(out)); if (rv == NS_ERROR_NOT_AVAILABLE) { LOG((" entry doomed, not writing it [channel=%p]", this)); // Entry is already doomed. // This may happen when expiration time is set to past and the entry // has been removed by the background eviction logic. return NS_OK; } if (rv == NS_ERROR_FILE_TOO_BIG) { LOG((" entry would exceed max allowed size, not writing it [channel=%p]", this)); mCacheEntry->AsyncDoom(nullptr); return NS_OK; } if (NS_FAILED(rv)) return rv; if (LoadCacheOnlyMetadata()) { LOG(("Not storing content, cacheOnlyMetadata set")); // We must open and then close the output stream of the cache entry. // This way we indicate the content has been written (despite with zero // length) and the entry is now in the ready state with "having data". out->Close(); return NS_OK; } // XXX disk cache does not support overlapped i/o yet #if 0 // Mark entry valid inorder to allow simultaneous reading... rv = mCacheEntry->MarkValid(); if (NS_FAILED(rv)) return rv; #endif nsCOMPtr tee = do_CreateInstance(kStreamListenerTeeCID, &rv); if (NS_FAILED(rv)) return rv; LOG(("nsHttpChannel::InstallCacheListener sync tee %p rv=%" PRIx32, tee.get(), static_cast(rv))); rv = tee->Init(mListener, out, nullptr); if (NS_FAILED(rv)) return rv; mListener = tee; return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel //----------------------------------------------------------------------------- nsresult nsHttpChannel::SetupReplacementChannel(nsIURI* newURI, nsIChannel* newChannel, bool preserveMethod, uint32_t redirectFlags) { LOG( ("nsHttpChannel::SetupReplacementChannel " "[this=%p newChannel=%p preserveMethod=%d]", this, newChannel, preserveMethod)); if (!mEndMarkerAdded && profiler_thread_is_being_profiled_for_markers()) { mEndMarkerAdded = true; nsAutoCString requestMethod; GetRequestMethod(requestMethod); int32_t priority = PRIORITY_NORMAL; GetPriority(&priority); TimingStruct timings; if (mTransaction) { timings = mTransaction->Timings(); } uint64_t size = 0; GetEncodedBodySize(&size); nsAutoCString contentType; if (mResponseHead) { mResponseHead->ContentType(contentType); } RefPtr newIdentChannel = do_QueryObject(newChannel); uint64_t channelId = 0; if (newIdentChannel) { channelId = newIdentChannel->ChannelId(); } profiler_add_network_marker( mURI, requestMethod, priority, mChannelId, NetworkLoadType::LOAD_REDIRECT, mLastStatusReported, TimeStamp::Now(), size, mCacheDisposition, mLoadInfo->GetInnerWindowID(), mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0, &timings, std::move(mSource), Some(nsDependentCString(contentType.get())), newURI, redirectFlags, channelId); } nsresult rv = HttpBaseChannel::SetupReplacementChannel( newURI, newChannel, preserveMethod, redirectFlags); if (NS_FAILED(rv)) return rv; rv = CheckRedirectLimit(redirectFlags); NS_ENSURE_SUCCESS(rv, rv); // pass on the early hint observer to be able to process `103 Early Hints` // responses after cross origin redirects if (mEarlyHintObserver) { if (RefPtr httpChannelImpl = do_QueryObject(newChannel)) { httpChannelImpl->SetEarlyHintObserver(mEarlyHintObserver); } mEarlyHintObserver = nullptr; } // We don't support redirection for WebTransport for now. mWebTransportSessionEventListener = nullptr; nsCOMPtr httpChannel = do_QueryInterface(newChannel); if (!httpChannel) return NS_OK; // no other options to set // convey the ApplyConversion flag (bug 91862) nsCOMPtr encodedChannel = do_QueryInterface(httpChannel); if (encodedChannel) encodedChannel->SetApplyConversion(LoadApplyConversion()); // transfer the resume information if (LoadResuming()) { nsCOMPtr resumableChannel( do_QueryInterface(newChannel)); if (!resumableChannel) { NS_WARNING( "Got asked to resume, but redirected to non-resumable channel!"); return NS_ERROR_NOT_RESUMABLE; } resumableChannel->ResumeAt(mStartPos, mEntityID); } nsCOMPtr internalChannel = do_QueryInterface(newChannel, &rv); if (NS_SUCCEEDED(rv)) { TimeStamp timestamp; rv = GetNavigationStartTimeStamp(×tamp); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (timestamp) { Unused << internalChannel->SetNavigationStartTimeStamp(timestamp); } } return NS_OK; } nsresult nsHttpChannel::AsyncProcessRedirection(uint32_t redirectType) { LOG(("nsHttpChannel::AsyncProcessRedirection [this=%p type=%u]\n", this, redirectType)); nsresult rv = ProcessCrossOriginSecurityHeaders(); if (NS_FAILED(rv)) { mStatus = rv; HandleAsyncAbort(); return rv; } nsAutoCString location; // if a location header was not given, then we can't perform the redirect, // so just carry on as though this were a normal response. if (NS_FAILED(mResponseHead->GetHeader(nsHttp::Location, location))) { return NS_ERROR_FAILURE; } // If we were told to not follow redirects automatically, then again // carry on as though this were a normal response. if (mLoadInfo->GetDontFollowRedirects()) { return NS_ERROR_FAILURE; } // make sure non-ASCII characters in the location header are escaped. nsAutoCString locationBuf; if (NS_EscapeURL(location.get(), -1, esc_OnlyNonASCII | esc_Spaces, locationBuf)) { location = locationBuf; } mRedirectType = redirectType; LOG(("redirecting to: %s [redirection-limit=%u]\n", location.get(), uint32_t(mRedirectionLimit))); rv = CreateNewURI(location.get(), getter_AddRefs(mRedirectURI)); if (NS_FAILED(rv)) { LOG(("Invalid URI for redirect: Location: %s\n", location.get())); return NS_ERROR_CORRUPTED_CONTENT; } if (!StaticPrefs::network_allow_redirect_to_data() && !mLoadInfo->GetAllowInsecureRedirectToDataURI() && mRedirectURI->SchemeIs("data")) { LOG(("Invalid data URI for redirect!")); nsContentSecurityManager::ReportBlockedDataURI(mRedirectURI, mLoadInfo, true); return NS_ERROR_DOM_BAD_URI; } // Perform the URL query string stripping for redirects. We will only strip // the query string if it is redirecting to a third-party URI in the top // level. if (StaticPrefs::privacy_query_stripping_redirect()) { ThirdPartyUtil* thirdPartyUtil = ThirdPartyUtil::GetInstance(); bool isThirdPartyRedirectURI = true; thirdPartyUtil->IsThirdPartyURI(mURI, mRedirectURI, &isThirdPartyRedirectURI); if (isThirdPartyRedirectURI && mLoadInfo->GetExternalContentPolicyType() == ExtContentPolicy::TYPE_DOCUMENT) { Telemetry::AccumulateCategorical( Telemetry::LABELS_QUERY_STRIPPING_COUNT::Redirect); nsCOMPtr prin; ContentBlockingAllowList::RecomputePrincipal( mRedirectURI, mLoadInfo->GetOriginAttributes(), getter_AddRefs(prin)); bool isRedirectURIInAllowList = false; if (prin) { ContentBlockingAllowList::Check(prin, mPrivateBrowsing, isRedirectURIInAllowList); } if (!isRedirectURIInAllowList) { nsCOMPtr strippedURI; nsCOMPtr queryStripper = components::URLQueryStringStripper::Service(&rv); NS_ENSURE_SUCCESS(rv, rv); uint32_t numStripped; rv = queryStripper->Strip(mRedirectURI, mPrivateBrowsing, getter_AddRefs(strippedURI), &numStripped); NS_ENSURE_SUCCESS(rv, rv); if (numStripped) { mUnstrippedRedirectURI = mRedirectURI; mRedirectURI = strippedURI; // Record telemetry, but only if we stripped any query params. Telemetry::AccumulateCategorical( Telemetry::LABELS_QUERY_STRIPPING_COUNT::StripForRedirect); Telemetry::Accumulate(Telemetry::QUERY_STRIPPING_PARAM_COUNT, numStripped); } } } } if (NS_WARN_IF(!mRedirectURI)) { LOG(("Invalid redirect URI after performaing query string stripping")); return NS_ERROR_FAILURE; } return ContinueProcessRedirectionAfterFallback(NS_OK); } nsresult nsHttpChannel::ContinueProcessRedirectionAfterFallback(nsresult rv) { // Kill the current cache entry if we are redirecting // back to ourself. bool redirectingBackToSameURI = false; if (mCacheEntry && LoadCacheEntryIsWriteOnly() && NS_SUCCEEDED(mURI->Equals(mRedirectURI, &redirectingBackToSameURI)) && redirectingBackToSameURI) { mCacheEntry->AsyncDoom(nullptr); } // move the reference of the old location to the new one if the new // one has none. PropagateReferenceIfNeeded(mURI, mRedirectURI); bool rewriteToGET = ShouldRewriteRedirectToGET(mRedirectType, mRequestHead.ParsedMethod()); // prompt if the method is not safe (such as POST, PUT, DELETE, ...) if (!rewriteToGET && !mRequestHead.IsSafeMethod()) { rv = PromptTempRedirect(); if (NS_FAILED(rv)) return rv; } uint32_t redirectFlags; if (nsHttp::IsPermanentRedirect(mRedirectType)) { redirectFlags = nsIChannelEventSink::REDIRECT_PERMANENT; } else { redirectFlags = nsIChannelEventSink::REDIRECT_TEMPORARY; } nsCOMPtr ioService; rv = gHttpHandler->GetIOService(getter_AddRefs(ioService)); if (NS_FAILED(rv)) return rv; nsCOMPtr newChannel; nsCOMPtr redirectLoadInfo = CloneLoadInfoForRedirect(mRedirectURI, redirectFlags); // Propagate the unstripped redirect URI. redirectLoadInfo->SetUnstrippedURI(mUnstrippedRedirectURI); rv = NS_NewChannelInternal(getter_AddRefs(newChannel), mRedirectURI, redirectLoadInfo, nullptr, // PerformanceStorage nullptr, // aLoadGroup nullptr, // aCallbacks nsIRequest::LOAD_NORMAL, ioService); NS_ENSURE_SUCCESS(rv, rv); rv = SetupReplacementChannel(mRedirectURI, newChannel, !rewriteToGET, redirectFlags); if (NS_FAILED(rv)) return rv; // verify that this is a legal redirect mRedirectChannel = newChannel; PushRedirectAsyncFunc(&nsHttpChannel::ContinueProcessRedirection); rv = gHttpHandler->AsyncOnChannelRedirect(this, newChannel, redirectFlags); if (NS_SUCCEEDED(rv)) rv = WaitForRedirectCallback(); if (NS_FAILED(rv)) { AutoRedirectVetoNotifier notifier(this, rv); PopRedirectAsyncFunc(&nsHttpChannel::ContinueProcessRedirection); } return rv; } nsresult nsHttpChannel::ContinueProcessRedirection(nsresult rv) { AutoRedirectVetoNotifier notifier(this, rv); LOG(("nsHttpChannel::ContinueProcessRedirection [rv=%" PRIx32 ",this=%p]\n", static_cast(rv), this)); if (NS_FAILED(rv)) return rv; MOZ_ASSERT(mRedirectChannel, "No redirect channel?"); // Make sure to do this after we received redirect veto answer, // i.e. after all sinks had been notified mRedirectChannel->SetOriginalURI(mOriginalURI); // XXX we used to talk directly with the script security manager, but that // should really be handled by the event sink implementation. // begin loading the new channel rv = mRedirectChannel->AsyncOpen(mListener); LOG((" new channel AsyncOpen returned %" PRIX32, static_cast(rv))); NS_ENSURE_SUCCESS(rv, rv); // close down this channel Cancel(NS_BINDING_REDIRECTED); notifier.RedirectSucceeded(); ReleaseListeners(); return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::OnAuthAvailable() { LOG(("nsHttpChannel::OnAuthAvailable [this=%p]", this)); // setting mAuthRetryPending flag and resuming the transaction // triggers process of throwing away the unauthenticated data already // coming from the network mIsAuthChannel = true; mAuthRetryPending = true; StoreProxyAuthPending(false); LOG(("Resuming the transaction, we got credentials from user")); if (mTransactionPump) { mTransactionPump->Resume(); } if (StaticPrefs::network_auth_use_redirect_for_retries()) { return CallOrWaitForResume( [](auto* self) { return self->RedirectToNewChannelForAuthRetry(); }); } return NS_OK; } NS_IMETHODIMP nsHttpChannel::OnAuthCancelled(bool userCancel) { LOG(("nsHttpChannel::OnAuthCancelled [this=%p]", this)); MOZ_ASSERT(mAuthRetryPending, "OnAuthCancelled should not be called twice"); if (mTransactionPump) { // If the channel is trying to authenticate to a proxy and // that was canceled we cannot show the http response body // from the 40x as that might mislead the user into thinking // it was a end host response instead of a proxy reponse. // This must check explicitly whether a proxy auth was being done // because we do want to show the content if this is an error from // the origin server. if (LoadProxyAuthPending()) Cancel(NS_ERROR_PROXY_CONNECTION_REFUSED); // Make sure to process security headers before calling CallOnStartRequest. nsresult rv = ProcessCrossOriginSecurityHeaders(); if (NS_FAILED(rv)) { mStatus = rv; HandleAsyncAbort(); return rv; } // ensure call of OnStartRequest of the current listener here, // it would not be called otherwise at all rv = CallOnStartRequest(); // drop mAuthRetryPending flag and resume the transaction // this resumes load of the unauthenticated content data (which // may have been canceled if we don't want to show it) mAuthRetryPending = false; LOG(("Resuming the transaction, user cancelled the auth dialog")); mTransactionPump->Resume(); if (NS_FAILED(rv)) mTransactionPump->Cancel(rv); } StoreProxyAuthPending(false); return NS_OK; } NS_IMETHODIMP nsHttpChannel::CloseStickyConnection() { LOG(("nsHttpChannel::CloseStickyConnection this=%p", this)); // Require we are between OnStartRequest and OnStopRequest, because // what we do here takes effect in OnStopRequest (not reusing the // connection for next authentication round). if (!LoadIsPending()) { LOG((" channel not pending")); NS_ERROR( "CloseStickyConnection not called before OnStopRequest, won't have any " "effect"); return NS_ERROR_UNEXPECTED; } MOZ_ASSERT(mTransaction); if (!mTransaction) { return NS_ERROR_UNEXPECTED; } if (!(mCaps & NS_HTTP_STICKY_CONNECTION || mTransaction->HasStickyConnection())) { LOG((" not sticky")); return NS_OK; } mTransaction->DontReuseConnection(); return NS_OK; } NS_IMETHODIMP nsHttpChannel::ConnectionRestartable(bool aRestartable) { LOG(("nsHttpChannel::ConnectionRestartable this=%p, restartable=%d", this, aRestartable)); StoreAuthConnectionRestartable(aRestartable); return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsISupports //----------------------------------------------------------------------------- NS_IMPL_ADDREF_INHERITED(nsHttpChannel, HttpBaseChannel) NS_IMPL_RELEASE_INHERITED(nsHttpChannel, HttpBaseChannel) NS_INTERFACE_MAP_BEGIN(nsHttpChannel) NS_INTERFACE_MAP_ENTRY(nsIRequest) NS_INTERFACE_MAP_ENTRY(nsIChannel) NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) NS_INTERFACE_MAP_ENTRY(nsIStreamListener) NS_INTERFACE_MAP_ENTRY(nsIHttpChannel) NS_INTERFACE_MAP_ENTRY(nsICacheInfoChannel) NS_INTERFACE_MAP_ENTRY(nsICachingChannel) NS_INTERFACE_MAP_ENTRY(nsIClassOfService) NS_INTERFACE_MAP_ENTRY(nsIUploadChannel) NS_INTERFACE_MAP_ENTRY(nsIFormPOSTActionChannel) NS_INTERFACE_MAP_ENTRY(nsIUploadChannel2) NS_INTERFACE_MAP_ENTRY(nsICacheEntryOpenCallback) NS_INTERFACE_MAP_ENTRY(nsIHttpChannelInternal) NS_INTERFACE_MAP_ENTRY(nsIResumableChannel) NS_INTERFACE_MAP_ENTRY(nsITransportEventSink) NS_INTERFACE_MAP_ENTRY(nsISupportsPriority) NS_INTERFACE_MAP_ENTRY(nsIProtocolProxyCallback) NS_INTERFACE_MAP_ENTRY(nsIProxiedChannel) NS_INTERFACE_MAP_ENTRY(nsIHttpAuthenticableChannel) NS_INTERFACE_MAP_ENTRY(nsIAsyncVerifyRedirectCallback) NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableRequest) NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableStreamListener) NS_INTERFACE_MAP_ENTRY(nsIDNSListener) NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) NS_INTERFACE_MAP_ENTRY(nsICorsPreflightCallback) NS_INTERFACE_MAP_ENTRY(nsIRaceCacheWithNetwork) NS_INTERFACE_MAP_ENTRY(nsIRequestTailUnblockCallback) NS_INTERFACE_MAP_ENTRY_CONCRETE(nsHttpChannel) NS_INTERFACE_MAP_ENTRY(nsIEarlyHintObserver) NS_INTERFACE_MAP_END_INHERITING(HttpBaseChannel) //----------------------------------------------------------------------------- // nsHttpChannel::nsIRequest //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::SetCanceledReason(const nsACString& aReason) { return SetCanceledReasonImpl(aReason); } NS_IMETHODIMP nsHttpChannel::GetCanceledReason(nsACString& aReason) { return GetCanceledReasonImpl(aReason); } NS_IMETHODIMP nsHttpChannel::CancelWithReason(nsresult aStatus, const nsACString& aReason) { return CancelWithReasonImpl(aStatus, aReason); } NS_IMETHODIMP nsHttpChannel::Cancel(nsresult status) { MOZ_ASSERT(NS_IsMainThread()); // We should never have a pump open while a CORS preflight is in progress. MOZ_ASSERT_IF(mPreflightChannel, !mCachePump); #ifdef DEBUG // We want to perform this check only when the chanel is being cancelled the // first time with a URL classifier blocking error code. If mStatus is // already set to such an error code then Cancel() may be called for some // other reason, for example because we've received notification about our // parent process side channel being canceled, in which case we cannot expect // that CancelByURLClassifier() would have handled this case. if (UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode(status) && !UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode(mStatus)) { MOZ_CRASH_UNSAFE_PRINTF("Blocking classifier error %" PRIx32 " need to be handled by CancelByURLClassifier()", static_cast(status)); } #endif LOG(("nsHttpChannel::Cancel [this=%p status=%" PRIx32 ", reason=%s]\n", this, static_cast(status), mCanceledReason.get())); MOZ_ASSERT_IF(!(mConnectionInfo && mConnectionInfo->UsingConnect()) && NS_SUCCEEDED(mStatus), !AllowedErrorForHTTPSRRFallback(status)); mEarlyHintObserver = nullptr; mWebTransportSessionEventListener = nullptr; if (mCanceled) { LOG((" ignoring; already canceled\n")); return NS_OK; } LogCallingScriptLocation(this); if (LoadWaitingForRedirectCallback()) { LOG(("channel canceled during wait for redirect callback")); } return CancelInternal(status); } NS_IMETHODIMP nsHttpChannel::CancelByURLClassifier(nsresult aErrorCode) { MOZ_ASSERT( UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode(aErrorCode)); MOZ_ASSERT(NS_IsMainThread()); // We should never have a pump open while a CORS preflight is in progress. MOZ_ASSERT_IF(mPreflightChannel, !mCachePump); LOG(("nsHttpChannel::CancelByURLClassifier [this=%p]\n", this)); if (mCanceled) { LOG((" ignoring; already canceled\n")); return NS_OK; } // We are being canceled by the channel classifier because of tracking // protection, but we haven't yet had a chance to dispatch the // "http-on-modify-request" notifications yet (this would normally be // done in PrepareToConnect()). So do that now, before proceeding to // cancel. // // Note that running these observers can itself result in the channel // being canceled. In that case, we accept that cancelation code as // the cause of the cancelation, as if the classification of the channel // would have occurred past this point! // notify "http-on-modify-request" observers CallOnModifyRequestObservers(); // Check if request was cancelled during on-modify-request if (mCanceled) { return mStatus; } if (mSuspendCount) { LOG(("Waiting until resume in Cancel [this=%p]\n", this)); MOZ_ASSERT(!mCallOnResume); StoreChannelClassifierCancellationPending(1); mCallOnResume = [aErrorCode](nsHttpChannel* self) { self->HandleContinueCancellingByURLClassifier(aErrorCode); return NS_OK; }; return NS_OK; } // Check to see if we should redirect this channel elsewhere by // nsIHttpChannel.redirectTo API request if (mAPIRedirectToURI) { StoreChannelClassifierCancellationPending(1); return AsyncCall(&nsHttpChannel::HandleAsyncAPIRedirect); } return CancelInternal(aErrorCode); } void nsHttpChannel::ContinueCancellingByURLClassifier(nsresult aErrorCode) { MOZ_ASSERT( UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode(aErrorCode)); MOZ_ASSERT(NS_IsMainThread()); // We should never have a pump open while a CORS preflight is in progress. MOZ_ASSERT_IF(mPreflightChannel, !mCachePump); LOG(("nsHttpChannel::ContinueCancellingByURLClassifier [this=%p]\n", this)); if (mCanceled) { LOG((" ignoring; already canceled\n")); return; } // Check to see if we should redirect this channel elsewhere by // nsIHttpChannel.redirectTo API request if (mAPIRedirectToURI) { Unused << AsyncCall(&nsHttpChannel::HandleAsyncAPIRedirect); return; } Unused << CancelInternal(aErrorCode); } nsresult nsHttpChannel::CancelInternal(nsresult status) { LOG(("nsHttpChannel::CancelInternal [this=%p]\n", this)); bool channelClassifierCancellationPending = !!LoadChannelClassifierCancellationPending(); if (UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode(status)) { StoreChannelClassifierCancellationPending(0); } // We don't want the content process to see any header values // when the request is blocked by ORB if (mChannelBlockedByOpaqueResponse && mCachedOpaqueResponseBlockingPref) { mResponseHead->ClearHeaders(); } mEarlyHintObserver = nullptr; mWebTransportSessionEventListener = nullptr; mCanceled = true; mStatus = NS_FAILED(status) ? status : NS_ERROR_ABORT; if (mLastStatusReported && !mEndMarkerAdded && profiler_thread_is_being_profiled_for_markers()) { // These do allocations/frees/etc; avoid if not active // mLastStatusReported can be null if Cancel is called before we added the // start marker. mEndMarkerAdded = true; nsAutoCString requestMethod; GetRequestMethod(requestMethod); int32_t priority = PRIORITY_NORMAL; GetPriority(&priority); uint64_t size = 0; GetEncodedBodySize(&size); profiler_add_network_marker( mURI, requestMethod, priority, mChannelId, NetworkLoadType::LOAD_CANCEL, mLastStatusReported, TimeStamp::Now(), size, mCacheDisposition, mLoadInfo->GetInnerWindowID(), mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0, &mTransactionTimings, std::move(mSource)); } // If we don't have mTransactionPump and mCachePump, we need to call // AsyncAbort to make sure this channel's listener got notified. bool needAsyncAbort = !mTransactionPump && !mCachePump; if (mProxyRequest) mProxyRequest->Cancel(status); CancelNetworkRequest(status); mCacheInputStream.CloseAndRelease(); if (mCachePump) mCachePump->Cancel(status); if (mAuthProvider) mAuthProvider->Cancel(status); if (mPreflightChannel) mPreflightChannel->Cancel(status); if (mRequestContext && mOnTailUnblock) { mOnTailUnblock = nullptr; mRequestContext->CancelTailedRequest(this); CloseCacheEntry(false); needAsyncAbort = false; Unused << AsyncAbort(status); } else if (channelClassifierCancellationPending) { // If mCallOnResume is not null here, it's set in // nsHttpChannel::CancelByURLClassifier. We can override mCallOnResume since // mCanceled is true and nsHttpChannel::ContinueCancellingByURLClassifier // does nothing. if (mCallOnResume) { mCallOnResume = nullptr; } // If we're coming from an asynchronous path when canceling a channel due // to safe-browsing protection, we need to AsyncAbort the channel now. needAsyncAbort = false; Unused << AsyncAbort(status); } // If we already have mCallOnResume, AsyncAbort will be called in // ResumeInternal. if (needAsyncAbort && !mCallOnResume && !mSuspendCount) { LOG(("nsHttpChannel::CancelInternal do AsyncAbort [this=%p]\n", this)); CloseCacheEntry(false); Unused << AsyncAbort(status); } return NS_OK; } void nsHttpChannel::CancelNetworkRequest(nsresult aStatus) { if (mTransaction) { nsresult rv = gHttpHandler->CancelTransaction(mTransaction, aStatus); if (NS_FAILED(rv)) { LOG(("failed to cancel the transaction\n")); } } if (mTransactionPump) mTransactionPump->Cancel(aStatus); mEarlyHintObserver = nullptr; mWebTransportSessionEventListener = nullptr; } NS_IMETHODIMP nsHttpChannel::Suspend() { NS_ENSURE_TRUE(LoadIsPending(), NS_ERROR_NOT_AVAILABLE); LOG(("nsHttpChannel::SuspendInternal [this=%p]\n", this)); LogCallingScriptLocation(this); ++mSuspendCount; if (mSuspendCount == 1) { mSuspendTimestamp = TimeStamp::NowLoRes(); } nsresult rvTransaction = NS_OK; if (mTransactionPump) { rvTransaction = mTransactionPump->Suspend(); } nsresult rvCache = NS_OK; if (mCachePump) { rvCache = mCachePump->Suspend(); } return NS_FAILED(rvTransaction) ? rvTransaction : rvCache; } NS_IMETHODIMP nsHttpChannel::Resume() { NS_ENSURE_TRUE(mSuspendCount > 0, NS_ERROR_UNEXPECTED); LOG(("nsHttpChannel::ResumeInternal [this=%p]\n", this)); LogCallingScriptLocation(this); if (--mSuspendCount == 0) { mSuspendTotalTime += (TimeStamp::NowLoRes() - mSuspendTimestamp).ToMilliseconds(); if (mCallOnResume) { // Resume the interrupted procedure first, then resume // the pump to continue process the input stream. // Any newly created pump MUST be suspended to prevent calling // its OnStartRequest before OnStopRequest of any pre-existing // pump. AsyncResumePending ensures that. MOZ_ASSERT(!LoadAsyncResumePending()); StoreAsyncResumePending(1); std::function callOnResume = nullptr; std::swap(callOnResume, mCallOnResume); RefPtr self(this); nsCOMPtr transactionPump = mTransactionPump; RefPtr cachePump = mCachePump; nsresult rv = NS_DispatchToCurrentThread(NS_NewRunnableFunction( "nsHttpChannel::CallOnResume", [callOnResume{std::move(callOnResume)}, self{std::move(self)}, transactionPump{std::move(transactionPump)}, cachePump{std::move(cachePump)}]() { MOZ_ASSERT(self->LoadAsyncResumePending()); nsresult rv = self->CallOrWaitForResume(callOnResume); if (NS_FAILED(rv)) { self->CloseCacheEntry(false); Unused << self->AsyncAbort(rv); } MOZ_ASSERT(self->LoadAsyncResumePending()); self->StoreAsyncResumePending(0); // And now actually resume the previously existing pumps. if (transactionPump) { LOG( ("nsHttpChannel::CallOnResume resuming previous transaction " "pump %p, this=%p", transactionPump.get(), self.get())); transactionPump->Resume(); } if (cachePump) { LOG( ("nsHttpChannel::CallOnResume resuming previous cache pump " "%p, this=%p", cachePump.get(), self.get())); cachePump->Resume(); } // Any newly created pumps were suspended once because of // AsyncResumePending. Problem is that the stream listener // notification is already pending in the queue right now, because // AsyncRead doesn't (regardless if called after Suspend) respect // the suspend coutner and the right order would not be preserved. // Hence, we do another dispatch round to actually Resume after // the notification from the original pump. if (transactionPump != self->mTransactionPump && self->mTransactionPump) { LOG( ("nsHttpChannel::CallOnResume async-resuming new " "transaction " "pump %p, this=%p", self->mTransactionPump.get(), self.get())); nsCOMPtr pump = self->mTransactionPump; NS_DispatchToCurrentThread(NS_NewRunnableFunction( "nsHttpChannel::CallOnResume new transaction", [pump{std::move(pump)}]() { pump->Resume(); })); } if (cachePump != self->mCachePump && self->mCachePump) { LOG( ("nsHttpChannel::CallOnResume async-resuming new cache pump " "%p, this=%p", self->mCachePump.get(), self.get())); RefPtr pump = self->mCachePump; NS_DispatchToCurrentThread(NS_NewRunnableFunction( "nsHttpChannel::CallOnResume new pump", [pump{std::move(pump)}]() { pump->Resume(); })); } })); NS_ENSURE_SUCCESS(rv, rv); return rv; } } nsresult rvTransaction = NS_OK; if (mTransactionPump) { rvTransaction = mTransactionPump->Resume(); } nsresult rvCache = NS_OK; if (mCachePump) { rvCache = mCachePump->Resume(); } return NS_FAILED(rvTransaction) ? rvTransaction : rvCache; } //----------------------------------------------------------------------------- // nsHttpChannel::nsIChannel //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::GetSecurityInfo(nsITransportSecurityInfo** securityInfo) { NS_ENSURE_ARG_POINTER(securityInfo); *securityInfo = do_AddRef(mSecurityInfo).take(); return NS_OK; } // If any of the functions that AsyncOpen calls returns immediately an error // AsyncAbort(which calls onStart/onStopRequest) does not need to be call. // To be sure that they are not call ReleaseListeners() is called. // If AsyncOpen returns NS_OK, after that point AsyncAbort must be called on // any error. NS_IMETHODIMP nsHttpChannel::AsyncOpen(nsIStreamListener* aListener) { nsCOMPtr listener = aListener; nsresult rv = nsContentSecurityManager::doContentSecurityCheck(this, listener); if (NS_WARN_IF(NS_FAILED(rv))) { ReleaseListeners(); return rv; } MOZ_ASSERT( mLoadInfo->GetSecurityMode() == 0 || mLoadInfo->GetInitialSecurityCheckDone() || (mLoadInfo->GetSecurityMode() == nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL && mLoadInfo->GetLoadingPrincipal() && mLoadInfo->GetLoadingPrincipal()->IsSystemPrincipal()), "security flags in loadInfo but doContentSecurityCheck() not called"); LOG(("nsHttpChannel::AsyncOpen [this=%p]\n", this)); mOpenerCallingScriptLocation = CallingScriptLocationString(); LogCallingScriptLocation(this, mOpenerCallingScriptLocation); NS_CompareLoadInfoAndLoadContext(this); #ifdef DEBUG AssertPrivateBrowsingId(); #endif NS_ENSURE_ARG_POINTER(listener); NS_ENSURE_TRUE(!LoadIsPending(), NS_ERROR_IN_PROGRESS); NS_ENSURE_TRUE(!LoadWasOpened(), NS_ERROR_ALREADY_OPENED); if (mCanceled) { ReleaseListeners(); return NS_FAILED(mStatus) ? mStatus : NS_ERROR_FAILURE; } if (MaybeWaitForUploadStreamNormalization(listener, nullptr)) { return NS_OK; } MOZ_ASSERT(NS_IsMainThread()); if (!gHttpHandler->Active()) { LOG((" after HTTP shutdown...")); ReleaseListeners(); return NS_ERROR_NOT_AVAILABLE; } rv = NS_CheckPortSafety(mURI); if (NS_FAILED(rv)) { ReleaseListeners(); return rv; } // If no one called SetLoadGroup or SetNotificationCallbacks, the private // state has not been updated on PrivateBrowsingChannel (which we derive // from) Same if the loadinfo has changed since the creation of the channel. // Hence, we have to call UpdatePrivateBrowsing() here UpdatePrivateBrowsing(); AntiTrackingUtils::UpdateAntiTrackingInfoForChannel(this); // Recalculate the default userAgent header after the AntiTrackingInfo gets // updated because we can only know whether the site is exempted from // fingerprinting protection after we have the AntiTracking Info. // // Note that we don't recalculate the header if it has been modified since the // channel was created because we want to preserve the modified header. if (!LoadIsUserAgentHeaderModified()) { rv = mRequestHead.SetHeader( nsHttp::User_Agent, gHttpHandler->UserAgent(nsContentUtils::ShouldResistFingerprinting( this, RFPTarget::HttpUserAgent)), false, nsHttpHeaderArray::eVarietyRequestEnforceDefault); MOZ_ASSERT(NS_SUCCEEDED(rv)); } if (WaitingForTailUnblock()) { // This channel is marked as Tail and is part of a request context // that has positive number of non-tailed requestst, hence this channel // has been put to a queue. // When tail is unblocked, OnTailUnblock on this channel will be called // to continue AsyncOpen. mListener = listener; MOZ_DIAGNOSTIC_ASSERT(!mOnTailUnblock); mOnTailUnblock = &nsHttpChannel::AsyncOpenOnTailUnblock; LOG((" put on hold until tail is unblocked")); return NS_OK; } // Remember the cookie header that was set, if any nsAutoCString cookieHeader; if (NS_SUCCEEDED(mRequestHead.GetHeader(nsHttp::Cookie, cookieHeader))) { mUserSetCookieHeader = cookieHeader; } // Set user agent override, do so before OnOpeningRequest notification // since we want to allow consumers of that notification change or remove // the User-Agent request header. HttpBaseChannel::SetDocshellUserAgentOverride(); // After we notify any observers (on-opening-request, loadGroup, etc) we // must return NS_OK and return any errors asynchronously via // OnStart/OnStopRequest. Observers may add a reference to the channel // and expect to get OnStopRequest so they know when to drop the reference, // etc. // notify "http-on-opening-request" observers, but not if this is a redirect if (!(mLoadFlags & LOAD_REPLACE)) { gHttpHandler->OnOpeningRequest(this); } StoreIsPending(true); StoreWasOpened(true); mListener = listener; if (nsIOService::UseSocketProcess() && !gIOService->IsSocketProcessLaunchComplete()) { RefPtr self = this; gIOService->CallOrWaitForSocketProcess( [self]() { self->AsyncOpenFinal(TimeStamp::Now()); }); return NS_OK; } AsyncOpenFinal(TimeStamp::Now()); return NS_OK; } void nsHttpChannel::AsyncOpenFinal(TimeStamp aTimeStamp) { // We save this timestamp from outside of the if block in case we enable the // profiler after AsyncOpen(). mLastStatusReported = TimeStamp::Now(); if (profiler_thread_is_being_profiled_for_markers()) { nsAutoCString requestMethod; GetRequestMethod(requestMethod); profiler_add_network_marker( mURI, requestMethod, mPriority, mChannelId, NetworkLoadType::LOAD_START, mChannelCreationTimestamp, mLastStatusReported, 0, mCacheDisposition, mLoadInfo->GetInnerWindowID(), mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0); } // Added due to PauseTask/DelayHttpChannel if (mLoadGroup) mLoadGroup->AddRequest(this, nullptr); // record asyncopen time unconditionally and clear it if we // don't want it after OnModifyRequest() weighs in. But waiting for // that to complete would mean we don't include proxy resolution in the // timing. if (!LoadAsyncOpenTimeOverriden()) { mAsyncOpenTime = aTimeStamp; } // Remember we have Authorization header set here. We need to check on it // just once and early, AsyncOpen is the best place. StoreCustomAuthHeader(mRequestHead.HasHeader(nsHttp::Authorization)); bool willCallback = false; // We are about to do an async lookup to check if the URI is a tracker. If // yes, this channel will be canceled by channel classifier. Chances are the // lookup is not needed so CheckIsTrackerWithLocalTable() will return an // error and then we can MaybeResolveProxyAndBeginConnect() right away. // We skip the check in case this is an internal redirected channel if (!LoadAuthRedirectedChannel() && NS_ShouldClassifyChannel(this)) { RefPtr self = this; willCallback = NS_SUCCEEDED( AsyncUrlChannelClassifier::CheckChannel(this, [self]() -> void { nsCOMPtr uri; self->GetURI(getter_AddRefs(uri)); MOZ_ASSERT(uri); // Finish the AntiTracking Heuristic before // MaybeResolveProxyAndBeginConnect(). FinishAntiTrackingRedirectHeuristic(self, uri); self->MaybeResolveProxyAndBeginConnect(); })); } if (!willCallback) { // We can do MaybeResolveProxyAndBeginConnect immediately if // CheckIsTrackerWithLocalTable is failed. Note that we don't need to // handle the failure because BeginConnect() will return synchronously and // the caller will be responsible for handling it. MaybeResolveProxyAndBeginConnect(); } } void nsHttpChannel::MaybeResolveProxyAndBeginConnect() { nsresult rv; // The common case for HTTP channels is to begin proxy resolution and return // at this point. The only time we know mProxyInfo already is if we're // proxying a non-http protocol like ftp. We don't need to discover proxy // settings if we are never going to make a network connection. if (!mProxyInfo && !(mLoadFlags & (LOAD_ONLY_FROM_CACHE | LOAD_NO_NETWORK_IO)) && !BypassProxy() && NS_SUCCEEDED(ResolveProxy())) { return; } if (!gHttpHandler->Active()) { LOG( ("nsHttpChannel::MaybeResolveProxyAndBeginConnect [this=%p] " "Handler no longer active.\n", this)); rv = NS_ERROR_NOT_AVAILABLE; } else { rv = BeginConnect(); } if (NS_FAILED(rv)) { CloseCacheEntry(false); Unused << AsyncAbort(rv); } } nsresult nsHttpChannel::AsyncOpenOnTailUnblock() { return AsyncOpen(mListener); } already_AddRefed nsHttpChannel::GetOrCreateChannelClassifier() { if (!mChannelClassifier) { mChannelClassifier = new nsChannelClassifier(this); LOG(("nsHttpChannel [%p] created nsChannelClassifier [%p]\n", this, mChannelClassifier.get())); } RefPtr classifier = mChannelClassifier; return classifier.forget(); } uint16_t nsHttpChannel::GetProxyDNSStrategy() { // This function currently only supports returning DNS_PREFETCH_ORIGIN. // Support for the rest of the DNS_* flags will be added later. if (!mProxyInfo) { return DNS_PREFETCH_ORIGIN; } nsAutoCString type; mProxyInfo->GetType(type); if (!StaticPrefs::network_proxy_socks_remote_dns()) { if (type.EqualsLiteral("socks")) { return DNS_PREFETCH_ORIGIN; } } return 0; } // BeginConnect() SHOULD NOT call AsyncAbort(). AsyncAbort will be called by // functions that called BeginConnect if needed. Only // MaybeResolveProxyAndBeginConnect and OnProxyAvailable ever call // BeginConnect. nsresult nsHttpChannel::BeginConnect() { LOG(("nsHttpChannel::BeginConnect [this=%p]\n", this)); nsresult rv; // It is the caller's responsibility to not call us late in shutdown. MOZ_ASSERT(gHttpHandler->Active()); // Construct connection info object nsAutoCString host; nsAutoCString scheme; int32_t port = -1; bool isHttps = mURI->SchemeIs("https"); rv = mURI->GetScheme(scheme); if (NS_SUCCEEDED(rv)) rv = mURI->GetAsciiHost(host); if (NS_SUCCEEDED(rv)) rv = mURI->GetPort(&port); if (NS_SUCCEEDED(rv)) rv = mURI->GetAsciiSpec(mSpec); if (NS_FAILED(rv)) { return rv; } // Just a warning here because some nsIURIs do not implement this method. Unused << NS_WARN_IF(NS_FAILED(mURI->GetUsername(mUsername))); // Reject the URL if it doesn't specify a host if (host.IsEmpty()) { rv = NS_ERROR_MALFORMED_URI; return rv; } LOG(("host=%s port=%d\n", host.get(), port)); LOG(("uri=%s\n", mSpec.get())); nsCOMPtr proxyInfo; if (mProxyInfo) proxyInfo = do_QueryInterface(mProxyInfo); if (mCaps & NS_HTTP_CONNECT_ONLY) { if (!proxyInfo) { LOG(("return failure: no proxy for connect-only channel\n")); return NS_ERROR_FAILURE; } if (!proxyInfo->IsHTTP() && !proxyInfo->IsHTTPS()) { LOG(("return failure: non-http proxy for connect-only channel\n")); return NS_ERROR_FAILURE; } } mRequestHead.SetHTTPS(isHttps); mRequestHead.SetOrigin(scheme, host, port); SetOriginHeader(); SetDoNotTrack(); SetGlobalPrivacyControl(); OriginAttributes originAttributes; // Regular principal in case we have a proxy. if (proxyInfo && !StaticPrefs::privacy_partition_network_state_connection_with_proxy()) { StoragePrincipalHelper::GetOriginAttributes( this, originAttributes, StoragePrincipalHelper::eRegularPrincipal); } else { StoragePrincipalHelper::GetOriginAttributesForNetworkState( this, originAttributes); } // Adjust mCaps according to our request headers: // - If "Connection: close" is set as a request header, then do not bother // trying to establish a keep-alive connection. if (mRequestHead.HasHeaderValue(nsHttp::Connection, "close")) { mCaps &= ~(NS_HTTP_ALLOW_KEEPALIVE); StoreAllowHttp3(false); } gHttpHandler->MaybeAddAltSvcForTesting(mURI, mUsername, mPrivateBrowsing, mCallbacks, originAttributes); RefPtr connInfo; #ifdef FUZZING if (StaticPrefs::fuzzing_necko_http3()) { connInfo = new nsHttpConnectionInfo(host, port, "h3"_ns, mUsername, proxyInfo, originAttributes, host, port, true); } else { #endif if (mWebTransportSessionEventListener) { connInfo = new nsHttpConnectionInfo(host, port, "h3"_ns, mUsername, proxyInfo, originAttributes, isHttps, true, true); bool dedicated = true; nsresult rv; nsCOMPtr wtconSettings = do_QueryInterface(mWebTransportSessionEventListener, &rv); NS_ENSURE_SUCCESS(rv, rv); wtconSettings->GetDedicated(&dedicated); if (dedicated) { connInfo->SetWebTransportId( gHttpHandler->ConnMgr()->GenerateNewWebTransportId()); } } else { connInfo = new nsHttpConnectionInfo(host, port, ""_ns, mUsername, proxyInfo, originAttributes, isHttps); } #ifdef FUZZING } #endif bool http2Allowed = !gHttpHandler->IsHttp2Excluded(connInfo); bool http3Allowed = Http3Allowed(); if (!http3Allowed) { mCaps |= NS_HTTP_DISALLOW_HTTP3; } RefPtr mapping; if (!mConnectionInfo && LoadAllowAltSvc() && // per channel !mWebTransportSessionEventListener && (http2Allowed || http3Allowed) && !(mLoadFlags & LOAD_FRESH_CONNECTION) && AltSvcMapping::AcceptableProxy(proxyInfo) && (scheme.EqualsLiteral("http") || scheme.EqualsLiteral("https")) && (mapping = gHttpHandler->GetAltServiceMapping( scheme, host, port, mPrivateBrowsing, originAttributes, http2Allowed, http3Allowed))) { LOG(("nsHttpChannel %p Alt Service Mapping Found %s://%s:%d [%s]\n", this, scheme.get(), mapping->AlternateHost().get(), mapping->AlternatePort(), mapping->HashKey().get())); if (!(mLoadFlags & LOAD_ANONYMOUS) && !mPrivateBrowsing) { nsAutoCString altUsedLine(mapping->AlternateHost()); bool defaultPort = mapping->AlternatePort() == (isHttps ? NS_HTTPS_DEFAULT_PORT : NS_HTTP_DEFAULT_PORT); if (!defaultPort) { altUsedLine.AppendLiteral(":"); altUsedLine.AppendInt(mapping->AlternatePort()); } // Like what we did for 'Authorization' header, we need to do the same for // 'Alt-Used' for avoiding this header being shown in the ServiceWorker // FetchEvent. Unused << mRequestHead.ClearHeader(nsHttp::Alternate_Service_Used); rv = mRequestHead.SetHeader(nsHttp::Alternate_Service_Used, altUsedLine, false, nsHttpHeaderArray::eVarietyRequestDefault); MOZ_ASSERT(NS_SUCCEEDED(rv)); } nsCOMPtr consoleService = do_GetService(NS_CONSOLESERVICE_CONTRACTID); if (consoleService && !host.Equals(mapping->AlternateHost())) { nsAutoString message(u"Alternate Service Mapping found: "_ns); AppendASCIItoUTF16(scheme, message); message.AppendLiteral(u"://"); AppendASCIItoUTF16(host, message); message.AppendLiteral(u":"); message.AppendInt(port); message.AppendLiteral(u" to "); AppendASCIItoUTF16(scheme, message); message.AppendLiteral(u"://"); AppendASCIItoUTF16(mapping->AlternateHost(), message); message.AppendLiteral(u":"); message.AppendInt(mapping->AlternatePort()); consoleService->LogStringMessage(message.get()); } LOG(("nsHttpChannel %p Using connection info from altsvc mapping", this)); mapping->GetConnectionInfo(getter_AddRefs(mConnectionInfo), proxyInfo, originAttributes); Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_USE_ALTSVC, true); Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_USE_ALTSVC_OE, !isHttps); } else if (mConnectionInfo) { LOG(("nsHttpChannel %p Using channel supplied connection info", this)); Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_USE_ALTSVC, false); } else { LOG(("nsHttpChannel %p Using default connection info", this)); mConnectionInfo = connInfo; Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_USE_ALTSVC, false); } bool httpsRRAllowed = !LoadBeConservative() && !(mCaps & NS_HTTP_BE_CONSERVATIVE) && !(mLoadInfo->TriggeringPrincipal()->IsSystemPrincipal() && mLoadInfo->GetExternalContentPolicyType() != ExtContentPolicy::TYPE_DOCUMENT) && !mConnectionInfo->UsingConnect(); if (!httpsRRAllowed) { mCaps |= NS_HTTP_DISALLOW_HTTPS_RR; } // No need to lookup HTTPSSVC record if mHTTPSSVCRecord already contains a // value. StoreUseHTTPSSVC(StaticPrefs::network_dns_upgrade_with_https_rr() && httpsRRAllowed && mHTTPSSVCRecord.isNothing()); // Need to re-ask the handler, since mConnectionInfo may not be the connInfo // we used earlier if (!mConnectionInfo->IsHttp3() && gHttpHandler->IsHttp2Excluded(mConnectionInfo)) { StoreAllowSpdy(0); mCaps |= NS_HTTP_DISALLOW_SPDY; mConnectionInfo->SetNoSpdy(true); } // We can be passed with the auth provider if this channel was // a result of redirect due to auth retry if (!mAuthProvider) { mAuthProvider = new nsHttpChannelAuthProvider(); } rv = mAuthProvider->Init(this); if (NS_FAILED(rv)) { return rv; } // check to see if authorization headers should be included // CustomAuthHeader is set in AsyncOpen if we find Authorization header rv = mAuthProvider->AddAuthorizationHeaders(LoadCustomAuthHeader()); if (NS_FAILED(rv)) { LOG(("nsHttpChannel %p AddAuthorizationHeaders failed (%08x)", this, static_cast(rv))); } // If TimingEnabled flag is not set after OnModifyRequest() then // clear the already recorded AsyncOpen value for consistency. if (!LoadTimingEnabled()) mAsyncOpenTime = TimeStamp(); // if this somehow fails we can go on without it Unused << gHttpHandler->AddConnectionHeader(&mRequestHead, mCaps); if (!LoadIsTRRServiceChannel() && (mLoadFlags & VALIDATE_ALWAYS || BYPASS_LOCAL_CACHE(mLoadFlags, LoadPreferCacheLoadOverBypass()))) { mCaps |= NS_HTTP_REFRESH_DNS; } if (gHttpHandler->CriticalRequestPrioritization()) { if (mClassOfService.Flags() & nsIClassOfService::Leader) { mCaps |= NS_HTTP_LOAD_AS_BLOCKING; } if (mClassOfService.Flags() & nsIClassOfService::Unblocked) { mCaps |= NS_HTTP_LOAD_UNBLOCKED; } if (mClassOfService.Flags() & nsIClassOfService::UrgentStart && gHttpHandler->IsUrgentStartEnabled()) { mCaps |= NS_HTTP_URGENT_START; SetPriority(nsISupportsPriority::PRIORITY_HIGHEST); } } // Force-Reload should reset the persistent connection pool for this host if (mLoadFlags & LOAD_FRESH_CONNECTION) { // just the initial document resets the whole pool if (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI) { gHttpHandler->AltServiceCache()->ClearAltServiceMappings(); rv = gHttpHandler->DoShiftReloadConnectionCleanupWithConnInfo( mConnectionInfo); if (NS_FAILED(rv)) { LOG(( "nsHttpChannel::BeginConnect " "DoShiftReloadConnectionCleanupWithConnInfo failed: %08x [this=%p]", static_cast(rv), this)); } } } // We may have been cancelled already, either by on-modify-request // listeners or load group observers; in that case, we should not send the // request to the server if (mCanceled) { return mStatus; } // skip classifier checks if this channel was the result of internal auth // redirect bool shouldBeClassified = !LoadAuthRedirectedChannel() && NS_ShouldClassifyChannel(this); if (shouldBeClassified) { if (LoadChannelClassifierCancellationPending()) { LOG( ("Waiting for safe-browsing protection cancellation in BeginConnect " "[this=%p]\n", this)); return NS_OK; } ReEvaluateReferrerAfterTrackingStatusIsKnown(); } rv = MaybeStartDNSPrefetch(); if (NS_FAILED(rv)) { auto dnsStrategy = GetProxyDNSStrategy(); if (dnsStrategy & DNS_BLOCK_ON_ORIGIN_RESOLVE) { // TODO: Should this be fatal? return rv; } // Otherwise this shouldn't be fatal. return NS_OK; } rv = CallOrWaitForResume( [](nsHttpChannel* self) { return self->PrepareToConnect(); }); if (NS_FAILED(rv)) { return rv; } if (shouldBeClassified) { // Start nsChannelClassifier to catch phishing and malware URIs. RefPtr channelClassifier = GetOrCreateChannelClassifier(); LOG(("nsHttpChannel::Starting nsChannelClassifier %p [this=%p]", channelClassifier.get(), this)); channelClassifier->Start(); } return NS_OK; } nsresult nsHttpChannel::MaybeStartDNSPrefetch() { // Start a DNS lookup very early in case the real open is queued the DNS can // happen in parallel. Do not do so in the presence of an HTTP proxy as // all lookups other than for the proxy itself are done by the proxy. // Also we don't do a lookup if the LOAD_NO_NETWORK_IO or // LOAD_ONLY_FROM_CACHE flags are set. // // We keep the DNS prefetch object around so that we can retrieve // timing information from it. There is no guarantee that we actually // use the DNS prefetch data for the real connection, but as we keep // this data around for 3 minutes by default, this should almost always // be correct, and even when it isn't, the timing still represents _a_ // valid DNS lookup timing for the site, even if it is not _the_ // timing we used. if ((mLoadFlags & (LOAD_NO_NETWORK_IO | LOAD_ONLY_FROM_CACHE)) || LoadAuthRedirectedChannel()) { return NS_OK; } auto dnsStrategy = GetProxyDNSStrategy(); LOG( ("nsHttpChannel::MaybeStartDNSPrefetch [this=%p, strategy=%u] " "prefetching%s\n", this, dnsStrategy, mCaps & NS_HTTP_REFRESH_DNS ? ", refresh requested" : "")); if (dnsStrategy & DNS_PREFETCH_ORIGIN) { OriginAttributes originAttributes; StoragePrincipalHelper::GetOriginAttributesForNetworkState( this, originAttributes); mDNSPrefetch = new nsDNSPrefetch(mURI, originAttributes, nsIRequest::GetTRRMode(), this, LoadTimingEnabled()); nsIDNSService::DNSFlags dnsFlags = nsIDNSService::RESOLVE_DEFAULT_FLAGS; if (mCaps & NS_HTTP_REFRESH_DNS) { dnsFlags |= nsIDNSService::RESOLVE_BYPASS_CACHE; } nsresult rv = mDNSPrefetch->PrefetchHigh(dnsFlags); if (dnsStrategy & DNS_BLOCK_ON_ORIGIN_RESOLVE) { LOG((" blocking on prefetching origin")); if (NS_WARN_IF(NS_FAILED(rv))) { LOG((" lookup failed with 0x%08" PRIx32 ", aborting request", static_cast(rv))); return rv; } // Resolved in OnLookupComplete. mDNSBlockingThenable = mDNSBlockingPromise.Ensure(__func__); } if (gHttpHandler->UseHTTPSRRAsAltSvcEnabled() && !mHTTPSSVCRecord && !(mCaps & NS_HTTP_DISALLOW_HTTPS_RR)) { MOZ_ASSERT(!mHTTPSSVCRecord); OriginAttributes originAttributes; StoragePrincipalHelper::GetOriginAttributesForHTTPSRR(this, originAttributes); RefPtr resolver = new nsDNSPrefetch(mURI, originAttributes, nsIRequest::GetTRRMode()); Unused << resolver->FetchHTTPSSVC(mCaps & NS_HTTP_REFRESH_DNS, true, [](nsIDNSHTTPSSVCRecord*) { // Do nothing. This is a DNS prefetch. }); } } return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetEncodedBodySize(uint64_t* aEncodedBodySize) { if (mCacheEntry && !LoadCacheEntryIsWriteOnly()) { int64_t dataSize = 0; mCacheEntry->GetDataSize(&dataSize); *aEncodedBodySize = dataSize; } else { *aEncodedBodySize = mLogicalOffset; } return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsIHttpChannelInternal //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::GetIsAuthChannel(bool* aIsAuthChannel) { *aIsAuthChannel = mIsAuthChannel; return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetChannelIsForDownload(bool aChannelIsForDownload) { if (aChannelIsForDownload) { AddClassFlags(nsIClassOfService::Throttleable); } else { ClearClassFlags(nsIClassOfService::Throttleable); } return HttpBaseChannel::SetChannelIsForDownload(aChannelIsForDownload); } base::ProcessId nsHttpChannel::ProcessId() { nsCOMPtr parentChannel; NS_QueryNotificationCallbacks(this, parentChannel); if (RefPtr httpParent = do_QueryObject(parentChannel)) { return httpParent->OtherPid(); } if (RefPtr docParent = do_QueryObject(parentChannel)) { return docParent->OtherPid(); } return base::GetCurrentProcId(); } auto nsHttpChannel::AttachStreamFilter() -> RefPtr { LOG(("nsHttpChannel::AttachStreamFilter [this=%p]", this)); MOZ_ASSERT(!LoadOnStartRequestCalled()); if (!ProcessId()) { return ChildEndpointPromise::CreateAndReject(false, __func__); } nsCOMPtr parentChannel; NS_QueryNotificationCallbacks(this, parentChannel); // If our listener is a DocumentLoadListener, then we might handle // multi-part responses here in the parent process. The current extension // API doesn't understand the parsed multipart format, so we defer responding // here until CallOnStartRequest, and attach the StreamFilter before the // multipart handler (in the parent process!) if applicable. if (RefPtr docParent = do_QueryObject(parentChannel)) { StreamFilterRequest* request = mStreamFilterRequests.AppendElement(); request->mPromise = new ChildEndpointPromise::Private(__func__); return request->mPromise; } mozilla::ipc::Endpoint parent; mozilla::ipc::Endpoint child; nsresult rv = extensions::PStreamFilter::CreateEndpoints(&parent, &child); if (NS_FAILED(rv)) { return ChildEndpointPromise::CreateAndReject(false, __func__); } if (RefPtr httpParent = do_QueryObject(parentChannel)) { return httpParent->AttachStreamFilter(std::move(parent), std::move(child)); } extensions::StreamFilterParent::Attach(this, std::move(parent)); return ChildEndpointPromise::CreateAndResolve(std::move(child), __func__); } NS_IMETHODIMP nsHttpChannel::GetNavigationStartTimeStamp(TimeStamp* aTimeStamp) { LOG(("nsHttpChannel::GetNavigationStartTimeStamp [this=%p]", this)); MOZ_ASSERT(aTimeStamp); *aTimeStamp = mNavigationStartTimeStamp; return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetNavigationStartTimeStamp(TimeStamp aTimeStamp) { LOG(("nsHttpChannel::SetNavigationStartTimeStamp [this=%p]", this)); mNavigationStartTimeStamp = aTimeStamp; return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsISupportsPriority //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::SetPriority(int32_t value) { int16_t newValue = clamped(value, INT16_MIN, INT16_MAX); if (mPriority == newValue) return NS_OK; LOG(("nsHttpChannel::SetPriority %p p=%d", this, newValue)); mPriority = newValue; if (mTransaction) { nsresult rv = gHttpHandler->RescheduleTransaction(mTransaction, mPriority); if (NS_FAILED(rv)) { LOG( ("nsHttpChannel::SetPriority [this=%p] " "RescheduleTransaction failed (%08x)", this, static_cast(rv))); } } // If this channel is the real channel for an e10s channel, notify the // child side about the priority change as well. nsCOMPtr parentChannel; NS_QueryNotificationCallbacks(this, parentChannel); RefPtr httpParent = do_QueryObject(parentChannel); if (httpParent) { httpParent->DoSendSetPriority(newValue); } return NS_OK; } //----------------------------------------------------------------------------- // HttpChannel::nsIClassOfService //----------------------------------------------------------------------------- void nsHttpChannel::OnClassOfServiceUpdated() { LOG(("nsHttpChannel::OnClassOfServiceUpdated this=%p, cos=%lu, inc=%d", this, mClassOfService.Flags(), mClassOfService.Incremental())); if (mTransaction) { gHttpHandler->UpdateClassOfServiceOnTransaction(mTransaction, mClassOfService); } if (EligibleForTailing()) { RemoveAsNonTailRequest(); } else { AddAsNonTailRequest(); } } NS_IMETHODIMP nsHttpChannel::SetClassFlags(uint32_t inFlags) { uint32_t previous = mClassOfService.Flags(); mClassOfService.SetFlags(inFlags); if (previous != mClassOfService.Flags()) { OnClassOfServiceUpdated(); } return NS_OK; } NS_IMETHODIMP nsHttpChannel::AddClassFlags(uint32_t inFlags) { uint32_t previous = mClassOfService.Flags(); mClassOfService.SetFlags(inFlags | mClassOfService.Flags()); if (previous != mClassOfService.Flags()) { OnClassOfServiceUpdated(); } return NS_OK; } NS_IMETHODIMP nsHttpChannel::ClearClassFlags(uint32_t inFlags) { uint32_t previous = mClassOfService.Flags(); mClassOfService.SetFlags(~inFlags & mClassOfService.Flags()); if (previous != mClassOfService.Flags()) { OnClassOfServiceUpdated(); } return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetClassOfService(ClassOfService cos) { ClassOfService previous = mClassOfService; mClassOfService = cos; if (previous != mClassOfService) { OnClassOfServiceUpdated(); } return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetIncremental(bool incremental) { bool previous = mClassOfService.Incremental(); mClassOfService.SetIncremental(incremental); if (previous != mClassOfService.Incremental()) { OnClassOfServiceUpdated(); } return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsIProtocolProxyCallback //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::OnProxyAvailable(nsICancelable* request, nsIChannel* channel, nsIProxyInfo* pi, nsresult status) { LOG(("nsHttpChannel::OnProxyAvailable [this=%p pi=%p status=%" PRIx32 " mStatus=%" PRIx32 "]\n", this, pi, static_cast(status), static_cast(static_cast(mStatus)))); mProxyRequest = nullptr; nsresult rv; // If status is a failure code, then it means that we failed to resolve // proxy info. That is a non-fatal error assuming it wasn't because the // request was canceled. We just failover to DIRECT when proxy resolution // fails (failure can mean that the PAC URL could not be loaded). if (NS_SUCCEEDED(status)) { mProxyInfo = pi; if (mProxyInfo) { nsAutoCStringN<8> type; mProxyInfo->GetType(type); uint32_t flags = 0; mProxyInfo->GetFlags(&flags); if (type.EqualsLiteral("socks")) { if (flags & nsIProxyInfo::TRANSPARENT_PROXY_RESOLVES_HOST) { glean::networking::proxy_info_type .EnumGet(glean::networking::ProxyInfoTypeLabel::eSocks5h) .Add(1); } else { glean::networking::proxy_info_type .EnumGet(glean::networking::ProxyInfoTypeLabel::eSocks5) .Add(1); } } else if (type.EqualsLiteral("socks4")) { if (flags & nsIProxyInfo::TRANSPARENT_PROXY_RESOLVES_HOST) { glean::networking::proxy_info_type .EnumGet(glean::networking::ProxyInfoTypeLabel::eSocks4a) .Add(1); } else { glean::networking::proxy_info_type .EnumGet(glean::networking::ProxyInfoTypeLabel::eSocks4) .Add(1); } } else if (type.EqualsLiteral("http")) { glean::networking::proxy_info_type .EnumGet(glean::networking::ProxyInfoTypeLabel::eHttp) .Add(1); } else if (type.EqualsLiteral("https")) { glean::networking::proxy_info_type .EnumGet(glean::networking::ProxyInfoTypeLabel::eHttps) .Add(1); } else if (type.EqualsLiteral("direct")) { glean::networking::proxy_info_type .EnumGet(glean::networking::ProxyInfoTypeLabel::eDirect) .Add(1); } else { glean::networking::proxy_info_type .EnumGet(glean::networking::ProxyInfoTypeLabel::eUnknown) .Add(1); } } } if (!gHttpHandler->Active()) { LOG( ("nsHttpChannel::OnProxyAvailable [this=%p] " "Handler no longer active.\n", this)); rv = NS_ERROR_NOT_AVAILABLE; } else { rv = BeginConnect(); } if (NS_FAILED(rv)) { CloseCacheEntry(false); Unused << AsyncAbort(rv); } return rv; } //----------------------------------------------------------------------------- // nsHttpChannel::nsIProxiedChannel //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::GetProxyInfo(nsIProxyInfo** result) { if (!mConnectionInfo) { *result = do_AddRef(mProxyInfo).take(); } else { *result = do_AddRef(mConnectionInfo->ProxyInfo()).take(); } return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsITimedChannel //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::GetDomainLookupStart(TimeStamp* _retval) { if (mTransaction) { *_retval = mTransaction->GetDomainLookupStart(); } else { *_retval = mTransactionTimings.domainLookupStart; } return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetDomainLookupEnd(TimeStamp* _retval) { if (mTransaction) { *_retval = mTransaction->GetDomainLookupEnd(); } else { *_retval = mTransactionTimings.domainLookupEnd; } return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetConnectStart(TimeStamp* _retval) { if (mTransaction) { *_retval = mTransaction->GetConnectStart(); } else { *_retval = mTransactionTimings.connectStart; } return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetTcpConnectEnd(TimeStamp* _retval) { if (mTransaction) { *_retval = mTransaction->GetTcpConnectEnd(); } else { *_retval = mTransactionTimings.tcpConnectEnd; } return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetSecureConnectionStart(TimeStamp* _retval) { if (mTransaction) { *_retval = mTransaction->GetSecureConnectionStart(); } else { *_retval = mTransactionTimings.secureConnectionStart; } return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetConnectEnd(TimeStamp* _retval) { if (mTransaction) { *_retval = mTransaction->GetConnectEnd(); } else { *_retval = mTransactionTimings.connectEnd; } return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetRequestStart(TimeStamp* _retval) { if (mTransaction) { *_retval = mTransaction->GetRequestStart(); } else { *_retval = mTransactionTimings.requestStart; } return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetResponseStart(TimeStamp* _retval) { if (mTransaction) { *_retval = mTransaction->GetResponseStart(); } else { *_retval = mTransactionTimings.responseStart; } return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetResponseEnd(TimeStamp* _retval) { if (mTransaction) { *_retval = mTransaction->GetResponseEnd(); } else { *_retval = mTransactionTimings.responseEnd; } return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetTransactionPending(TimeStamp* _retval) { if (mTransaction) { *_retval = mTransaction->GetPendingTime(); } else { *_retval = mTransactionTimings.transactionPending; } return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsIHttpAuthenticableChannel //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::GetIsSSL(bool* aIsSSL) { // this attribute is really misnamed - it wants to know if // https:// is being used. SSL might be used to cover http:// // in some circumstances (proxies, http/2, etc..) return mURI->SchemeIs("https", aIsSSL); } NS_IMETHODIMP nsHttpChannel::GetProxyMethodIsConnect(bool* aProxyMethodIsConnect) { *aProxyMethodIsConnect = mConnectionInfo->UsingConnect(); return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetServerResponseHeader(nsACString& value) { if (!mResponseHead) return NS_ERROR_NOT_AVAILABLE; return mResponseHead->GetHeader(nsHttp::Server, value); } NS_IMETHODIMP nsHttpChannel::GetProxyChallenges(nsACString& value) { if (!mResponseHead) return NS_ERROR_UNEXPECTED; return mResponseHead->GetHeader(nsHttp::Proxy_Authenticate, value); } NS_IMETHODIMP nsHttpChannel::GetWWWChallenges(nsACString& value) { if (!mResponseHead) return NS_ERROR_UNEXPECTED; return mResponseHead->GetHeader(nsHttp::WWW_Authenticate, value); } NS_IMETHODIMP nsHttpChannel::SetProxyCredentials(const nsACString& value) { return mRequestHead.SetHeader(nsHttp::Proxy_Authorization, value); } NS_IMETHODIMP nsHttpChannel::SetWWWCredentials(const nsACString& value) { // This method is called when various browser initiated authorization // code sets the credentials. We need to flag this header as the // "browser default" so it does not show up in the ServiceWorker // FetchEvent. This may actually get called more than once, though, // so we clear the header first since "default" headers are not // allowed to overwrite normally. Unused << mRequestHead.ClearHeader(nsHttp::Authorization); return mRequestHead.SetHeader(nsHttp::Authorization, value, false, nsHttpHeaderArray::eVarietyRequestDefault); } //----------------------------------------------------------------------------- // Methods that nsIHttpAuthenticableChannel dupes from other IDLs, which we // get from HttpBaseChannel, must be explicitly forwarded, because C++ sucks. //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::GetLoadFlags(nsLoadFlags* aLoadFlags) { return HttpBaseChannel::GetLoadFlags(aLoadFlags); } NS_IMETHODIMP nsHttpChannel::GetURI(nsIURI** aURI) { return HttpBaseChannel::GetURI(aURI); } NS_IMETHODIMP nsHttpChannel::GetNotificationCallbacks(nsIInterfaceRequestor** aCallbacks) { return HttpBaseChannel::GetNotificationCallbacks(aCallbacks); } NS_IMETHODIMP nsHttpChannel::GetLoadGroup(nsILoadGroup** aLoadGroup) { return HttpBaseChannel::GetLoadGroup(aLoadGroup); } NS_IMETHODIMP nsHttpChannel::GetRequestMethod(nsACString& aMethod) { return HttpBaseChannel::GetRequestMethod(aMethod); } //----------------------------------------------------------------------------- // nsHttpChannel::nsIRequestObserver //----------------------------------------------------------------------------- void nsHttpChannel::RecordOnStartTelemetry(nsresult aStatus, bool aIsNavigation) { Telemetry::Accumulate(Telemetry::HTTP_CHANNEL_ONSTART_SUCCESS, NS_SUCCEEDED(aStatus)); if (mTransaction) { Telemetry::Accumulate( Telemetry::HTTP3_CHANNEL_ONSTART_SUCCESS, (mTransaction->IsHttp3Used()) ? "http3"_ns : "no_http3"_ns, NS_SUCCEEDED(aStatus)); } enum class HttpOnStartState : uint32_t { Success = 0, DNSError = 1, Others = 2, }; if (TRRService::Get() && TRRService::Get()->IsConfirmed()) { // Note this telemetry probe is not working when DNS resolution is done in // the socket process. HttpOnStartState state = HttpOnStartState::Others; if (NS_SUCCEEDED(aStatus)) { state = HttpOnStartState::Success; } else if (aStatus == NS_ERROR_UNKNOWN_HOST || aStatus == NS_ERROR_UNKNOWN_PROXY_HOST) { state = HttpOnStartState::DNSError; } if (aIsNavigation) { Telemetry::Accumulate(Telemetry::HTTP_CHANNEL_PAGE_ONSTART_SUCCESS_TRR3, TRRService::ProviderKey(), static_cast(state)); } else { Telemetry::Accumulate(Telemetry::HTTP_CHANNEL_SUB_ONSTART_SUCCESS_TRR3, TRRService::ProviderKey(), static_cast(state)); } } if (nsIOService::UseSocketProcess() && mTransaction) { const TimeStamp now = TimeStamp::Now(); TimeStamp responseEnd = mTransaction->GetResponseEnd(); if (!responseEnd.IsNull()) { PerfStats::RecordMeasurement(PerfStats::Metric::ResponseEndSocketToParent, now - responseEnd); } mOnStartRequestStartTime = mTransaction->GetOnStartRequestStartTime(); if (!mOnStartRequestStartTime.IsNull()) { PerfStats::RecordMeasurement( PerfStats::Metric::OnStartRequestSocketToParent, now - mOnStartRequestStartTime); } } else { mOnStartRequestStartTime = TimeStamp::Now(); } } NS_IMETHODIMP nsHttpChannel::OnStartRequest(nsIRequest* request) { nsresult rv; MOZ_ASSERT(LoadRequestObserversCalled()); AUTO_PROFILER_LABEL("nsHttpChannel::OnStartRequest", NETWORK); if (!(mCanceled || NS_FAILED(mStatus)) && !WRONG_RACING_RESPONSE_SOURCE(request)) { // capture the request's status, so our consumers will know ASAP of any // connection failures, etc - bug 93581 nsresult status; request->GetStatus(&status); mStatus = status; } if (mStatus == NS_ERROR_NON_LOCAL_CONNECTION_REFUSED) { MOZ_CRASH_UNSAFE(nsPrintfCString("Attempting to connect to non-local " "address! opener is [%s], uri is " "[%s]", mOpenerCallingScriptLocation ? mOpenerCallingScriptLocation->get() : "unknown", mURI->GetSpecOrDefault().get()) .get()); } LOG(("nsHttpChannel::OnStartRequest [this=%p request=%p status=%" PRIx32 "]\n", this, request, static_cast(static_cast(mStatus)))); RecordOnStartTelemetry(mStatus, IsNavigation()); if (mRaceCacheWithNetwork) { LOG( (" racingNetAndCache - mFirstResponseSource:%d fromCache:%d " "fromNet:%d\n", static_cast(mFirstResponseSource), request == mCachePump, request == mTransactionPump)); if (mFirstResponseSource == RESPONSE_PENDING) { // When the cache wins mFirstResponseSource is set to // RESPONSE_FROM_CACHE earlier in ReadFromCache, so this must be a // response from the network. MOZ_ASSERT(request == mTransactionPump); LOG((" First response from network\n")); { // Race condition with OnCacheEntryCheck, which is not limited // to main thread. mozilla::MutexAutoLock lock(mRCWNLock); mFirstResponseSource = RESPONSE_FROM_NETWORK; mOnStartRequestTimestamp = TimeStamp::Now(); // Conditional or byte range header could be added in // OnCacheEntryCheck. We need to remove them because the // request might be sent again due to auth retry and we must // not send these headers without having the entry. if (mDidReval) { LOG((" Removing conditional request headers")); UntieValidationRequest(); mDidReval = false; } if (LoadCachedContentIsPartial()) { LOG((" Removing byte range request headers")); UntieByteRangeRequest(); StoreCachedContentIsPartial(false); } } mAvailableCachedAltDataType.Truncate(); StoreDeliveringAltData(false); } else if (WRONG_RACING_RESPONSE_SOURCE(request)) { LOG((" Early return when racing. This response not needed.")); return NS_OK; } } // Make sure things are what we expect them to be... MOZ_ASSERT(request == mCachePump || request == mTransactionPump, "Unexpected request"); MOZ_ASSERT(mRaceCacheWithNetwork || !(mTransactionPump && mCachePump) || LoadCachedContentIsPartial() || LoadTransactionReplaced(), "If we have both pumps, we're racing cache with network, the cache" " content is partial, or the cache entry was revalidated and " "OnStopRequest was not called yet for the transaction pump."); StoreAfterOnStartRequestBegun(true); if (mOnStartRequestTimestamp.IsNull()) { mOnStartRequestTimestamp = TimeStamp::Now(); } Telemetry::Accumulate(Telemetry::HTTP_ONSTART_SUSPEND_TOTAL_TIME, mSuspendTotalTime); if (mTransaction) { mProxyConnectResponseCode = mTransaction->GetProxyConnectResponseCode(); if (request == mTransactionPump) { StoreDataSentToChildProcess(mTransaction->DataSentToChildProcess()); } if (!mSecurityInfo && !mCachePump) { // grab the security info from the connection object; the transaction // is guaranteed to own a reference to the connection. mSecurityInfo = mTransaction->SecurityInfo(); } uint32_t stage = mTransaction->HTTPSSVCReceivedStage(); if (!LoadHTTPSSVCTelemetryReported() && stage != HTTPSSVC_NOT_USED) { Telemetry::Accumulate(Telemetry::DNS_HTTPSSVC_RECORD_RECEIVING_STAGE, stage); } if (HTTPS_RR_IS_USED(stage)) { nsAutoCString suffix(LoadEchConfigUsed() ? "_ech_used" : ""); // Determine the result string based on the status. nsAutoCString result(NS_SUCCEEDED(mStatus) ? "success" : "failure"); result.Append(suffix); mozilla::glean::networking::http_channel_onstart_success_https_rr .Get(result) .Add(1); StoreHasHTTPSRR(true); } StoreLoadedBySocketProcess(mTransaction->AsHttpTransactionParent() != nullptr); bool isTrr; bool echConfigUsed; mTransaction->GetNetworkAddresses(mSelfAddr, mPeerAddr, isTrr, mEffectiveTRRMode, mTRRSkipReason, echConfigUsed); StoreResolvedByTRR(isTrr); StoreEchConfigUsed(echConfigUsed); } // don't enter this block if we're reading from the cache... if (NS_SUCCEEDED(mStatus) && !mCachePump && mTransaction) { // mTransactionPump doesn't hit OnInputStreamReady and call this until // all of the response headers have been acquired, so we can take // ownership of them from the transaction. mResponseHead = mTransaction->TakeResponseHead(); mSupportsHTTP3 = mTransaction->GetSupportsHTTP3(); // the response head may be null if the transaction was cancelled. in // which case we just need to call OnStartRequest/OnStopRequest. if (mResponseHead) return ProcessResponse(); NS_WARNING("No response head in OnStartRequest"); } // cache file could be deleted on our behalf, it could contain errors or // it failed to allocate memory, reload from network here. if (mCacheEntry && mCachePump && RECOVER_FROM_CACHE_FILE_ERROR(mStatus)) { LOG((" cache file error, reloading from server")); mCacheEntry->AsyncDoom(nullptr); rv = StartRedirectChannelToURI(mURI, nsIChannelEventSink::REDIRECT_INTERNAL); if (NS_SUCCEEDED(rv)) return NS_OK; } // avoid crashing if mListener happens to be null... if (!mListener) { MOZ_ASSERT_UNREACHABLE("mListener is null"); return NS_OK; } rv = ProcessCrossOriginSecurityHeaders(); if (NS_FAILED(rv)) { mStatus = rv; HandleAsyncAbort(); return rv; } // No process change is needed, so continue on to ContinueOnStartRequest1. return ContinueOnStartRequest1(rv); } nsresult nsHttpChannel::ContinueOnStartRequest1(nsresult result) { nsresult rv; // if process selection failed, cancel this load. if (NS_FAILED(result) && !mCanceled) { Cancel(result); return CallOnStartRequest(); } // before we start any content load, check for redirectTo being called // this code is executed mainly before we start load from the cache if (mAPIRedirectToURI && !mCanceled) { nsAutoCString redirectToSpec; mAPIRedirectToURI->GetAsciiSpec(redirectToSpec); LOG((" redirectTo called with uri=%s", redirectToSpec.BeginReading())); MOZ_ASSERT(!LoadOnStartRequestCalled()); nsCOMPtr redirectTo; mAPIRedirectToURI.swap(redirectTo); PushRedirectAsyncFunc(&nsHttpChannel::ContinueOnStartRequest2); rv = StartRedirectChannelToURI(redirectTo, nsIChannelEventSink::REDIRECT_TEMPORARY); if (NS_SUCCEEDED(rv)) { return NS_OK; } PopRedirectAsyncFunc(&nsHttpChannel::ContinueOnStartRequest2); } // Hack: ContinueOnStartRequest2 uses NS_OK to detect successful redirects, // so we distinguish this codepath (a non-redirect that's processing // normally) by passing in a bogus error code. return ContinueOnStartRequest2(NS_BINDING_FAILED); } nsresult nsHttpChannel::ContinueOnStartRequest2(nsresult result) { if (NS_SUCCEEDED(result)) { // Redirect has passed through, we don't want to go on with this // channel. It will now be canceled by the redirect handling code // that called this function. return NS_OK; } // on proxy errors, try to failover if (mConnectionInfo->ProxyInfo() && (mStatus == NS_ERROR_PROXY_CONNECTION_REFUSED || mStatus == NS_ERROR_UNKNOWN_PROXY_HOST || mStatus == NS_ERROR_NET_TIMEOUT || mStatus == NS_ERROR_NET_RESET)) { PushRedirectAsyncFunc(&nsHttpChannel::ContinueOnStartRequest3); if (NS_SUCCEEDED(ProxyFailover())) { mProxyConnectResponseCode = 0; return NS_OK; } PopRedirectAsyncFunc(&nsHttpChannel::ContinueOnStartRequest3); } // Hack: ContinueOnStartRequest3 uses NS_OK to detect successful redirects, // so we distinguish this codepath (a non-redirect that's processing // normally) by passing in a bogus error code. return ContinueOnStartRequest3(NS_BINDING_FAILED); } nsresult nsHttpChannel::ContinueOnStartRequest3(nsresult result) { if (NS_SUCCEEDED(result)) { // Redirect has passed through, we don't want to go on with this // channel. It will now be canceled by the redirect handling code // that called this function. return NS_OK; } return ContinueOnStartRequest4(NS_OK); } nsresult nsHttpChannel::ContinueOnStartRequest4(nsresult result) { LOG(("nsHttpChannel::ContinueOnStartRequest4 [this=%p]", this)); if (NS_SUCCEEDED(mStatus) && mResponseHead && mAuthProvider) { uint32_t httpStatus = mResponseHead->Status(); if (httpStatus != 401 && httpStatus != 407) { nsresult rv = mAuthProvider->CheckForSuperfluousAuth(); if (NS_FAILED(rv)) { mStatus = rv; LOG((" CheckForSuperfluousAuth failed (%08x)", static_cast(rv))); } } } return CallOnStartRequest(); } static void ReportHTTPSRRTelemetry( const Maybe>& aMaybeRecord) { bool hasHTTPSRR = aMaybeRecord && (aMaybeRecord.ref() != nullptr); if (!hasHTTPSRR) { mozilla::glean::networking::https_rr_presented.Get("none"_ns).Add(1); return; } const nsCOMPtr& record = aMaybeRecord.ref(); nsCOMPtr svcbRecord; if (NS_SUCCEEDED(record->GetServiceModeRecord(false, false, getter_AddRefs(svcbRecord)))) { MOZ_ASSERT(svcbRecord); Maybe> alpn = svcbRecord->GetAlpn(); bool isHttp3 = alpn ? IsHttp3(std::get<1>(*alpn)) : false; mozilla::glean::networking::https_rr_presented .Get(isHttp3 ? "presented_with_http3"_ns : "presented"_ns) .Add(1); } } static nsLiteralCString ContentTypeToTelemetryLabel(nsHttpChannel* aChannel) { nsAutoCString contentType; aChannel->GetContentType(contentType); if (StringBeginsWith(contentType, "text/"_ns)) { if (contentType.EqualsLiteral(TEXT_HTML)) { return "text_html"_ns; } if (contentType.EqualsLiteral(TEXT_CSS)) { return "text_css"_ns; } if (contentType.EqualsLiteral(TEXT_JSON)) { return "text_json"_ns; } if (contentType.EqualsLiteral(TEXT_PLAIN)) { return "text_plain"_ns; } if (contentType.EqualsLiteral(TEXT_JAVASCRIPT)) { return "text_javascript"_ns; } return "text_other"_ns; } if (StringBeginsWith(contentType, "audio/"_ns)) { return "audio"_ns; } if (StringBeginsWith(contentType, "video/"_ns)) { return "video"_ns; } if (StringBeginsWith(contentType, "multipart/"_ns)) { return "multipart"_ns; } if (StringBeginsWith(contentType, "image/"_ns)) { if (contentType.EqualsLiteral(IMAGE_ICO) || contentType.EqualsLiteral(IMAGE_ICO_MS) || contentType.EqualsLiteral(IMAGE_ICON_MS)) { return "icon"_ns; } return "image"_ns; } if (StringBeginsWith(contentType, "application/"_ns)) { if (contentType.EqualsLiteral(APPLICATION_JSON)) { return "text_json"_ns; } if (contentType.EqualsLiteral(APPLICATION_OGG)) { return "video"_ns; } if (contentType.EqualsLiteral("application/ocsp-response")) { return "ocsp"_ns; } if (contentType.EqualsLiteral(APPLICATION_XPINSTALL)) { return "xpinstall"_ns; } if (contentType.EqualsLiteral(APPLICATION_WASM)) { return "wasm"_ns; } if (contentType.EqualsLiteral(APPLICATION_PDF) || contentType.EqualsLiteral(APPLICATION_POSTSCRIPT)) { return "pdf"_ns; } if (contentType.EqualsLiteral(APPLICATION_OCTET_STREAM)) { return "octet_stream"_ns; } if (contentType.EqualsLiteral(APPLICATION_ECMASCRIPT) || contentType.EqualsLiteral(APPLICATION_JAVASCRIPT) || contentType.EqualsLiteral(APPLICATION_XJAVASCRIPT)) { return "text_javascript"_ns; } if (contentType.EqualsLiteral(APPLICATION_NS_PROXY_AUTOCONFIG) || contentType.EqualsLiteral(APPLICATION_NS_JAVASCRIPT_AUTOCONFIG)) { return "proxy"_ns; } if (contentType.EqualsLiteral(APPLICATION_BROTLI) || contentType.EqualsLiteral(APPLICATION_ZSTD) || contentType.Find("zip") != kNotFound || contentType.Find("compress") != kNotFound) { return "compressed"_ns; } if (contentType.Find("x509") != kNotFound) { return "x509"_ns; } return "application_other"_ns; } if (contentType.EqualsLiteral(BINARY_OCTET_STREAM)) { return "octet_stream"_ns; } return "other"_ns; } nsresult nsHttpChannel::LogConsoleError(const char* aTag) { nsCOMPtr console( do_GetService(NS_CONSOLESERVICE_CONTRACTID)); NS_ENSURE_TRUE(console, NS_ERROR_OUT_OF_MEMORY); nsCOMPtr loadInfo = LoadInfo(); NS_ENSURE_TRUE(console, NS_ERROR_OUT_OF_MEMORY); uint64_t innerWindowID = loadInfo->GetInnerWindowID(); nsAutoString errorText; nsresult rv = nsContentUtils::GetLocalizedString( nsContentUtils::eNECKO_PROPERTIES, aTag, errorText); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr error(do_CreateInstance(NS_SCRIPTERROR_CONTRACTID)); NS_ENSURE_TRUE(error, NS_ERROR_OUT_OF_MEMORY); rv = error->InitWithSourceURI(errorText, mURI, u""_ns, 0, 0, nsIScriptError::errorFlag, "Invalid HTTP Status Lines"_ns, innerWindowID); NS_ENSURE_SUCCESS(rv, rv); console->LogMessage(error); return NS_OK; } NS_IMETHODIMP nsHttpChannel::OnStopRequest(nsIRequest* request, nsresult status) { AUTO_PROFILER_LABEL("nsHttpChannel::OnStopRequest", NETWORK); LOG(("nsHttpChannel::OnStopRequest [this=%p request=%p status=%" PRIx32 "]\n", this, request, static_cast(status))); LOG(("OnStopRequest %p requestFromCache: %d mFirstResponseSource: %d\n", this, request == mCachePump, static_cast(mFirstResponseSource))); MOZ_ASSERT(NS_IsMainThread(), "OnStopRequest should only be called from the main thread"); if (mStatus == NS_ERROR_PARSING_HTTP_STATUS_LINE) { Unused << LogConsoleError("InvalidHTTPResponseStatusLine"); } if (WRONG_RACING_RESPONSE_SOURCE(request)) { return NS_OK; } // It's possible that LoadUseHTTPSSVC() is false, but we already have // mHTTPSSVCRecord. if (LoadUseHTTPSSVC() || mHTTPSSVCRecord) { ReportHTTPSRRTelemetry(mHTTPSSVCRecord); } // If this load failed because of a security error, it may be because we // are in a captive portal - trigger an async check to make sure. int32_t nsprError = -1 * NS_ERROR_GET_CODE(status); if (mozilla::psm::IsNSSErrorCode(nsprError) && IsHTTPS()) { gIOService->RecheckCaptivePortal(); } if (LoadTimingEnabled() && request == mCachePump) { mCacheReadEnd = TimeStamp::Now(); ReportNetVSCacheTelemetry(); } // allow content to be cached if it was loaded successfully (bug #482935) bool contentComplete = NS_SUCCEEDED(status); // honor the cancelation status even if the underlying transaction // completed. if (mCanceled || NS_FAILED(mStatus)) status = mStatus; if (LoadCachedContentIsPartial()) { if (NS_SUCCEEDED(status)) { // mTransactionPump should be suspended MOZ_ASSERT(request != mTransactionPump, "byte-range transaction finished prematurely"); if (request == mCachePump) { bool streamDone; status = OnDoneReadingPartialCacheEntry(&streamDone); if (NS_SUCCEEDED(status) && !streamDone) return status; // otherwise, fall through and fire OnStopRequest... } else if (request == mTransactionPump) { MOZ_ASSERT(LoadConcurrentCacheAccess()); } else { MOZ_ASSERT_UNREACHABLE("unexpected request"); } } // Do not to leave the transaction in a suspended state in error cases. if (NS_FAILED(status) && mTransaction) { nsresult rv = gHttpHandler->CancelTransaction(mTransaction, status); if (NS_FAILED(rv)) { LOG((" CancelTransaction failed (%08x)", static_cast(rv))); } } } nsCOMPtr conv = do_QueryInterface(mCompressListener); if (conv) { conv->GetDecodedDataLength(&mDecodedBodySize); } bool isFromNet = request == mTransactionPump; if (mTransaction) { // determine if we should call DoAuthRetry bool authRetry = (mAuthRetryPending && NS_SUCCEEDED(status) && // we should only auth retry in this channel if are not // redirecting a new channel for authentication retries !StaticPrefs::network_auth_use_redirect_for_retries()); StoreStronglyFramed(mTransaction->ResponseIsComplete()); LOG(("nsHttpChannel %p has a strongly framed transaction: %d", this, LoadStronglyFramed())); // Save the reference of |mTransaction| to |transactionWithStickyConn| // when it has a sticky connection. // In the case we need to retry an authentication request, we need to // reuse the connection of |transactionWithStickyConn|. RefPtr transactionWithStickyConn; if (mCaps & NS_HTTP_STICKY_CONNECTION || mTransaction->HasStickyConnection()) { transactionWithStickyConn = mTransaction; // Make sure we use the updated caps and connection info from transaction. // We read these values when the transaction is already closed, so there // should be no race. if (mTransaction->Http2Disabled()) { mCaps |= NS_HTTP_DISALLOW_SPDY; } if (mTransaction->Http3Disabled()) { mCaps |= NS_HTTP_DISALLOW_HTTP3; } mConnectionInfo = mTransaction->GetConnInfo(); LOG((" transaction %p has sticky connection", transactionWithStickyConn.get())); } // this code relies on the code in nsHttpTransaction::Close, which // tests for NS_HTTP_STICKY_CONNECTION to determine whether or not to // keep the connection around after the transaction is finished. // LOG((" mAuthRetryPending=%d, status=%" PRIx32 ", sticky conn cap=%d", static_cast(mAuthRetryPending), static_cast(status), mCaps & NS_HTTP_STICKY_CONNECTION)); // We must check caps for stickinness also on the transaction because it // might have been updated by the transaction itself during inspection of // the reposnse headers yet on the socket thread (found connection based // auth schema). if ((NS_FAILED(status)) && transactionWithStickyConn) { // Close (don't reuse) the sticky connection if this channel has been // cancelled. There are proxy servers known to get confused when we send // a new request over such a half-stated connection. if (!LoadAuthConnectionRestartable()) { LOG((" not reusing a half-authenticated sticky connection")); transactionWithStickyConn->DontReuseConnection(); } } if (mCaps & NS_HTTP_STICKY_CONNECTION) { mTransaction->SetH2WSConnRefTaken(); } mTransferSize = mTransaction->GetTransferSize(); mRequestSize = mTransaction->GetRequestSize(); // Make sure the size does not overflow. int32_t totalSize = static_cast( std::clamp(mRequestSize + mTransferSize, 0LU, std::numeric_limits::max())); // Record telemetry for transferred size keyed by contentType nsLiteralCString label = ContentTypeToTelemetryLabel(this); if (mPrivateBrowsing) { mozilla::glean::network::data_size_pb_per_type.Get(label).Add(totalSize); } else { mozilla::glean::network::data_size_per_type.Get(label).Add(totalSize); } // If we are using the transaction to serve content, we also save the // time since async open in the cache entry so we can compare telemetry // between cache and net response. // Do not store the time of conditional requests because even if we // fetch the data from the server, the time includes loading of the old // cache entry which would skew the network load time. if (request == mTransactionPump && mCacheEntry && !mDidReval && !LoadCustomConditionalRequest() && !mAsyncOpenTime.IsNull() && !mOnStartRequestTimestamp.IsNull()) { uint64_t onStartTime = (mOnStartRequestTimestamp - mAsyncOpenTime).ToMilliseconds(); uint64_t onStopTime = (TimeStamp::Now() - mAsyncOpenTime).ToMilliseconds(); Unused << mCacheEntry->SetNetworkTimes(onStartTime, onStopTime); } mResponseTrailers = mTransaction->TakeResponseTrailers(); if (nsIOService::UseSocketProcess() && mTransaction) { mOnStopRequestStartTime = mTransaction->GetOnStopRequestStartTime(); if (!mOnStopRequestStartTime.IsNull()) { PerfStats::RecordMeasurement( PerfStats::Metric::OnStopRequestSocketToParent, TimeStamp::Now() - mOnStopRequestStartTime); } } else { mOnStopRequestStartTime = TimeStamp::Now(); } // at this point, we're done with the transaction mTransactionTimings = mTransaction->Timings(); mTransaction = nullptr; mTransactionPump = nullptr; // We no longer need the dns prefetch object if (mDNSPrefetch && mDNSPrefetch->TimingsValid() && !mTransactionTimings.requestStart.IsNull() && !mTransactionTimings.connectStart.IsNull() && mDNSPrefetch->EndTimestamp() <= mTransactionTimings.connectStart) { // We only need the domainLookup timestamps when not using a // persistent connection, meaning if the endTimestamp < connectStart mTransactionTimings.domainLookupStart = mDNSPrefetch->StartTimestamp(); mTransactionTimings.domainLookupEnd = mDNSPrefetch->EndTimestamp(); } mDNSPrefetch = nullptr; // handle auth retry... if (authRetry) { mAuthRetryPending = false; auto continueOSR = [authRetry, isFromNet, contentComplete, transactionWithStickyConn](auto* self, nsresult aStatus) { return self->ContinueOnStopRequestAfterAuthRetry( aStatus, authRetry, isFromNet, contentComplete, transactionWithStickyConn); }; status = DoAuthRetry(transactionWithStickyConn, continueOSR); if (NS_SUCCEEDED(status)) { return NS_OK; } } return ContinueOnStopRequestAfterAuthRetry(status, authRetry, isFromNet, contentComplete, transactionWithStickyConn); } return ContinueOnStopRequest(status, isFromNet, contentComplete); } nsresult nsHttpChannel::ContinueOnStopRequestAfterAuthRetry( nsresult aStatus, bool aAuthRetry, bool aIsFromNet, bool aContentComplete, HttpTransactionShell* aTransWithStickyConn) { LOG( ("nsHttpChannel::ContinueOnStopRequestAfterAuthRetry " "[this=%p, aStatus=%" PRIx32 " aAuthRetry=%d, aIsFromNet=%d, aTransWithStickyConn=%p]\n", this, static_cast(aStatus), aAuthRetry, aIsFromNet, aTransWithStickyConn)); if (aAuthRetry && NS_SUCCEEDED(aStatus)) { return NS_OK; } // If DoAuthRetry failed, or if we have been cancelled since showing // the auth. dialog, then we need to send OnStartRequest now if (aAuthRetry || (mAuthRetryPending && NS_FAILED(aStatus))) { MOZ_ASSERT(NS_FAILED(aStatus), "should have a failure code here"); // NOTE: since we have a failure status, we can ignore the return // value from onStartRequest. LOG((" calling mListener->OnStartRequest [this=%p, listener=%p]\n", this, mListener.get())); if (mListener) { MOZ_ASSERT(!LoadOnStartRequestCalled(), "We should not call OnStartRequest twice."); if (!LoadOnStartRequestCalled()) { nsCOMPtr listener(mListener); StoreOnStartRequestCalled(true); listener->OnStartRequest(this); } } else { StoreOnStartRequestCalled(true); NS_WARNING("OnStartRequest skipped because of null listener"); } mAuthRetryPending = false; } // if this transaction has been replaced, then bail. if (LoadTransactionReplaced()) { LOG(("Transaction replaced\n")); // This was just the network check for a 304 response. mFirstResponseSource = RESPONSE_PENDING; return NS_OK; } bool upgradeWebsocket = mUpgradeProtocolCallback && aTransWithStickyConn && mResponseHead && ((mResponseHead->Status() == 101 && mResponseHead->Version() == HttpVersion::v1_1) || (mResponseHead->Status() == 200 && mResponseHead->Version() == HttpVersion::v2_0)); bool upgradeConnect = mUpgradeProtocolCallback && aTransWithStickyConn && (mCaps & NS_HTTP_CONNECT_ONLY) && mResponseHead && mResponseHead->Status() == 200; if (upgradeWebsocket || upgradeConnect) { if (nsIOService::UseSocketProcess() && upgradeConnect) { // TODO: Support connection upgrade for socket process in bug 1632809. Unused << mUpgradeProtocolCallback->OnUpgradeFailed( NS_ERROR_NOT_IMPLEMENTED); return ContinueOnStopRequest(aStatus, aIsFromNet, aContentComplete); } nsresult rv = gHttpHandler->CompleteUpgrade(aTransWithStickyConn, mUpgradeProtocolCallback); mUpgradeProtocolCallback = nullptr; if (NS_FAILED(rv)) { LOG((" CompleteUpgrade failed with %" PRIx32, static_cast(rv))); // This ensures that WebSocketChannel::OnStopRequest will be // called with an error so the session is properly aborted. aStatus = rv; } } return ContinueOnStopRequest(aStatus, aIsFromNet, aContentComplete); } nsresult nsHttpChannel::ContinueOnStopRequest(nsresult aStatus, bool aIsFromNet, bool aContentComplete) { LOG( ("nsHttpChannel::ContinueOnStopRequest " "[this=%p aStatus=%" PRIx32 ", aIsFromNet=%d]\n", this, static_cast(aStatus), aIsFromNet)); // HTTP_CHANNEL_DISPOSITION TELEMETRY enum ChannelDisposition { kHttpCanceled = 0, kHttpDisk = 1, kHttpNetOK = 2, kHttpNetEarlyFail = 3, kHttpNetLateFail = 4, kHttpsCanceled = 8, kHttpsDisk = 9, kHttpsNetOK = 10, kHttpsNetEarlyFail = 11, kHttpsNetLateFail = 12 } chanDisposition = kHttpCanceled; // HTTP_CHANNEL_DISPOSITION_UPGRADE TELEMETRY Telemetry::LABELS_HTTP_CHANNEL_DISPOSITION_UPGRADE upgradeChanDisposition = Telemetry::LABELS_HTTP_CHANNEL_DISPOSITION_UPGRADE::cancel; // HTTP 0.9 is more likely to be an error than really 0.9, so count it that // way if (mCanceled) { chanDisposition = kHttpCanceled; upgradeChanDisposition = Telemetry::LABELS_HTTP_CHANNEL_DISPOSITION_UPGRADE::cancel; } else if (!LoadUsedNetwork() || (mRaceCacheWithNetwork && mFirstResponseSource == RESPONSE_FROM_CACHE)) { chanDisposition = kHttpDisk; upgradeChanDisposition = Telemetry::LABELS_HTTP_CHANNEL_DISPOSITION_UPGRADE::disk; } else if (NS_SUCCEEDED(aStatus) && mResponseHead && mResponseHead->Version() != HttpVersion::v0_9) { chanDisposition = kHttpNetOK; upgradeChanDisposition = Telemetry::LABELS_HTTP_CHANNEL_DISPOSITION_UPGRADE::netOk; } else if (!mTransferSize) { chanDisposition = kHttpNetEarlyFail; upgradeChanDisposition = Telemetry::LABELS_HTTP_CHANNEL_DISPOSITION_UPGRADE::netEarlyFail; } else { chanDisposition = kHttpNetLateFail; upgradeChanDisposition = Telemetry::LABELS_HTTP_CHANNEL_DISPOSITION_UPGRADE::netLateFail; } // Browser upgrading only happens on HTTPS pages for mixed passive content // when upgrading is enabled. nsCString upgradeKey; if (IsHTTPS()) { // Browser upgrading is disabled and the content is already HTTPS upgradeKey = "disabledNoReason"_ns; // Checks "security.mixed_content.upgrade_display_content" is true if (StaticPrefs::security_mixed_content_upgrade_display_content()) { if (mLoadInfo->GetBrowserUpgradeInsecureRequests()) { // HTTP content the browser has upgraded to HTTPS upgradeKey = "enabledUpgrade"_ns; } else { // Content wasn't upgraded but is already HTTPS upgradeKey = "enabledNoReason"_ns; } } // shift http to https disposition enums chanDisposition = static_cast(chanDisposition + kHttpsCanceled); } else if (mLoadInfo->GetBrowserWouldUpgradeInsecureRequests()) { // HTTP content the browser would upgrade to HTTPS if upgrading was // enabled upgradeKey = "disabledUpgrade"_ns; } else { // HTTP content that wouldn't upgrade upgradeKey = StaticPrefs::security_mixed_content_upgrade_display_content() ? "enabledWont"_ns : "disabledWont"_ns; } Telemetry::AccumulateCategoricalKeyed(upgradeKey, upgradeChanDisposition); LOG((" nsHttpChannel::OnStopRequest ChannelDisposition %d\n", chanDisposition)); Telemetry::Accumulate(Telemetry::HTTP_CHANNEL_DISPOSITION, chanDisposition); // Collect specific telemetry for measuring image, video, audio // success/failure rates in regular browsing mode and when auto upgrading of // subresources is enabled. Note that we only evaluate actual image types, not // favicons. nsContentPolicyType internalLoadType; mLoadInfo->GetInternalContentPolicyType(&internalLoadType); bool statusIsSuccess = NS_SUCCEEDED(aStatus); if (internalLoadType == nsIContentPolicy::TYPE_INTERNAL_IMAGE || internalLoadType == nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD) { if (mLoadInfo->GetBrowserDidUpgradeInsecureRequests()) { Telemetry::AccumulateCategorical( statusIsSuccess ? Telemetry::LABELS_MIXED_CONTENT_IMAGES::ImgUpSuccess : Telemetry::LABELS_MIXED_CONTENT_IMAGES::ImgUpFailure); } else { Telemetry::AccumulateCategorical( statusIsSuccess ? Telemetry::LABELS_MIXED_CONTENT_IMAGES::ImgNoUpSuccess : Telemetry::LABELS_MIXED_CONTENT_IMAGES::ImgNoUpFailure); } } if (internalLoadType == nsIContentPolicy::TYPE_INTERNAL_VIDEO) { if (mLoadInfo->GetBrowserDidUpgradeInsecureRequests()) { Telemetry::AccumulateCategorical( statusIsSuccess ? Telemetry::LABELS_MIXED_CONTENT_VIDEO::VideoUpSuccess : Telemetry::LABELS_MIXED_CONTENT_VIDEO::VideoUpFailure); } else { Telemetry::AccumulateCategorical( statusIsSuccess ? Telemetry::LABELS_MIXED_CONTENT_VIDEO::VideoNoUpSuccess : Telemetry::LABELS_MIXED_CONTENT_VIDEO::VideoNoUpFailure); } } if (internalLoadType == nsIContentPolicy::TYPE_INTERNAL_AUDIO) { if (mLoadInfo->GetBrowserDidUpgradeInsecureRequests()) { Telemetry::AccumulateCategorical( statusIsSuccess ? Telemetry::LABELS_MIXED_CONTENT_AUDIO::AudioUpSuccess : Telemetry::LABELS_MIXED_CONTENT_AUDIO::AudioUpFailure); } else { Telemetry::AccumulateCategorical( statusIsSuccess ? Telemetry::LABELS_MIXED_CONTENT_AUDIO::AudioNoUpSuccess : Telemetry::LABELS_MIXED_CONTENT_AUDIO::AudioNoUpFailure); } } // if needed, check cache entry has all data we expect if (mCacheEntry && mCachePump && LoadConcurrentCacheAccess() && aContentComplete) { int64_t size, contentLength; nsresult rv = CheckPartial(mCacheEntry, &size, &contentLength); if (NS_SUCCEEDED(rv)) { if (size == int64_t(-1)) { // mayhemer TODO - we have to restart read from cache here at the size // offset MOZ_ASSERT(false); LOG( (" cache entry write is still in progress, but we just " "finished reading the cache entry")); } else if (contentLength != int64_t(-1) && contentLength != size) { LOG((" concurrent cache entry write has been interrupted")); mCachedResponseHead = std::move(mResponseHead); // Ignore zero partial length because we also want to resume when // no data at all has been read from the cache. rv = MaybeSetupByteRangeRequest(size, contentLength, true); if (NS_SUCCEEDED(rv) && LoadIsPartialRequest()) { // Prevent read from cache again mCachedContentIsValid = false; StoreCachedContentIsPartial(1); // We are about to perform a different network request. // We must set mRaceCacheWithNetwork to false because otherwise // we would ignore the network response thinking we didn't need it. mRaceCacheWithNetwork = false; // Perform the range request rv = ContinueConnect(); if (NS_SUCCEEDED(rv)) { LOG((" performing range request")); mCachePump = nullptr; return NS_OK; } LOG((" but range request perform failed 0x%08" PRIx32, static_cast(rv))); aStatus = NS_ERROR_NET_INTERRUPT; } else { LOG((" but range request setup failed rv=0x%08" PRIx32 ", failing load", static_cast(rv))); } } } } StoreIsPending(false); mStatus = aStatus; // perform any final cache operations before we close the cache entry. if (mCacheEntry && LoadRequestTimeInitialized()) { bool writeAccess; // New implementation just returns value of the !LoadCacheEntryIsReadOnly() // flag passed in. Old implementation checks on nsICache::ACCESS_WRITE // flag. mCacheEntry->HasWriteAccess(!LoadCacheEntryIsReadOnly(), &writeAccess); if (writeAccess) { nsresult rv = FinalizeCacheEntry(); if (NS_FAILED(rv)) { LOG(("FinalizeCacheEntry failed (%08x)", static_cast(rv))); } } } ReportRcwnStats(aIsFromNet); // Register entry to the PerformanceStorage resource timing MaybeReportTimingData(); MaybeFlushConsoleReports(); if (!mEndMarkerAdded && profiler_thread_is_being_profiled_for_markers()) { // These do allocations/frees/etc; avoid if not active mEndMarkerAdded = true; nsAutoCString requestMethod; GetRequestMethod(requestMethod); int32_t priority = PRIORITY_NORMAL; GetPriority(&priority); uint64_t size = 0; GetEncodedBodySize(&size); nsAutoCString contentType; if (mResponseHead) { mResponseHead->ContentType(contentType); } profiler_add_network_marker( mURI, requestMethod, priority, mChannelId, NetworkLoadType::LOAD_STOP, mLastStatusReported, TimeStamp::Now(), size, mCacheDisposition, mLoadInfo->GetInnerWindowID(), mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0, &mTransactionTimings, std::move(mSource), Some(nsDependentCString(contentType.get()))); } if (mAuthRetryPending && StaticPrefs::network_auth_use_redirect_for_retries()) { nsresult rv = OpenRedirectChannel(aStatus); LOG(("Opening redirect channel for auth retry %x", static_cast(rv))); if (NS_FAILED(rv)) { if (mListener) { MOZ_ASSERT(!LoadOnStartRequestCalled(), "We should not call OnStartRequest twice."); if (!LoadOnStartRequestCalled()) { nsCOMPtr listener(mListener); StoreOnStartRequestCalled(true); listener->OnStartRequest(this); } } else { StoreOnStartRequestCalled(true); NS_WARNING("OnStartRequest skipped because of null listener"); } } mAuthRetryPending = false; } // notify "http-on-before-stop-request" observers gHttpHandler->OnBeforeStopRequest(this); if (mListener) { LOG(("nsHttpChannel %p calling OnStopRequest\n", this)); MOZ_ASSERT(LoadOnStartRequestCalled(), "OnStartRequest should be called before OnStopRequest"); MOZ_ASSERT(!LoadOnStopRequestCalled(), "We should not call OnStopRequest twice"); StoreOnStopRequestCalled(true); mListener->OnStopRequest(this, aStatus); } StoreOnStopRequestCalled(true); // The prefetch needs to be released on the main thread mDNSPrefetch = nullptr; mRedirectChannel = nullptr; // notify "http-on-stop-request" observers gHttpHandler->OnStopRequest(this); RemoveAsNonTailRequest(); if (mChannelBlockedByOpaqueResponse && mCachedOpaqueResponseBlockingPref) { mResponseHead->ClearHeaders(); } // If a preferred alt-data type was set, this signals the consumer is // interested in reading and/or writing the alt-data representation. // We need to hold a reference to the cache entry in case the listener calls // openAlternativeOutputStream() after CloseCacheEntry() clears mCacheEntry. if (!mPreferredCachedAltDataTypes.IsEmpty()) { mAltDataCacheEntry = mCacheEntry; } CloseCacheEntry(!aContentComplete); if (mLoadGroup) { mLoadGroup->RemoveRequest(this, nullptr, aStatus); } // We don't need this info anymore CleanRedirectCacheChainIfNecessary(); ReleaseListeners(); return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsIStreamListener //----------------------------------------------------------------------------- class OnTransportStatusAsyncEvent : public Runnable { public: OnTransportStatusAsyncEvent(nsITransportEventSink* aEventSink, nsresult aTransportStatus, int64_t aProgress, int64_t aProgressMax) : Runnable("net::OnTransportStatusAsyncEvent"), mEventSink(aEventSink), mTransportStatus(aTransportStatus), mProgress(aProgress), mProgressMax(aProgressMax) { MOZ_ASSERT(!NS_IsMainThread(), "Shouldn't be created on main thread"); } NS_IMETHOD Run() override { MOZ_ASSERT(NS_IsMainThread(), "Should run on main thread"); if (mEventSink) { mEventSink->OnTransportStatus(nullptr, mTransportStatus, mProgress, mProgressMax); } return NS_OK; } private: nsCOMPtr mEventSink; nsresult mTransportStatus; int64_t mProgress; int64_t mProgressMax; }; NS_IMETHODIMP nsHttpChannel::OnDataAvailable(nsIRequest* request, nsIInputStream* input, uint64_t offset, uint32_t count) { nsresult rv; AUTO_PROFILER_LABEL("nsHttpChannel::OnDataAvailable", NETWORK); LOG(("nsHttpChannel::OnDataAvailable [this=%p request=%p offset=%" PRIu64 " count=%" PRIu32 "]\n", this, request, offset, count)); LOG((" requestFromCache: %d mFirstResponseSource: %d\n", request == mCachePump, static_cast(mFirstResponseSource))); // don't send out OnDataAvailable notifications if we've been canceled. if (mCanceled) return mStatus; if (mAuthRetryPending || WRONG_RACING_RESPONSE_SOURCE(request) || (request == mTransactionPump && LoadTransactionReplaced())) { uint32_t n; return input->ReadSegments(NS_DiscardSegment, nullptr, count, &n); } MOZ_ASSERT(mResponseHead, "No response head in ODA!!"); MOZ_ASSERT(!(LoadCachedContentIsPartial() && (request == mTransactionPump)), "transaction pump not suspended"); mIsReadingFromCache = (request == mCachePump); if (mListener) { // // synthesize transport progress event. we do this here since we want // to delay OnProgress events until we start streaming data. this is // crucially important since it impacts the lock icon (see bug 240053). // nsresult transportStatus; if (request == mCachePump) { transportStatus = NS_NET_STATUS_READING; } else { transportStatus = NS_NET_STATUS_RECEIVING_FROM; } // mResponseHead may reference new or cached headers, but either way it // holds our best estimate of the total content length. Even in the case // of a byte range request, the content length stored in the cached // response headers is what we want to use here. int64_t progressMax = -1; rv = GetContentLength(&progressMax); if (NS_FAILED(rv)) { NS_WARNING("GetContentLength failed"); } int64_t progress = mLogicalOffset + count; if ((progress > progressMax) && (progressMax != -1)) { NS_WARNING( "unexpected progress values - " "is server exceeding content length?"); } // make sure params are in range for js if (!InScriptableRange(progressMax)) { progressMax = -1; } if (!InScriptableRange(progress)) { progress = -1; } if (NS_IsMainThread()) { OnTransportStatus(nullptr, transportStatus, progress, progressMax); } else { rv = NS_DispatchToMainThread(new OnTransportStatusAsyncEvent( this, transportStatus, progress, progressMax)); NS_ENSURE_SUCCESS(rv, rv); } // // we have to manually keep the logical offset of the stream up-to-date. // we cannot depend solely on the offset provided, since we may have // already streamed some data from another source (see, for example, // OnDoneReadingPartialCacheEntry). // int64_t offsetBefore = 0; nsCOMPtr seekable = do_QueryInterface(input); if (seekable && NS_FAILED(seekable->Tell(&offsetBefore))) { seekable = nullptr; } if (nsIOService::UseSocketProcess() && mTransaction) { mOnDataAvailableStartTime = mTransaction->GetDataAvailableStartTime(); if (!mOnDataAvailableStartTime.IsNull()) { PerfStats::RecordMeasurement( PerfStats::Metric::OnDataAvailableSocketToParent, TimeStamp::Now() - mOnDataAvailableStartTime); } } else { mOnDataAvailableStartTime = TimeStamp::Now(); } nsresult rv = mListener->OnDataAvailable(this, input, mLogicalOffset, count); if (NS_SUCCEEDED(rv)) { // by contract mListener must read all of "count" bytes, but // nsInputStreamPump is tolerant to seekable streams that violate that // and it will redeliver incompletely read data. So we need to do // the same thing when updating the progress counter to stay in sync. int64_t offsetAfter, delta; if (seekable && NS_SUCCEEDED(seekable->Tell(&offsetAfter))) { delta = offsetAfter - offsetBefore; if (delta != count) { count = delta; NS_WARNING("Listener OnDataAvailable contract violation"); nsCOMPtr consoleService = do_GetService(NS_CONSOLESERVICE_CONTRACTID); nsAutoString message(nsLiteralString( u"http channel Listener OnDataAvailable contract violation")); if (consoleService) { consoleService->LogStringMessage(message.get()); } } } mLogicalOffset += count; } return rv; } return NS_ERROR_ABORT; } //----------------------------------------------------------------------------- // nsHttpChannel::nsIThreadRetargetableRequest //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::RetargetDeliveryTo(nsISerialEventTarget* aNewTarget) { MOZ_ASSERT(NS_IsMainThread(), "Should be called on main thread only"); NS_ENSURE_ARG(aNewTarget); if (aNewTarget->IsOnCurrentThread()) { NS_WARNING("Retargeting delivery to same thread"); return NS_OK; } if (!mTransactionPump && !mCachePump) { LOG(("nsHttpChannel::RetargetDeliveryTo %p %p no pump available\n", this, aNewTarget)); return NS_ERROR_NOT_AVAILABLE; } nsresult rv = NS_OK; // If both cache pump and transaction pump exist, we're probably dealing // with partially cached content. So, we must be able to retarget both. nsCOMPtr retargetableCachePump; nsCOMPtr retargetableTransactionPump; if (mCachePump) { retargetableCachePump = do_QueryObject(mCachePump); // nsInputStreamPump should implement this interface. MOZ_ASSERT(retargetableCachePump); rv = retargetableCachePump->RetargetDeliveryTo(aNewTarget); } if (NS_SUCCEEDED(rv) && mTransactionPump) { retargetableTransactionPump = do_QueryObject(mTransactionPump); // nsInputStreamPump should implement this interface. MOZ_ASSERT(retargetableTransactionPump); rv = retargetableTransactionPump->RetargetDeliveryTo(aNewTarget); // If retarget fails for transaction pump, we must restore mCachePump. if (NS_FAILED(rv) && retargetableCachePump) { nsCOMPtr main = GetMainThreadSerialEventTarget(); NS_ENSURE_TRUE(main, NS_ERROR_UNEXPECTED); rv = retargetableCachePump->RetargetDeliveryTo(main); } } return rv; } NS_IMETHODIMP nsHttpChannel::GetDeliveryTarget(nsISerialEventTarget** aEventTarget) { if (mCachePump) { return mCachePump->GetDeliveryTarget(aEventTarget); } if (mTransactionPump) { nsCOMPtr request = do_QueryInterface(mTransactionPump); return request->GetDeliveryTarget(aEventTarget); } return NS_ERROR_NOT_AVAILABLE; } //----------------------------------------------------------------------------- // nsHttpChannel::nsThreadRetargetableStreamListener //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::CheckListenerChain() { NS_ASSERTION(NS_IsMainThread(), "Should be on main thread!"); nsresult rv = NS_OK; nsCOMPtr retargetableListener = do_QueryInterface(mListener, &rv); if (retargetableListener) { rv = retargetableListener->CheckListenerChain(); } return rv; } NS_IMETHODIMP nsHttpChannel::OnDataFinished(nsresult aStatus) { nsCOMPtr listener = do_QueryInterface(mListener); if (listener) { return listener->OnDataFinished(aStatus); } return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsITransportEventSink //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::OnTransportStatus(nsITransport* trans, nsresult status, int64_t progress, int64_t progressMax) { MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread only"); // cache the progress sink so we don't have to query for it each time. if (!mProgressSink) GetCallback(mProgressSink); if (status == NS_NET_STATUS_CONNECTED_TO || status == NS_NET_STATUS_WAITING_FOR) { bool isTrr = false; bool echConfigUsed = false; if (mTransaction) { mTransaction->GetNetworkAddresses(mSelfAddr, mPeerAddr, isTrr, mEffectiveTRRMode, mTRRSkipReason, echConfigUsed); } else { nsCOMPtr socketTransport = do_QueryInterface(trans); if (socketTransport) { socketTransport->GetSelfAddr(&mSelfAddr); socketTransport->GetPeerAddr(&mPeerAddr); socketTransport->ResolvedByTRR(&isTrr); socketTransport->GetEffectiveTRRMode(&mEffectiveTRRMode); socketTransport->GetEchConfigUsed(&echConfigUsed); } } StoreResolvedByTRR(isTrr); StoreEchConfigUsed(echConfigUsed); } // block socket status event after Cancel or OnStopRequest has been called. if (mProgressSink && NS_SUCCEEDED(mStatus) && LoadIsPending()) { LOG(("sending progress%s notification [this=%p status=%" PRIx32 " progress=%" PRId64 "/%" PRId64 "]\n", (mLoadFlags & LOAD_BACKGROUND) ? "" : " and status", this, static_cast(status), progress, progressMax)); nsAutoCString host; mURI->GetHost(host); if (!(mLoadFlags & LOAD_BACKGROUND)) { mProgressSink->OnStatus(this, status, NS_ConvertUTF8toUTF16(host).get()); } else { nsCOMPtr parentChannel; NS_QueryNotificationCallbacks(this, parentChannel); // If the event sink is |HttpChannelParent|, we have to send status // events to it even if LOAD_BACKGROUND is set. |HttpChannelParent| // needs to be aware of whether the status is // |NS_NET_STATUS_RECEIVING_FROM| or |NS_NET_STATUS_READING|. // LOAD_BACKGROUND is checked again in |HttpChannelChild|, so the final // consumer won't get this event. if (SameCOMIdentity(parentChannel, mProgressSink)) { mProgressSink->OnStatus(this, status, NS_ConvertUTF8toUTF16(host).get()); } } if (progress > 0) { if ((progress > progressMax) && (progressMax != -1)) { NS_WARNING("unexpected progress values"); } // Try to get mProgressSink if it was nulled out during OnStatus. if (!mProgressSink) { GetCallback(mProgressSink); } if (mProgressSink) { mProgressSink->OnProgress(this, progress, progressMax); } } } return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsICacheInfoChannel //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::IsFromCache(bool* value) { if (!LoadIsPending()) return NS_ERROR_NOT_AVAILABLE; if (!mRaceCacheWithNetwork) { // return false if reading a partial cache entry; the data isn't // entirely from the cache! *value = (mCachePump || (mLoadFlags & LOAD_ONLY_IF_MODIFIED)) && mCachedContentIsValid && !LoadCachedContentIsPartial(); return NS_OK; } // If we are racing network and cache (or skipping the cache) // we just return the first response source. *value = mFirstResponseSource == RESPONSE_FROM_CACHE; return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetCacheEntryId(uint64_t* aCacheEntryId) { bool fromCache = false; if (NS_FAILED(IsFromCache(&fromCache)) || !fromCache || !mCacheEntry || NS_FAILED(mCacheEntry->GetCacheEntryId(aCacheEntryId))) { return NS_ERROR_NOT_AVAILABLE; } return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetCacheTokenFetchCount(uint32_t* _retval) { NS_ENSURE_ARG_POINTER(_retval); nsCOMPtr cacheEntry = mCacheEntry ? mCacheEntry : mAltDataCacheEntry; if (!cacheEntry) { return NS_ERROR_NOT_AVAILABLE; } return cacheEntry->GetFetchCount(_retval); } NS_IMETHODIMP nsHttpChannel::GetCacheTokenExpirationTime(uint32_t* _retval) { NS_ENSURE_ARG_POINTER(_retval); if (!mCacheEntry) return NS_ERROR_NOT_AVAILABLE; return mCacheEntry->GetExpirationTime(_retval); } NS_IMETHODIMP nsHttpChannel::SetAllowStaleCacheContent(bool aAllowStaleCacheContent) { LOG(("nsHttpChannel::SetAllowStaleCacheContent [this=%p, allow=%d]", this, aAllowStaleCacheContent)); StoreAllowStaleCacheContent(aAllowStaleCacheContent); return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetAllowStaleCacheContent(bool* aAllowStaleCacheContent) { NS_ENSURE_ARG(aAllowStaleCacheContent); *aAllowStaleCacheContent = LoadAllowStaleCacheContent(); return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetForceValidateCacheContent(bool aForceValidateCacheContent) { LOG(("nsHttpChannel::SetForceValidateCacheContent [this=%p, allow=%d]", this, aForceValidateCacheContent)); StoreForceValidateCacheContent(aForceValidateCacheContent); return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetForceValidateCacheContent(bool* aForceValidateCacheContent) { NS_ENSURE_ARG(aForceValidateCacheContent); *aForceValidateCacheContent = LoadForceValidateCacheContent(); return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetPreferCacheLoadOverBypass(bool aPreferCacheLoadOverBypass) { StorePreferCacheLoadOverBypass(aPreferCacheLoadOverBypass); return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetPreferCacheLoadOverBypass(bool* aPreferCacheLoadOverBypass) { NS_ENSURE_ARG(aPreferCacheLoadOverBypass); *aPreferCacheLoadOverBypass = LoadPreferCacheLoadOverBypass(); return NS_OK; } NS_IMETHODIMP nsHttpChannel::PreferAlternativeDataType( const nsACString& aType, const nsACString& aContentType, PreferredAlternativeDataDeliveryType aDeliverAltData) { ENSURE_CALLED_BEFORE_ASYNC_OPEN(); mPreferredCachedAltDataTypes.AppendElement(PreferredAlternativeDataTypeParams( nsCString(aType), nsCString(aContentType), aDeliverAltData)); return NS_OK; } const nsTArray& nsHttpChannel::PreferredAlternativeDataTypes() { return mPreferredCachedAltDataTypes; } NS_IMETHODIMP nsHttpChannel::GetAlternativeDataType(nsACString& aType) { // must be called during or after OnStartRequest if (!LoadAfterOnStartRequestBegun()) { return NS_ERROR_NOT_AVAILABLE; } aType = mAvailableCachedAltDataType; return NS_OK; } NS_IMETHODIMP nsHttpChannel::OpenAlternativeOutputStream(const nsACString& type, int64_t predictedSize, nsIAsyncOutputStream** _retval) { // OnStopRequest will clear mCacheEntry, but we may use mAltDataCacheEntry // if the consumer called PreferAlternativeDataType() nsCOMPtr cacheEntry = mCacheEntry ? mCacheEntry : mAltDataCacheEntry; if (!cacheEntry) { return NS_ERROR_NOT_AVAILABLE; } nsresult rv = cacheEntry->OpenAlternativeOutputStream(type, predictedSize, _retval); if (NS_SUCCEEDED(rv)) { // Clear this metadata flag in case it exists. // The caller of this method may set it again. cacheEntry->SetMetaDataElement("alt-data-from-child", nullptr); } return rv; } NS_IMETHODIMP nsHttpChannel::GetOriginalInputStream(nsIInputStreamReceiver* aReceiver) { if (aReceiver == nullptr) { return NS_ERROR_INVALID_ARG; } nsCOMPtr inputStream; nsCOMPtr cacheEntry = mCacheEntry ? mCacheEntry : mAltDataCacheEntry; if (cacheEntry) { cacheEntry->OpenInputStream(0, getter_AddRefs(inputStream)); } aReceiver->OnInputStreamReady(inputStream); return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetAlternativeDataInputStream(nsIInputStream** aInputStream) { NS_ENSURE_ARG_POINTER(aInputStream); *aInputStream = nullptr; nsCOMPtr cacheEntry = mCacheEntry ? mCacheEntry : mAltDataCacheEntry; if (!mAvailableCachedAltDataType.IsEmpty() && cacheEntry) { nsresult rv = cacheEntry->OpenAlternativeInputStream( mAvailableCachedAltDataType, aInputStream); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsICachingChannel //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::IsRacing(bool* aIsRacing) { if (!LoadAfterOnStartRequestBegun()) { return NS_ERROR_NOT_AVAILABLE; } *aIsRacing = mRaceCacheWithNetwork; return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetCacheToken(nsISupports** token) { NS_ENSURE_ARG_POINTER(token); if (!mCacheEntry) return NS_ERROR_NOT_AVAILABLE; return CallQueryInterface(mCacheEntry, token); } NS_IMETHODIMP nsHttpChannel::SetCacheToken(nsISupports* token) { return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP nsHttpChannel::GetCacheKey(uint32_t* key) { NS_ENSURE_ARG_POINTER(key); LOG(("nsHttpChannel::GetCacheKey [this=%p]\n", this)); *key = mPostID; return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetCacheKey(uint32_t key) { LOG(("nsHttpChannel::SetCacheKey [this=%p key=%u]\n", this, key)); ENSURE_CALLED_BEFORE_CONNECT(); mPostID = key; return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetCacheOnlyMetadata(bool* aOnlyMetadata) { NS_ENSURE_ARG(aOnlyMetadata); *aOnlyMetadata = LoadCacheOnlyMetadata(); return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetCacheOnlyMetadata(bool aOnlyMetadata) { LOG(("nsHttpChannel::SetCacheOnlyMetadata [this=%p only-metadata=%d]\n", this, aOnlyMetadata)); ENSURE_CALLED_BEFORE_ASYNC_OPEN(); StoreCacheOnlyMetadata(aOnlyMetadata); if (aOnlyMetadata) { mLoadFlags |= LOAD_ONLY_IF_MODIFIED; } return NS_OK; } NS_IMETHODIMP nsHttpChannel::GetPin(bool* aPin) { NS_ENSURE_ARG(aPin); *aPin = LoadPinCacheContent(); return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetPin(bool aPin) { LOG(("nsHttpChannel::SetPin [this=%p pin=%d]\n", this, aPin)); ENSURE_CALLED_BEFORE_CONNECT(); StorePinCacheContent(aPin); return NS_OK; } NS_IMETHODIMP nsHttpChannel::ForceCacheEntryValidFor(uint32_t aSecondsToTheFuture) { if (!mCacheEntry) { LOG( ("nsHttpChannel::ForceCacheEntryValidFor found no cache entry " "for this channel [this=%p].", this)); } else { mCacheEntry->ForceValidFor(aSecondsToTheFuture); nsAutoCString key; mCacheEntry->GetKey(key); LOG( ("nsHttpChannel::ForceCacheEntryValidFor successfully forced valid " "entry with key %s for %d seconds. [this=%p]", key.get(), aSecondsToTheFuture, this)); } return NS_OK; } //----------------------------------------------------------------------------- // nsHttpChannel::nsIResumableChannel //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::ResumeAt(uint64_t aStartPos, const nsACString& aEntityID) { LOG(("nsHttpChannel::ResumeAt [this=%p startPos=%" PRIu64 " id='%s']\n", this, aStartPos, PromiseFlatCString(aEntityID).get())); mEntityID = aEntityID; mStartPos = aStartPos; StoreResuming(true); return NS_OK; } nsresult nsHttpChannel::DoAuthRetry( HttpTransactionShell* aTransWithStickyConn, const std::function& aContinueOnStopRequestFunc) { LOG(("nsHttpChannel::DoAuthRetry [this=%p, aTransWithStickyConn=%p]\n", this, aTransWithStickyConn)); MOZ_ASSERT(!mTransaction, "should not have a transaction"); // Note that we don't have to toggle |IsPending| anymore. See the reasons // below. // 1. We can't suspend the channel during "http-on-modify-request" // when |IsPending| is false. // 2. We don't check |IsPending| in SetRequestHeader now. // Reset RequestObserversCalled because we've probably called the request // observers once already. StoreRequestObserversCalled(false); // fetch cookies, and add them to the request header. // the server response could have included cookies that must be sent with // this authentication attempt (bug 84794). // TODO: save cookies from auth response and send them here (bug 572151). AddCookiesToRequest(); // notify "http-on-modify-request" observers CallOnModifyRequestObservers(); RefPtr trans(aTransWithStickyConn); return CallOrWaitForResume( [trans{std::move(trans)}, aContinueOnStopRequestFunc](auto* self) { return self->ContinueDoAuthRetry(trans, aContinueOnStopRequestFunc); }); } nsresult nsHttpChannel::ContinueDoAuthRetry( HttpTransactionShell* aTransWithStickyConn, const std::function& aContinueOnStopRequestFunc) { LOG(("nsHttpChannel::ContinueDoAuthRetry [this=%p]\n", this)); StoreIsPending(true); // get rid of the old response headers mResponseHead = nullptr; // rewind the upload stream if (mUploadStream) { nsCOMPtr seekable = do_QueryInterface(mUploadStream); nsresult rv = NS_ERROR_NO_INTERFACE; if (seekable) { rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0); } // This should not normally happen, but it's possible that big memory // blobs originating in the other process can't be rewinded. // In that case we just fail the request, otherwise the content length // will not match and this load will never complete. NS_ENSURE_SUCCESS(rv, rv); } // always set sticky connection flag mCaps |= NS_HTTP_STICKY_CONNECTION; // and when needed, allow restart regardless the sticky flag if (LoadAuthConnectionRestartable()) { LOG((" connection made restartable")); mCaps |= NS_HTTP_CONNECTION_RESTARTABLE; StoreAuthConnectionRestartable(false); } else { LOG((" connection made non-restartable")); mCaps &= ~NS_HTTP_CONNECTION_RESTARTABLE; } // notify "http-on-before-connect" observers gHttpHandler->OnBeforeConnect(this); RefPtr trans(aTransWithStickyConn); return CallOrWaitForResume( [trans{std::move(trans)}, aContinueOnStopRequestFunc](auto* self) { nsresult rv = self->DoConnect(trans); return aContinueOnStopRequestFunc(self, rv); }); } //----------------------------------------------------------------------------- // nsHttpChannel::nsIAsyncVerifyRedirectCallback //----------------------------------------------------------------------------- nsresult nsHttpChannel::WaitForRedirectCallback() { nsresult rv; LOG(("nsHttpChannel::WaitForRedirectCallback [this=%p]\n", this)); if (mTransactionPump) { rv = mTransactionPump->Suspend(); NS_ENSURE_SUCCESS(rv, rv); } if (mCachePump) { rv = mCachePump->Suspend(); if (NS_FAILED(rv) && mTransactionPump) { #ifdef DEBUG nsresult resume = #endif mTransactionPump->Resume(); MOZ_ASSERT(NS_SUCCEEDED(resume), "Failed to resume transaction pump"); } NS_ENSURE_SUCCESS(rv, rv); } StoreWaitingForRedirectCallback(true); return NS_OK; } NS_IMETHODIMP nsHttpChannel::OnRedirectVerifyCallback(nsresult result) { LOG( ("nsHttpChannel::OnRedirectVerifyCallback [this=%p] " "result=%" PRIx32 " stack=%zu WaitingForRedirectCallback=%u\n", this, static_cast(result), mRedirectFuncStack.Length(), LoadWaitingForRedirectCallback())); MOZ_ASSERT(LoadWaitingForRedirectCallback(), "Someone forgot to call WaitForRedirectCallback() ?!"); StoreWaitingForRedirectCallback(false); if (mCanceled && NS_SUCCEEDED(result)) result = NS_BINDING_ABORTED; for (uint32_t i = mRedirectFuncStack.Length(); i > 0;) { --i; // Pop the last function pushed to the stack nsContinueRedirectionFunc func = mRedirectFuncStack.PopLastElement(); // Call it with the result we got from the callback or the deeper // function call. result = (this->*func)(result); // If a new function has been pushed to the stack and placed us in the // waiting state, we need to break the chain and wait for the callback // again. if (LoadWaitingForRedirectCallback()) break; } if (NS_FAILED(result) && !mCanceled) { // First, cancel this channel if we are in failure state to set mStatus // and let it be propagated to pumps. Cancel(result); } if (!LoadWaitingForRedirectCallback()) { // We are not waiting for the callback. At this moment we must release // reference to the redirect target channel, otherwise we may leak. // However, if we are redirecting due to auth retries, we open the // redirected channel after OnStopRequest. In that case we should not // release the reference if (!StaticPrefs::network_auth_use_redirect_for_retries() || !mAuthRetryPending) { mRedirectChannel = nullptr; } } // We always resume the pumps here. If all functions on stack have been // called we need OnStopRequest to be triggered, and if we broke out of the // loop above (and are thus waiting for a new callback) the suspension // count must be balanced in the pumps. if (mTransactionPump) mTransactionPump->Resume(); if (mCachePump) mCachePump->Resume(); return result; } void nsHttpChannel::PushRedirectAsyncFunc(nsContinueRedirectionFunc func) { mRedirectFuncStack.AppendElement(func); } void nsHttpChannel::PopRedirectAsyncFunc(nsContinueRedirectionFunc func) { MOZ_ASSERT(func == mRedirectFuncStack.LastElement(), "Trying to pop wrong method from redirect async stack!"); mRedirectFuncStack.RemoveLastElement(); } //----------------------------------------------------------------------------- // nsIDNSListener functions //----------------------------------------------------------------------------- NS_IMETHODIMP nsHttpChannel::OnLookupComplete(nsICancelable* request, nsIDNSRecord* rec, nsresult status) { MOZ_ASSERT(NS_IsMainThread(), "Expecting DNS callback on main thread."); LOG( ("nsHttpChannel::OnLookupComplete [this=%p] prefetch complete%s: " "%s status[0x%" PRIx32 "]\n", this, mCaps & NS_HTTP_REFRESH_DNS ? ", refresh requested" : "", NS_SUCCEEDED(status) ? "success" : "failure", static_cast(status))); // Unset DNS cache refresh if it was requested, if (mCaps & NS_HTTP_REFRESH_DNS) { mCaps &= ~NS_HTTP_REFRESH_DNS; if (mTransaction) { mTransaction->SetDNSWasRefreshed(); } } if (!mDNSBlockingPromise.IsEmpty()) { if (NS_SUCCEEDED(status)) { nsCOMPtr record(rec); mDNSBlockingPromise.Resolve(record, __func__); } else { mDNSBlockingPromise.Reject(status, __func__); } } return NS_OK; } void nsHttpChannel::OnHTTPSRRAvailable(nsIDNSHTTPSSVCRecord* aRecord) { MOZ_ASSERT(NS_IsMainThread(), "Expecting DNS callback on main thread."); LOG(("nsHttpChannel::OnHTTPSRRAvailable [this=%p, aRecord=%p]\n", this, aRecord)); if (mHTTPSSVCRecord) { MOZ_ASSERT(false, "OnHTTPSRRAvailable called twice!"); return; } nsCOMPtr record = aRecord; mHTTPSSVCRecord.emplace(std::move(record)); const nsCOMPtr& httprr = mHTTPSSVCRecord.ref(); if (LoadWaitHTTPSSVCRecord()) { MOZ_ASSERT(mURI->SchemeIs("http")); StoreWaitHTTPSSVCRecord(false); nsresult rv = ContinueOnBeforeConnect(!!httprr, mStatus, !!httprr); if (NS_FAILED(rv)) { CloseCacheEntry(false); Unused << AsyncAbort(rv); } } else { // This channel is not canceled and the transaction is not created. if (httprr && NS_SUCCEEDED(mStatus) && !mTransaction && (mFirstResponseSource != RESPONSE_FROM_CACHE)) { bool hasIPAddress = false; Unused << httprr->GetHasIPAddresses(&hasIPAddress); Telemetry::Accumulate(Telemetry::DNS_HTTPSSVC_RECORD_RECEIVING_STAGE, hasIPAddress ? HTTPSSVC_WITH_IPHINT_RECEIVED_STAGE_0 : HTTPSSVC_WITHOUT_IPHINT_RECEIVED_STAGE_0); StoreHTTPSSVCTelemetryReported(true); } } } //----------------------------------------------------------------------------- // nsHttpChannel internal functions //----------------------------------------------------------------------------- // Creates an URI to the given location using current URI for base and charset nsresult nsHttpChannel::CreateNewURI(const char* loc, nsIURI** newURI) { nsCOMPtr ioService; nsresult rv = gHttpHandler->GetIOService(getter_AddRefs(ioService)); if (NS_FAILED(rv)) return rv; return ioService->NewURI(nsDependentCString(loc), nullptr, mURI, newURI); } void nsHttpChannel::MaybeInvalidateCacheEntryForSubsequentGet() { // See RFC 2616 section 5.1.1. These are considered valid // methods which DO NOT invalidate cache-entries for the // referred resource. POST, PUT and DELETE as well as any // other method not listed here will potentially invalidate // any cached copy of the resource if (mRequestHead.IsGet() || mRequestHead.IsOptions() || mRequestHead.IsHead() || mRequestHead.IsTrace() || mRequestHead.IsConnect()) { return; } // Invalidate the request-uri. if (LOG_ENABLED()) { nsAutoCString key; mURI->GetAsciiSpec(key); LOG(("MaybeInvalidateCacheEntryForSubsequentGet [this=%p uri=%s]\n", this, key.get())); } DoInvalidateCacheEntry(mURI); // Invalidate Location-header if set nsAutoCString location; Unused << mResponseHead->GetHeader(nsHttp::Location, location); if (!location.IsEmpty()) { LOG((" Location-header=%s\n", location.get())); InvalidateCacheEntryForLocation(location.get()); } // Invalidate Content-Location-header if set Unused << mResponseHead->GetHeader(nsHttp::Content_Location, location); if (!location.IsEmpty()) { LOG((" Content-Location-header=%s\n", location.get())); InvalidateCacheEntryForLocation(location.get()); } } void nsHttpChannel::InvalidateCacheEntryForLocation(const char* location) { nsAutoCString tmpCacheKey, tmpSpec; nsCOMPtr resultingURI; nsresult rv = CreateNewURI(location, getter_AddRefs(resultingURI)); if (NS_SUCCEEDED(rv) && HostPartIsTheSame(resultingURI)) { DoInvalidateCacheEntry(resultingURI); } else { LOG((" hosts not matching\n")); } } void nsHttpChannel::DoInvalidateCacheEntry(nsIURI* aURI) { // NOTE: // Following comments 24,32 and 33 in bug #327765, we only care about // the cache in the protocol-handler. // The logic below deviates from the original logic in OpenCacheEntry on // one point by using only READ_ONLY access-policy. I think this is safe. nsresult rv; nsAutoCString key; if (LOG_ENABLED()) { aURI->GetAsciiSpec(key); } LOG(("DoInvalidateCacheEntry [channel=%p key=%s]", this, key.get())); nsCOMPtr cacheStorageService( components::CacheStorage::Service()); rv = cacheStorageService ? NS_OK : NS_ERROR_FAILURE; nsCOMPtr cacheStorage; if (NS_SUCCEEDED(rv)) { RefPtr info = GetLoadContextInfo(this); rv = cacheStorageService->DiskCacheStorage(info, getter_AddRefs(cacheStorage)); } if (NS_SUCCEEDED(rv)) { rv = cacheStorage->AsyncDoomURI(aURI, ""_ns, nullptr); } LOG(("DoInvalidateCacheEntry [channel=%p key=%s rv=%d]", this, key.get(), int(rv))); } void nsHttpChannel::AsyncOnExamineCachedResponse() { gHttpHandler->OnExamineCachedResponse(this); } void nsHttpChannel::UpdateAggregateCallbacks() { if (!mTransaction) { return; } nsCOMPtr callbacks; NS_NewNotificationCallbacksAggregation(mCallbacks, mLoadGroup, GetCurrentSerialEventTarget(), getter_AddRefs(callbacks)); mTransaction->SetSecurityCallbacks(callbacks); } NS_IMETHODIMP nsHttpChannel::SetLoadGroup(nsILoadGroup* aLoadGroup) { MOZ_ASSERT(NS_IsMainThread(), "Wrong thread."); nsresult rv = HttpBaseChannel::SetLoadGroup(aLoadGroup); if (NS_SUCCEEDED(rv)) { UpdateAggregateCallbacks(); } return rv; } NS_IMETHODIMP nsHttpChannel::SetNotificationCallbacks(nsIInterfaceRequestor* aCallbacks) { MOZ_ASSERT(NS_IsMainThread(), "Wrong thread."); nsresult rv = HttpBaseChannel::SetNotificationCallbacks(aCallbacks); if (NS_SUCCEEDED(rv)) { UpdateAggregateCallbacks(); } return rv; } bool nsHttpChannel::AwaitingCacheCallbacks() { return LoadWaitForCacheEntry() != 0; } void nsHttpChannel::SetPushedStreamTransactionAndId( HttpTransactionShell* aTransWithPushedStream, uint32_t aPushedStreamId) { MOZ_ASSERT(!mTransWithPushedStream); LOG(("nsHttpChannel::SetPushedStreamTransaction [this=%p] trans=%p", this, aTransWithPushedStream)); mTransWithPushedStream = aTransWithPushedStream; mPushedStreamId = aPushedStreamId; } nsresult nsHttpChannel::OnPush(uint32_t aPushedStreamId, const nsACString& aUrl, const nsACString& aRequestString, HttpTransactionShell* aTransaction) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aTransaction); LOG(("nsHttpChannel::OnPush [this=%p, trans=%p]\n", this, aTransaction)); MOZ_ASSERT(mCaps & NS_HTTP_ONPUSH_LISTENER); nsCOMPtr pushListener; NS_QueryNotificationCallbacks(mCallbacks, mLoadGroup, NS_GET_IID(nsIHttpPushListener), getter_AddRefs(pushListener)); if (!pushListener) { LOG( ("nsHttpChannel::OnPush [this=%p] notification callbacks do not " "implement nsIHttpPushListener\n", this)); return NS_ERROR_NOT_AVAILABLE; } nsCOMPtr pushResource; nsresult rv; // Create a Channel for the Push Resource rv = NS_NewURI(getter_AddRefs(pushResource), aUrl); if (NS_FAILED(rv)) { return NS_ERROR_FAILURE; } nsCOMPtr ioService; rv = gHttpHandler->GetIOService(getter_AddRefs(ioService)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr pushChannel; rv = NS_NewChannelInternal(getter_AddRefs(pushChannel), pushResource, mLoadInfo, nullptr, // PerformanceStorage nullptr, // aLoadGroup nullptr, // aCallbacks nsIRequest::LOAD_NORMAL, ioService); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr pushHttpChannel = do_QueryInterface(pushChannel); MOZ_ASSERT(pushHttpChannel); if (!pushHttpChannel) { return NS_ERROR_UNEXPECTED; } RefPtr channel; CallQueryInterface(pushHttpChannel, channel.StartAssignment()); MOZ_ASSERT(channel); if (!channel) { return NS_ERROR_UNEXPECTED; } // new channel needs mrqeuesthead and headers from pushedStream channel->mRequestHead.ParseHeaderSet(aRequestString.BeginReading()); channel->mLoadGroup = mLoadGroup; channel->mLoadInfo = mLoadInfo; channel->mCallbacks = mCallbacks; // Link the trans with pushed stream and the new channel and call listener channel->SetPushedStreamTransactionAndId(aTransaction, aPushedStreamId); rv = pushListener->OnPush(this, pushHttpChannel); return rv; } // static bool nsHttpChannel::IsRedirectStatus(uint32_t status) { // 305 disabled as a security measure (see bug 187996). return status == 301 || status == 302 || status == 303 || status == 307 || status == 308; } void nsHttpChannel::SetCouldBeSynthesized() { MOZ_ASSERT(!BypassServiceWorker()); StoreResponseCouldBeSynthesized(true); } NS_IMETHODIMP nsHttpChannel::OnPreflightSucceeded() { MOZ_ASSERT(LoadRequireCORSPreflight(), "Why did a preflight happen?"); StoreIsCorsPreflightDone(1); mPreflightChannel = nullptr; return ContinueConnect(); } NS_IMETHODIMP nsHttpChannel::OnPreflightFailed(nsresult aError) { MOZ_ASSERT(LoadRequireCORSPreflight(), "Why did a preflight happen?"); StoreIsCorsPreflightDone(1); mPreflightChannel = nullptr; CloseCacheEntry(false); Unused << AsyncAbort(aError); return NS_OK; } nsresult nsHttpChannel::CallOrWaitForResume( const std::function& aFunc) { if (mCanceled) { MOZ_ASSERT(NS_FAILED(mStatus)); return mStatus; } if (mSuspendCount) { LOG(("Waiting until resume [this=%p]\n", this)); MOZ_ASSERT(!mCallOnResume); mCallOnResume = aFunc; return NS_OK; } return aFunc(this); } // This is loosely based on: // https://fetch.spec.whatwg.org/#serializing-a-request-origin static bool HasNullRequestOrigin(nsHttpChannel* aChannel, nsIURI* aURI, bool isAddonRequest) { // Step 1. If request has a redirect-tainted origin, then return "null". if (aChannel->HasRedirectTaintedOrigin()) { if (StaticPrefs::network_http_origin_redirectTainted()) { return true; } } // Non-standard: Only allow HTTP and HTTPS origins. if (!ReferrerInfo::IsReferrerSchemeAllowed(aURI)) { // And moz-extension: for add-on initiated requests. if (!aURI->SchemeIs("moz-extension") || !isAddonRequest) { return true; } } // Non-standard: Hide onion URLs. if (StaticPrefs::network_http_referer_hideOnionSource()) { nsAutoCString host; if (NS_SUCCEEDED(aURI->GetAsciiHost(host)) && StringEndsWith(host, ".onion"_ns)) { return ReferrerInfo::IsCrossOriginRequest(aChannel); } } // Step 2. Return request’s origin, serialized. return false; } // Step 8.12. of HTTP-network-or-cache fetch // // https://fetch.spec.whatwg.org/#append-a-request-origin-header void nsHttpChannel::SetOriginHeader() { auto* triggeringPrincipal = BasePrincipal::Cast(mLoadInfo->TriggeringPrincipal()); if (triggeringPrincipal->IsSystemPrincipal()) { // We can't infer an Origin header from the system principal, // this means system requests use whatever Origin header was specified. return; } bool isAddonRequest = triggeringPrincipal->AddonPolicy() || triggeringPrincipal->ContentScriptAddonPolicy(); // Non-standard: Handle already existing Origin header. nsAutoCString existingHeader; Unused << mRequestHead.GetHeader(nsHttp::Origin, existingHeader); if (!existingHeader.IsEmpty()) { LOG(("nsHttpChannel::SetOriginHeader Origin header already present")); auto const shouldNullifyOriginHeader = [&existingHeader, isAddonRequest](nsHttpChannel* aChannel) { nsCOMPtr uri; nsresult rv = NS_NewURI(getter_AddRefs(uri), existingHeader); if (NS_FAILED(rv)) { return false; } if (HasNullRequestOrigin(aChannel, uri, isAddonRequest)) { return true; } nsCOMPtr info = aChannel->LoadInfo(); if (info->GetTainting() == mozilla::LoadTainting::CORS) { return false; } return ReferrerInfo::ShouldSetNullOriginHeader(aChannel, uri); }; if (!existingHeader.EqualsLiteral("null") && shouldNullifyOriginHeader(this)) { LOG(("nsHttpChannel::SetOriginHeader null Origin by Referrer-Policy")); MOZ_ALWAYS_SUCCEEDS( mRequestHead.SetHeader(nsHttp::Origin, "null"_ns, false /* merge */)); } return; } if (StaticPrefs::network_http_sendOriginHeader() == 0) { // Custom user setting: 0 means never send Origin header. return; } // Step 1. Let serializedOrigin be the result of byte-serializing a request // origin with request. nsAutoCString serializedOrigin; nsCOMPtr uri; { if (NS_FAILED(triggeringPrincipal->GetURI(getter_AddRefs(uri)))) { return; } if (!uri) { if (isAddonRequest) { // For add-on compatibility prefer sending no header at all // instead of `Origin: null`. return; } // Otherwise use "null" when the triggeringPrincipal's URI is nullptr. serializedOrigin.AssignLiteral("null"); } else if (HasNullRequestOrigin(this, uri, isAddonRequest)) { serializedOrigin.AssignLiteral("null"); } else { nsContentUtils::GetWebExposedOriginSerialization(uri, serializedOrigin); } } // Step 2. If request’s response tainting is "cors" or request’s mode is // "websocket", then append (`Origin`, serializedOrigin) to request’s header // list. // // Note: We don't handle "websocket" here (yet?). if (mLoadInfo->GetTainting() == mozilla::LoadTainting::CORS) { MOZ_ALWAYS_SUCCEEDS(mRequestHead.SetHeader(nsHttp::Origin, serializedOrigin, false /* merge */)); return; } // Step 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then: if (mRequestHead.IsGet() || mRequestHead.IsHead()) { return; } if (!serializedOrigin.EqualsLiteral("null")) { // Step 3.1. (Implemented by ReferrerInfo::ShouldSetNullOriginHeader) if (ReferrerInfo::ShouldSetNullOriginHeader(this, uri)) { serializedOrigin.AssignLiteral("null"); } else if (StaticPrefs::network_http_sendOriginHeader() == 1) { // Non-standard: Restrict Origin to same-origin loads if requested by user nsAutoCString currentOrigin; nsContentUtils::GetWebExposedOriginSerialization(mURI, currentOrigin); if (!serializedOrigin.EqualsIgnoreCase(currentOrigin.get())) { // Origin header suppressed by user setting. serializedOrigin.AssignLiteral("null"); } } } // Step 3.2. Append (`Origin`, serializedOrigin) to request’s header list. MOZ_ALWAYS_SUCCEEDS(mRequestHead.SetHeader(nsHttp::Origin, serializedOrigin, false /* merge */)); } void nsHttpChannel::SetDoNotTrack() { /** * 'DoNotTrack' header should be added if 'privacy.donottrackheader.enabled' * is true or tracking protection is enabled. See bug 1258033. */ nsCOMPtr loadContext; NS_QueryNotificationCallbacks(this, loadContext); if ((loadContext && loadContext->UseTrackingProtection()) || StaticPrefs::privacy_donottrackheader_enabled()) { DebugOnly rv = mRequestHead.SetHeader(nsHttp::DoNotTrack, "1"_ns, false); MOZ_ASSERT(NS_SUCCEEDED(rv)); } } void nsHttpChannel::SetGlobalPrivacyControl() { MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread"); if (StaticPrefs::privacy_globalprivacycontrol_functionality_enabled() && (StaticPrefs::privacy_globalprivacycontrol_enabled() || (StaticPrefs::privacy_globalprivacycontrol_pbmode_enabled() && NS_UsePrivateBrowsing(this)))) { // Send the header with a value of 1 to indicate opting-out DebugOnly rv = mRequestHead.SetHeader(nsHttp::GlobalPrivacyControl, "1"_ns, false); } } void nsHttpChannel::ReportRcwnStats(bool isFromNet) { if (!StaticPrefs::network_http_rcwn_enabled()) { return; } if (isFromNet) { if (mRaceCacheWithNetwork) { gIOService->IncrementNetWonRequestNumber(); Telemetry::Accumulate( Telemetry::NETWORK_RACE_CACHE_BANDWIDTH_RACE_NETWORK_WIN, mTransferSize); if (mRaceDelay) { AccumulateCategorical( Telemetry::LABELS_NETWORK_RACE_CACHE_WITH_NETWORK_USAGE_2:: NetworkDelayedRace); } else { AccumulateCategorical( Telemetry::LABELS_NETWORK_RACE_CACHE_WITH_NETWORK_USAGE_2:: NetworkRace); } } else { Telemetry::Accumulate(Telemetry::NETWORK_RACE_CACHE_BANDWIDTH_NOT_RACE, mTransferSize); AccumulateCategorical( Telemetry::LABELS_NETWORK_RACE_CACHE_WITH_NETWORK_USAGE_2:: NetworkNoRace); } } else { if (mRaceCacheWithNetwork || mRaceDelay) { gIOService->IncrementCacheWonRequestNumber(); Telemetry::Accumulate( Telemetry::NETWORK_RACE_CACHE_BANDWIDTH_RACE_CACHE_WIN, mTransferSize); if (mRaceDelay) { AccumulateCategorical( Telemetry::LABELS_NETWORK_RACE_CACHE_WITH_NETWORK_USAGE_2:: CacheDelayedRace); } else { AccumulateCategorical( Telemetry::LABELS_NETWORK_RACE_CACHE_WITH_NETWORK_USAGE_2:: CacheRace); } } else { Telemetry::Accumulate(Telemetry::NETWORK_RACE_CACHE_BANDWIDTH_NOT_RACE, mTransferSize); AccumulateCategorical( Telemetry::LABELS_NETWORK_RACE_CACHE_WITH_NETWORK_USAGE_2:: CacheNoRace); } } gIOService->IncrementRequestNumber(); } static const size_t kPositiveBucketNumbers = 34; static const int64_t kPositiveBucketLevels[kPositiveBucketNumbers] = { 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000, 20000, 30000, 40000, 50000, 60000}; /** * For space efficiency, we collect finer resolution for small difference * between net and cache time, coarser for larger. * Bucket #40 for a tie. * #41 to #50 indicates cache wins by 1ms to 100ms, split equally. * #51 to #59 indicates cache wins by 101ms to 1000ms. * #60 to #68 indicates cache wins by 1s to 10s. * #69 to #73 indicates cache wins by 11s to 60s. * #74 indicates cache wins by more than 1 minute. * * #39 to #30 indicates network wins by 1ms to 100ms, split equally. * #29 to #21 indicates network wins by 101ms to 1000ms. * #20 to #12 indicates network wins by 1s to 10s. * #11 to #7 indicates network wins by 11s to 60s. * #6 indicates network wins by more than 1 minute. * * Other bucket numbers are reserved. */ inline int64_t nsHttpChannel::ComputeTelemetryBucketNumber( int64_t difftime_ms) { int64_t absBucketIndex = std::lower_bound(kPositiveBucketLevels, kPositiveBucketLevels + kPositiveBucketNumbers, static_cast(mozilla::Abs(difftime_ms))) - kPositiveBucketLevels; return difftime_ms >= 0 ? 40 + absBucketIndex : 40 - absBucketIndex; } void nsHttpChannel::ReportNetVSCacheTelemetry() { nsresult rv; if (!mCacheEntry) { return; } // We only report telemetry if the entry is persistent (on disk) bool persistent; rv = mCacheEntry->GetPersistent(&persistent); if (NS_FAILED(rv) || !persistent) { return; } uint64_t onStartNetTime = 0; if (NS_FAILED(mCacheEntry->GetOnStartTime(&onStartNetTime))) { return; } uint64_t onStopNetTime = 0; if (NS_FAILED(mCacheEntry->GetOnStopTime(&onStopNetTime))) { return; } uint64_t onStartCacheTime = (mOnStartRequestTimestamp - mAsyncOpenTime).ToMilliseconds(); int64_t onStartDiff = onStartNetTime - onStartCacheTime; onStartDiff = ComputeTelemetryBucketNumber(onStartDiff); uint64_t onStopCacheTime = (mCacheReadEnd - mAsyncOpenTime).ToMilliseconds(); int64_t onStopDiff = onStopNetTime - onStopCacheTime; onStopDiff = ComputeTelemetryBucketNumber(onStopDiff); if (mDidReval) { Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTART_REVALIDATED_V2, onStartDiff); Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_REVALIDATED_V2, onStopDiff); } else { Telemetry::Accumulate( Telemetry::HTTP_NET_VS_CACHE_ONSTART_NOTREVALIDATED_V2, onStartDiff); Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_NOTREVALIDATED_V2, onStopDiff); } if (mDidReval) { // We don't report revalidated probes as the data would be skewed. return; } if (mCacheOpenWithPriority) { if (mCacheQueueSizeWhenOpen < 5) { Telemetry::Accumulate( Telemetry::HTTP_NET_VS_CACHE_ONSTART_QSMALL_HIGHPRI_V2, onStartDiff); Telemetry::Accumulate( Telemetry::HTTP_NET_VS_CACHE_ONSTOP_QSMALL_HIGHPRI_V2, onStopDiff); } else if (mCacheQueueSizeWhenOpen < 10) { Telemetry::Accumulate( Telemetry::HTTP_NET_VS_CACHE_ONSTART_QMED_HIGHPRI_V2, onStartDiff); Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_QMED_HIGHPRI_V2, onStopDiff); } else { Telemetry::Accumulate( Telemetry::HTTP_NET_VS_CACHE_ONSTART_QBIG_HIGHPRI_V2, onStartDiff); Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_QBIG_HIGHPRI_V2, onStopDiff); } } else { // The limits are higher for normal priority cache queues if (mCacheQueueSizeWhenOpen < 10) { Telemetry::Accumulate( Telemetry::HTTP_NET_VS_CACHE_ONSTART_QSMALL_NORMALPRI_V2, onStartDiff); Telemetry::Accumulate( Telemetry::HTTP_NET_VS_CACHE_ONSTOP_QSMALL_NORMALPRI_V2, onStopDiff); } else if (mCacheQueueSizeWhenOpen < 50) { Telemetry::Accumulate( Telemetry::HTTP_NET_VS_CACHE_ONSTART_QMED_NORMALPRI_V2, onStartDiff); Telemetry::Accumulate( Telemetry::HTTP_NET_VS_CACHE_ONSTOP_QMED_NORMALPRI_V2, onStopDiff); } else { Telemetry::Accumulate( Telemetry::HTTP_NET_VS_CACHE_ONSTART_QBIG_NORMALPRI_V2, onStartDiff); Telemetry::Accumulate( Telemetry::HTTP_NET_VS_CACHE_ONSTOP_QBIG_NORMALPRI_V2, onStopDiff); } } uint32_t diskStorageSizeK = 0; rv = mCacheEntry->GetDiskStorageSizeInKB(&diskStorageSizeK); if (NS_FAILED(rv)) { return; } // No significant difference was observed between different sizes for // |onStartDiff| if (diskStorageSizeK < 256) { Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_SMALL_V2, onStopDiff); } else { Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_LARGE_V2, onStopDiff); } } NS_IMETHODIMP nsHttpChannel::Test_delayCacheEntryOpeningBy(int32_t aTimeout) { LOG(("nsHttpChannel::Test_delayCacheEntryOpeningBy this=%p timeout=%d", this, aTimeout)); MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread"); mRaceCacheWithNetwork = true; mCacheOpenDelay = aTimeout; if (mCacheOpenTimer) { mCacheOpenTimer->SetDelay(aTimeout); } return NS_OK; } NS_IMETHODIMP nsHttpChannel::Test_triggerDelayedOpenCacheEntry() { LOG(("nsHttpChannel::Test_triggerDelayedOpenCacheEntry this=%p", this)); MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread"); nsresult rv; if (!mCacheOpenDelay) { // No delay was set. return NS_ERROR_NOT_AVAILABLE; } if (!mCacheOpenFunc) { // There should be a runnable. return NS_ERROR_FAILURE; } if (mCacheOpenTimer) { rv = mCacheOpenTimer->Cancel(); if (NS_FAILED(rv)) { return rv; } mCacheOpenTimer = nullptr; } mCacheOpenDelay = 0; // Avoid re-entrancy issues by nulling our mCacheOpenFunc before calling it. std::function cacheOpenFunc = nullptr; std::swap(cacheOpenFunc, mCacheOpenFunc); cacheOpenFunc(this); return NS_OK; } nsresult nsHttpChannel::TriggerNetworkWithDelay(uint32_t aDelay) { MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread"); LOG(("nsHttpChannel::TriggerNetworkWithDelay [this=%p, delay=%u]\n", this, aDelay)); if (mCanceled) { LOG((" channel was canceled.\n")); return mStatus; } // If a network request has already gone out, there is no point in // doing this again. if (mNetworkTriggered) { LOG((" network already triggered. Returning.\n")); return NS_OK; } if (mNetworkTriggerDelay) { aDelay = mNetworkTriggerDelay; } if (!aDelay) { // We cannot call TriggerNetwork() directly here, because it would // cause performance regression in tp6 tests, see bug 1398847. return NS_DispatchToMainThread( NewRunnableMethod("net::nsHttpChannel::TriggerNetworkWithDelay", this, &nsHttpChannel::TriggerNetwork), NS_DISPATCH_NORMAL); } MOZ_ASSERT(!mNetworkTriggerTimer); mNetworkTriggerTimer = NS_NewTimer(); auto callback = MakeRefPtr(this); LOG(("Creating new networkTriggertimer for delay")); mNetworkTriggerTimer->InitWithCallback(callback, aDelay, nsITimer::TYPE_ONE_SHOT); return NS_OK; } nsresult nsHttpChannel::TriggerNetwork() { MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread"); LOG(("nsHttpChannel::TriggerNetwork [this=%p]\n", this)); if (mCanceled) { LOG((" channel was canceled.\n")); return mStatus; } // If a network request has already gone out, there is no point in // doing this again. if (mNetworkTriggered) { LOG((" network already triggered. Returning.\n")); return NS_OK; } mNetworkTriggered = true; if (mNetworkTriggerTimer) { mNetworkTriggerTimer->Cancel(); mNetworkTriggerTimer = nullptr; } // If we are waiting for a proxy request, that means we can't trigger // the next step just yet. We need for mConnectionInfo to be non-null // before we call ContinueConnect. OnProxyAvailable will trigger // BeginConnect, and Connect will call ContinueConnect even if it's // for the cache callbacks. if (mProxyRequest) { LOG((" proxy request in progress. Delaying network trigger.\n")); mWaitingForProxy = true; return NS_OK; } // If |mCacheOpenFunc| is assigned, we're delaying opening the entry to // simulate racing. Although cache entry opening hasn't started yet, we're // actually racing, so we must set mRaceCacheWithNetwork to true now. mRaceCacheWithNetwork = AwaitingCacheCallbacks() && (mCacheOpenFunc || StaticPrefs::network_http_rcwn_enabled()); LOG((" triggering network rcwn=%d\n", bool(mRaceCacheWithNetwork))); return ContinueConnect(); } void nsHttpChannel::MaybeRaceCacheWithNetwork() { nsresult rv; nsCOMPtr netLinkSvc = do_GetService(NS_NETWORK_LINK_SERVICE_CONTRACTID, &rv); if (NS_FAILED(rv)) { return; } uint32_t linkType; rv = netLinkSvc->GetLinkType(&linkType); if (NS_FAILED(rv)) { return; } if (!(linkType == nsINetworkLinkService::LINK_TYPE_ETHERNET || #ifndef MOZ_WIDGET_ANDROID // On Android we don't assume an unknown link type is unmetered linkType == nsINetworkLinkService::LINK_TYPE_UNKNOWN || #endif linkType == nsINetworkLinkService::LINK_TYPE_USB || linkType == nsINetworkLinkService::LINK_TYPE_WIFI)) { return; } // Don't trigger the network if the load flags say so. if (mLoadFlags & (LOAD_ONLY_FROM_CACHE | LOAD_NO_NETWORK_IO)) { return; } // We must not race if the channel has a failure status code. if (NS_FAILED(mStatus)) { return; } // If a CORS Preflight is required we must not race. if (LoadRequireCORSPreflight() && !LoadIsCorsPreflightDone()) { return; } if (CacheFileUtils::CachePerfStats::IsCacheSlow()) { // If the cache is slow, trigger the network request immediately. mRaceDelay = 0; } else { // Give cache a headstart of 3 times the average cache entry open time. mRaceDelay = CacheFileUtils::CachePerfStats::GetAverage( CacheFileUtils::CachePerfStats::ENTRY_OPEN, true) * 3; // We use microseconds in CachePerfStats but we need milliseconds // for TriggerNetwork. mRaceDelay /= 1000; } mRaceDelay = clamped( mRaceDelay, StaticPrefs::network_http_rcwn_min_wait_before_racing_ms(), StaticPrefs::network_http_rcwn_max_wait_before_racing_ms()); MOZ_ASSERT(StaticPrefs::network_http_rcwn_enabled() || mNetworkTriggerDelay, "The pref must be turned on."); LOG(("nsHttpChannel::MaybeRaceCacheWithNetwork [this=%p, delay=%u]\n", this, mRaceDelay)); TriggerNetworkWithDelay(mRaceDelay); } NS_IMETHODIMP nsHttpChannel::Test_triggerNetwork(int32_t aTimeout) { LOG(("nsHttpChannel::Test_triggerNetwork this=%p timeout=%d", this, aTimeout)); MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread"); // We set the trigger delay to the specified timeout. mRaceCacheWithNetwork = true; mNetworkTriggerDelay = aTimeout; // If we already have a timer, set the delay/ if (mNetworkTriggerTimer) { // If the timeout is 0 and there is a timer, we can trigger // the network immediately. MOZ_ASSERT(LoadWasOpened(), "Must have been opened before"); if (!aTimeout) { return TriggerNetwork(); } mNetworkTriggerTimer->SetDelay(aTimeout); } return NS_OK; } nsHttpChannel::TimerCallback::TimerCallback(nsHttpChannel* aChannel) : mChannel(aChannel) {} NS_IMPL_ISUPPORTS(nsHttpChannel::TimerCallback, nsITimerCallback, nsINamed) NS_IMETHODIMP nsHttpChannel::TimerCallback::GetName(nsACString& aName) { aName.AssignLiteral("nsHttpChannel"); return NS_OK; } NS_IMETHODIMP nsHttpChannel::TimerCallback::Notify(nsITimer* aTimer) { if (aTimer == mChannel->mCacheOpenTimer) { return mChannel->Test_triggerDelayedOpenCacheEntry(); } if (aTimer == mChannel->mNetworkTriggerTimer) { return mChannel->TriggerNetwork(); } MOZ_CRASH("Unknown timer"); return NS_OK; } bool nsHttpChannel::EligibleForTailing() { if (!(mClassOfService.Flags() & nsIClassOfService::Tail)) { return false; } if (mClassOfService.Flags() & (nsIClassOfService::UrgentStart | nsIClassOfService::Leader | nsIClassOfService::TailForbidden)) { return false; } if (mClassOfService.Flags() & nsIClassOfService::Unblocked && !(mClassOfService.Flags() & nsIClassOfService::TailAllowed)) { return false; } if (IsNavigation()) { return false; } return true; } bool nsHttpChannel::WaitingForTailUnblock() { nsresult rv; if (!gHttpHandler->IsTailBlockingEnabled()) { LOG(("nsHttpChannel %p tail-blocking disabled", this)); return false; } if (!EligibleForTailing()) { LOG(("nsHttpChannel %p not eligible for tail-blocking", this)); AddAsNonTailRequest(); return false; } if (!EnsureRequestContext()) { LOG(("nsHttpChannel %p no request context", this)); return false; } LOG(("nsHttpChannel::WaitingForTailUnblock this=%p, rc=%p", this, mRequestContext.get())); bool blocked; rv = mRequestContext->IsContextTailBlocked(this, &blocked); if (NS_FAILED(rv)) { return false; } LOG((" blocked=%d", blocked)); return blocked; } //----------------------------------------------------------------------------- // nsHttpChannel::nsIRequestTailUnblockCallback //----------------------------------------------------------------------------- // Must be implemented in the leaf class because we don't have // AsyncAbort in HttpBaseChannel. NS_IMETHODIMP nsHttpChannel::OnTailUnblock(nsresult rv) { LOG(("nsHttpChannel::OnTailUnblock this=%p rv=%" PRIx32 " rc=%p", this, static_cast(rv), mRequestContext.get())); MOZ_RELEASE_ASSERT(mOnTailUnblock); if (NS_FAILED(mStatus)) { rv = mStatus; } if (NS_SUCCEEDED(rv)) { auto callback = mOnTailUnblock; mOnTailUnblock = nullptr; rv = (this->*callback)(); } if (NS_FAILED(rv)) { CloseCacheEntry(false); return AsyncAbort(rv); } return NS_OK; } void nsHttpChannel::SetWarningReporter( HttpChannelSecurityWarningReporter* aReporter) { LOG(("nsHttpChannel [this=%p] SetWarningReporter [%p]", this, aReporter)); mWarningReporter = aReporter; } HttpChannelSecurityWarningReporter* nsHttpChannel::GetWarningReporter() { LOG(("nsHttpChannel [this=%p] GetWarningReporter [%p]", this, mWarningReporter.get())); return mWarningReporter.get(); } // The specification for ORB is currently being written: // https://whatpr.org/fetch/1442.html#orb-algorithm // The `opaque-response-safelist check` is implemented in: // * `HttpBaseChannel::PerformOpaqueResponseSafelistCheckBeforeSniff` // * `nsHttpChannel::DisableIsOpaqueResponseAllowedAfterSniffCheck` // * `HttpBaseChannel::PerformOpaqueResponseSafelistCheckAfterSniff` // * `OpaqueResponseBlocker::ValidateJavaScript` // // Should only be called by nsMediaSniffer::GetMIMETypeFromContent and // imageLoader::GetMIMETypeFromContent when the content type can be // recognized by these sniffers. void nsHttpChannel::DisableIsOpaqueResponseAllowedAfterSniffCheck( SnifferType aType) { // https://whatpr.org/fetch/1442.html#orb-algorithm // This method covers steps, 8 and 10. MOZ_ASSERT(XRE_IsParentProcess()); if (NeedOpaqueResponseAllowedCheckAfterSniff()) { MOZ_ASSERT(mCachedOpaqueResponseBlockingPref); // If the sniffer type is media and the request comes from a media element, // we would like to check: // - Whether the information provided by the media element shows it's an // initial request. // - Whether the response's status is either 200 or 206. // // If any of the results is false, then we set // mBlockOpaqueResponseAfterSniff to true and block the response later. if (aType == SnifferType::Media) { // Step 8 MOZ_ASSERT(mLoadInfo); bool isMediaRequest; mLoadInfo->GetIsMediaRequest(&isMediaRequest); if (isMediaRequest) { bool isInitialRequest; mLoadInfo->GetIsMediaInitialRequest(&isInitialRequest); MOZ_ASSERT(isInitialRequest); if (!isInitialRequest) { // Step 8.1 BlockOpaqueResponseAfterSniff( u"media request after sniffing, but not initial request"_ns, OpaqueResponseBlockedTelemetryReason::MEDIA_NOT_INITIAL); return; } if (mResponseHead->Status() != 200 && mResponseHead->Status() != 206) { // Step 8.2 BlockOpaqueResponseAfterSniff( u"media request's response status is neither 200 nor 206"_ns, OpaqueResponseBlockedTelemetryReason::MEDIA_INCORRECT_RESP); return; } } } // Step 8.3 if `aType == SnifferType::Media` // Step 9 can be skipped, only `HTMLMediaElement` ever sets isMediaRequest. // Step 10 if `aType == SnifferType::Image` AllowOpaqueResponseAfterSniff(); } } namespace { class CopyNonDefaultHeaderVisitor final : public nsIHttpHeaderVisitor { nsCOMPtr mTarget; ~CopyNonDefaultHeaderVisitor() = default; NS_IMETHOD VisitHeader(const nsACString& aHeader, const nsACString& aValue) override { if (aValue.IsEmpty()) { return mTarget->SetEmptyRequestHeader(aHeader); } return mTarget->SetRequestHeader(aHeader, aValue, false /* merge */); } public: explicit CopyNonDefaultHeaderVisitor(nsIHttpChannel* aTarget) : mTarget(aTarget) { MOZ_DIAGNOSTIC_ASSERT(mTarget); } NS_DECL_ISUPPORTS }; NS_IMPL_ISUPPORTS(CopyNonDefaultHeaderVisitor, nsIHttpHeaderVisitor) } // anonymous namespace nsresult nsHttpChannel::RedirectToInterceptedChannel() { nsCOMPtr controller; GetCallback(controller); RefPtr intercepted = InterceptedHttpChannel::CreateForInterception( mChannelCreationTime, mChannelCreationTimestamp, mAsyncOpenTime); ExtContentPolicyType type = mLoadInfo->GetExternalContentPolicyType(); nsCOMPtr redirectLoadInfo = CloneLoadInfoForRedirect(mURI, nsIChannelEventSink::REDIRECT_INTERNAL); nsresult rv = intercepted->Init( mURI, mCaps, static_cast(mProxyInfo.get()), mProxyResolveFlags, mProxyURI, mChannelId, type, redirectLoadInfo); rv = SetupReplacementChannel(mURI, intercepted, true, nsIChannelEventSink::REDIRECT_INTERNAL); NS_ENSURE_SUCCESS(rv, rv); // Some APIs, like fetch(), allow content to set non-standard headers. // Normally these APIs are responsible for copying these headers across // redirects. In the e10s parent-side intercept case, though, we currently // "hide" the internal redirect to the InterceptedHttpChannel. So the // fetch() API does not have the opportunity to move headers over. // Therefore, we do it automatically here. // // Once child-side interception is removed and the internal redirect no // longer needs to be "hidden", then this header copying code can be // removed. nsCOMPtr visitor = new CopyNonDefaultHeaderVisitor(intercepted); rv = VisitNonDefaultRequestHeaders(visitor); NS_ENSURE_SUCCESS(rv, rv); mRedirectChannel = intercepted; PushRedirectAsyncFunc(&nsHttpChannel::ContinueAsyncRedirectChannelToURI); rv = gHttpHandler->AsyncOnChannelRedirect( this, intercepted, nsIChannelEventSink::REDIRECT_INTERNAL); if (NS_SUCCEEDED(rv)) { rv = WaitForRedirectCallback(); } if (NS_FAILED(rv)) { AutoRedirectVetoNotifier notifier(this, rv); PopRedirectAsyncFunc(&nsHttpChannel::ContinueAsyncRedirectChannelToURI); } return rv; } void nsHttpChannel::ReEvaluateReferrerAfterTrackingStatusIsKnown() { nsCOMPtr cjs; if (mLoadInfo) { Unused << mLoadInfo->GetCookieJarSettings(getter_AddRefs(cjs)); } if (!cjs) { cjs = net::CookieJarSettings::Create(mLoadInfo->GetLoadingPrincipal()); } if (cjs->GetRejectThirdPartyContexts()) { bool isPrivate = mLoadInfo->GetOriginAttributes().mPrivateBrowsingId > 0; // If our referrer has been set before, and our referrer policy is unset // (default policy) if we thought the channel wasn't a third-party // tracking channel, we may need to set our referrer with referrer policy // once again to ensure our defaults properly take effect now. if (mReferrerInfo) { ReferrerInfo* referrerInfo = static_cast(mReferrerInfo.get()); if (referrerInfo->IsPolicyOverrided() && referrerInfo->ReferrerPolicy() == ReferrerInfo::GetDefaultReferrerPolicy(nullptr, nullptr, isPrivate)) { nsCOMPtr newReferrerInfo = referrerInfo->CloneWithNewPolicy( ReferrerInfo::GetDefaultReferrerPolicy(this, mURI, isPrivate)); // The arguments passed to SetReferrerInfoInternal here should mirror // the arguments passed in // HttpChannelChild::RecvOverrideReferrerInfoDuringBeginConnect(). SetReferrerInfoInternal(newReferrerInfo, false, true, true); nsCOMPtr parentChannel; NS_QueryNotificationCallbacks(this, parentChannel); RefPtr httpParent = do_QueryObject(parentChannel); if (httpParent) { httpParent->OverrideReferrerInfoDuringBeginConnect(newReferrerInfo); } } } } } namespace { class BackgroundRevalidatingListener : public nsIStreamListener { NS_DECL_ISUPPORTS NS_DECL_NSISTREAMLISTENER NS_DECL_NSIREQUESTOBSERVER private: virtual ~BackgroundRevalidatingListener() = default; }; NS_IMETHODIMP BackgroundRevalidatingListener::OnStartRequest(nsIRequest* request) { return NS_OK; } NS_IMETHODIMP BackgroundRevalidatingListener::OnDataAvailable(nsIRequest* request, nsIInputStream* input, uint64_t offset, uint32_t count) { uint32_t bytesRead = 0; return input->ReadSegments(NS_DiscardSegment, nullptr, count, &bytesRead); } NS_IMETHODIMP BackgroundRevalidatingListener::OnStopRequest(nsIRequest* request, nsresult status) { if (NS_FAILED(status)) { return status; } nsCOMPtr channel(do_QueryInterface(request)); if (gHttpHandler) { gHttpHandler->OnBackgroundRevalidation(channel); } return NS_OK; } NS_IMPL_ISUPPORTS(BackgroundRevalidatingListener, nsIStreamListener, nsIRequestObserver) } // namespace void nsHttpChannel::PerformBackgroundCacheRevalidation() { if (!StaticPrefs::network_http_stale_while_revalidate_enabled()) { return; } // This is a channel doing a revalidation. It shouldn't do it again. if (mStaleRevalidation) { return; } LOG(("nsHttpChannel::PerformBackgroundCacheRevalidation %p", this)); Unused << NS_DispatchToMainThreadQueue( NewIdleRunnableMethod( "nsHttpChannel::PerformBackgroundCacheRevalidation", this, &nsHttpChannel::PerformBackgroundCacheRevalidationNow), EventQueuePriority::Idle); } void nsHttpChannel::PerformBackgroundCacheRevalidationNow() { LOG(("nsHttpChannel::PerformBackgroundCacheRevalidationNow %p", this)); MOZ_ASSERT(NS_IsMainThread()); nsresult rv; nsLoadFlags loadFlags = mLoadFlags | LOAD_ONLY_IF_MODIFIED | VALIDATE_ALWAYS | LOAD_BACKGROUND | LOAD_BYPASS_SERVICE_WORKER; nsCOMPtr validatingChannel; rv = NS_NewChannelInternal(getter_AddRefs(validatingChannel), mURI, mLoadInfo, nullptr /* performance storage */, mLoadGroup, mCallbacks, loadFlags); if (NS_FAILED(rv)) { LOG((" failed to created the channel, rv=0x%08x", static_cast(rv))); return; } nsCOMPtr priority(do_QueryInterface(validatingChannel)); if (priority) { priority->SetPriority(nsISupportsPriority::PRIORITY_LOWEST); } nsCOMPtr cos(do_QueryInterface(validatingChannel)); if (cos) { cos->AddClassFlags(nsIClassOfService::Tail); } RefPtr httpChan = do_QueryObject(validatingChannel); if (httpChan) { httpChan->mStaleRevalidation = true; } RefPtr listener = new BackgroundRevalidatingListener(); rv = validatingChannel->AsyncOpen(listener); if (NS_FAILED(rv)) { LOG((" failed to open the channel, rv=0x%08x", static_cast(rv))); return; } LOG((" %p is re-validating with a new channel %p", this, validatingChannel.get())); } NS_IMETHODIMP nsHttpChannel::SetEarlyHintObserver(nsIEarlyHintObserver* aObserver) { mEarlyHintObserver = aObserver; return NS_OK; } NS_IMETHODIMP nsHttpChannel::EarlyHint(const nsACString& aLinkHeader, const nsACString& aReferrerPolicy, const nsACString& aCspHeader) { LOG(("nsHttpChannel::EarlyHint.\n")); if (mEarlyHintObserver && nsContentUtils::ComputeIsSecureContext(this)) { LOG(("nsHttpChannel::EarlyHint propagated.\n")); mEarlyHintObserver->EarlyHint(aLinkHeader, aReferrerPolicy, aCspHeader); } return NS_OK; } NS_IMETHODIMP nsHttpChannel::SetWebTransportSessionEventListener( WebTransportSessionEventListener* aListener) { mWebTransportSessionEventListener = aListener; return NS_OK; } already_AddRefed nsHttpChannel::GetWebTransportSessionEventListener() { RefPtr wt = mWebTransportSessionEventListener; return wt.forget(); } } // namespace net } // namespace mozilla