Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
// HttpLog.h should generally be included first
#include "HttpLog.h"
#include <inttypes.h>
#include "mozilla/ScopeExit.h"
#include "mozilla/Sprintf.h"
#include "mozilla/ToString.h"
#include "mozilla/dom/nsCSPContext.h"
#include "mozilla/dom/NavigatorLogin.h"
#include "mozilla/glean/AntitrackingMetrics.h"
#include "mozilla/glean/NetwerkMetrics.h"
#include "mozilla/glean/NetwerkProtocolHttpMetrics.h"
#include "mozilla/net/CookieServiceParent.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 "nsICookieNotification.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 "nsQueryObject.h"
#include "nsThreadUtils.h"
#include "nsIConsoleService.h"
#include "nsINetworkErrorLogging.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/FlowMarkers.h"
#include "mozilla/Components.h"
#include "mozilla/StaticPrefs_dom.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 "CookieService.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 "nsIURIMutator.h"
#include "nsString.h"
#include "nsStringStream.h"
#include "mozilla/dom/PerformanceStorage.h"
#include "mozilla/dom/ReferrerInfo.h"
#include "mozilla/glean/DomSecurityMetrics.h"
#include "mozilla/Telemetry.h"
#include "mozilla/Services.h"
#include "nsISystemInfo.h"
#include "mozilla/Components.h"
#include "AlternateServices.h"
#include "NetworkMarker.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/net/SFVService.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/dom/WindowGlobalParent.h"
#include "mozilla/net/TRRService.h"
#include "LNAPermissionRequest.h"
#include "nsUnknownDecoder.h"
#ifdef XP_WIN
# include "HttpWinUtils.h"
#endif
#ifdef XP_MACOSX
# include "MicrosoftEntraSSOUtils.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);
enum ChannelDisposition {
kHttpCanceled = 0,
kHttpDisk = 1,
kHttpNetOK = 2,
kHttpNetEarlyFail = 3,
kHttpNetLateFail = 4,
kHttpsCanceled = 8,
kHttpsDisk = 9,
kHttpsNetOK = 10,
kHttpsNetEarlyFail = 11,
kHttpsNetLateFail = 12
};
static nsLiteralCString CacheDispositionToTelemetryLabel(
nsICacheInfoChannel::CacheDisposition hitOrMiss) {
switch (hitOrMiss) {
case nsICacheInfoChannel::kCacheUnresolved:
return "Unresolved"_ns;
case nsICacheInfoChannel::kCacheHit:
return "Hit"_ns;
case nsICacheInfoChannel::kCacheHitViaReval:
return "HitViaReval"_ns;
case nsICacheInfoChannel::kCacheMissedViaReval:
return "MissedViaReval"_ns;
case nsICacheInfoChannel::kCacheMissed:
return "Missed"_ns;
case nsICacheInfoChannel::kCacheUnknown:
return "Unknown"_ns;
default:
return "Invalid"_ns;
}
}
void AccumulateCacheHitTelemetry(
nsICacheInfoChannel::CacheDisposition hitOrMiss, nsIChannel* aChannel) {
nsCString key("UNKNOWN");
nsCOMPtr<nsILoadInfo> 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");
}
}
nsLiteralCString label = CacheDispositionToTelemetryLabel(hitOrMiss);
glean::http::cache_disposition.Get(key, label).Add();
glean::http::cache_disposition.Get("ALL"_ns, label).Add();
}
// 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<nsICryptoHash> 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<unsigned const char*>(buf), strlen(buf));
NS_ENSURE_SUCCESS(rv, rv);
rv = hasher->Finish(true, hash);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
class CookieVisitor final {
public:
explicit CookieVisitor(nsHttpResponseHead* aResponseHead) {
nsAutoCString cookieHeader;
if (NS_SUCCEEDED(
aResponseHead->GetHeader(nsHttp::Set_Cookie, cookieHeader))) {
for (const auto& cookie : cookieHeader.Split('\n')) {
mCookieHeaders.AppendElement(cookie);
}
}
}
~CookieVisitor() = default;
const nsTArray<nsCString>& CookieHeaders() const { return mCookieHeaders; }
private:
nsTArray<nsCString> mCookieHeaders;
};
class CookieObserver final : public nsIObserver,
public nsSupportsWeakReference {
public:
NS_DECL_ISUPPORTS
NS_DECL_NSIOBSERVER
static already_AddRefed<CookieObserver> Create(bool aPrivateBrowsing);
void StealChanges(nsTArray<CookieChange>& aChanges) {
aChanges.SwapElements(mChanges);
}
private:
CookieObserver() = default;
~CookieObserver() = default;
nsTArray<CookieChange> mChanges;
};
NS_IMPL_ISUPPORTS(CookieObserver, nsIObserver, nsISupportsWeakReference)
// static
already_AddRefed<CookieObserver> CookieObserver::Create(bool aPrivateBrowsing) {
MOZ_ASSERT(NS_IsMainThread());
RefPtr<CookieObserver> observer = new CookieObserver();
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
if (NS_WARN_IF(!os)) {
return nullptr;
}
nsresult rv = os->AddObserver(
observer, aPrivateBrowsing ? "private-cookie-changed" : "cookie-changed",
true);
if (NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
return observer.forget();
}
NS_IMETHODIMP
CookieObserver::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t* aData) {
MOZ_ASSERT(NS_IsMainThread());
nsCOMPtr<nsICookieNotification> notification = do_QueryInterface(aSubject);
NS_ENSURE_TRUE(notification, NS_ERROR_FAILURE);
nsCOMPtr<nsICookie> xpcCookie;
nsresult rv = notification->GetCookie(getter_AddRefs(xpcCookie));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (!xpcCookie) {
return NS_OK;
}
const Cookie& cookie = xpcCookie->AsCookie();
nsICookieNotification::Action action = notification->GetAction();
switch (action) {
case nsICookieNotification::COOKIE_DELETED:
mChanges.AppendElement(CookieChange{/* added */ false, cookie.ToIPC(),
cookie.OriginAttributesRef()});
break;
case nsICookieNotification::COOKIE_ADDED:
[[fallthrough]];
case nsICookieNotification::COOKIE_CHANGED:
mChanges.AppendElement(CookieChange{/* added */ true, cookie.ToIPC(),
cookie.OriginAttributesRef()});
break;
default:
// We don't care about other actions because none of them can be
// triggered by the Set-Cookie header.
break;
}
return NS_OK;
}
void MaybeInitializeCookieProcessingGuard(
nsHttpChannel* aChannel, CookieServiceParent::CookieProcessingGuard& aGuard,
RefPtr<CookieObserver>& aCookieObserver,
RefPtr<HttpChannelParent>& aHttpChannelParent, uint32_t aHttpStatus) {
nsCOMPtr<nsIParentChannel> parentChannel;
NS_QueryNotificationCallbacks(aChannel, parentChannel);
aHttpChannelParent = do_QueryObject(parentChannel);
if (!aHttpChannelParent) {
return;
}
aCookieObserver = CookieObserver::Create(NS_UsePrivateBrowsing(aChannel));
PNeckoParent* neckoParent = aHttpChannelParent->Manager();
if (!neckoParent) {
return;
}
PCookieServiceParent* csParent =
LoneManagedOrNullAsserts(neckoParent->ManagedPCookieServiceParent());
CookieServiceParent* cookieServiceParent =
static_cast<CookieServiceParent*>(csParent);
if (!cookieServiceParent) {
return;
}
// on redirect we don't want to use processing guard
// because it will prevent cookies from being set in the
// content process that originated the request
if (nsHttpChannel::IsRedirectStatus(aHttpStatus)) {
return;
}
aGuard.Initialize(cookieServiceParent);
}
} // 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<nsIRedirectResultListener> 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 <public>
//-----------------------------------------------------------------------------
nsHttpChannel::nsHttpChannel() : HttpAsyncAborter<nsHttpChannel>(this) {
LOG(("Creating nsHttpChannel [this=%p, nsIChannel=%p]\n", this,
static_cast<nsIChannel*>(this)));
mChannelCreationTime = PR_Now();
mChannelCreationTimestamp = TimeStamp::Now();
}
nsHttpChannel::~nsHttpChannel() {
PROFILER_MARKER("~nsHttpChannel", NETWORK, {}, TerminatingFlowMarker,
Flow::FromPointer(this));
LOG(("Destroying nsHttpChannel [this=%p, nsIChannel=%p]\n", this,
static_cast<nsIChannel*>(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<nsresult> rv = mAuthProvider->Disconnect(NS_ERROR_ABORT);
MOZ_ASSERT(NS_SUCCEEDED(rv));
}
ReleaseMainThreadOnlyReferences();
if (gHttpHandler) {
gHttpHandler->RemoveHttpChannel(mChannelId);
}
if (mDictDecompress && mUsingDictionary) {
mDictDecompress->UseCompleted();
}
}
void nsHttpChannel::ReleaseMainThreadOnlyReferences() {
if (NS_IsMainThread()) {
// Already on main thread, let dtor to
// take care of releasing references
return;
}
nsTArray<nsCOMPtr<nsISupports>> 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<nsIURIClassifierCallback>());
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, nsILoadInfo* aLoadInfo) {
LOG1(("nsHttpChannel::Init [this=%p]\n", this));
nsresult rv = HttpBaseChannel::Init(uri, caps, proxyInfo, proxyResolveFlags,
proxyURI, channelId, aLoadInfo);
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 <private>
//-----------------------------------------------------------------------------
void nsHttpChannel::AddStorageAccessHeadersToRequest() {
if (!StaticPrefs::dom_storage_access_enabled() ||
!StaticPrefs::dom_storage_access_headers_enabled()) {
return;
}
// check if request is eligible for storage-access
uint32_t cookiePolicy = 0;
if (mLoadInfo->GetCookiePolicy(&cookiePolicy) != NS_OK) {
return;
}
if (cookiePolicy != nsILoadInfo::SEC_COOKIES_INCLUDE) {
return;
}
// check whether we have storage-access permission set in channel
nsILoadInfo::StoragePermissionState storageAccess =
AntiTrackingUtils::GetStoragePermissionStateInParent(this);
switch (storageAccess) {
case nsILoadInfo::HasStoragePermission:
case nsILoadInfo::StoragePermissionAllowListed:
SetRequestHeader(nsHttp::Sec_Fetch_Storage_Access.val(), "active"_ns,
false);
break;
case nsILoadInfo::InactiveStoragePermission:
SetRequestHeader(nsHttp::Sec_Fetch_Storage_Access.val(), "inactive"_ns,
false);
break;
case nsILoadInfo::DisabledStoragePermission:
SetRequestHeader(nsHttp::Sec_Fetch_Storage_Access.val(), "none"_ns,
false);
break;
case nsILoadInfo::NoStoragePermission:
break;
}
}
bool nsHttpChannel::StorageAccessReloadedChannel() {
return LoadStorageAccessReloadChannel();
}
nsresult nsHttpChannel::PrepareToConnect() {
LOG(("nsHttpChannel::PrepareToConnect [this=%p]\n", this));
// This may be async; the dictionary headers may need to fetch an origin
// dictionary cache entry from disk before adding the headers. We can
// continue with channel creation, and just block on this being done later
AUTO_PROFILER_FLOW_MARKER("nsHttpHandler::AddAcceptAndDictionaryHeaders",
NETWORK, Flow::FromPointer(this));
// AddAcceptAndDictionaryHeaders must call this->Suspend before kicking
// off the async operation that can result in calling the lambda (which
// will Resume), to avoid a race condition.
nsresult rv = gHttpHandler->AddAcceptAndDictionaryHeaders(
mURI, mLoadInfo->GetExternalContentPolicyType(), &mRequestHead, IsHTTPS(),
this, nsHttpChannel::StaticSuspend,
[self = RefPtr(this)](bool aNeedsResume, DictionaryCacheEntry* aDict) {
self->mDictDecompress = aDict;
if (aNeedsResume) {
LOG_DICTIONARIES(("Resuming after getting Dictionary headers"));
self->Resume();
}
if (self->mDictDecompress) {
LOG_DICTIONARIES(
("Added dictionary header for %p, DirectoryCacheEntry %p",
self.get(), aDict));
AUTO_PROFILER_FLOW_MARKER(
"nsHttpHandler::AddAcceptAndDictionaryHeaders Add "
"Available-Dictionary",
NETWORK, Flow::FromPointer(self));
// mDictDecompress is set if we added Available-Dictionary
self->mDictDecompress->InUse();
self->mUsingDictionary = true;
PROFILER_MARKER("Dictionary Prefetch", NETWORK,
MarkerTiming::IntervalStart(), FlowMarker,
Flow::FromPointer(self));
// XXX if this fails, retry the connection (we assume that the
// DictionaryCacheEntry has been removed). Failure should be only in
// weird cases like no storage service.
return NS_SUCCEEDED(self->mDictDecompress->Prefetch(
GetLoadContextInfo(self), self->mShouldSuspendForDictionary,
[self]() {
// this is called when the prefetch is complete to
// un-Suspend the channel
MOZ_ASSERT(self->mDictDecompress->DictionaryReady());
if (self->mSuspendedForDictionary) {
LOG(
("nsHttpChannel::SetupChannelForTransaction [this=%p] "
"Resuming channel "
"suspended for Dictionary",
self.get()));
self->mSuspendedForDictionary = false;
self->Resume();
}
PROFILER_MARKER("Dictionary Prefetch", NETWORK,
MarkerTiming::IntervalEnd(), FlowMarker,
Flow::FromPointer(self));
}));
}
return true;
});
if (NS_FAILED(rv)) return rv;
// notify "http-on-modify-request-before-cookies" observers
gHttpHandler->OnModifyRequestBeforeCookies(this);
if (mStaleRevalidation) {
// This is a revalidating channel.
// The cookies (user set cookies + cookies from the cookeservice) are
// already copied to the request headers when opening this channel in
// PerformBackgroundCacheRevalidationNow().
} else {
AddCookiesToRequest();
}
#if defined(XP_WIN) || defined(XP_MACOSX)
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);
nsAutoCString prefName;
# ifdef XP_WIN
prefName = nsPrintfCString("network.http.windows-sso.container-enabled.%u",
containerId);
# endif
# ifdef XP_MACOSX
prefName = nsPrintfCString(
"network.http.microsoft-entra-sso.container-enabled.%u", containerId);
# endif
bool enabled = false;
Preferences::GetBool(prefName.get(), &enabled);
LOG(("Pref for %s is %d\n", prefName.get(), enabled));
return enabled;
};
#endif // defined(XP_WIN) || defined(XP_MACOSX)
#ifdef XP_WIN
// 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
#ifdef XP_MACOSX
auto isUriMSAuthority = [&]() {
nsAutoCString endPoint;
nsresult rv = mURI->GetHost(endPoint);
if (!NS_SUCCEEDED(rv)) {
return false;
}
LOG(("endPoint is %s\n", endPoint.get()));
return gHttpHandler->IsHostMSAuthority(endPoint);
};
// If macOS 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_microsoft_entra_sso_enabled() &&
mURI->SchemeIs("https") && !(mLoadFlags & LOAD_ANONYMOUS) &&
!mPrivateBrowsing) {
ExtContentPolicyType type = mLoadInfo->GetExternalContentPolicyType();
if ((type == ExtContentPolicy::TYPE_DOCUMENT ||
type == ExtContentPolicy::TYPE_SUBDOCUMENT) &&
prefEnabledForCurrentContainer() && isUriMSAuthority()) {
nsMainThreadPtrHandle<nsHttpChannel> self(
new nsMainThreadPtrHolder<nsHttpChannel>(
"nsHttpChannel::PrepareToConnect::self", this));
auto resultCallback = [self(self)]() {
MOZ_ASSERT(NS_IsMainThread());
nsresult rv = self->ContinuePrepareToConnect();
if (NS_FAILED(rv)) {
self->CloseCacheEntry(false);
(void)self->AsyncAbort(rv);
}
};
nsresult rv = AddMicrosoftEntraSSO(this, std::move(resultCallback));
// Returns NS_OK if performRequests is called in MicrosoftEntraSSOUtils
// This temporarily stops the channel setup for the delegate.
if (NS_SUCCEEDED(rv)) {
return rv;
}
}
}
#endif
return ContinuePrepareToConnect();
}
nsresult nsHttpChannel::ContinuePrepareToConnect() {
// 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() {
nsAutoCString userSetPriority;
(void)GetRequestHeader("Priority"_ns, userSetPriority);
if (!userSetPriority.IsEmpty()) {
// If the Priority header is set by the user, do not override it.
return;
}
uint8_t urgency =
nsHttpHandler::UrgencyFromCoSFlags(mClassOfService.Flags(), mPriority);
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 (mAPIRedirectTo) {
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:
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<nsIURI> unstrippedURI;
mLoadInfo->GetUnstrippedURI(getter_AddRefs(unstrippedURI));
if (unstrippedURI) {
return AsyncCall(&nsHttpChannel::HandleAsyncRedirectToUnstrippedURI);
}
}
nsCOMPtr<nsIPrincipal> 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<mozilla::dom::BrowsingContext> bc;
mLoadInfo->GetBrowsingContext(getter_AddRefs(bc));
// If bypassing the cache and we're forced offline
// we can just return the error here.
if (bc && bc->Top()->GetForceOffline() &&
BYPASS_LOCAL_CACHE(mLoadFlags, LoadPreferCacheLoadOverBypass())) {
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.
nsMainThreadPtrHandle<nsHttpChannel> self(
new nsMainThreadPtrHolder<nsHttpChannel>(
"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);
(void)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<nsHTTPSOnlyStreamListener> 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<uint32_t>(rv)));
if (NS_FAILED(rv) || MOZ_UNLIKELY(willCallback)) {
return rv;
}
}
}
return MaybeUseHTTPSRRForUpgrade(shouldUpgrade, NS_OK);
}
// Returns true if the network connectivity checker indicated
// that HTTPS records can be resolved on this network - false otherwise.
// When TRR is enabled, we always return true, as resolving HTTPS
// records don't depend on the network.
static bool canUseHTTPSRRonNetwork(bool& aTRREnabled) {
// Respect the pref.
if (StaticPrefs::network_dns_force_use_https_rr()) {
aTRREnabled = true;
return true;
}
aTRREnabled = false;
if (nsCOMPtr<nsIDNSService> dns = mozilla::components::DNS::Service()) {
nsIDNSService::ResolverMode mode;
// If the browser is currently using TRR/DoH, then it can
// definitely resolve HTTPS records.
if (NS_SUCCEEDED(dns->GetCurrentTrrMode(&mode))) {
if (mode == nsIDNSService::MODE_TRRFIRST) {
RefPtr<TRRService> trr = TRRService::Get();
if (trr && trr->IsConfirmed()) {
aTRREnabled = true;
}
} else if (mode == nsIDNSService::MODE_TRRONLY) {
aTRREnabled = true;
}
if (aTRREnabled) {
return true;
}
}
}
if (RefPtr<NetworkConnectivityService> ncs =
NetworkConnectivityService::GetSingleton()) {
nsINetworkConnectivityService::ConnectivityState state;
if (NS_SUCCEEDED(ncs->GetDNS_HTTPS(&state)) &&
state == nsINetworkConnectivityService::NOT_AVAILABLE) {
return false;
}
}
return true;
}
nsresult nsHttpChannel::MaybeUseHTTPSRRForUpgrade(bool aShouldUpgrade,
nsresult aStatus) {
if (NS_FAILED(aStatus)) {
return aStatus;
}
RefPtr<mozilla::dom::BrowsingContext> bc;
mLoadInfo->GetBrowsingContext(getter_AddRefs(bc));
bool forceOffline = bc && bc->Top()->GetForceOffline();
if (mURI->SchemeIs("https") || aShouldUpgrade || !LoadUseHTTPSSVC() ||
forceOffline) {
return ContinueOnBeforeConnect(aShouldUpgrade, aStatus);
}
auto shouldSkipUpgradeWithHTTPSRR = [&]() -> bool {
if (mCaps & NS_HTTP_DISALLOW_HTTPS_RR) {
return true;
}
// 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;
}
// If the network connectivity checker indicates the network is
// blocking HTTPS requests, then we should skip them so we don't
// needlessly wait for a timeout.
bool trrEnabled = false;
if (!canUseHTTPSRRonNetwork(trrEnabled)) {
return true;
}
// Don't block the channel when TRR is not used.
if (!trrEnabled) {
return true;
}
auto dnsStrategy = GetProxyDNSStrategy();
if (dnsStrategy != ProxyDNSStrategy::ORIGIN) {
return true;
}
nsAutoCString uriHost;
mURI->GetAsciiHost(uriHost);
return gHttpHandler->IsHostExcludedForHTTPSRR(uriHost);
};
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.
DisallowHTTPSRR(mCaps);
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);
}
LOG(("nsHttpChannel::MaybeUseHTTPSRRForUpgrade [%p] wait for HTTPS RR",
this));
OriginAttributes originAttributes;
StoragePrincipalHelper::GetOriginAttributesForHTTPSRR(this, originAttributes);
RefPtr<nsDNSPrefetch> resolver =
new nsDNSPrefetch(mURI, originAttributes, nsIRequest::GetTRRMode());
nsWeakPtr weakPtrThis(
do_GetWeakReference(static_cast<nsIHttpChannel*>(this)));
nsresult rv = resolver->FetchHTTPSSVC(
mCaps & NS_HTTP_REFRESH_DNS, !LoadUseHTTPSSVC(),
[weakPtrThis](nsIDNSHTTPSSVCRecord* aRecord) {
nsCOMPtr<nsIHttpChannel> channel = do_QueryReferent(weakPtrThis);
RefPtr<nsHttpChannel> httpChannelImpl = do_QueryObject(channel);
if (httpChannelImpl) {
httpChannelImpl->OnHTTPSRRAvailable(aRecord);
}
});
if (NS_FAILED(rv)) {
LOG((" FetchHTTPSSVC failed with 0x%08" PRIx32,
static_cast<uint32_t>(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<uint32_t>(aStatus)));
MOZ_ASSERT(!LoadWaitHTTPSSVCRecord());
if (NS_FAILED(aStatus)) {
return aStatus;
}
if (aShouldUpgrade && !mURI->SchemeIs("https")) {
// only set HTTPS_RR to be responsbile for the upgrade in the loadinfo
// if it actually was responsible, otherwise the correct flag is
// already present in the loadinfo.
if (aUpgradeWithHTTPSRR) {
mLoadInfo->SetHttpsUpgradeTelemetry(nsILoadInfo::HTTPS_RR);
}
return AsyncCall(&nsHttpChannel::HandleAsyncRedirectChannelToHttps);
}
// ensure that we are using a valid hostname
if (!net_IsValidDNSHost(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.
// TODO: When mUpgradeProtocolCallback is not null, we should allow HTTP/3
// for connect-udp.
mCaps |= NS_HTTP_DISALLOW_HTTP3;
// Because NS_HTTP_STICKY_CONNECTION breaks HTTPS RR fallabck mecnahism, we
// can not use HTTPS RR for upgrade requests.
DisallowHTTPSRR(mCaps);
}
if (LoadIsTRRServiceChannel()) {
mCaps |= NS_HTTP_LARGE_KEEPALIVE;
DisallowHTTPSRR(mCaps);
}
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<RefPtr<nsIWebTransportHash>> aServerCertHashes;
nsresult rv;
nsCOMPtr<WebTransportConnectionSettings> wtconSettings =
do_QueryInterface(mWebTransportSessionEventListener, &rv);
NS_ENSURE_SUCCESS(rv, rv);
wtconSettings->GetServerCertificateHashes(aServerCertHashes);
gHttpHandler->ConnMgr()->StoreServerCertHashes(
mConnectionInfo, gHttpHandler->IsHttp2Excluded(mConnectionInfo),
!Http3Allowed(), std::move(aServerCertHashes));
}
if (ShouldIntercept()) {
return RedirectToInterceptedChannel();
}
// notify "http-on-before-connect" observers
gHttpHandler->OnBeforeConnect(this);
return CallOrWaitForResume([](auto* self) { return self->Connect(); });
}
class MOZ_STACK_CLASS AddResponseHeadersToResponseHead final
: public nsIHttpHeaderVisitor {
public:
explicit AddResponseHeadersToResponseHead(nsHttpResponseHead* aResponseHead)
: mResponseHead(aResponseHead) {}
NS_IMETHOD VisitHeader(const nsACString& aHeader,
const nsACString& aValue) override {
nsAutoCString headerLine = aHeader + ": "_ns + aValue;
DebugOnly<nsresult> rv = mResponseHead->ParseHeaderLine(headerLine);
MOZ_ASSERT(NS_SUCCEEDED(rv));
return NS_OK;
}
NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override;
// Stub AddRef/Release since this is a stack class.
NS_IMETHOD_(MozExternalRefCountType) AddRef(void) override {
return ++mRefCnt;
}
NS_IMETHOD_(MozExternalRefCountType) Release(void) override {
return --mRefCnt;
}
virtual ~AddResponseHeadersToResponseHead() {
MOZ_DIAGNOSTIC_ASSERT(mRefCnt == 0);
}
private:
nsHttpResponseHead* mResponseHead;
nsrefcnt mRefCnt = 0;
};
NS_IMPL_QUERY_INTERFACE(AddResponseHeadersToResponseHead, nsIHttpHeaderVisitor)
nsresult nsHttpChannel::HandleOverrideResponse() {
// Start building a response with the data from mOverrideResponse.
mResponseHead = MakeUnique<nsHttpResponseHead>();
// Apply override response status code and status text.
uint32_t statusCode;
nsresult rv = mOverrideResponse->GetResponseStatus(&statusCode);
NS_ENSURE_SUCCESS(rv, rv);
nsAutoCString statusText;
rv = mOverrideResponse->GetResponseStatusText(statusText);
NS_ENSURE_SUCCESS(rv, rv);
// Hardcoding protocol HTTP/1.1
nsPrintfCString line("HTTP/1.1 %u %s", statusCode, statusText.get());
rv = mResponseHead->ParseStatusLine(line);
NS_ENSURE_SUCCESS(rv, rv);
// Apply override response headers.
AddResponseHeadersToResponseHead visitor(mResponseHead.get());
rv = mOverrideResponse->VisitResponseHeaders(&visitor);
NS_ENSURE_SUCCESS(rv, rv);
if (WillRedirect(*mResponseHead)) {
// to avoid event dispatching latency.
LOG(("Skipping read of overridden response redirect entity\n"));
return AsyncCall(&nsHttpChannel::HandleAsyncRedirect);
}
// This block parses the cookie header, collects any cookie changes,
// and sends them to the parent actor.
{
RefPtr<HttpChannelParent> httpParent;
RefPtr<CookieObserver> cookieObserver;
CookieServiceParent::CookieProcessingGuard cookieProcessingGuard;
MaybeInitializeCookieProcessingGuard(
this, cookieProcessingGuard, cookieObserver, httpParent, statusCode);
// Handle Set-Cookie headers as if the response was from networking.
CookieVisitor cookieVisitor(mResponseHead.get());
SetCookieHeaders(cookieVisitor.CookieHeaders());
if (cookieObserver) {
nsTArray<CookieChange> cookieChanges;
cookieObserver->StealChanges(cookieChanges);
if (!cookieChanges.IsEmpty()) {
MOZ_ASSERT(httpParent);
httpParent->SetCookieChanges(std::move(cookieChanges));
}
}
}
rv = ProcessSecurityHeaders();
if (NS_FAILED(rv)) {
NS_WARNING("ProcessSecurityHeaders failed, continuing load.");
}
if ((statusCode < 500) && (statusCode != 421)) {
ProcessAltService();
}
nsAutoCString body;
rv = mOverrideResponse->GetResponseBody(body);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIInputStream> stringStream;
rv = NS_NewCStringInputStream(getter_AddRefs(stringStream), body);
NS_ENSURE_SUCCESS(rv, rv);
rv = nsInputStreamPump::Create(getter_AddRefs(mCachePump), stringStream, 0, 0,
true);
if (NS_FAILED(rv)) {
stringStream->Close();
return rv;
}
rv = mCachePump->AsyncRead(this);
if (NS_FAILED(rv)) return rv;
return NS_OK;
}
nsresult nsHttpChannel::Connect() {
LOG(("nsHttpChannel::Connect [this=%p]\n", this));
if (mAPIRedirectTo) {
LOG(("nsHttpChannel::Connect [transparent=%d]\n",
mAPIRedirectTo->second()));
nsresult rv = StartRedirectChannelToURI(
mAPIRedirectTo->first(),
mAPIRedirectTo->second() ? nsIChannelEventSink::REDIRECT_PERMANENT |
nsIChannelEventSink::REDIRECT_TRANSPARENT
: nsIChannelEventSink::REDIRECT_PERMANENT);
mAPIRedirectTo = Nothing();
if (NS_SUCCEEDED(rv)) {
return NS_OK;
}
return NS_ERROR_FAILURE;
}
// If mOverrideResponse is set, bypass the rest of the connection and reply
// immediately with a response built using the data from mOverrideResponse.
if (mOverrideResponse) {
return HandleOverrideResponse();
}
// 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;
}
// Step 8.18 of HTTP-network-or-cache fetch
nsAutoCString rangeVal;
if (NS_SUCCEEDED(mRequestHead.GetHeader(nsHttp::Range, rangeVal))) {
SetRequestHeader("Accept-Encoding"_ns, "identity"_ns, true);
}
if (mRequestHead.IsPost() || mRequestHead.IsPatch()) {
// If the post id is already set then this is an attempt to replay
// a post/patch transaction via the cache. Otherwise, we need a unique
// post id for this transaction.
if (mPostID == 0) {
mPostID = gHttpHandler->GenerateUniqueID();
}
if (StaticPrefs::network_http_idempotencyKey_enabled() &&
!mRequestHead.HasHeader(nsHttp::Idempotency_Key)) {
// check if we need to add
// idempotency-key header
nsAutoCString key;
gHttpHandler->GenerateIdempotencyKeyForPost(mPostID, mLoadInfo, key);
MOZ_ALWAYS_SUCCEEDS(
mRequestHead.SetHeader(nsHttp::Idempotency_Key, key, false));
}
}
#ifdef MOZ_WIDGET_ANDROID
bool val = false;
if (nsIOService::ShouldAddAdditionalSearchHeaders(mURI, &val)) {
SetRequestHeader("X-Search-Subdivision"_ns, val ? "1"_ns : "0"_ns, false);
}
#endif
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));
AUTO_PROFILER_FLOW_MARKER("nsHttpChannel::ConnectOnTailUnblock", NETWORK,
Flow::FromPointer(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<uint32_t>(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 && !CachedContentIsValid() &&
(mDidReval || LoadCachedContentIsPartial())) ||
mIgnoreCacheEntry)) {
// We won't send the conditional request because the unconditional
glean::network::race_cache_validation
.EnumGet(glean::network::RaceCacheValidationLabel::eNotsent)
.Add();
}
// 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 && CachedContentIsValid()) {
(void)ReadFromCache();
}
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.
AUTO_PROFILER_FLOW_MARKER("nsHttpChannel::ContinueConnect", NETWORK,
Flow::FromPointer(this));
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");
RefPtr<mozilla::dom::BrowsingContext> bc;
mLoadInfo->GetBrowsingContext(getter_AddRefs(bc));
// we may or may not have a cache entry at this point
if (mCacheEntry) {
// read straight from the cache if possible...
if (CachedContentIsValid()) {
// If we're forced offline, and set to bypass the cache, return offline.
if (bc && bc->Top()->GetForceOffline() &&
BYPASS_LOCAL_CACHE(mLoadFlags, LoadPreferCacheLoadOverBypass())) {
return NS_ERROR_OFFLINE;
}
nsRunnableMethod<nsHttpChannel>* event = nullptr;
nsresult rv;
if (!LoadCachedContentIsPartial()) {
rv = AsyncCall(&nsHttpChannel::AsyncOnExamineCachedResponse, &event);
if (NS_FAILED(rv)) {
LOG((" AsyncCall failed (%08x)", static_cast<uint32_t>(rv)));
}
}
rv = ReadFromCache();
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((" !CachedContentIsValid() && 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;
}
// We're about to hit the network. Don't if we're forced offline.
if (bc && bc->Top()->GetForceOffline()) {
return NS_ERROR_OFFLINE;
}
// 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<nsISerialEventTarget> target(do_GetMainThread());
RefPtr<nsHttpChannel> self(this);
mDNSBlockingThenable->Then(
target, __func__,
[self](const nsCOMPtr<nsIDNSRecord>& aRec) {
nsresult rv = self->DoConnectActual(nullptr);
if (NS_FAILED(rv)) {
self->CloseCacheEntry(false);
(void)self->AsyncAbort(rv);
}
},
[self](nsresult err) {
self->CloseCacheEntry(false);
(void)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 = SetupChannelForTransaction();
if (NS_FAILED(rv)) {
return rv;
}
return CallOrWaitForResume(
[trans = RefPtr(aTransWithStickyConn)](auto* self) {
return self->DispatchTransaction(trans);
});
}
nsresult nsHttpChannel::DispatchTransaction(
HttpTransactionShell* aTransWithStickyConn) {
LOG(("nsHttpChannel::DispatchTransaction [this=%p, aTransWithStickyConn=%p]",
this, aTransWithStickyConn));
nsresult rv = InitTransaction();
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<mozilla::dom::BrowsingContext> 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<nsIInterfaceRequestor> callbacks;
NS_NewNotificationCallbacksAggregation(mCallbacks, mLoadGroup,
getter_AddRefs(callbacks));
if (!callbacks) return;
bool httpsRRAllowed = !(mCaps & NS_HTTP_DISALLOW_HTTPS_RR);
(void)gHttpHandler->MaybeSpeculativeConnectWithHTTPSRR(
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),
nsHttpHandler::EchConfigEnabled() && httpsRRAllowed);
}
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) {
(void)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.
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<uint32_t>(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::SetupChannelForTransaction() {
LOG((
"nsHttpChannel::SetupChannelForTransaction [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
// 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<nsIURI> 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.
// But we should not touch Pragma if Cache-Control is already set
if (!mRequestHead.HasHeader(nsHttp::Pragma)) {
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'. But likewise don't touch Cache-Control if it's already set.
if (mRequestHead.Version() >= HttpVersion::v1_1 &&
!mRequestHead.HasHeader(nsHttp::Cache_Control)) {
rv = mRequestHead.SetHeaderOnce(nsHttp::Cache_Control, "no-cache", true);
MOZ_ASSERT(NS_SUCCEEDED(rv));
}
} else if (mLoadFlags & VALIDATE_ALWAYS) {
// 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'
//
// But don't send the headers if they're already set:
if (mRequestHead.Version() >= HttpVersion::v1_1) {
if (!mRequestHead.HasHeader(nsHttp::Cache_Control)) {
rv = mRequestHead.SetHeaderOnce(nsHttp::Cache_Control, "max-age=0",
true);
}
} else {
if (!mRequestHead.HasHeader(nsHttp::Pragma)) {
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));
}
}
}
// See bug #466080. Transfer LOAD_ANONYMOUS flag to socket-layer.
if (mLoadFlags & LOAD_ANONYMOUS) mCaps |= NS_HTTP_LOAD_ANONYMOUS;
if (mUpgradeProtocolCallback) {
rv = mRequestHead.SetHeader(nsHttp::Upgrade, mUpgradeProtocol, false);
MOZ_ASSERT(NS_SUCCEEDED(rv));
rv = mRequestHead.SetHeaderOnce(nsHttp::Connection, nsHttp::Upgrade.get(),
false);
MOZ_ASSERT(NS_SUCCEEDED(rv));
mCaps |= NS_HTTP_STICKY_CONNECTION;
mCaps &= ~NS_HTTP_ALLOW_KEEPALIVE;
}
if (mWebTransportSessionEventListener) {
mCaps |= NS_HTTP_STICKY_CONNECTION;
}
return NS_OK;
}
// Updates mLNAPermission members based on existing permission and tracking
// flags in load info
LNAPermission nsHttpChannel::UpdateLocalNetworkAccessPermissions(
const nsACString& aPermissionType) {
// We should arrive at this point after LNA has been detected at the
// transaction layer and has errored
MOZ_ASSERT(aPermissionType == LOCAL_HOST_PERMISSION_KEY ||
aPermissionType == LOCAL_NETWORK_PERMISSION_KEY);
LNAPermission userPerms = aPermissionType == LOCAL_HOST_PERMISSION_KEY
? mLNAPermission.mLocalHostPermission
: mLNAPermission.mLocalNetworkPermission;
if (NS_WARN_IF(userPerms != LNAPermission::Pending)) {
// Unexpected condition, we should not hit this case
MOZ_ASSERT(false,
"UpdateLocalNetworkAccessPermissions called with non-pending "
"permission");
return userPerms;
}
MOZ_ASSERT(mLoadInfo->TriggeringPrincipal(), "need triggering principal");
// Step 1. Check for Existing Allow or Deny permission
if (nsContentUtils::IsExactSitePermAllow(mLoadInfo->TriggeringPrincipal(),
aPermissionType)) {
userPerms = LNAPermission::Granted;
return userPerms;
}
if (nsContentUtils::IsExactSitePermDeny(mLoadInfo->TriggeringPrincipal(),
aPermissionType)) {
userPerms = LNAPermission::Denied;
return userPerms;
}
// Step 2.If this is from third Party Tracker, just block
uint32_t flags = 0;
using CF = nsIClassifiedChannel::ClassificationFlags;
if (StaticPrefs::network_lna_block_trackers() &&
NS_SUCCEEDED(
mLoadInfo->GetTriggeringThirdPartyClassificationFlags(&flags)) &&
(flags & (CF::CLASSIFIED_ANY_BASIC_TRACKING |
CF::CLASSIFIED_ANY_SOCIAL_TRACKING)) != 0) {
userPerms = LNAPermission::Denied;
return userPerms;
}
// Step 3
// could not determine the permission, lets prompt user
// for permission if lna blocking is enabled
if (StaticPrefs::network_lna_blocking()) {
return userPerms;
}
// we dont have reasons to block the request so we allow this LNA request
// Ideally we should not hit this case once the feature is fully shipped
userPerms = LNAPermission::Granted;
return userPerms;
}
nsresult nsHttpChannel::InitTransaction() {
nsresult rv;
// create wrapper for this channel's notification callbacks
nsCOMPtr<nsIInterfaceRequestor> 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;
}
RefPtr<SocketProcessParent> socketProcess =
SocketProcessParent::GetSingleton();
if (!socketProcess->CanSend()) {
return NS_ERROR_NOT_AVAILABLE;
}
nsCOMPtr<nsIParentChannel> parentChannel;
NS_QueryNotificationCallbacks(this, parentChannel);
RefPtr<DocumentLoadListener> 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<HttpTransactionParent> 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));
EnsureBrowserId();
EnsureRequestContext();
HttpTrafficCategory category = CreateTrafficCategory();
std::function<void(TransactionObserverResult&&)> observer;
if (mTransactionObserver) {
observer = [transactionObserver{std::move(mTransactionObserver)}](
TransactionObserverResult&& aResult) {
transactionObserver->Complete(aResult.versionOk(), aResult.authOk(),
aResult.closeReason());
};
}
mTransaction->SetIsForWebTransport(!!mWebTransportSessionEventListener);
RefPtr<mozilla::dom::BrowsingContext> bc;
mLoadInfo->GetBrowsingContext(getter_AddRefs(bc));
nsILoadInfo::IPAddressSpace parentAddressSpace =
nsILoadInfo::IPAddressSpace::Unknown;
if (!bc) {
parentAddressSpace = mLoadInfo->GetParentIpAddressSpace();
} else {
parentAddressSpace = bc->GetCurrentIPAddressSpace();
}
// Check if this is a top-level navigation load and grant LNA permissions
// to skip local network access verification for navigational loads
if (mLoadInfo && StaticPrefs::network_lna_allow_top_level_navigation()) {
ExtContentPolicyType contentPolicyType =
mLoadInfo->GetExternalContentPolicyType();
if (contentPolicyType == ExtContentPolicy::TYPE_DOCUMENT) {
// Grant permissions for top-level navigation loads
mLNAPermission.mLocalHostPermission = LNAPermission::Granted;
mLNAPermission.mLocalNetworkPermission = LNAPermission::Granted;
}
}
// Grant LNA permissions for captive portal tabs to allow them to access
// local network resources without prompting the user
if (bc && bc->GetIsCaptivePortalTab()) {
mLNAPermission.mLocalNetworkPermission = LNAPermission::Granted;
}
rv = mTransaction->Init(
mCaps, mConnectionInfo, &mRequestHead, mUploadStream, mReqContentLength,
LoadUploadStreamHasHeaders(), GetCurrentSerialEventTarget(), callbacks,
this, mBrowserId, category, mRequestContext, mClassOfService,
mInitialRwin, LoadResponseTimeoutEnabled(), mChannelId,
std::move(observer), parentAddressSpace, mLNAPermission);
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 (StaticPrefs::network_http_network_error_logging_enabled() &&
mResponseHead && mResponseHead->HasHeader(nsHttp::NEL) &&
LoadUsedNetwork()) {
// If the policy gets cleared, but this response is still in the cache,
// we probably don't want to reinstall the policy when the entry is
// reloaded. That means it's important to only call RegisterPolicy when
// LoadUsedNetwork() is true.
if (nsCOMPtr<nsINetworkErrorLogging> nel =
components::NetworkErrorLogging::Service()) {
nel->RegisterPolicy(this);
}
}
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<nsIStreamListener> 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_BINDING_ABORTED,
"OpaqueResponseBlocker::BlockResponse"_ns);
return NS_BINDING_ABORTED;
}
// 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<nsIChannel*>(this);
bool typeSniffersCalled = false;
if (mCachePump) {
typeSniffersCalled =
NS_SUCCEEDED(mCachePump->PeekStream(CallTypeSniffers, thisChannel));
}
if (!typeSniffersCalled && mTransactionPump) {
RefPtr<nsInputStreamPump> pump = do_QueryObject(mTransactionPump);
if (pump) {
pump->PeekStream(CallTypeSniffers, thisChannel);
} else {
MOZ_ASSERT(nsIOService::UseSocketProcess());
RefPtr<HttpTransactionParent> 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<nsIParentChannel> parentChannel;
NS_QueryNotificationCallbacks(this, parentChannel);
RefPtr<DocumentLoadListener> docListener = do_QueryObject(parentChannel);
if (mResponseHead && docListener && docListener->GetChannel() == this) {
nsAutoCString contentType;
mResponseHead->ContentType(contentType);
if (contentType.Equals("multipart/x-mixed-replace"_ns)) {
nsCOMPtr<nsIStreamConverterService> convServ(
mozilla::components::StreamConverter::Service(&rv));
if (NS_SUCCEEDED(rv)) {
nsCOMPtr<nsIStreamListener> toListener(mListener);
nsCOMPtr<nsIStreamListener> 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<extensions::PStreamFilterParent> parent;
mozilla::ipc::Endpoint<extensions::PStreamFilterChild> 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<nsIStreamListener> 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<nsIStreamListener> 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<uint32_t>(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);
(void)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;
(void)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;
(void)AddSecurityMessage(consoleErrorTag, consoleErrorCategory);
}
}
void nsHttpChannel::ProcessAltService(nsHttpConnectionInfo* aTransConnInfo) {
// 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;
(void)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<nsIInterfaceRequestor> callbacks;
nsCOMPtr<nsProxyInfo> 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, aTransConnInfo);
}
nsresult nsHttpChannel::ProcessResponse(nsHttpConnectionInfo* aConnInfo) {
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.
glean::http::transaction_is_ssl
.EnumGet(static_cast<glean::http::TransactionIsSslLabel>(
mConnectionInfo->EndToEndSSL()))
.Add();
if (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI) {
glean::http::pageload_is_ssl
.EnumGet(static_cast<glean::http::PageloadIsSslLabel>(
mConnectionInfo->EndToEndSSL()))
.Add();
}
if (Telemetry::CanRecordPrereleaseData()) {
// Gather data on various response status to monitor any increased frequency
switch (httpStatus) {
case 200:
mozilla::glean::networking::http_response_status_code.Get("200_ok"_ns)
.Add(1);
break;
case 301:
mozilla::glean::networking::http_response_status_code
.Get("301_moved_permanently"_ns)
.Add(1);
break;
case 302:
mozilla::glean::networking::http_response_status_code
.Get("302_found"_ns)
.Add(1);
break;
case 304:
mozilla::glean::networking::http_response_status_code
.Get("304_not_modified"_ns)
.Add(1);
break;
case 307:
mozilla::glean::networking::http_response_status_code
.Get("307_temporary_redirect"_ns)
.Add(1);
break;
case 308:
mozilla::glean::networking::http_response_status_code
.Get("308_permanent_redirect"_ns)
.Add(1);
break;
case 400:
mozilla::glean::networking::http_response_status_code
.Get("400_bad_request"_ns)
.Add(1);
break;
case 401:
mozilla::glean::networking::http_response_status_code
.Get("401_unauthorized"_ns)
.Add(1);
break;
case 403:
mozilla::glean::networking::http_response_status_code
.Get("403_forbidden"_ns)
.Add(1);
break;
case 404:
mozilla::glean::networking::http_response_status_code
.Get("404_not_found"_ns)
.Add(1);
break;
case 421:
mozilla::glean::networking::http_response_status_code
.Get("421_misdirected_request"_ns)
.Add(1);
break;
case 425:
mozilla::glean::networking::http_response_status_code
.Get("425_too_early"_ns)
.Add(1);
break;
case 429:
mozilla::glean::networking::http_response_status_code
.Get("429_too_many_requests"_ns)
.Add(1);
break;
case 500:
mozilla::glean::networking::http_response_status_code
.Get("other_5xx"_ns)
.Add(1);
break;
default:
if (httpStatus >= 400 && httpStatus < 500) {
mozilla::glean::networking::http_response_status_code
.Get("other_4xx"_ns)
.Add(1);
} else if (httpStatus > 500) {
mozilla::glean::networking::http_response_status_code
.Get("other_5xx"_ns)
.Add(1);
} else {
mozilla::glean::networking::http_response_status_code.Get("other"_ns)
.Add(1);
}
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).
if (StaticPrefs::network_predictor_enabled()) {
nsCOMPtr<nsIURI> referrer = GetReferringPage();
if (!referrer && mReferrerInfo) {
referrer = mReferrerInfo->GetOriginalReferrer();
}
if (referrer) {
nsCOMPtr<nsILoadContextInfo> 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(!CachedContentIsValid() || 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(aConnInfo);
}
void nsHttpChannel::AsyncContinueProcessResponse(
nsHttpConnectionInfo* aConnInfo) {
nsresult rv;
rv = ContinueProcessResponse1(aConnInfo);
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.
(void)Cancel(rv);
}
}
nsresult nsHttpChannel::ContinueProcessResponse1(
nsHttpConnectionInfo* aConnInfo) {
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 = [connInfo = RefPtr{aConnInfo}](nsHttpChannel* self) {
self->AsyncContinueProcessResponse(connInfo);
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)) {
// This block parses the cookie header, collects any cookie changes,
// and sends them to the parent actor.
{
RefPtr<CookieObserver> cookieObserver;
RefPtr<HttpChannelParent> httpParent;
CookieServiceParent::CookieProcessingGuard cookieProcessingGuard;
if (!LoadOnStartRequestCalled()) {
// This can only happen when a range request is created again in
// nsHttpChannel::ContinueOnStopRequest. If OnStartRequest is already
// called, we shouldn't call SetCookieHeaders.
MaybeInitializeCookieProcessingGuard(this, cookieProcessingGuard,
cookieObserver, httpParent,
httpStatus);
}
CookieVisitor cookieVisitor(mResponseHead.get());
SetCookieHeaders(cookieVisitor.CookieHeaders());
if (cookieObserver) {
nsTArray<CookieChange> cookieChanges;
cookieObserver->StealChanges(cookieChanges);
if (!cookieChanges.IsEmpty()) {
MOZ_ASSERT(httpParent);
httpParent->SetCookieChanges(std::move(cookieChanges));
}
}
}
// 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(aConnInfo);
}
}
if (LoadConcurrentCacheAccess() && LoadCachedContentIsPartial() &&
httpStatus != 206) {
LOG(
(" only expecting 206 when doing partial request during "
"interrupted cache concurrent read"));
return NS_ERROR_CORRUPTED_CONTENT;
}
if (httpStatus != 401 && httpStatus != 407) {
if (!mAuthRetryPending) {
MOZ_DIAGNOSTIC_ASSERT(mAuthProvider);
rv = mAuthProvider ? mAuthProvider->CheckForSuperfluousAuth()
: NS_ERROR_UNEXPECTED;
if (NS_FAILED(rv)) {
mStatus = rv;
LOG((" CheckForSuperfluousAuth failed (%08x)",
static_cast<uint32_t>(rv)));
}
}
if (mCanceled) return CallOnStartRequest();
// reset the authentication's current continuation state because ourvr
// last authentication attempt has been completed successfully
MOZ_DIAGNOSTIC_ASSERT(mAuthProvider);
rv = mAuthProvider ? mAuthProvider->Disconnect(NS_ERROR_ABORT)
: NS_ERROR_UNEXPECTED;
if (NS_FAILED(rv)) {
LOG((" Disconnect failed (%08x)", static_cast<uint32_t>(rv)));
}
mAuthProvider = nullptr;
LOG((" continuation state has been reset"));
}
gHttpHandler->OnAfterExamineResponse(this);
// No process switch needed, continue as normal.
return ContinueProcessResponse2(rv);
}
nsresult nsHttpChannel::ContinueProcessResponse2(nsresult rv) {
if (mSuspendCount) {
LOG(("Waiting until resume to finish processing response [this=%p]\n",
this));
mCallOnResume = [rv](nsHttpChannel* self) {
(void)self->ContinueProcessResponse2(rv);
return NS_OK;
};
return NS_OK;
}
if (NS_FAILED(rv) && !mCanceled) {
// The process switch failed, cancel this channel.
Cancel(rv);
return CallOnStartRequest();
}
if (mAPIRedirectTo && !mCanceled) {
MOZ_ASSERT(!LoadOnStartRequestCalled());
PushRedirectAsyncFunc(&nsHttpChannel::ContinueProcessResponse3);
rv = StartRedirectChannelToURI(
mAPIRedirectTo->first(),
mAPIRedirectTo->second() ? nsIChannelEventSink::REDIRECT_TEMPORARY |
nsIChannelEventSink::REDIRECT_TRANSPARENT
: nsIChannelEventSink::REDIRECT_TEMPORARY);
mAPIRedirectTo = Nothing();
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<uint32_t>(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
#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<uint32_t>(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.
MOZ_DIAGNOSTIC_ASSERT(mAuthProvider);
if (!mAuthProvider) {
mStatus = NS_ERROR_UNEXPECTED;
return ProcessNormal();
}
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
rv = NS_ERROR_FAILURE;
} else {
MOZ_DIAGNOSTIC_ASSERT(mAuthProvider);
rv = mAuthProvider
? mAuthProvider->ProcessAuthentication(
httpStatus, mConnectionInfo->EndToEndSSL() &&
mTransaction &&
mTransaction->ProxyConnectFailed())
: NS_ERROR_UNEXPECTED;
}
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"));
Suspend();
#ifdef DEBUG
gHttpHandler->OnTransactionSuspendedDueToAuthentication(this);
#endif
rv = NS_OK;
} else if (NS_FAILED(rv)) {
LOG(("ProcessAuthentication failed [rv=%" PRIx32 "]\n",
static_cast<uint32_t>(rv)));
if (mTransaction && mTransaction->ProxyConnectFailed()) {
return ProcessFailedProxyConnect(httpStatus);
}
if (rv == NS_ERROR_BASIC_HTTP_AUTH_DISABLED) {
mStatus = rv;
} else if (!mAuthRetryPending) {
MOZ_DIAGNOSTIC_ASSERT(mAuthProvider);
rv = mAuthProvider ? mAuthProvider->CheckForSuperfluousAuth()
: NS_ERROR_UNEXPECTED;
if (NS_FAILED(rv)) {
mStatus = rv;
LOG(("CheckForSuperfluousAuth failed [rv=%x]\n",
static_cast<uint32_t>(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<uint32_t>(aRv)));
UpdateCacheDisposition(false, NS_SUCCEEDED(aRv));
return aRv;
}
nsresult nsHttpChannel::ContinueProcessResponseAfterNotModified(nsresult aRv) {
LOG(
("nsHttpChannel::ContinueProcessResponseAfterNotModified "
"[this=%p, rv=%" PRIx32 "]",
this, static_cast<uint32_t>(aRv)));
if (NS_SUCCEEDED(aRv)) {
StoreTransactionReplaced(true);
UpdateCacheDisposition(true, false);
return NS_OK;
}
LOG(("ProcessNotModified failed [rv=%" PRIx32 "]\n",
static_cast<uint32_t>(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()) {
glean::http::response_version.AccumulateSingleSample(
static_cast<uint32_t>(version));
}
mozilla::glean::networking::http_response_version
.Get(HttpVersionToTelemetryLabel(version))
.Add(1);
}
void nsHttpChannel::UpdateCacheDisposition(bool aSuccessfulReval,
bool aPartialContentUsed) {
if (mRaceDelay && !mRaceCacheWithNetwork &&
(LoadCachedContentIsPartial() || mDidReval)) {
if (aSuccessfulReval || aPartialContentUsed) {
glean::network::race_cache_validation
.EnumGet(glean::network::RaceCacheValidationLabel::eCachedcontentused)
.Add();
} else {
glean::network::race_cache_validation
.EnumGet(
glean::network::RaceCacheValidationLabel::eCachedcontentnotused)
.Add();
}
}
PROFILER_MARKER_TEXT(
"CacheDisposition", NETWORK, {},
nsPrintfCString(
!mDidReval ? "Missed"
: (aSuccessfulReval ? "HitViaReval" : "MissedViaReval")));
if (Telemetry::CanRecordPrereleaseData()) {
CacheDisposition cacheDisposition;
if (!mDidReval) {
cacheDisposition = kCacheMissed;
} else if (aSuccessfulReval) {
cacheDisposition = kCacheHitViaReval;
} else {
cacheDisposition = kCacheMissedViaReval;
}
AccumulateCacheHitTelemetry(cacheDisposition, this);
mCacheDisposition = cacheDisposition;
}
ReportHttpResponseVersion(mResponseHead->Version());
}
// Only used for redirects (3XX responses)
nsresult nsHttpChannel::ContinueProcessResponse4(nsresult rv) {
bool doNotRender = DoNotRender3xxBody(rv);
if (rv == NS_ERROR_DOM_BAD_URI && mRedirectURI &&
!net::SchemeIsHttpOrHttps(mRedirectURI)) {
// 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<uint32_t>(rv)));
}
CloseCacheEntry(false);
return NS_OK;
}
LOG(("ContinueProcessResponse4 got failure result [rv=%" PRIx32 "]\n",
static_cast<uint32_t>(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
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
if (mCacheEntry) {
rv = InitCacheEntry();
if (NS_FAILED(rv)) CloseCacheEntry(true);
}
// We may need to install the cache listener before CallonStartRequest,
// since InstallCacheListener can modify the Content-Encoding to remove
// dcb/dcz (and perhaps others), and CallOnStartRequest() sends the
// Content-Encoding to the content process. If this doesn't install a
// listener (because this isn't a dictionary or dictionary-compressed),
// call it after CallOnStartRequest so that we save the compressed data
// in the cache, and run the decompressor in the content process.
bool isDictionaryCompressed = false;
nsAutoCString contentEncoding;
(void)mResponseHead->GetHeader(nsHttp::Content_Encoding, contentEncoding);
// Note: doesn't handle dcb, gzip or gzip, dcb (etc)
if (contentEncoding.Equals("dcb") || contentEncoding.Equals("dcz")) {
isDictionaryCompressed = true;
}
if (mCacheEntry && !LoadCacheEntryIsReadOnly()) {
// XXX We may want to consider recompressing any dcb/dcz files to save space
// and improve hitrate. Downside is CPU use, complexity and perhaps delay,
// maybe.
nsAutoCString dictionary;
if (StaticPrefs::network_http_dictionaries_enable() && IsHTTPS()) {
(void)mResponseHead->GetHeader(nsHttp::Use_As_Dictionary, dictionary);
if (!dictionary.IsEmpty()) {
if (!ParseDictionary(mCacheEntry, mResponseHead.get(), true)) {
LOG_DICTIONARIES(("Failed to parse use-as-dictionary"));
} else {
MOZ_ASSERT(mDictSaving);
// We need to record the hash as we save it
mCacheEntry->SetDictionary(mDictSaving);
}
}
}
if (isDictionaryCompressed || mDictSaving) {
LOG(("Decompressing before saving into cache [channel=%p]", this));
rv = DoInstallCacheListener(isDictionaryCompressed, &dictionary, 0);
}
} else {
if (isDictionaryCompressed) {
// We still need to decompress in the parent if it's dcb or dcz even if
// not saving to the cache
LOG_DICTIONARIES(
("Removing Content-Encoding %s for %p", contentEncoding.get(), this));
nsCOMPtr<nsIStreamListener> listener;
// otherwise we won't convert in the parent process
// XXX may be redundant, but safe
SetApplyConversion(true);
rv = DoApplyContentConversionsInternal(
mListener, getter_AddRefs(listener), true, nullptr);
if (NS_FAILED(rv)) {
return rv;
}
if (listener) {
LOG_DICTIONARIES(("Installed nsHTTPCompressConv %p without cache tee",
listener.get()));
mListener = listener;
mCompressListener = listener;
StoreHasAppliedConversion(true);
} else {
LOG_DICTIONARIES(("Didn't install decompressor without cache tee"));
}
}
} // else we'll call InstallCacheListener after CallOnStartRequest
// 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);
}
}
}
// If we don't have the entire dictionary yet, Suspend() the channel
// until the dictionary is in-memory.
if (mDictDecompress && mUsingDictionary && mShouldSuspendForDictionary &&
!mDictDecompress->DictionaryReady()) {
LOG(
("nsHttpChannel::ContinueProcessNormal [this=%p] Suspending the "
"transaction, waiting for dictionary",
this));
Suspend();
mSuspendedForDictionary = true;
}
rv = CallOnStartRequest();
if (NS_FAILED(rv)) return rv;
// If we didn't install cache listeners to decompress above
// install the cache listener now (so they'll get compressed data)
if (!isDictionaryCompressed && !mDictSaving) {
// 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<nsIStringBundleService> bundleService;
bundleService = mozilla::components::StringBundle::Service(&rv);
if (NS_FAILED(rv)) return rv;
nsCOMPtr<nsIStringBundle> 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<nsIPrompt> 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<nsIProtocolProxyService> pps;
pps = mozilla::components::ProtocolProxy::Service(&rv);
if (NS_FAILED(rv)) return rv;
nsCOMPtr<nsIProxyInfo> 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<nsIDNSHTTPSSVCRecord>&& aRecord) {
LOG(("nsHttpChannel::SetHTTPSSVCRecord [this=%p]\n", this));
nsCOMPtr<nsIDNSHTTPSSVCRecord> 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<uint32_t>(rv), this));
}
}
}
nsresult nsHttpChannel::StartRedirectChannelToHttps() {
LOG(("nsHttpChannel::HandleAsyncRedirectChannelToHttps() [STS]\n"));
nsCOMPtr<nsIURI> 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(mAPIRedirectTo, "How did that happen?");
MOZ_ASSERT(mAPIRedirectTo->first(), "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(
mAPIRedirectTo->first(),
mAPIRedirectTo->second() ? nsIChannelEventSink::REDIRECT_PERMANENT |
nsIChannelEventSink::REDIRECT_TRANSPARENT
: nsIChannelEventSink::REDIRECT_PERMANENT);
if (NS_FAILED(rv)) {
rv = ContinueAsyncRedirectChannelToURI(rv);
if (NS_FAILED(rv)) {
LOG(("ContinueAsyncRedirectChannelToURI failed (%08x) [this=%p]\n",
static_cast<uint32_t>(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<nsIURI> 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<uint32_t>(rv), this));
}
}
}
nsresult nsHttpChannel::RedirectToNewChannelForAuthRetry() {
LOG(("nsHttpChannel::RedirectToNewChannelForAuthRetry %p", this));
nsresult rv = NS_OK;
nsCOMPtr<nsILoadInfo> redirectLoadInfo = CloneLoadInfoForRedirect(
mURI, nsIChannelEventSink::REDIRECT_INTERNAL |
nsIChannelEventSink::REDIRECT_AUTH_RETRY);
nsCOMPtr<nsIIOService> ioService;
rv = gHttpHandler->GetIOService(getter_AddRefs(ioService));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIChannel> 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<nsISeekableStream> 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<nsHttpChannel> 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;
}
}
// always set sticky connection flag
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) {
return StartRedirectChannelToURI(upgradedURI, flags, [](nsIChannel*) {});
}
nsresult nsHttpChannel::StartRedirectChannelToURI(
nsIURI* upgradedURI, uint32_t flags,
std::function<void(nsIChannel*)>&& aCallback) {
nsresult rv = NS_OK;
LOG(("nsHttpChannel::StartRedirectChannelToURI()\n"));
nsCOMPtr<nsIChannel> newChannel;
nsCOMPtr<nsILoadInfo> redirectLoadInfo =
CloneLoadInfoForRedirect(upgradedURI, flags);
nsCOMPtr<nsIIOService> 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<nsHttpChannel> httpChan = do_QueryObject(newChannel);
nsCOMPtr<nsIDNSHTTPSSVCRecord> rec = mHTTPSSVCRecord.ref();
if (httpChan && rec) {
httpChan->SetHTTPSSVCRecord(rec.forget());
}
}
// Inform consumers about this fake redirect
mRedirectChannel = newChannel;
aCallback(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 mAPIRedirectTo uri also after on-examine-response handler
// rather drop it here to avoid any redirect loops, even just hypothetical.
mAPIRedirectTo = Nothing();
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<nsIChannel> 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<nsIProtocolProxyService> pps;
pps = mozilla::components::ProtocolProxy::Service(&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<nsIProtocolProxyService2> pps2 = do_QueryInterface(pps);
if (pps2) {
rv = pps2->AsyncResolve2(this, mProxyResolveFlags, this, nullptr,
getter_AddRefs(mProxyRequest));
} else {
rv = pps->AsyncResolve(static_cast<nsIChannel*>(this), mProxyResolveFlags,
this, nullptr, getter_AddRefs(mProxyRequest));
}
return rv;
}
bool nsHttpChannel::ResponseWouldVary(nsICacheEntry* entry) {
nsresult rv;
nsAutoCString buf, metaKey;
(void)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;
}
// Remove an entry from Vary header, if it exists
void RemoveFromVary(nsHttpResponseHead* aResponseHead,
const nsACString& aRemove) {
nsAutoCString buf;
(void)aResponseHead->GetHeader(nsHttp::Vary, buf);
bool remove = false;
for (const nsACString& token :
nsCCharSeparatedTokenizer(buf, NS_HTTP_HEADER_SEP).ToRange()) {
if (token.Equals(aRemove)) {
// Need to build a new string without aRemove
remove = true;
break;
}
}
if (!remove) {
return;
}
nsAutoCString newValue;
for (const nsACString& token :
nsCCharSeparatedTokenizer(buf, NS_HTTP_HEADER_SEP).ToRange()) {
if (!token.Equals(aRemove)) {
if (!newValue.IsEmpty()) {
newValue += ","_ns;
}
newValue += token;
}
}
LOG(("RemoveFromVary %s removed, new value -> %s",
PromiseFlatCString(aRemove).get(), newValue.get()));
(void)aResponseHead->SetHeaderOverride(nsHttp::Vary, newValue);
}
// 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<nsHttpChannel>::HandleAsyncAbort();
}
//-----------------------------------------------------------------------------
// nsHttpChannel <byte-range>
//-----------------------------------------------------------------------------
bool nsHttpChannel::IsResumable(int64_t partialLen, int64_t contentLength,
bool ignoreMissingPartialLen) const {
bool hasContentEncoding =
mCachedResponseHead->HasHeader(nsHttp::Content_Encoding);
nsAutoCString etag;
(void)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;
(void)mCachedResponseHead->GetHeader(nsHttp::ETag, val);
if (val.IsEmpty()) {
(void)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<nsresult> 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<nsresult> 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<nsresult(nsHttpChannel*, nsresult)>&
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
(void)mResponseHead->GetHeader(nsHttp::Content_Encoding, contentEncoding);
(void)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;
(void)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 =