diff options
Diffstat (limited to 'netwerk/protocol/http/nsHttpChannel.cpp')
-rw-r--r-- | netwerk/protocol/http/nsHttpChannel.cpp | 8563 |
1 files changed, 8563 insertions, 0 deletions
diff --git a/netwerk/protocol/http/nsHttpChannel.cpp b/netwerk/protocol/http/nsHttpChannel.cpp new file mode 100644 index 000000000..0e570e8cb --- /dev/null +++ b/netwerk/protocol/http/nsHttpChannel.cpp @@ -0,0 +1,8563 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* vim:set expandtab ts=4 sw=4 sts=4 cin: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// HttpLog.h should generally be included first +#include "HttpLog.h" + +#include <inttypes.h> + +#include "mozilla/dom/nsCSPContext.h" +#include "mozilla/Sprintf.h" + +#include "nsHttp.h" +#include "nsHttpChannel.h" +#include "nsHttpHandler.h" +#include "nsIApplicationCacheService.h" +#include "nsIApplicationCacheContainer.h" +#include "nsICacheStorageService.h" +#include "nsICacheStorage.h" +#include "nsICacheEntry.h" +#include "nsICaptivePortalService.h" +#include "nsICryptoHash.h" +#include "nsINetworkInterceptController.h" +#include "nsINSSErrorsService.h" +#include "nsISecurityReporter.h" +#include "nsIStringBundle.h" +#include "nsIStreamListenerTee.h" +#include "nsISeekableStream.h" +#include "nsILoadGroupChild.h" +#include "nsIProtocolProxyService2.h" +#include "nsIURIClassifier.h" +#include "nsMimeTypes.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsIURL.h" +#include "nsIStreamTransportService.h" +#include "prnetdb.h" +#include "nsEscape.h" +#include "nsStreamUtils.h" +#include "nsIOService.h" +#include "nsDNSPrefetch.h" +#include "nsChannelClassifier.h" +#include "nsIRedirectResultListener.h" +#include "mozilla/dom/ContentVerifier.h" +#include "mozilla/TimeStamp.h" +#include "nsError.h" +#include "nsPrintfCString.h" +#include "nsAlgorithm.h" +#include "nsQueryObject.h" +#include "GeckoProfiler.h" +#include "nsIConsoleService.h" +#include "mozilla/Attributes.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Preferences.h" +#include "nsISSLSocketControl.h" +#include "sslt.h" +#include "nsContentUtils.h" +#include "nsContentSecurityManager.h" +#include "nsIClassOfService.h" +#include "nsIPermissionManager.h" +#include "nsIPrincipal.h" +#include "nsIScriptSecurityManager.h" +#include "nsISSLStatus.h" +#include "nsISSLStatusProvider.h" +#include "nsITransportSecurityInfo.h" +#include "nsIWebProgressListener.h" +#include "LoadContextInfo.h" +#include "netCore.h" +#include "nsHttpTransaction.h" +#include "nsICacheEntryDescriptor.h" +#include "nsICancelable.h" +#include "nsIHttpChannelAuthProvider.h" +#include "nsIHttpChannelInternal.h" +#include "nsIHttpEventSink.h" +#include "nsIPrompt.h" +#include "nsInputStreamPump.h" +#include "nsURLHelper.h" +#include "nsISocketTransport.h" +#include "nsIStreamConverterService.h" +#include "nsISiteSecurityService.h" +#include "nsString.h" +#include "nsCRT.h" +#include "CacheObserver.h" +#include "mozilla/dom/Performance.h" +#include "mozilla/Telemetry.h" +#include "AlternateServices.h" +#include "InterceptedChannel.h" +#include "nsIHttpPushListener.h" +#include "nsIX509Cert.h" +#include "ScopedNSSTypes.h" +#include "nsNullPrincipal.h" +#include "nsIDeprecationWarner.h" +#include "nsIDocument.h" +#include "nsIDOMDocument.h" +#include "nsICompressConvStats.h" +#include "nsCORSListenerProxy.h" +#include "nsISocketProvider.h" +#include "mozilla/net/Predictor.h" +#include "CacheControlParser.h" +#include "nsMixedContentBlocker.h" +#include "HSTSPrimerListener.h" +#include "CacheStorageService.h" + +namespace mozilla { namespace net { + +namespace { + +// Monotonically increasing ID for generating unique cache entries per +// intercepted channel. +static uint64_t gNumIntercepted = 0; + +// True if the local cache should be bypassed when processing a request. +#define BYPASS_LOCAL_CACHE(loadFlags) \ + (loadFlags & (nsIRequest::LOAD_BYPASS_CACHE | \ + nsICachingChannel::LOAD_BYPASS_LOCAL_CACHE)) + +#define RECOVER_FROM_CACHE_FILE_ERROR(result) \ + ((result) == NS_ERROR_FILE_NOT_FOUND || \ + (result) == NS_ERROR_FILE_CORRUPTED || \ + (result) == NS_ERROR_OUT_OF_MEMORY) + +static NS_DEFINE_CID(kStreamListenerTeeCID, NS_STREAMLISTENERTEE_CID); +static NS_DEFINE_CID(kStreamTransportServiceCID, + NS_STREAMTRANSPORTSERVICE_CID); + +enum CacheDisposition { + kCacheHit = 1, + kCacheHitViaReval = 2, + kCacheMissedViaReval = 3, + kCacheMissed = 4 +}; + +void +AccumulateCacheHitTelemetry(CacheDisposition hitOrMiss) +{ + if (!CacheObserver::UseNewCache()) { + Telemetry::Accumulate(Telemetry::HTTP_CACHE_DISPOSITION_2, hitOrMiss); + } + else { + Telemetry::Accumulate(Telemetry::HTTP_CACHE_DISPOSITION_2_V2, hitOrMiss); + + int32_t experiment = CacheObserver::HalfLifeExperiment(); + if (experiment > 0 && hitOrMiss == kCacheMissed) { + Telemetry::Accumulate(Telemetry::HTTP_CACHE_MISS_HALFLIFE_EXPERIMENT_2, + experiment - 1); + } + } +} + +// 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; +} + +bool +IsInSubpathOfAppCacheManifest(nsIApplicationCache *cache, nsACString const& uriSpec) +{ + MOZ_ASSERT(cache); + + static bool sForbid = true; + static nsresult once = Preferences::AddBoolVarCache(&sForbid, "network.appcache.forbid-fallback-outside-manifest-path", true); + Unused << once; + + if (!sForbid) { + return true; + } + + nsresult rv; + + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), uriSpec); + if (NS_FAILED(rv)) { + return false; + } + + nsCOMPtr<nsIURL> url(do_QueryInterface(uri, &rv)); + if (NS_FAILED(rv)) { + return false; + } + + nsAutoCString directory; + rv = url->GetDirectory(directory); + if (NS_FAILED(rv)) { + return false; + } + + nsCOMPtr<nsIURI> manifestURI; + rv = cache->GetManifestURI(getter_AddRefs(manifestURI)); + if (NS_FAILED(rv)) { + return false; + } + + nsCOMPtr<nsIURL> manifestURL(do_QueryInterface(manifestURI, &rv)); + if (NS_FAILED(rv)) { + return false; + } + + nsAutoCString manifestDirectory; + rv = manifestURL->GetDirectory(manifestDirectory); + if (NS_FAILED(rv)) { + return false; + } + + return StringBeginsWith(directory, manifestDirectory); +} + +} // unnamed namespace + +// We only treat 3xx responses as redirects if they have a Location header and +// the status code is in a whitelist. +bool +WillRedirect(nsHttpResponseHead * response) +{ + return nsHttpChannel::IsRedirectStatus(response->Status()) && + response->HasHeader(nsHttp::Location); +} + +nsresult +StoreAuthorizationMetaData(nsICacheEntry *entry, nsHttpRequestHead *requestHead); + +class AutoRedirectVetoNotifier +{ +public: + explicit AutoRedirectVetoNotifier(nsHttpChannel* channel) : mChannel(channel) + { + if (mChannel->mHasAutoRedirectVetoNotifier) { + MOZ_CRASH("Nested AutoRedirectVetoNotifier on the stack"); + mChannel = nullptr; + return; + } + + mChannel->mHasAutoRedirectVetoNotifier = true; + } + ~AutoRedirectVetoNotifier() {ReportRedirectResult(false);} + void RedirectSucceeded() {ReportRedirectResult(true);} + +private: + nsHttpChannel* mChannel; + void ReportRedirectResult(bool succeeded); +}; + +void +AutoRedirectVetoNotifier::ReportRedirectResult(bool succeeded) +{ + if (!mChannel) + return; + + mChannel->mRedirectChannel = nullptr; + + nsCOMPtr<nsIRedirectResultListener> vetoHook; + NS_QueryNotificationCallbacks(mChannel, + NS_GET_IID(nsIRedirectResultListener), + getter_AddRefs(vetoHook)); + + nsHttpChannel* channel = mChannel; + mChannel = nullptr; + + if (vetoHook) + vetoHook->OnRedirectResult(succeeded); + + // Drop after the notification + channel->mHasAutoRedirectVetoNotifier = false; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel <public> +//----------------------------------------------------------------------------- + +nsHttpChannel::nsHttpChannel() + : HttpAsyncAborter<nsHttpChannel>(this) + , mLogicalOffset(0) + , mPostID(0) + , mRequestTime(0) + , mOfflineCacheLastModifiedTime(0) + , mInterceptCache(DO_NOT_INTERCEPT) + , mInterceptionID(gNumIntercepted++) + , mCacheOpenWithPriority(false) + , mCacheQueueSizeWhenOpen(0) + , mCachedContentIsValid(false) + , mCachedContentIsPartial(false) + , mCacheOnlyMetadata(false) + , mTransactionReplaced(false) + , mAuthRetryPending(false) + , mProxyAuthPending(false) + , mCustomAuthHeader(false) + , mResuming(false) + , mInitedCacheEntry(false) + , mFallbackChannel(false) + , mCustomConditionalRequest(false) + , mFallingBack(false) + , mWaitingForRedirectCallback(false) + , mRequestTimeInitialized(false) + , mCacheEntryIsReadOnly(false) + , mCacheEntryIsWriteOnly(false) + , mCacheEntriesToWaitFor(0) + , mHasQueryString(0) + , mConcurrentCacheAccess(0) + , mIsPartialRequest(0) + , mHasAutoRedirectVetoNotifier(0) + , mPinCacheContent(0) + , mIsCorsPreflightDone(0) + , mStronglyFramed(false) + , mUsedNetwork(0) + , mAuthConnectionRestartable(0) + , mPushedStream(nullptr) + , mLocalBlocklist(false) + , mWarningReporter(nullptr) + , mDidReval(false) +{ + LOG(("Creating nsHttpChannel [this=%p]\n", this)); + mChannelCreationTime = PR_Now(); + mChannelCreationTimestamp = TimeStamp::Now(); +} + +nsHttpChannel::~nsHttpChannel() +{ + LOG(("Destroying nsHttpChannel [this=%p]\n", this)); + + if (mAuthProvider) + mAuthProvider->Disconnect(NS_ERROR_ABORT); +} + +nsresult +nsHttpChannel::Init(nsIURI *uri, + uint32_t caps, + nsProxyInfo *proxyInfo, + uint32_t proxyResolveFlags, + nsIURI *proxyURI, + const nsID& channelId) +{ + nsresult rv = HttpBaseChannel::Init(uri, caps, proxyInfo, + proxyResolveFlags, proxyURI, channelId); + if (NS_FAILED(rv)) + return rv; + + LOG(("nsHttpChannel::Init [this=%p]\n", this)); + + return rv; +} + +nsresult +nsHttpChannel::AddSecurityMessage(const nsAString& aMessageTag, + const nsAString& aMessageCategory) +{ + if (mWarningReporter) { + return mWarningReporter->ReportSecurityMessage(aMessageTag, + aMessageCategory); + } + return HttpBaseChannel::AddSecurityMessage(aMessageTag, + aMessageCategory); +} + +//----------------------------------------------------------------------------- +// nsHttpChannel <private> +//----------------------------------------------------------------------------- + +nsresult +nsHttpChannel::Connect() +{ + nsresult rv; + + LOG(("nsHttpChannel::Connect [this=%p]\n", this)); + + // Note that we are only setting the "Upgrade-Insecure-Requests" request + // header for *all* navigational requests instead of all requests as + // defined in the spec, see: + // https://www.w3.org/TR/upgrade-insecure-requests/#preference + nsContentPolicyType type = mLoadInfo ? + mLoadInfo->GetExternalContentPolicyType() : + nsIContentPolicy::TYPE_OTHER; + + if (type == nsIContentPolicy::TYPE_DOCUMENT || + type == nsIContentPolicy::TYPE_SUBDOCUMENT) { + rv = SetRequestHeader(NS_LITERAL_CSTRING("Upgrade-Insecure-Requests"), + NS_LITERAL_CSTRING("1"), false); + NS_ENSURE_SUCCESS(rv, rv); + } + + bool isHttps = false; + rv = mURI->SchemeIs("https", &isHttps); + NS_ENSURE_SUCCESS(rv,rv); + nsCOMPtr<nsIPrincipal> resultPrincipal; + if (!isHttps && mLoadInfo) { + nsContentUtils::GetSecurityManager()-> + GetChannelResultPrincipal(this, getter_AddRefs(resultPrincipal)); + } + bool shouldUpgrade = false; + rv = NS_ShouldSecureUpgrade(mURI, + mLoadInfo, + resultPrincipal, + mPrivateBrowsing, + mAllowSTS, + shouldUpgrade); + NS_ENSURE_SUCCESS(rv, rv); + if (shouldUpgrade) { + return AsyncCall(&nsHttpChannel::HandleAsyncRedirectChannelToHttps); + } + + // ensure that we are using a valid hostname + if (!net_IsValidHostName(nsDependentCString(mConnectionInfo->Origin()))) + return NS_ERROR_UNKNOWN_HOST; + + if (mUpgradeProtocolCallback) { + mCaps |= NS_HTTP_DISALLOW_SPDY; + } + + // 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) || mBeConservative); + + // Consider opening a TCP connection right away. + SpeculativeConnect(); + + // Don't allow resuming when cache must be used + if (mResuming && (mLoadFlags & LOAD_ONLY_FROM_CACHE)) { + LOG(("Resuming from cache is not supported yet")); + return NS_ERROR_DOCUMENT_NOT_CACHED; + } + + // open a cache entry for this channel... + rv = OpenCacheEntry(isHttps); + + // 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"); + return NS_OK; + } + + if (NS_FAILED(rv)) { + LOG(("OpenCacheEntry failed [rv=%x]\n", 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) { + // If we have a fallback URI (and we're not already + // falling back), process the fallback asynchronously. + if (!mFallbackChannel && !mFallbackKey.IsEmpty()) { + return AsyncCall(&nsHttpChannel::HandleAsyncFallback); + } + return NS_ERROR_DOCUMENT_NOT_CACHED; + } + // otherwise, let's just proceed without using the cache. + } + + return TryHSTSPriming(); +} + +nsresult +nsHttpChannel::TryHSTSPriming() +{ + if (mLoadInfo) { + // HSTS priming requires the LoadInfo provided with AsyncOpen2 + bool requireHSTSPriming = + mLoadInfo->GetForceHSTSPriming(); + + if (requireHSTSPriming && + nsMixedContentBlocker::sSendHSTSPriming && + mInterceptCache == DO_NOT_INTERCEPT) { + bool isHttpsScheme; + nsresult rv = mURI->SchemeIs("https", &isHttpsScheme); + NS_ENSURE_SUCCESS(rv, rv); + if (!isHttpsScheme) { + rv = HSTSPrimingListener::StartHSTSPriming(this, this); + + if (NS_FAILED(rv)) { + CloseCacheEntry(false); + return rv; + } + + return NS_OK; + } + + // The request was already upgraded, for example by + // upgrade-insecure-requests or a prior successful priming request + Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS_PRIMING_RESULT, + HSTSPrimingResult::eHSTS_PRIMING_ALREADY_UPGRADED); + mLoadInfo->ClearHSTSPriming(); + } + } + + return ContinueConnect(); +} + +nsresult +nsHttpChannel::ContinueConnect() +{ + // If we have had HSTS priming, we need to reevaluate whether we need + // a CORS preflight. Bug: 1272440 + // If we need to start a CORS preflight, do it now! + // Note that it is important to do this before the early returns below. + if (!mIsCorsPreflightDone && mRequireCORSPreflight && + mInterceptCache != INTERCEPTED) { + MOZ_ASSERT(!mPreflightChannel); + nsresult rv = + nsCORSListenerProxy::StartCORSPreflight(this, this, + mUnsafeHeaders, + getter_AddRefs(mPreflightChannel)); + return rv; + } + + MOZ_RELEASE_ASSERT(!(mRequireCORSPreflight && + mInterceptCache != INTERCEPTED) || + mIsCorsPreflightDone, + "CORS preflight must have been finished by the time we " + "do the rest of ContinueConnect"); + + // we may or may not have a cache entry at this point + if (mCacheEntry) { + // read straight from the cache if possible... + if (mCachedContentIsValid) { + nsRunnableMethod<nsHttpChannel> *event = nullptr; + if (!mCachedContentIsPartial) { + AsyncCall(&nsHttpChannel::AsyncOnExamineCachedResponse, &event); + } + nsresult rv = ReadFromCache(true); + if (NS_FAILED(rv) && event) { + event->Revoke(); + } + + // Don't accumulate the cache hit telemetry for intercepted channels. + if (mInterceptCache != INTERCEPTED) { + AccumulateCacheHitTelemetry(kCacheHit); + } + + return rv; + } + else if (mLoadFlags & LOAD_ONLY_FROM_CACHE) { + // the cache contains the requested resource, but it must be + // validated before we can reuse it. since we are not allowed + // to hit the net, there's nothing more to do. the document + // is effectively not in the cache. + LOG((" !mCachedContentIsValid && mLoadFlags & LOAD_ONLY_FROM_CACHE")); + return NS_ERROR_DOCUMENT_NOT_CACHED; + } + } + else if (mLoadFlags & LOAD_ONLY_FROM_CACHE) { + // If we have a fallback URI (and we're not already + // falling back), process the fallback asynchronously. + if (!mFallbackChannel && !mFallbackKey.IsEmpty()) { + return AsyncCall(&nsHttpChannel::HandleAsyncFallback); + } + LOG((" !mCacheEntry && mLoadFlags & LOAD_ONLY_FROM_CACHE")); + return NS_ERROR_DOCUMENT_NOT_CACHED; + } + + if (mLoadFlags & LOAD_NO_NETWORK_IO) { + LOG((" mLoadFlags & LOAD_NO_NETWORK_IO")); + return NS_ERROR_DOCUMENT_NOT_CACHED; + } + + // hit the net... + nsresult rv = SetupTransaction(); + if (NS_FAILED(rv)) return rv; + + rv = gHttpHandler->InitiateTransaction(mTransaction, mPriority); + if (NS_FAILED(rv)) return rv; + + rv = mTransactionPump->AsyncRead(this, nullptr); + if (NS_FAILED(rv)) return rv; + + uint32_t suspendCount = mSuspendCount; + 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 on a local blocklist, on uses of the offline + // application cache, 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). + if (mLocalBlocklist || mApplicationCache || gIOService->IsOffline() || + mUpgradeProtocolCallback || !(mCaps & NS_HTTP_ALLOW_KEEPALIVE)) + return; + + // LOAD_ONLY_FROM_CACHE and LOAD_NO_NETWORK_IO must not hit network. + // LOAD_FROM_CACHE and LOAD_CHECK_OFFLINE_CACHE are unlikely to hit network, + // so skip preconnects for them. + if (mLoadFlags & (LOAD_ONLY_FROM_CACHE | LOAD_FROM_CACHE | + LOAD_NO_NETWORK_IO | LOAD_CHECK_OFFLINE_CACHE)) + return; + + if (mAllowStaleCacheContent) { + return; + } + + nsCOMPtr<nsIInterfaceRequestor> callbacks; + NS_NewNotificationCallbacksAggregation(mCallbacks, mLoadGroup, + getter_AddRefs(callbacks)); + if (!callbacks) + return; + + gHttpHandler->SpeculativeConnect( + mConnectionInfo, callbacks, mCaps & NS_HTTP_DISALLOW_SPDY); +} + +void +nsHttpChannel::DoNotifyListenerCleanup() +{ + // We don't need this info anymore + CleanRedirectCacheChainIfNecessary(); +} + +void +nsHttpChannel::HandleAsyncRedirect() +{ + NS_PRECONDITION(!mCallOnResume, "How did that happen?"); + + if (mSuspendCount) { + LOG(("Waiting until resume to do async redirect [this=%p]\n", this)); + mCallOnResume = &nsHttpChannel::HandleAsyncRedirect; + return; + } + + nsresult rv = NS_OK; + + LOG(("nsHttpChannel::HandleAsyncRedirect [this=%p]\n", this)); + + // since this event is handled asynchronously, it is possible that this + // channel could have been canceled, in which case there would be no point + // in processing the redirect. + if (NS_SUCCEEDED(mStatus)) { + PushRedirectAsyncFunc(&nsHttpChannel::ContinueHandleAsyncRedirect); + rv = AsyncProcessRedirection(mResponseHead->Status()); + if (NS_FAILED(rv)) { + PopRedirectAsyncFunc(&nsHttpChannel::ContinueHandleAsyncRedirect); + // TODO: if !DoNotRender3xxBody(), render redirect body instead. + // But first we need to cache 3xx bodies (bug 748510) + ContinueHandleAsyncRedirect(rv); + } + } + else { + ContinueHandleAsyncRedirect(mStatus); + } +} + +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=%x]\n", rv)); + + bool redirectsEnabled = + !mLoadInfo || !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); + + mIsPending = false; + + if (mLoadGroup) + mLoadGroup->RemoveRequest(this, nullptr, mStatus); + + return NS_OK; +} + +void +nsHttpChannel::HandleAsyncNotModified() +{ + NS_PRECONDITION(!mCallOnResume, "How did that happen?"); + + if (mSuspendCount) { + LOG(("Waiting until resume to do async not-modified [this=%p]\n", + this)); + mCallOnResume = &nsHttpChannel::HandleAsyncNotModified; + return; + } + + LOG(("nsHttpChannel::HandleAsyncNotModified [this=%p]\n", this)); + + DoNotifyListener(); + + CloseCacheEntry(false); + + mIsPending = false; + + if (mLoadGroup) + mLoadGroup->RemoveRequest(this, nullptr, mStatus); +} + +void +nsHttpChannel::HandleAsyncFallback() +{ + NS_PRECONDITION(!mCallOnResume, "How did that happen?"); + + if (mSuspendCount) { + LOG(("Waiting until resume to do async fallback [this=%p]\n", this)); + mCallOnResume = &nsHttpChannel::HandleAsyncFallback; + return; + } + + nsresult rv = NS_OK; + + LOG(("nsHttpChannel::HandleAsyncFallback [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 fallback. + if (!mCanceled) { + PushRedirectAsyncFunc(&nsHttpChannel::ContinueHandleAsyncFallback); + bool waitingForRedirectCallback; + rv = ProcessFallback(&waitingForRedirectCallback); + if (waitingForRedirectCallback) + return; + PopRedirectAsyncFunc(&nsHttpChannel::ContinueHandleAsyncFallback); + } + + ContinueHandleAsyncFallback(rv); +} + +nsresult +nsHttpChannel::ContinueHandleAsyncFallback(nsresult rv) +{ + if (!mCanceled && (NS_FAILED(rv) || !mFallingBack)) { + // If ProcessFallback fails, then we have to send out the + // OnStart/OnStop notifications. + LOG(("ProcessFallback failed [rv=%x, %d]\n", rv, mFallingBack)); + mStatus = NS_FAILED(rv) ? rv : NS_ERROR_DOCUMENT_NOT_CACHED; + DoNotifyListener(); + } + + mIsPending = false; + + if (mLoadGroup) + mLoadGroup->RemoveRequest(this, nullptr, mStatus); + + return rv; +} + +void +nsHttpChannel::SetupTransactionRequestContext() +{ + if (!EnsureRequestContextID()) { + return; + } + + nsIRequestContextService *rcsvc = + gHttpHandler->GetRequestContextService(); + if (!rcsvc) { + return; + } + + nsCOMPtr<nsIRequestContext> rc; + nsresult rv = rcsvc->GetRequestContext(mRequestContextID, + getter_AddRefs(rc)); + + if (NS_FAILED(rv)) { + return; + } + + mTransaction->SetRequestContext(rc); +} + +static bool +SafeForPipelining(nsHttpRequestHead::ParsedMethodType method, + const nsCString &methodString) +{ + if (method == nsHttpRequestHead::kMethod_Get || + method == nsHttpRequestHead::kMethod_Head || + method == nsHttpRequestHead::kMethod_Options) { + return true; + } + + if (method != nsHttpRequestHead::kMethod_Custom) { + return false; + } + + return (!strcmp(methodString.get(), "PROPFIND") || + !strcmp(methodString.get(), "PROPPATCH")); +} + +nsresult +nsHttpChannel::SetupTransaction() +{ + LOG(("nsHttpChannel::SetupTransaction [this=%p]\n", this)); + + NS_ENSURE_TRUE(!mTransaction, NS_ERROR_ALREADY_INITIALIZED); + + nsresult rv; + + mUsedNetwork = 1; + if (mCaps & NS_HTTP_ALLOW_PIPELINING) { + // + // disable pipelining if: + // (1) pipelining has been disabled by config + // (2) pipelining has been disabled by connection mgr info + // (3) request corresponds to a top-level document load (link click) + // (4) request method is non-idempotent + // (5) request is marked slow (e.g XHR) + // + nsAutoCString method; + mRequestHead.Method(method); + if (!mAllowPipelining || + (mLoadFlags & (LOAD_INITIAL_DOCUMENT_URI | INHIBIT_PIPELINE)) || + !SafeForPipelining(mRequestHead.ParsedMethod(), method)) { + LOG((" pipelining disallowed\n")); + mCaps &= ~NS_HTTP_ALLOW_PIPELINING; + } + } + + if (!mAllowSpdy) { + mCaps |= NS_HTTP_DISALLOW_SPDY; + } + if (mBeConservative) { + mCaps |= NS_HTTP_BE_CONSERVATIVE; + } + + // 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->GetPath(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, buf)) { + requestURI = &buf; + } else { + requestURI = &path; + } + + // trim off the #ref portion if any... + int32_t ref1 = requestURI->FindChar('#'); + if (ref1 != kNotFound) { + requestURI->SetLength(ref1); + } + + if (mConnectionInfo->UsingConnect() || !mConnectionInfo->UsingHttpProxy()) { + mRequestHead.SetVersion(gHttpHandler->HttpVersion()); + } + else { + mRequestHead.SetPath(*requestURI); + + // RequestURI should be the absolute uri H1 proxy syntax "http://foo/index.html" + // so we will overwrite the relative version in requestURI + rv = mURI->GetUserPass(buf); + if (NS_FAILED(rv)) return rv; + if (!buf.IsEmpty() && ((strncmp(mSpec.get(), "http:", 5) == 0) || + strncmp(mSpec.get(), "https:", 6) == 0)) { + nsCOMPtr<nsIURI> tempURI; + rv = mURI->Clone(getter_AddRefs(tempURI)); + if (NS_FAILED(rv)) return rv; + rv = tempURI->SetUserPass(EmptyCString()); + if (NS_FAILED(rv)) return rv; + 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(); + mRequestTimeInitialized = 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. + mRequestHead.SetHeaderOnce(nsHttp::Pragma, "no-cache", true); + // If we're configured to speak HTTP/1.1 then also send 'Cache-control: + // no-cache' + if (mRequestHead.Version() >= NS_HTTP_VERSION_1_1) + mRequestHead.SetHeaderOnce(nsHttp::Cache_Control, "no-cache", true); + } + else if ((mLoadFlags & VALIDATE_ALWAYS) && !mCacheEntryIsWriteOnly) { + // We need to send 'Cache-Control: max-age=0' to force each cache along + // the path to the origin server to revalidate its own entry, if any, + // with the next cache or server. See bug #84847. + // + // If we're configured to speak HTTP/1.0 then just send 'Pragma: no-cache' + if (mRequestHead.Version() >= NS_HTTP_VERSION_1_1) + mRequestHead.SetHeaderOnce(nsHttp::Cache_Control, "max-age=0", true); + else + mRequestHead.SetHeaderOnce(nsHttp::Pragma, "no-cache", true); + } + + if (mResuming) { + char byteRange[32]; + SprintfLiteral(byteRange, "bytes=%" PRIu64 "-", mStartPos); + mRequestHead.SetHeader(nsHttp::Range, nsDependentCString(byteRange)); + + 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; + mRequestHead.SetHeader(nsHttp::If_Match, + NS_UnescapeURL(Substring(start, slash), 0, ifMatch)); + + ++slash; // Incrementing, so that searching for '/' won't find + // the same slash again + } + + if (FindCharInReadable('/', slash, end)) { + mRequestHead.SetHeader(nsHttp::If_Unmodified_Since, + Substring(++slash, end)); + } + } + } + + // create wrapper for this channel's notification callbacks + nsCOMPtr<nsIInterfaceRequestor> callbacks; + NS_NewNotificationCallbacksAggregation(mCallbacks, mLoadGroup, + getter_AddRefs(callbacks)); + + // create the transaction object + mTransaction = new nsHttpTransaction(); + LOG(("nsHttpChannel %p created nsHttpTransaction %p\n", this, mTransaction.get())); + mTransaction->SetTransactionObserver(mTransactionObserver); + mTransactionObserver = nullptr; + + // See bug #466080. Transfer LOAD_ANONYMOUS flag to socket-layer. + if (mLoadFlags & LOAD_ANONYMOUS) + mCaps |= NS_HTTP_LOAD_ANONYMOUS; + + if (mTimingEnabled) + mCaps |= NS_HTTP_TIMING_ENABLED; + + if (mUpgradeProtocolCallback) { + mRequestHead.SetHeader(nsHttp::Upgrade, mUpgradeProtocol, false); + mRequestHead.SetHeaderOnce(nsHttp::Connection, + nsHttp::Upgrade.get(), + true); + mCaps |= NS_HTTP_STICKY_CONNECTION; + mCaps &= ~NS_HTTP_ALLOW_PIPELINING; + mCaps &= ~NS_HTTP_ALLOW_KEEPALIVE; + } + + if (mPushedStream) { + mTransaction->SetPushedStream(mPushedStream); + mPushedStream = nullptr; + } + + nsCOMPtr<nsIHttpPushListener> pushListener; + NS_QueryNotificationCallbacks(mCallbacks, + mLoadGroup, + NS_GET_IID(nsIHttpPushListener), + getter_AddRefs(pushListener)); + if (pushListener) { + mCaps |= NS_HTTP_ONPUSH_LISTENER; + } + + nsCOMPtr<nsIAsyncInputStream> responseStream; + rv = mTransaction->Init(mCaps, mConnectionInfo, &mRequestHead, + mUploadStream, mUploadStreamHasHeaders, + NS_GetCurrentThread(), callbacks, this, + getter_AddRefs(responseStream)); + if (NS_FAILED(rv)) { + mTransaction = nullptr; + return rv; + } + + mTransaction->SetClassOfService(mClassOfService); + SetupTransactionRequestContext(); + + rv = nsInputStreamPump::Create(getter_AddRefs(mTransactionPump), + responseStream); + return rv; +} + +// NOTE: This function duplicates code from nsBaseChannel. This will go away +// once HTTP uses nsBaseChannel (part of bug 312760) +static void +CallTypeSniffers(void *aClosure, const uint8_t *aData, uint32_t aCount) +{ + nsIChannel *chan = static_cast<nsIChannel*>(aClosure); + + nsAutoCString newType; + NS_SniffContent(NS_CONTENT_SNIFFER_CATEGORY, chan, aData, aCount, newType); + if (!newType.IsEmpty()) { + chan->SetContentType(newType); + } +} + +// Helper Function to report messages to the console when loading +// a resource was blocked due to a MIME type mismatch. +void +ReportTypeBlocking(nsIURI* aURI, + nsILoadInfo* aLoadInfo, + const char* aMessageName) +{ + NS_ConvertUTF8toUTF16 specUTF16(aURI->GetSpecOrDefault()); + const char16_t* params[] = { specUTF16.get() }; + nsCOMPtr<nsIDocument> doc; + if (aLoadInfo) { + nsCOMPtr<nsIDOMDocument> domDoc; + aLoadInfo->GetLoadingDocument(getter_AddRefs(domDoc)); + if (domDoc) { + doc = do_QueryInterface(domDoc); + } + } + nsContentUtils::ReportToConsole(nsIScriptError::errorFlag, + NS_LITERAL_CSTRING("MIMEMISMATCH"), + doc, + nsContentUtils::eSECURITY_PROPERTIES, + aMessageName, + params, ArrayLength(params)); +} + +// Check and potentially enforce X-Content-Type-Options: nosniff +nsresult +ProcessXCTO(nsIURI* aURI, nsHttpResponseHead* aResponseHead, nsILoadInfo* aLoadInfo) +{ + if (!aURI || !aResponseHead || !aLoadInfo) { + // if there is no uri, no response head or no loadInfo, then there is nothing to do + return NS_OK; + } + + // 1) Query the XCTO header and check if 'nosniff' is the first value. + nsAutoCString contentTypeOptionsHeader; + aResponseHead->GetHeader(nsHttp::X_Content_Type_Options, contentTypeOptionsHeader); + if (contentTypeOptionsHeader.IsEmpty()) { + // if there is no XCTO header, then there is nothing to do. + return NS_OK; + } + // XCTO header might contain multiple values which are comma separated, so: + // a) let's skip all subsequent values + // e.g. " NoSniFF , foo " will be " NoSniFF " + int32_t idx = contentTypeOptionsHeader.Find(","); + if (idx > 0) { + contentTypeOptionsHeader = Substring(contentTypeOptionsHeader, 0, idx); + } + // b) let's trim all surrounding whitespace + // e.g. " NoSniFF " -> "NoSniFF" + contentTypeOptionsHeader.StripWhitespace(); + // c) let's compare the header (ignoring case) + // e.g. "NoSniFF" -> "nosniff" + // if it's not 'nosniff' then there is nothing to do here + if (!contentTypeOptionsHeader.EqualsIgnoreCase("nosniff")) { + // since we are getting here, the XCTO header was sent; + // a non matching value most likely means a mistake happenend; + // e.g. sending 'nosnif' instead of 'nosniff', let's log a warning. + NS_ConvertUTF8toUTF16 char16_header(contentTypeOptionsHeader); + const char16_t* params[] = { char16_header.get() }; + nsCOMPtr<nsIDocument> doc; + nsCOMPtr<nsIDOMDocument> domDoc; + aLoadInfo->GetLoadingDocument(getter_AddRefs(domDoc)); + if (domDoc) { + doc = do_QueryInterface(domDoc); + } + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, + NS_LITERAL_CSTRING("XCTO"), + doc, + nsContentUtils::eSECURITY_PROPERTIES, + "XCTOHeaderValueMissing", + params, ArrayLength(params)); + return NS_OK; + } + + // 2) Query the content type from the channel + nsAutoCString contentType; + aResponseHead->ContentType(contentType); + + // 3) Compare the expected MIME type with the actual type + if (aLoadInfo->GetExternalContentPolicyType() == nsIContentPolicy::TYPE_STYLESHEET) { + if (contentType.EqualsLiteral(TEXT_CSS)) { + return NS_OK; + } + ReportTypeBlocking(aURI, aLoadInfo, "MimeTypeMismatch"); + return NS_ERROR_CORRUPTED_CONTENT; + } + + if (aLoadInfo->GetExternalContentPolicyType() == nsIContentPolicy::TYPE_IMAGE) { + if (StringBeginsWith(contentType, NS_LITERAL_CSTRING("image/"))) { + Accumulate(Telemetry::XCTO_NOSNIFF_BLOCK_IMAGE, 0); + return NS_OK; + } + Accumulate(Telemetry::XCTO_NOSNIFF_BLOCK_IMAGE, 1); + // Instead of consulting Preferences::GetBool() all the time we + // can cache the result to speed things up. + static bool sXCTONosniffBlockImages = false; + static bool sIsInited = false; + if (!sIsInited) { + sIsInited = true; + Preferences::AddBoolVarCache(&sXCTONosniffBlockImages, + "security.xcto_nosniff_block_images"); + } + if (!sXCTONosniffBlockImages) { + return NS_OK; + } + ReportTypeBlocking(aURI, aLoadInfo, "MimeTypeMismatch"); + return NS_ERROR_CORRUPTED_CONTENT; + } + + if (aLoadInfo->GetExternalContentPolicyType() == nsIContentPolicy::TYPE_SCRIPT) { + if (nsContentUtils::IsScriptType(contentType)) { + return NS_OK; + } + ReportTypeBlocking(aURI, aLoadInfo, "MimeTypeMismatch"); + return NS_ERROR_CORRUPTED_CONTENT; + } + return NS_OK; +} + +// Ensure that a load of type script has correct MIME type +nsresult +EnsureMIMEOfScript(nsIURI* aURI, nsHttpResponseHead* aResponseHead, nsILoadInfo* aLoadInfo) +{ + if (!aURI || !aResponseHead || !aLoadInfo) { + // if there is no uri, no response head or no loadInfo, then there is nothing to do + return NS_OK; + } + + if (aLoadInfo->GetExternalContentPolicyType() != nsIContentPolicy::TYPE_SCRIPT) { + // if this is not a script load, then there is nothing to do + return NS_OK; + } + + nsAutoCString contentType; + aResponseHead->ContentType(contentType); + NS_ConvertUTF8toUTF16 typeString(contentType); + + if (nsContentUtils::IsJavascriptMIMEType(typeString)) { + // script load has type script + Telemetry::Accumulate(Telemetry::SCRIPT_BLOCK_INCORRECT_MIME, 1); + return NS_OK; + } + + bool block = false; + if (StringBeginsWith(contentType, NS_LITERAL_CSTRING("image/"))) { + // script load has type image + Telemetry::Accumulate(Telemetry::SCRIPT_BLOCK_INCORRECT_MIME, 2); + block = true; + } else if (StringBeginsWith(contentType, NS_LITERAL_CSTRING("audio/"))) { + // script load has type audio + Telemetry::Accumulate(Telemetry::SCRIPT_BLOCK_INCORRECT_MIME, 3); + block = true; + } else if (StringBeginsWith(contentType, NS_LITERAL_CSTRING("video/"))) { + // script load has type video + Telemetry::Accumulate(Telemetry::SCRIPT_BLOCK_INCORRECT_MIME, 4); + block = true; + } else if (StringBeginsWith(contentType, NS_LITERAL_CSTRING("text/csv"))) { + // script load has type text/csv + Telemetry::Accumulate(Telemetry::SCRIPT_BLOCK_INCORRECT_MIME, 6); + block = true; + } + + if (block) { + // Instead of consulting Preferences::GetBool() all the time we + // can cache the result to speed things up. + static bool sCachedBlockScriptWithWrongMime = false; + static bool sIsInited = false; + if (!sIsInited) { + sIsInited = true; + Preferences::AddBoolVarCache(&sCachedBlockScriptWithWrongMime, + "security.block_script_with_wrong_mime"); + } + + // Do not block the load if the feature is not enabled. + if (!sCachedBlockScriptWithWrongMime) { + return NS_OK; + } + + ReportTypeBlocking(aURI, aLoadInfo, "BlockScriptWithWrongMimeType"); + return NS_ERROR_CORRUPTED_CONTENT; + } + + if (StringBeginsWith(contentType, NS_LITERAL_CSTRING("text/plain"))) { + // script load has type text/plain + Telemetry::Accumulate(Telemetry::SCRIPT_BLOCK_INCORRECT_MIME, 5); + return NS_OK; + } + + if (StringBeginsWith(contentType, NS_LITERAL_CSTRING("text/xml"))) { + // script load has type text/xml + Telemetry::Accumulate(Telemetry::SCRIPT_BLOCK_INCORRECT_MIME, 7); + return NS_OK; + } + + if (StringBeginsWith(contentType, NS_LITERAL_CSTRING("application/octet-stream"))) { + // script load has type application/octet-stream + Telemetry::Accumulate(Telemetry::SCRIPT_BLOCK_INCORRECT_MIME, 8); + return NS_OK; + } + + if (StringBeginsWith(contentType, NS_LITERAL_CSTRING("application/xml"))) { + // script load has type application/xml + Telemetry::Accumulate(Telemetry::SCRIPT_BLOCK_INCORRECT_MIME, 9); + return NS_OK; + } + + if (StringBeginsWith(contentType, NS_LITERAL_CSTRING("text/html"))) { + // script load has type text/html + Telemetry::Accumulate(Telemetry::SCRIPT_BLOCK_INCORRECT_MIME, 10); + return NS_OK; + } + + if (contentType.IsEmpty()) { + // script load has no type + Telemetry::Accumulate(Telemetry::SCRIPT_BLOCK_INCORRECT_MIME, 11); + return NS_OK; + } + + // script load has unknown type + Telemetry::Accumulate(Telemetry::SCRIPT_BLOCK_INCORRECT_MIME, 0); + return NS_OK; +} + + +nsresult +nsHttpChannel::CallOnStartRequest() +{ + MOZ_RELEASE_ASSERT(!(mRequireCORSPreflight && + mInterceptCache != INTERCEPTED) || + mIsCorsPreflightDone, + "CORS preflight must have been finished by the time we " + "call OnStartRequest"); + + nsresult rv = EnsureMIMEOfScript(mURI, mResponseHead, mLoadInfo); + NS_ENSURE_SUCCESS(rv, rv); + + rv = ProcessXCTO(mURI, mResponseHead, mLoadInfo); + NS_ENSURE_SUCCESS(rv, rv); + + if (mOnStartRequestCalled) { + // 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(mConcurrentCacheAccess); + LOG(("CallOnStartRequest already invoked before")); + return mStatus; + } + + mTracingEnabled = false; + + // 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) { + mTransactionPump->PeekStream(CallTypeSniffers, thisChannel); + } + } + + 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() == NS_HTTP_VERSION_0_9 && + mConnectionInfo->OriginPort() != mConnectionInfo->DefaultPort()) + mResponseHead->SetContentType(NS_LITERAL_CSTRING(TEXT_PLAIN)); + else { + // Uh-oh. We had better find out what type we are! + nsCOMPtr<nsIStreamConverterService> serv; + rv = gHttpHandler-> + GetStreamConverterService(getter_AddRefs(serv)); + // If we failed, we just fall through to the "normal" case + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIStreamListener> converter; + rv = serv->AsyncConvertData(UNKNOWN_CONTENT_TYPE, + "*/*", + mListener, + mListenerContext, + getter_AddRefs(converter)); + if (NS_SUCCEEDED(rv)) { + mListener = converter; + unknownDecoderStarted = true; + } + } + } + } + + if (mResponseHead && !mResponseHead->HasContentCharset()) + mResponseHead->SetContentCharset(mContentCharsetHint); + + if (mResponseHead && mCacheEntry) { + // If we have a cache entry, set its predicted size to TotalEntitySize to + // avoid caching an entry that will exceed the max size limit. + rv = mCacheEntry->SetPredictedDataSize( + mResponseHead->TotalEntitySize()); + if (NS_ERROR_FILE_TOO_BIG == rv) { + // Don't throw the entry away, we will need it later. + LOG((" entry too big")); + } else { + NS_ENSURE_SUCCESS(rv, rv); + } + } + + LOG((" calling mListener->OnStartRequest\n")); + if (mListener) { + MOZ_ASSERT(!mOnStartRequestCalled, + "We should not call OsStartRequest twice"); + nsCOMPtr<nsIStreamListener> deleteProtector(mListener); + rv = deleteProtector->OnStartRequest(this, mListenerContext); + mOnStartRequestCalled = true; + if (NS_FAILED(rv)) + return rv; + } else { + NS_WARNING("OnStartRequest skipped because of null listener"); + mOnStartRequestCalled = true; + } + + // Install stream converter if required. + // If we use unknownDecoder, stream converters will be installed later (in + // nsUnknownDecoder) after OnStartRequest is called for the real listener. + if (!unknownDecoderStarted) { + nsCOMPtr<nsIStreamListener> listener; + nsISupports *ctxt = mListenerContext; + rv = DoApplyContentConversions(mListener, getter_AddRefs(listener), ctxt); + if (NS_FAILED(rv)) { + return rv; + } + if (listener) { + mListener = listener; + mCompressListener = listener; + } + } + + rv = EnsureAssocReq(); + if (NS_FAILED(rv)) + return rv; + + // if this channel is for a download, close off access to the cache. + if (mCacheEntry && mChannelIsForDownload) { + 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. + if (!mCachedContentIsPartial && !mConcurrentCacheAccess) + CloseCacheEntry(false); + } + + if (!mCanceled) { + // create offline cache entry if offline caching was requested + if (ShouldUpdateOfflineCacheEntry()) { + LOG(("writing to the offline cache")); + rv = InitOfflineCacheEntry(); + if (NS_FAILED(rv)) return rv; + + // InitOfflineCacheEntry may have closed mOfflineCacheEntry + if (mOfflineCacheEntry) { + rv = InstallOfflineCacheListener(); + if (NS_FAILED(rv)) return rv; + } + } else if (mApplicationCacheForWrite) { + LOG(("offline cache is up to date, not updating")); + CloseOfflineCacheEntry(); + } + } + + // Check for a Content-Signature header and inject mediator if the header is + // requested and available. + // If requested (mLoadInfo->GetVerifySignedContent), but not present, or + // present but not valid, fail this channel and return + // NS_ERROR_INVALID_SIGNATURE to indicate a signature error and trigger a + // fallback load in nsDocShell. + // Note that OnStartRequest has already been called on the target stream + // listener at this point. We have to add the listener here that late to + // ensure that it's the last listener and can thus block the load in + // OnStopRequest. + if (!mCanceled) { + rv = ProcessContentSignatureHeader(mResponseHead); + if (NS_FAILED(rv)) { + LOG(("Content-signature verification failed.\n")); + return rv; + } + } + + 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; + switch (httpStatus) + { + case 300: case 301: case 302: case 303: case 307: case 308: + // Bad redirect: not top-level, or it's a POST, bad/missing Location, + // or ProcessRedirect() failed for some other reason. Legal + // redirects that fail because site not available, etc., are handled + // elsewhere, in the regular codepath. + rv = NS_ERROR_CONNECTION_REFUSED; + break; + case 403: // HTTP/1.1: "Forbidden" + case 407: // ProcessAuthentication() failed + case 501: // HTTP/1.1: "Not Implemented" + // user sees boilerplate Mozilla "Proxy Refused Connection" page. + rv = NS_ERROR_PROXY_CONNECTION_REFUSED; + break; + // Squid sends 404 if DNS fails (regular 404 from target is tunneled) + case 404: // HTTP/1.1: "Not Found" + // RFC 2616: "some deployed proxies are known to return 400 or 500 when + // DNS lookups time out." (Squid uses 500 if it runs out of sockets: so + // we have a conflict here). + case 400: // HTTP/1.1 "Bad Request" + case 500: // HTTP/1.1: "Internal Server Error" + /* User sees: "Address Not Found: Firefox can't find the server at + * www.foo.com." + */ + rv = NS_ERROR_UNKNOWN_HOST; + break; + case 502: // HTTP/1.1: "Bad Gateway" (invalid resp from target server) + // Squid returns 503 if target request fails for anything but DNS. + case 503: // HTTP/1.1: "Service Unavailable" + /* User sees: "Failed to Connect: + * Firefox can't establish a connection to the server at + * www.foo.com. Though the site seems valid, the browser + * was unable to establish a connection." + */ + rv = NS_ERROR_CONNECTION_REFUSED; + break; + // RFC 2616 uses 504 for both DNS and target timeout, so not clear what to + // do here: picking target timeout, as DNS covered by 400/404/500 + case 504: // HTTP/1.1: "Gateway Timeout" + // user sees: "Network Timeout: The server at www.foo.com + // is taking too long to respond." + rv = NS_ERROR_NET_TIMEOUT; + break; + // Confused proxy server or malicious response + default: + rv = NS_ERROR_PROXY_CONNECTION_REFUSED; + break; + } + LOG(("Cancelling failed proxy CONNECT [this=%p httpStatus=%u]\n", + this, httpStatus)); + Cancel(rv); + CallOnStartRequest(); + return rv; +} + +static void +GetSTSConsoleErrorTag(uint32_t failureResult, nsAString& consoleErrorTag) +{ + switch (failureResult) { + case nsISiteSecurityService::ERROR_UNTRUSTWORTHY_CONNECTION: + consoleErrorTag = NS_LITERAL_STRING("STSUntrustworthyConnection"); + break; + case nsISiteSecurityService::ERROR_COULD_NOT_PARSE_HEADER: + consoleErrorTag = NS_LITERAL_STRING("STSCouldNotParseHeader"); + break; + case nsISiteSecurityService::ERROR_NO_MAX_AGE: + consoleErrorTag = NS_LITERAL_STRING("STSNoMaxAge"); + break; + case nsISiteSecurityService::ERROR_MULTIPLE_MAX_AGES: + consoleErrorTag = NS_LITERAL_STRING("STSMultipleMaxAges"); + break; + case nsISiteSecurityService::ERROR_INVALID_MAX_AGE: + consoleErrorTag = NS_LITERAL_STRING("STSInvalidMaxAge"); + break; + case nsISiteSecurityService::ERROR_MULTIPLE_INCLUDE_SUBDOMAINS: + consoleErrorTag = NS_LITERAL_STRING("STSMultipleIncludeSubdomains"); + break; + case nsISiteSecurityService::ERROR_INVALID_INCLUDE_SUBDOMAINS: + consoleErrorTag = NS_LITERAL_STRING("STSInvalidIncludeSubdomains"); + break; + case nsISiteSecurityService::ERROR_COULD_NOT_SAVE_STATE: + consoleErrorTag = NS_LITERAL_STRING("STSCouldNotSaveState"); + break; + default: + consoleErrorTag = NS_LITERAL_STRING("STSUnknownError"); + break; + } +} + +static void +GetPKPConsoleErrorTag(uint32_t failureResult, nsAString& consoleErrorTag) +{ + switch (failureResult) { + case nsISiteSecurityService::ERROR_UNTRUSTWORTHY_CONNECTION: + consoleErrorTag = NS_LITERAL_STRING("PKPUntrustworthyConnection"); + break; + case nsISiteSecurityService::ERROR_COULD_NOT_PARSE_HEADER: + consoleErrorTag = NS_LITERAL_STRING("PKPCouldNotParseHeader"); + break; + case nsISiteSecurityService::ERROR_NO_MAX_AGE: + consoleErrorTag = NS_LITERAL_STRING("PKPNoMaxAge"); + break; + case nsISiteSecurityService::ERROR_MULTIPLE_MAX_AGES: + consoleErrorTag = NS_LITERAL_STRING("PKPMultipleMaxAges"); + break; + case nsISiteSecurityService::ERROR_INVALID_MAX_AGE: + consoleErrorTag = NS_LITERAL_STRING("PKPInvalidMaxAge"); + break; + case nsISiteSecurityService::ERROR_MULTIPLE_INCLUDE_SUBDOMAINS: + consoleErrorTag = NS_LITERAL_STRING("PKPMultipleIncludeSubdomains"); + break; + case nsISiteSecurityService::ERROR_INVALID_INCLUDE_SUBDOMAINS: + consoleErrorTag = NS_LITERAL_STRING("PKPInvalidIncludeSubdomains"); + break; + case nsISiteSecurityService::ERROR_INVALID_PIN: + consoleErrorTag = NS_LITERAL_STRING("PKPInvalidPin"); + break; + case nsISiteSecurityService::ERROR_MULTIPLE_REPORT_URIS: + consoleErrorTag = NS_LITERAL_STRING("PKPMultipleReportURIs"); + break; + case nsISiteSecurityService::ERROR_PINSET_DOES_NOT_MATCH_CHAIN: + consoleErrorTag = NS_LITERAL_STRING("PKPPinsetDoesNotMatch"); + break; + case nsISiteSecurityService::ERROR_NO_BACKUP_PIN: + consoleErrorTag = NS_LITERAL_STRING("PKPNoBackupPin"); + break; + case nsISiteSecurityService::ERROR_COULD_NOT_SAVE_STATE: + consoleErrorTag = NS_LITERAL_STRING("PKPCouldNotSaveState"); + break; + case nsISiteSecurityService::ERROR_ROOT_NOT_BUILT_IN: + consoleErrorTag = NS_LITERAL_STRING("PKPRootNotBuiltIn"); + break; + default: + consoleErrorTag = NS_LITERAL_STRING("PKPUnknownError"); + break; + } +} + +/** + * Process a single security header. Only two types are supported: HSTS and HPKP. + */ +nsresult +nsHttpChannel::ProcessSingleSecurityHeader(uint32_t aType, + nsISSLStatus *aSSLStatus, + uint32_t aFlags) +{ + nsHttpAtom atom; + switch (aType) { + case nsISiteSecurityService::HEADER_HSTS: + atom = nsHttp::ResolveAtom("Strict-Transport-Security"); + break; + case nsISiteSecurityService::HEADER_HPKP: + atom = nsHttp::ResolveAtom("Public-Key-Pins"); + break; + default: + NS_NOTREACHED("Invalid security header type"); + return NS_ERROR_FAILURE; + } + + nsAutoCString securityHeader; + nsresult rv = mResponseHead->GetHeader(atom, securityHeader); + if (NS_SUCCEEDED(rv)) { + nsISiteSecurityService* sss = gHttpHandler->GetSSService(); + NS_ENSURE_TRUE(sss, NS_ERROR_OUT_OF_MEMORY); + // Process header will now discard the headers itself if the channel + // wasn't secure (whereas before it had to be checked manually) + uint32_t failureResult; + rv = sss->ProcessHeader(aType, mURI, securityHeader.get(), aSSLStatus, + aFlags, nullptr, nullptr, &failureResult); + if (NS_FAILED(rv)) { + nsAutoString consoleErrorCategory; + nsAutoString consoleErrorTag; + switch (aType) { + case nsISiteSecurityService::HEADER_HSTS: + GetSTSConsoleErrorTag(failureResult, consoleErrorTag); + consoleErrorCategory = NS_LITERAL_STRING("Invalid HSTS Headers"); + break; + case nsISiteSecurityService::HEADER_HPKP: + GetPKPConsoleErrorTag(failureResult, consoleErrorTag); + consoleErrorCategory = NS_LITERAL_STRING("Invalid HPKP Headers"); + break; + default: + return NS_ERROR_FAILURE; + } + AddSecurityMessage(consoleErrorTag, consoleErrorCategory); + LOG(("nsHttpChannel: Failed to parse %s header, continuing load.\n", + atom.get())); + } + } else { + if (rv != NS_ERROR_NOT_AVAILABLE) { + // All other errors are fatal + NS_ENSURE_SUCCESS(rv, rv); + } + LOG(("nsHttpChannel: No %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() +{ + nsresult rv; + bool isHttps = false; + rv = mURI->SchemeIs("https", &isHttps); + NS_ENSURE_SUCCESS(rv, rv); + + // 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 (!isHttps) + return NS_OK; + + nsAutoCString asciiHost; + 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 + PRNetAddr hostAddr; + if (PR_SUCCESS == PR_StringToNetAddr(asciiHost.get(), &hostAddr)) + 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); + + uint32_t flags = + NS_UsePrivateBrowsing(this) ? nsISocketProvider::NO_PERMANENT_STORAGE : 0; + + // Get the SSLStatus + nsCOMPtr<nsISSLStatusProvider> sslprov = do_QueryInterface(mSecurityInfo); + NS_ENSURE_TRUE(sslprov, NS_ERROR_FAILURE); + nsCOMPtr<nsISSLStatus> sslStatus; + rv = sslprov->GetSSLStatus(getter_AddRefs(sslStatus)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(sslStatus, NS_ERROR_FAILURE); + + rv = ProcessSingleSecurityHeader(nsISiteSecurityService::HEADER_HSTS, + sslStatus, flags); + NS_ENSURE_SUCCESS(rv, rv); + + rv = ProcessSingleSecurityHeader(nsISiteSecurityService::HEADER_HPKP, + sslStatus, flags); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +nsHttpChannel::ProcessContentSignatureHeader(nsHttpResponseHead *aResponseHead) +{ + nsresult rv = NS_OK; + + // we only do this if we require it in loadInfo + if (!mLoadInfo || !mLoadInfo->GetVerifySignedContent()) { + return NS_OK; + } + + // check if we verify content signatures on this newtab channel + if (gHttpHandler->NewTabContentSignaturesDisabled()) { + return NS_OK; + } + + NS_ENSURE_TRUE(aResponseHead, NS_ERROR_ABORT); + nsAutoCString contentSignatureHeader; + nsHttpAtom atom = nsHttp::ResolveAtom("Content-Signature"); + rv = aResponseHead->GetHeader(atom, contentSignatureHeader); + if (NS_FAILED(rv)) { + LOG(("Content-Signature header is missing but expected.")); + DoInvalidateCacheEntry(mURI); + return NS_ERROR_INVALID_SIGNATURE; + } + + // if we require a signature but it is empty, fail + if (contentSignatureHeader.IsEmpty()) { + DoInvalidateCacheEntry(mURI); + LOG(("An expected content-signature header is missing.\n")); + return NS_ERROR_INVALID_SIGNATURE; + } + + // we ensure a content type here to avoid running into problems with + // content sniffing, which might sniff parts of the content before we can + // verify the signature + if (!aResponseHead->HasContentType()) { + NS_WARNING("Empty content type can get us in trouble when verifying " + "content signatures"); + return NS_ERROR_INVALID_SIGNATURE; + } + // create a new listener that meadiates the content + RefPtr<ContentVerifier> contentVerifyingMediator = + new ContentVerifier(mListener, mListenerContext); + rv = contentVerifyingMediator->Init(contentSignatureHeader, this, + mListenerContext); + NS_ENSURE_SUCCESS(rv, NS_ERROR_INVALID_SIGNATURE); + mListener = contentVerifyingMediator; + + return NS_OK; +} + +/** + * Decide whether or not to send a security report and, if so, give the + * SecurityReporter the information required to send such a report. + */ +void +nsHttpChannel::ProcessSecurityReport(nsresult status) { + uint32_t errorClass; + nsCOMPtr<nsINSSErrorsService> errSvc = + do_GetService("@mozilla.org/nss_errors_service;1"); + // getErrorClass will throw a generic NS_ERROR_FAILURE if the error code is + // not in the set of errors covered by the NSS errors service. + nsresult rv = errSvc->GetErrorClass(status, &errorClass); + if (!NS_SUCCEEDED(rv)) { + return; + } + + // if the content was not loaded succesfully and we have security info, + // send a TLS error report - we must do this early as other parts of + // OnStopRequest can return early + bool reportingEnabled = + Preferences::GetBool("security.ssl.errorReporting.enabled"); + bool reportingAutomatic = + Preferences::GetBool("security.ssl.errorReporting.automatic"); + if (!mSecurityInfo || !reportingEnabled || !reportingAutomatic) { + return; + } + + nsCOMPtr<nsITransportSecurityInfo> secInfo = + do_QueryInterface(mSecurityInfo); + nsCOMPtr<nsISecurityReporter> errorReporter = + do_GetService("@mozilla.org/securityreporter;1"); + + if (!secInfo || !mURI) { + return; + } + + nsAutoCString hostStr; + int32_t port; + rv = mURI->GetHost(hostStr); + if (!NS_SUCCEEDED(rv)) { + return; + } + + rv = mURI->GetPort(&port); + + if (NS_SUCCEEDED(rv)) { + errorReporter->ReportTLSError(secInfo, hostStr, port); + } +} + +bool +nsHttpChannel::IsHTTPS() +{ + bool isHttps; + if (NS_FAILED(mURI->SchemeIs("https", &isHttps)) || !isHttps) + return false; + return true; +} + +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; + + nsCOMPtr<nsISSLStatusProvider> statusProvider = + do_QueryInterface(mSecurityInfo); + if (!statusProvider) + return; + nsCOMPtr<nsISSLStatus> sslstat; + statusProvider->GetSSLStatus(getter_AddRefs(sslstat)); + if (!sslstat) + return; + + nsCOMPtr<nsITransportSecurityInfo> securityInfo = + do_QueryInterface(mSecurityInfo); + uint32_t state; + if (securityInfo && + NS_SUCCEEDED(securityInfo->GetSecurityState(&state)) && + (state & nsIWebProgressListener::STATE_IS_BROKEN)) { + // Send weak crypto warnings to the web console + if (state & nsIWebProgressListener::STATE_USES_WEAK_CRYPTO) { + nsString consoleErrorTag = NS_LITERAL_STRING("WeakCipherSuiteWarning"); + nsString consoleErrorCategory = NS_LITERAL_STRING("SSL"); + AddSecurityMessage(consoleErrorTag, consoleErrorCategory); + } + } + + // Send (SHA-1) signature algorithm errors to the web console + nsCOMPtr<nsIX509Cert> cert; + sslstat->GetServerCert(getter_AddRefs(cert)); + if (cert) { + UniqueCERTCertificate nssCert(cert->GetCert()); + if (nssCert) { + SECOidTag tag = SECOID_GetAlgorithmTag(&nssCert->signature); + LOG(("Checking certificate signature: The OID tag is %i [this=%p]\n", tag, this)); + // Check to see if the signature is sha-1 based. + // Not including checks for SEC_OID_ISO_SHA1_WITH_RSA_SIGNATURE + // from http://tools.ietf.org/html/rfc2437#section-8 since I + // can't see reference to it outside this spec + if (tag == SEC_OID_PKCS1_SHA1_WITH_RSA_ENCRYPTION || + tag == SEC_OID_ANSIX9_DSA_SIGNATURE_WITH_SHA1_DIGEST || + tag == SEC_OID_ANSIX962_ECDSA_SHA1_SIGNATURE) { + nsString consoleErrorTag = NS_LITERAL_STRING("SHA1Sig"); + nsString consoleErrorMessage + = NS_LITERAL_STRING("SHA-1 Signature"); + AddSecurityMessage(consoleErrorTag, consoleErrorMessage); + } + } + } +} + +void +nsHttpChannel::ProcessAltService() +{ + // e.g. Alt-Svc: h2=":443"; ma=60 + // e.g. Alt-Svc: h2="otherhost:443" + // Alt-Svc = 1#( alternative *( OWS ";" OWS parameter ) ) + // alternative = protocol-id "=" alt-authority + // protocol-id = token ; percent-encoded ALPN protocol identifier + // alt-authority = quoted-string ; containing [ uri-host ] ":" port + + if (!mAllowAltSvc) { // per channel opt out + return; + } + + if (!gHttpHandler->AllowAltSvc() || (mCaps & NS_HTTP_DISALLOW_SPDY)) { + return; + } + + nsAutoCString scheme; + mURI->GetScheme(scheme); + bool isHttp = scheme.Equals(NS_LITERAL_CSTRING("http")); + if (!isHttp && !scheme.Equals(NS_LITERAL_CSTRING("https"))) { + return; + } + + nsAutoCString altSvc; + 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->GetHost(originHost))) { + return; + } + + nsCOMPtr<nsIInterfaceRequestor> callbacks; + nsCOMPtr<nsProxyInfo> proxyInfo; + NS_NewNotificationCallbacksAggregation(mCallbacks, mLoadGroup, + getter_AddRefs(callbacks)); + if (mProxyInfo) { + proxyInfo = do_QueryInterface(mProxyInfo); + } + + NeckoOriginAttributes originAttributes; + NS_GetOriginAttributes(this, originAttributes); + + AltSvcMapping::ProcessHeader(altSvc, scheme, originHost, originPort, + mUsername, mPrivateBrowsing, callbacks, proxyInfo, + mCaps & NS_HTTP_DISALLOW_SPDY, + originAttributes); +} + +nsresult +nsHttpChannel::ProcessResponse() +{ + uint32_t httpStatus = mResponseHead->Status(); + + LOG(("nsHttpChannel::ProcessResponse [this=%p httpStatus=%u]\n", + this, httpStatus)); + + // do some telemetry + if (gHttpHandler->IsTelemetryEnabled()) { + // Gather data on whether the transaction and page (if this is + // the initial page load) is being loaded with SSL. + Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_IS_SSL, + mConnectionInfo->EndToEndSSL()); + if (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI) { + Telemetry::Accumulate(Telemetry::HTTP_PAGELOAD_IS_SSL, + mConnectionInfo->EndToEndSSL()); + } + + // how often do we see something like Alternate-Protocol: "443:quic,p=1" + nsAutoCString alt_protocol; + mResponseHead->GetHeader(nsHttp::Alternate_Protocol, alt_protocol); + bool saw_quic = (!alt_protocol.IsEmpty() && + PL_strstr(alt_protocol.get(), "quic")) ? 1 : 0; + Telemetry::Accumulate(Telemetry::HTTP_SAW_QUIC_ALT_PROTOCOL, saw_quic); + + // Gather data on how many URLS get redirected + switch (httpStatus) { + case 200: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 0); + break; + case 301: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 1); + break; + case 302: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 2); + break; + case 304: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 3); + break; + case 307: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 4); + break; + case 308: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 5); + break; + case 400: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 6); + break; + case 401: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 7); + break; + case 403: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 8); + break; + case 404: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 9); + break; + case 500: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 10); + break; + default: + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_STATUS_CODE, 11); + break; + } + } + + // Let the predictor know whether this was a cacheable response or not so + // that it knows whether or not to possibly prefetch this resource in the + // future. + // We use GetReferringPage because mReferrer may not be set at all, or may + // not be a full URI (HttpBaseChannel::SetReferrer has the gorey details). + // If that's null, though, we'll fall back to mReferrer just in case (this + // is especially useful in xpcshell tests, where we don't have an actual + // pageload to get a referrer from). + nsCOMPtr<nsIURI> referrer = GetReferringPage(); + if (!referrer) { + referrer = mReferrer; + } + if (referrer) { + nsCOMPtr<nsILoadContextInfo> lci = GetLoadContextInfo(this); + mozilla::net::Predictor::UpdateCacheability(referrer, mURI, httpStatus, + mRequestHead, mResponseHead, + lci); + } + + if (mTransaction->ProxyConnectFailed()) { + // Only allow 407 (authentication required) to continue + if (httpStatus != 407) + return ProcessFailedProxyConnect(httpStatus); + // If proxy CONNECT response needs to complete, wait to process connection + // for Strict-Transport-Security. + } else { + // Given a successful connection, process any STS or PKP data that's + // relevant. + DebugOnly<nsresult> rv = ProcessSecurityHeaders(); + MOZ_ASSERT(NS_SUCCEEDED(rv), "ProcessSTSHeader failed, continuing load."); + } + + MOZ_ASSERT(!mCachedContentIsValid); + + ProcessSSLInformation(); + + // notify "http-on-examine-response" observers + gHttpHandler->OnExamineResponse(this); + + return ContinueProcessResponse1(); +} + +void +nsHttpChannel::AsyncContinueProcessResponse() +{ + nsresult rv; + rv = ContinueProcessResponse1(); + if (NS_FAILED(rv)) { + // A synchronous failure here would normally be passed as the return + // value from OnStartRequest, which would in turn cancel the request. + // If we're continuing asynchronously, we need to cancel the request + // ourselves. + Unused << Cancel(rv); + } +} + +nsresult +nsHttpChannel::ContinueProcessResponse1() +{ + NS_PRECONDITION(!mCallOnResume, "How did that happen?"); + nsresult rv; + + if (mSuspendCount) { + LOG(("Waiting until resume to finish processing response [this=%p]\n", this)); + mCallOnResume = &nsHttpChannel::AsyncContinueProcessResponse; + return NS_OK; + } + + uint32_t httpStatus = mResponseHead->Status(); + + // Cookies and Alt-Service should not be handled on proxy failure either. + // This would be consolidated with ProcessSecurityHeaders but it should + // happen after OnExamineResponse. + if (!mTransaction->ProxyConnectFailed() && (httpStatus != 407)) { + nsAutoCString cookie; + if (NS_SUCCEEDED(mResponseHead->GetHeader(nsHttp::Set_Cookie, cookie))) { + SetCookie(cookie.get()); + } + if ((httpStatus < 500) && (httpStatus != 421)) { + ProcessAltService(); + } + } + + if (mConcurrentCacheAccess && mCachedContentIsPartial && httpStatus != 206) { + LOG((" only expecting 206 when doing partial request during " + "interrupted cache concurrent read")); + return NS_ERROR_CORRUPTED_CONTENT; + } + + // handle unused username and password in url (see bug 232567) + if (httpStatus != 401 && httpStatus != 407) { + if (!mAuthRetryPending) + mAuthProvider->CheckForSuperfluousAuth(); + if (mCanceled) + return CallOnStartRequest(); + + // reset the authentication's current continuation state because our + // last authentication attempt has been completed successfully + mAuthProvider->Disconnect(NS_ERROR_ABORT); + mAuthProvider = nullptr; + LOG((" continuation state has been reset")); + } + + if (mAPIRedirectToURI && !mCanceled) { + MOZ_ASSERT(!mOnStartRequestCalled); + nsCOMPtr<nsIURI> redirectTo; + mAPIRedirectToURI.swap(redirectTo); + + PushRedirectAsyncFunc(&nsHttpChannel::ContinueProcessResponse2); + rv = StartRedirectChannelToURI(redirectTo, nsIChannelEventSink::REDIRECT_TEMPORARY); + if (NS_SUCCEEDED(rv)) { + return NS_OK; + } + PopRedirectAsyncFunc(&nsHttpChannel::ContinueProcessResponse2); + } + + // Hack: ContinueProcessResponse2 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 ContinueProcessResponse2(NS_BINDING_FAILED); +} + +nsresult +nsHttpChannel::ContinueProcessResponse2(nsresult 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 successfulReval = false; + + // 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 (mResuming && 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 (mCachedContentIsPartial) // an internal byte range request... + rv = ProcessPartialContent(); + else { + mCacheInputStream.CloseAndRelease(); + rv = ProcessNormal(); + } + break; + case 300: + case 301: + case 302: + case 307: + case 308: + case 303: +#if 0 + case 305: // disabled as a security measure (see bug 187996). +#endif + // don't store the response body for redirects + MaybeInvalidateCacheEntryForSubsequentGet(); + PushRedirectAsyncFunc(&nsHttpChannel::ContinueProcessResponse3); + rv = AsyncProcessRedirection(httpStatus); + if (NS_FAILED(rv)) { + PopRedirectAsyncFunc(&nsHttpChannel::ContinueProcessResponse3); + LOG(("AsyncProcessRedirection failed [rv=%x]\n", rv)); + // don't cache failed redirect responses. + if (mCacheEntry) + mCacheEntry->AsyncDoom(nullptr); + if (DoNotRender3xxBody(rv)) { + mStatus = rv; + DoNotifyListener(); + } else { + rv = ContinueProcessResponse3(rv); + } + } + break; + case 304: + if (!ShouldBypassProcessNotModified()) { + rv = ProcessNotModified(); + if (NS_SUCCEEDED(rv)) { + successfulReval = true; + break; + } + + LOG(("ProcessNotModified failed [rv=%x]\n", rv)); + + // 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; + } + + rv = StartRedirectChannelToURI(mURI, nsIChannelEventSink::REDIRECT_INTERNAL); + if (NS_SUCCEEDED(rv)) { + return NS_OK; + } + } + + if (ShouldBypassProcessNotModified() || NS_FAILED(rv)) { + rv = ProcessNormal(); + } + break; + case 401: + case 407: + if (MOZ_UNLIKELY(mCustomAuthHeader) && 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 { + rv = mAuthProvider->ProcessAuthentication( + httpStatus, + mConnectionInfo->EndToEndSSL() && mTransaction->ProxyConnectFailed()); + } + if (rv == NS_ERROR_IN_PROGRESS) { + // authentication prompt has been invoked and result + // is expected asynchronously + mAuthRetryPending = true; + if (httpStatus == 407 || mTransaction->ProxyConnectFailed()) + mProxyAuthPending = true; + + // suspend the transaction pump to stop receiving the + // unauthenticated content data. We will throw that data + // away when user provides credentials or resume the pump + // when user refuses to authenticate. + LOG(("Suspending the transaction, asynchronously prompting for credentials")); + mTransactionPump->Suspend(); + rv = NS_OK; + } else if (NS_FAILED(rv)) { + LOG(("ProcessAuthentication failed [rv=%x]\n", rv)); + if (mTransaction->ProxyConnectFailed()) + return ProcessFailedProxyConnect(httpStatus); + if (!mAuthRetryPending) + mAuthProvider->CheckForSuperfluousAuth(); + rv = ProcessNormal(); + } else { + mAuthRetryPending = true; // see DoAuthRetry + } + break; + default: + rv = ProcessNormal(); + MaybeInvalidateCacheEntryForSubsequentGet(); + break; + } + + if (gHttpHandler->IsTelemetryEnabled()) { + CacheDisposition cacheDisposition; + if (!mDidReval) { + cacheDisposition = kCacheMissed; + } else if (successfulReval) { + cacheDisposition = kCacheHitViaReval; + } else { + cacheDisposition = kCacheMissedViaReval; + } + AccumulateCacheHitTelemetry(cacheDisposition); + + Telemetry::Accumulate(Telemetry::HTTP_RESPONSE_VERSION, + mResponseHead->Version()); + + if (mResponseHead->Version() == NS_HTTP_VERSION_0_9) { + // DefaultPortTopLevel = 0, DefaultPortSubResource = 1, + // NonDefaultPortTopLevel = 2, NonDefaultPortSubResource = 3 + uint32_t v09Info = 0; + if (!(mLoadFlags & LOAD_INITIAL_DOCUMENT_URI)) { + v09Info += 1; + } + if (mConnectionInfo->OriginPort() != mConnectionInfo->DefaultPort()) { + v09Info += 2; + } + Telemetry::Accumulate(Telemetry::HTTP_09_INFO, v09Info); + } + } + return rv; +} + +nsresult +nsHttpChannel::ContinueProcessResponse3(nsresult rv) +{ + bool doNotRender = DoNotRender3xxBody(rv); + + if (rv == NS_ERROR_DOM_BAD_URI && mRedirectURI) { + bool isHTTP = false; + if (NS_FAILED(mRedirectURI->SchemeIs("http", &isHTTP))) + isHTTP = false; + if (!isHTTP && NS_FAILED(mRedirectURI->SchemeIs("https", &isHTTP))) + isHTTP = false; + + if (!isHTTP) { + // This was a blocked attempt to redirect and subvert the system by + // redirecting to another protocol (perhaps javascript:) + // In that case we want to throw an error instead of displaying the + // non-redirected response body. + LOG(("ContinueProcessResponse3 detected rejected Non-HTTP Redirection")); + doNotRender = true; + rv = NS_ERROR_CORRUPTED_CONTENT; + } + } + + if (doNotRender) { + Cancel(rv); + DoNotifyListener(); + return rv; + } + + if (NS_SUCCEEDED(rv)) { + UpdateInhibitPersistentCachingFlag(); + + InitCacheEntry(); + CloseCacheEntry(false); + + if (mApplicationCacheForWrite) { + // Store response in the offline cache + InitOfflineCacheEntry(); + CloseOfflineCacheEntry(); + } + return NS_OK; + } + + LOG(("ContinueProcessResponse3 got failure result [rv=%x]\n", rv)); + if (mTransaction && mTransaction->ProxyConnectFailed()) { + return ProcessFailedProxyConnect(mRedirectType); + } + return ProcessNormal(); +} + +nsresult +nsHttpChannel::ProcessNormal() +{ + nsresult rv; + + LOG(("nsHttpChannel::ProcessNormal [this=%p]\n", this)); + + bool succeeded; + rv = GetRequestSucceeded(&succeeded); + if (NS_SUCCEEDED(rv) && !succeeded) { + PushRedirectAsyncFunc(&nsHttpChannel::ContinueProcessNormal); + bool waitingForRedirectCallback; + (void)ProcessFallback(&waitingForRedirectCallback); + if (waitingForRedirectCallback) { + // The transaction has been suspended by ProcessFallback. + return NS_OK; + } + PopRedirectAsyncFunc(&nsHttpChannel::ContinueProcessNormal); + } + + return ContinueProcessNormal(NS_OK); +} + +nsresult +nsHttpChannel::ContinueProcessNormal(nsresult rv) +{ + 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; + } + + if (mFallingBack) { + // Do not continue with normal processing, fallback is in + // progress now. + return NS_OK; + } + + // if we're here, then any byte-range requests failed to result in a partial + // response. we must clear this flag to prevent BufferPartialContent from + // being called inside our OnDataAvailable (see bug 136678). + mCachedContentIsPartial = false; + + ClearBogusContentEncodingIfNeeded(); + + UpdateInhibitPersistentCachingFlag(); + + // this must be called before firing OnStartRequest, since http clients, + // such as imagelib, expect our cache entry to already have the correct + // expiration time (bug 87710). + if (mCacheEntry) { + rv = InitCacheEntry(); + if (NS_FAILED(rv)) + CloseCacheEntry(true); + } + + // Check that the server sent us what we were asking for + if (mResuming) { + // Create an entity id from the response + nsAutoCString id; + rv = GetEntityID(id); + if (NS_FAILED(rv)) { + // If creating an entity id is not possible -> error + Cancel(NS_ERROR_NOT_RESUMABLE); + } + else if (mResponseHead->Status() != 206 && + mResponseHead->Status() != 200) { + // Probably 404 Not Found, 412 Precondition Failed or + // 416 Invalid Range -> error + LOG(("Unexpected response status while resuming, aborting [this=%p]\n", + this)); + Cancel(NS_ERROR_ENTITY_CHANGED); + } + // If we were passed an entity id, verify it's equal to the server's + else if (!mEntityID.IsEmpty()) { + if (!mEntityID.Equals(id)) { + LOG(("Entity mismatch, expected '%s', got '%s', aborting [this=%p]", + mEntityID.get(), id.get(), this)); + Cancel(NS_ERROR_ENTITY_CHANGED); + } + } + } + + rv = CallOnStartRequest(); + if (NS_FAILED(rv)) return rv; + + // install cache listener if we still have a cache entry open + if (mCacheEntry && !mCacheEntryIsReadOnly) { + 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 = + do_GetService(NS_STRINGBUNDLE_CONTRACTID, &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; + + nsXPIDLString messageString; + rv = stringBundle->GetStringFromName(u"RepostFormData", getter_Copies(messageString)); + // GetStringFromName can return NS_OK and nullptr messageString. + if (NS_SUCCEEDED(rv) && messageString) { + bool repost = false; + + nsCOMPtr<nsIPrompt> prompt; + GetCallback(prompt); + if (!prompt) + return NS_ERROR_NO_INTERFACE; + + prompt->Confirm(nullptr, messageString, &repost); + if (!repost) + return NS_ERROR_FAILURE; + } + + return rv; +} + +nsresult +nsHttpChannel::ProxyFailover() +{ + LOG(("nsHttpChannel::ProxyFailover [this=%p]\n", this)); + + nsresult rv; + + nsCOMPtr<nsIProtocolProxyService> pps = + do_GetService(NS_PROTOCOLPROXYSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) + return rv; + + nsCOMPtr<nsIProxyInfo> pi; + rv = pps->GetFailoverForProxy(mConnectionInfo->ProxyInfo(), mURI, mStatus, + getter_AddRefs(pi)); + if (NS_FAILED(rv)) + return rv; + + // XXXbz so where does this codepath remove us from the loadgroup, + // exactly? + return AsyncDoReplaceWithProxy(pi); +} + +void +nsHttpChannel::HandleAsyncRedirectChannelToHttps() +{ + NS_PRECONDITION(!mCallOnResume, "How did that happen?"); + + if (mSuspendCount) { + LOG(("Waiting until resume to do async redirect to https [this=%p]\n", this)); + mCallOnResume = &nsHttpChannel::HandleAsyncRedirectChannelToHttps; + return; + } + + nsresult rv = StartRedirectChannelToHttps(); + if (NS_FAILED(rv)) + ContinueAsyncRedirectChannelToURI(rv); +} + +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() +{ + NS_PRECONDITION(!mCallOnResume, "How did that happen?"); + NS_PRECONDITION(mAPIRedirectToURI, "How did that happen?"); + + if (mSuspendCount) { + LOG(("Waiting until resume to do async API redirect [this=%p]\n", this)); + mCallOnResume = &nsHttpChannel::HandleAsyncAPIRedirect; + return; + } + + nsresult rv = StartRedirectChannelToURI(mAPIRedirectToURI, + nsIChannelEventSink::REDIRECT_PERMANENT); + if (NS_FAILED(rv)) + ContinueAsyncRedirectChannelToURI(rv); + + return; +} + +nsresult +nsHttpChannel::StartRedirectChannelToURI(nsIURI *upgradedURI, uint32_t flags) +{ + nsresult rv = NS_OK; + LOG(("nsHttpChannel::StartRedirectChannelToURI()\n")); + + nsCOMPtr<nsIChannel> newChannel; + + nsCOMPtr<nsIIOService> ioService; + rv = gHttpHandler->GetIOService(getter_AddRefs(ioService)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_NewChannelInternal(getter_AddRefs(newChannel), + upgradedURI, + mLoadInfo, + nullptr, // aLoadGroup + nullptr, // aCallbacks + nsIRequest::LOAD_NORMAL, + ioService); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetupReplacementChannel(upgradedURI, newChannel, true, flags); + NS_ENSURE_SUCCESS(rv, rv); + + // Inform consumers about this fake redirect + mRedirectChannel = newChannel; + + if (!(flags & nsIChannelEventSink::REDIRECT_STS_UPGRADE) && + mInterceptCache == INTERCEPTED) { + // Mark the channel as intercepted in order to propagate the response URL. + nsCOMPtr<nsIHttpChannelInternal> httpRedirect = do_QueryInterface(mRedirectChannel); + if (httpRedirect) { + httpRedirect->ForceIntercepted(mInterceptionID); + } + } + + PushRedirectAsyncFunc( + &nsHttpChannel::ContinueAsyncRedirectChannelToURI); + rv = gHttpHandler->AsyncOnChannelRedirect(this, newChannel, flags); + + if (NS_SUCCEEDED(rv)) + rv = WaitForRedirectCallback(); + + if (NS_FAILED(rv)) { + AutoRedirectVetoNotifier notifier(this); + + /* 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) +{ + // Since we handle mAPIRedirectToURI also after on-examine-response handler + // rather drop it here to avoid any redirect loops, even just hypothetical. + mAPIRedirectToURI = nullptr; + + if (NS_SUCCEEDED(rv)) { + rv = OpenRedirectChannel(rv); + } + + if (NS_FAILED(rv)) { + // Fill the failure status here, the update to https had been vetoed + // but from the security reasons we have to discard the whole channel + // load. + mStatus = rv; + } + + if (mLoadGroup) { + mLoadGroup->RemoveRequest(this, nullptr, mStatus); + } + + if (NS_FAILED(rv)) { + // 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); + + // Make sure to do this after we received redirect veto answer, + // i.e. after all sinks had been notified + mRedirectChannel->SetOriginalURI(mOriginalURI); + + // And now, notify observers the deprecated way + nsCOMPtr<nsIHttpEventSink> httpEventSink; + GetCallback(httpEventSink); + if (httpEventSink) { + // NOTE: nsIHttpEventSink is only used for compatibility with pre-1.8 + // versions. + rv = httpEventSink->OnRedirect(this, mRedirectChannel); + if (NS_FAILED(rv)) { + return rv; + } + } + + // open new channel + if (mLoadInfo && mLoadInfo->GetEnforceSecurity()) { + MOZ_ASSERT(!mListenerContext, "mListenerContext should be null!"); + rv = mRedirectChannel->AsyncOpen2(mListener); + } + else { + rv = mRedirectChannel->AsyncOpen(mListener, mListenerContext); + } + 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->NewProxiedChannel2(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::ContinueDoReplaceWithProxy); + rv = gHttpHandler->AsyncOnChannelRedirect(this, newChannel, flags); + + if (NS_SUCCEEDED(rv)) + rv = WaitForRedirectCallback(); + + if (NS_FAILED(rv)) { + AutoRedirectVetoNotifier notifier(this); + PopRedirectAsyncFunc(&nsHttpChannel::ContinueDoReplaceWithProxy); + } + + return rv; +} + +nsresult +nsHttpChannel::ContinueDoReplaceWithProxy(nsresult rv) +{ + AutoRedirectVetoNotifier notifier(this); + + if (NS_FAILED(rv)) + return rv; + + NS_PRECONDITION(mRedirectChannel, "No redirect channel?"); + + // Make sure to do this after we received redirect veto answer, + // i.e. after all sinks had been notified + mRedirectChannel->SetOriginalURI(mOriginalURI); + + // open new channel + if (mLoadInfo && mLoadInfo->GetEnforceSecurity()) { + MOZ_ASSERT(!mListenerContext, "mListenerContext should be null!"); + rv = mRedirectChannel->AsyncOpen2(mListener); + } + else { + rv = mRedirectChannel->AsyncOpen(mListener, mListenerContext); + } + NS_ENSURE_SUCCESS(rv, rv); + + mStatus = NS_BINDING_REDIRECTED; + + notifier.RedirectSucceeded(); + + ReleaseListeners(); + + return rv; +} + +nsresult +nsHttpChannel::ResolveProxy() +{ + LOG(("nsHttpChannel::ResolveProxy [this=%p]\n", this)); + + nsresult rv; + + nsCOMPtr<nsIProtocolProxyService> pps = + do_GetService(NS_PROTOCOLPROXYSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) + return rv; + + // using the nsIProtocolProxyService2 allows a minor performance + // optimization, but if an add-on has only provided the original interface + // then it is ok to use that version. + nsCOMPtr<nsIProtocolProxyService2> pps2 = do_QueryInterface(pps); + if (pps2) { + rv = pps2->AsyncResolve2(this, mProxyResolveFlags, + this, getter_AddRefs(mProxyRequest)); + } else { + rv = pps->AsyncResolve(static_cast<nsIChannel*>(this), mProxyResolveFlags, + this, getter_AddRefs(mProxyRequest)); + } + + return rv; +} + +bool +nsHttpChannel::ResponseWouldVary(nsICacheEntry* entry) +{ + nsresult rv; + nsAutoCString buf, metaKey; + mCachedResponseHead->GetHeader(nsHttp::Vary, buf); + if (!buf.IsEmpty()) { + NS_NAMED_LITERAL_CSTRING(prefix, "request-"); + + // enumerate the elements of the Vary header... + char *val = buf.BeginWriting(); // going to munge buf + char *token = nsCRT::strtok(val, NS_HTTP_HEADER_SEPS, &val); + while (token) { + LOG(("nsHttpChannel::ResponseWouldVary [channel=%p] " \ + "processing %s\n", + this, token)); + // + // 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 == '*') + return true; // if we encounter this, just get out of here + + // build cache meta data key... + metaKey = prefix + nsDependentCString(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. + nsXPIDLCString 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; + } + + // next token... + token = nsCRT::strtok(val, NS_HTTP_HEADER_SEPS, &val); + } + } + return false; +} + +// We need to have an implementation of this function just so that we can keep +// all references to mCallOnResume of type nsHttpChannel: it's not OK in C++ +// to set a member function ptr to a base class function. +void +nsHttpChannel::HandleAsyncAbort() +{ + HttpAsyncAborter<nsHttpChannel>::HandleAsyncAbort(); +} + + +nsresult +nsHttpChannel::EnsureAssocReq() +{ + // Confirm Assoc-Req response header on pipelined transactions + // per draft-nottingham-http-pipeline-01.txt + // of the form: GET http://blah.com/foo/bar?qv + // return NS_OK as long as we don't find a violation + // (i.e. no header is ok, as are malformed headers, as are + // transactions that have not been pipelined (unless those have been + // opted in via pragma)) + + if (!mResponseHead) + return NS_OK; + + nsAutoCString assoc_val; + if (NS_FAILED(mResponseHead->GetHeader(nsHttp::Assoc_Req, assoc_val))) { + return NS_OK; + } + + if (!mTransaction || !mURI) + return NS_OK; + + if (!mTransaction->PipelinePosition()) { + // "Pragma: X-Verify-Assoc-Req" can be used to verify even non pipelined + // transactions. It is used by test harness. + + nsAutoCString pragma_val; + mResponseHead->GetHeader(nsHttp::Pragma, pragma_val); + if (pragma_val.IsEmpty() || + !nsHttp::FindToken(pragma_val.get(), "X-Verify-Assoc-Req", + HTTP_HEADER_VALUE_SEPS)) + return NS_OK; + } + + char *method = net_FindCharNotInSet(assoc_val.get(), HTTP_LWS); + if (!method) + return NS_OK; + + bool equals; + char *endofmethod; + + char * assoc_valChar = nullptr; + endofmethod = net_FindCharInSet(method, HTTP_LWS); + if (endofmethod) + assoc_valChar = net_FindCharNotInSet(endofmethod, HTTP_LWS); + if (!assoc_valChar) + return NS_OK; + + // check the method + nsAutoCString methodHead; + mRequestHead.Method(methodHead); + if ((((int32_t)methodHead.Length()) != (endofmethod - method)) || + PL_strncmp(method, + methodHead.get(), + endofmethod - method)) { + LOG((" Assoc-Req failure Method %s", method)); + if (mConnectionInfo) + gHttpHandler->ConnMgr()-> + PipelineFeedbackInfo(mConnectionInfo, + nsHttpConnectionMgr::RedCorruptedContent, + nullptr, 0); + + nsCOMPtr<nsIConsoleService> consoleService = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + if (consoleService) { + nsAutoString message + (NS_LITERAL_STRING("Failed Assoc-Req. Received ")); + nsAutoCString assoc_req; + mResponseHead->GetHeader(nsHttp::Assoc_Req, assoc_req); + AppendASCIItoUTF16(assoc_req, message); + message += NS_LITERAL_STRING(" expected method "); + AppendASCIItoUTF16(methodHead, message); + consoleService->LogStringMessage(message.get()); + } + + if (gHttpHandler->EnforceAssocReq()) + return NS_ERROR_CORRUPTED_CONTENT; + return NS_OK; + } + + // check the URL + nsCOMPtr<nsIURI> assoc_url; + if (NS_FAILED(NS_NewURI(getter_AddRefs(assoc_url), assoc_valChar)) || + !assoc_url) + return NS_OK; + + mURI->Equals(assoc_url, &equals); + if (!equals) { + LOG((" Assoc-Req failure URL %s", assoc_valChar)); + if (mConnectionInfo) + gHttpHandler->ConnMgr()-> + PipelineFeedbackInfo(mConnectionInfo, + nsHttpConnectionMgr::RedCorruptedContent, + nullptr, 0); + + nsCOMPtr<nsIConsoleService> consoleService = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + if (consoleService) { + nsAutoString message + (NS_LITERAL_STRING("Failed Assoc-Req. Received ")); + nsAutoCString assoc_req; + mResponseHead->GetHeader(nsHttp::Assoc_Req, assoc_req); + AppendASCIItoUTF16(assoc_req, message); + message += NS_LITERAL_STRING(" expected URL "); + AppendASCIItoUTF16(mSpec.get(), message); + consoleService->LogStringMessage(message.get()); + } + + if (gHttpHandler->EnforceAssocReq()) + return NS_ERROR_CORRUPTED_CONTENT; + } + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel <byte-range> +//----------------------------------------------------------------------------- + +bool +nsHttpChannel::IsResumable(int64_t partialLen, int64_t contentLength, + bool ignoreMissingPartialLen) const +{ + bool hasContentEncoding = + mCachedResponseHead->HasHeader(nsHttp::Content_Encoding); + + nsAutoCString etag; + mCachedResponseHead->GetHeader(nsHttp::ETag, etag); + bool hasWeakEtag = !etag.IsEmpty() && + StringBeginsWith(etag, NS_LITERAL_CSTRING("W/")); + + return (partialLen < contentLength) && + (partialLen > 0 || ignoreMissingPartialLen) && + !hasContentEncoding && !hasWeakEtag && + mCachedResponseHead->IsResumable() && + !mCustomConditionalRequest && + !mCachedResponseHead->NoStore(); +} + +nsresult +nsHttpChannel::MaybeSetupByteRangeRequest(int64_t partialLen, int64_t contentLength, + bool ignoreMissingPartialLen) +{ + // Be pesimistic + mIsPartialRequest = 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; + mCachedResponseHead->GetHeader(nsHttp::ETag, val); + if (val.IsEmpty()) + 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. + NS_NOTREACHED("no cache validator"); + mIsPartialRequest = false; + return NS_ERROR_FAILURE; + } + + char buf[64]; + SprintfLiteral(buf, "bytes=%" PRId64 "-", partialLen); + + mRequestHead.SetHeader(nsHttp::Range, nsDependentCString(buf)); + mRequestHead.SetHeader(nsHttp::If_Range, val); + mIsPartialRequest = true; + + return NS_OK; +} + +void +nsHttpChannel::UntieByteRangeRequest() +{ + mRequestHead.ClearHeader(nsHttp::Range); + mRequestHead.ClearHeader(nsHttp::If_Range); +} + +nsresult +nsHttpChannel::ProcessPartialContent() +{ + // 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); + + // Make sure to clear bogus content-encodings before looking at the header + ClearBogusContentEncodingIfNeeded(); + + // Check if the content-encoding we now got is different from the one we + // got before + nsAutoCString contentEncoding, cachedContentEncoding; + mResponseHead->GetHeader(nsHttp::Content_Encoding, contentEncoding); + mCachedResponseHead->GetHeader(nsHttp::Content_Encoding, + cachedContentEncoding); + if (PL_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; + mResponseHead->GetHeader(nsHttp::Content_Range, contentRange); + LOG(("nsHttpChannel::ProcessPartialContent [this=%p trans=%p] " + "original content-length %lld, entity-size %lld, 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 (mConcurrentCacheAccess) { + // 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; + + if (mOfflineCacheEntry) { + rv = InstallOfflineCacheListener(mLogicalOffset); + if (NS_FAILED(rv)) return rv; + } + } else { + // suspend the current transaction + rv = mTransactionPump->Suspend(); + if (NS_FAILED(rv)) return rv; + } + + // merge any new headers with the cached response headers + rv = mCachedResponseHead->UpdateHeaders(mResponseHead); + if (NS_FAILED(rv)) return rv; + + // update the cached response head + nsAutoCString head; + mCachedResponseHead->Flatten(head, true); + rv = mCacheEntry->SetMetaDataElement("response-head", head.get()); + if (NS_FAILED(rv)) return rv; + + // make the cached response be the current response + mResponseHead = Move(mCachedResponseHead); + + UpdateInhibitPersistentCachingFlag(); + + rv = UpdateExpirationTime(); + if (NS_FAILED(rv)) return rv; + + // notify observers interested in looking at a response that has been + // merged with any cached headers (http-on-examine-merged-response). + gHttpHandler->OnExamineMergedResponse(this); + + if (mConcurrentCacheAccess) { + mCachedContentIsPartial = false; + // Leave the mConcurrentCacheAccess flag set, we want to use it + // to prevent duplicate OnStartRequest call on the target listener + // in case this channel is canceled before it gets its OnStartRequest + // from the http transaction. + + // Now we continue reading the network response. + } else { + // the cached content is valid, although incomplete. + mCachedContentIsValid = true; + rv = ReadFromCache(false); + } + + return rv; +} + +nsresult +nsHttpChannel::OnDoneReadingPartialCacheEntry(bool *streamDone) +{ + nsresult rv; + + LOG(("nsHttpChannel::OnDoneReadingPartialCacheEntry [this=%p]", this)); + + // by default, assume we would have streamed all data or failed... + *streamDone = true; + + // setup cache listener to append to cache entry + int64_t size; + rv = mCacheEntry->GetDataSize(&size); + if (NS_FAILED(rv)) return rv; + + rv = InstallCacheListener(size); + if (NS_FAILED(rv)) return rv; + + // Entry is valid, do it now, after the output stream has been opened, + // otherwise when done earlier, pending readers would consider the cache + // entry still as partial (CacheEntry::GetDataSize would return the partial + // data size) and consumers would do the conditional request again. + rv = mCacheEntry->SetValid(); + if (NS_FAILED(rv)) return rv; + + // need to track the logical offset of the data being sent to our listener + mLogicalOffset = size; + + // we're now completing the cached content, so we can clear this flag. + // this puts us in the state of a regular download. + mCachedContentIsPartial = false; + // The cache input stream pump is finished, we do not need it any more. + // (see bug 1313923) + mCachePump = nullptr; + + // resume the transaction if it exists, otherwise the pipe contained the + // remaining part of the document and we've now streamed all of the data. + if (mTransactionPump) { + rv = mTransactionPump->Resume(); + if (NS_SUCCEEDED(rv)) + *streamDone = false; + } + else + NS_NOTREACHED("no transaction"); + return rv; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel <cache> +//----------------------------------------------------------------------------- + +bool +nsHttpChannel::ShouldBypassProcessNotModified() +{ + if (mCustomConditionalRequest) { + LOG(("Bypassing ProcessNotModified due to custom conditional headers")); + return true; + } + + if (!mDidReval) { + LOG(("Server returned a 304 response even though we did not send a " + "conditional request")); + return true; + } + + return false; +} + +nsresult +nsHttpChannel::ProcessNotModified() +{ + nsresult rv; + + LOG(("nsHttpChannel::ProcessNotModified [this=%p]\n", this)); + + // Assert ShouldBypassProcessNotModified() has been checked before call to + // ProcessNotModified(). + MOZ_ASSERT(!ShouldBypassProcessNotModified()); + + MOZ_ASSERT(mCachedResponseHead); + MOZ_ASSERT(mCacheEntry); + NS_ENSURE_TRUE(mCachedResponseHead && mCacheEntry, NS_ERROR_UNEXPECTED); + + // If the 304 response contains a Last-Modified different than the + // one in our cache that is pretty suspicious and is, in at least the + // case of bug 716840, a sign of the server having previously corrupted + // our cache with a bad response. Take the minor step here of just dooming + // that cache entry so there is a fighting chance of getting things on the + // right track as well as disabling pipelining for that host. + + nsAutoCString lastModifiedCached; + nsAutoCString lastModified304; + + rv = mCachedResponseHead->GetHeader(nsHttp::Last_Modified, + lastModifiedCached); + if (NS_SUCCEEDED(rv)) { + rv = mResponseHead->GetHeader(nsHttp::Last_Modified, + lastModified304); + } + + if (NS_SUCCEEDED(rv) && !lastModified304.Equals(lastModifiedCached)) { + LOG(("Cache Entry and 304 Last-Modified Headers Do Not Match " + "[%s] and [%s]\n", + lastModifiedCached.get(), lastModified304.get())); + + mCacheEntry->AsyncDoom(nullptr); + if (mConnectionInfo) + gHttpHandler->ConnMgr()-> + PipelineFeedbackInfo(mConnectionInfo, + nsHttpConnectionMgr::RedCorruptedContent, + nullptr, 0); + Telemetry::Accumulate(Telemetry::CACHE_LM_INCONSISTENT, true); + } + + // merge any new headers with the cached response headers + rv = mCachedResponseHead->UpdateHeaders(mResponseHead); + if (NS_FAILED(rv)) return rv; + + // update the cached response head + nsAutoCString head; + mCachedResponseHead->Flatten(head, true); + rv = mCacheEntry->SetMetaDataElement("response-head", head.get()); + if (NS_FAILED(rv)) return rv; + + // make the cached response be the current response + mResponseHead = Move(mCachedResponseHead); + + UpdateInhibitPersistentCachingFlag(); + + rv = UpdateExpirationTime(); + if (NS_FAILED(rv)) return rv; + + rv = AddCacheEntryHeaders(mCacheEntry); + if (NS_FAILED(rv)) return rv; + + // notify observers interested in looking at a reponse that has been + // merged with any cached headers + gHttpHandler->OnExamineMergedResponse(this); + + mCachedContentIsValid = true; + + // Tell other consumers the entry is OK to use + rv = mCacheEntry->SetValid(); + if (NS_FAILED(rv)) return rv; + + rv = ReadFromCache(false); + if (NS_FAILED(rv)) return rv; + + mTransactionReplaced = true; + return NS_OK; +} + +nsresult +nsHttpChannel::ProcessFallback(bool *waitingForRedirectCallback) +{ + LOG(("nsHttpChannel::ProcessFallback [this=%p]\n", this)); + nsresult rv; + + *waitingForRedirectCallback = false; + mFallingBack = false; + + // At this point a load has failed (either due to network problems + // or an error returned on the server). Perform an application + // cache fallback if we have a URI to fall back to. + if (!mApplicationCache || mFallbackKey.IsEmpty() || mFallbackChannel) { + LOG((" choosing not to fallback [%p,%s,%d]", + mApplicationCache.get(), mFallbackKey.get(), mFallbackChannel)); + return NS_OK; + } + + // Make sure the fallback entry hasn't been marked as a foreign + // entry. + uint32_t fallbackEntryType; + rv = mApplicationCache->GetTypes(mFallbackKey, &fallbackEntryType); + NS_ENSURE_SUCCESS(rv, rv); + + if (fallbackEntryType & nsIApplicationCache::ITEM_FOREIGN) { + // This cache points to a fallback that refers to a different + // manifest. Refuse to fall back. + return NS_OK; + } + + if (!IsInSubpathOfAppCacheManifest(mApplicationCache, mFallbackKey)) { + // Refuse to fallback if the fallback key is not contained in the same + // path as the cache manifest. + return NS_OK; + } + + MOZ_ASSERT(fallbackEntryType & nsIApplicationCache::ITEM_FALLBACK, + "Fallback entry not marked correctly!"); + + // Kill any offline cache entry, and disable offline caching for the + // fallback. + if (mOfflineCacheEntry) { + mOfflineCacheEntry->AsyncDoom(nullptr); + mOfflineCacheEntry = nullptr; + } + + mApplicationCacheForWrite = nullptr; + mOfflineCacheEntry = nullptr; + + // Close the current cache entry. + CloseCacheEntry(true); + + // Create a new channel to load the fallback entry. + RefPtr<nsIChannel> newChannel; + rv = gHttpHandler->NewChannel2(mURI, + mLoadInfo, + getter_AddRefs(newChannel)); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t redirectFlags = nsIChannelEventSink::REDIRECT_INTERNAL; + rv = SetupReplacementChannel(mURI, newChannel, true, redirectFlags); + NS_ENSURE_SUCCESS(rv, rv); + + // Make sure the new channel loads from the fallback key. + nsCOMPtr<nsIHttpChannelInternal> httpInternal = + do_QueryInterface(newChannel, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = httpInternal->SetupFallbackChannel(mFallbackKey.get()); + NS_ENSURE_SUCCESS(rv, rv); + + // ... and fallbacks should only load from the cache. + uint32_t newLoadFlags = mLoadFlags | LOAD_REPLACE | LOAD_ONLY_FROM_CACHE; + rv = newChannel->SetLoadFlags(newLoadFlags); + + // Inform consumers about this fake redirect + mRedirectChannel = newChannel; + + PushRedirectAsyncFunc(&nsHttpChannel::ContinueProcessFallback); + rv = gHttpHandler->AsyncOnChannelRedirect(this, newChannel, redirectFlags); + + if (NS_SUCCEEDED(rv)) + rv = WaitForRedirectCallback(); + + if (NS_FAILED(rv)) { + AutoRedirectVetoNotifier notifier(this); + PopRedirectAsyncFunc(&nsHttpChannel::ContinueProcessFallback); + return rv; + } + + // Indicate we are now waiting for the asynchronous redirect callback + // if all went OK. + *waitingForRedirectCallback = true; + return NS_OK; +} + +nsresult +nsHttpChannel::ContinueProcessFallback(nsresult rv) +{ + AutoRedirectVetoNotifier notifier(this); + + if (NS_FAILED(rv)) + return rv; + + NS_PRECONDITION(mRedirectChannel, "No redirect channel?"); + + // Make sure to do this after we received redirect veto answer, + // i.e. after all sinks had been notified + mRedirectChannel->SetOriginalURI(mOriginalURI); + + if (mLoadInfo && mLoadInfo->GetEnforceSecurity()) { + MOZ_ASSERT(!mListenerContext, "mListenerContext should be null!"); + rv = mRedirectChannel->AsyncOpen2(mListener); + } + else { + rv = mRedirectChannel->AsyncOpen(mListener, mListenerContext); + } + NS_ENSURE_SUCCESS(rv, rv); + + if (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI) { + MaybeWarnAboutAppCache(); + } + + // close down this channel + Cancel(NS_BINDING_REDIRECTED); + + notifier.RedirectSucceeded(); + + ReleaseListeners(); + + mFallingBack = true; + + return NS_OK; +} + +// Determines if a request is a byte range request for a subrange, +// i.e. is a byte range request, but not a 0- byte range request. +static bool +IsSubRangeRequest(nsHttpRequestHead &aRequestHead) +{ + nsAutoCString byteRange; + if (NS_FAILED(aRequestHead.GetHeader(nsHttp::Range, byteRange))) { + return false; + } + return !byteRange.EqualsLiteral("bytes=0-"); +} + +nsresult +nsHttpChannel::OpenCacheEntry(bool isHttps) +{ + // Handle correctly mCacheEntriesToWaitFor + AutoCacheWaitFlags waitFlags(this); + + // Drop this flag here + mConcurrentCacheAccess = 0; + + nsresult rv; + + mLoadedFromApplicationCache = false; + mHasQueryString = HasQueryString(mRequestHead.ParsedMethod(), mURI); + + LOG(("nsHttpChannel::OpenCacheEntry [this=%p]", this)); + + // make sure we're not abusing this function + NS_PRECONDITION(!mCacheEntry, "cache entry already open"); + + nsAutoCString cacheKey; + nsAutoCString extension; + + if (mRequestHead.IsPost()) { + // If the post id is already set then this is an attempt to replay + // a post transaction via the cache. Otherwise, we need a unique + // post id for this transaction. + if (mPostID == 0) + mPostID = gHttpHandler->GenerateUniqueID(); + } + else if (!PossiblyIntercepted() && !mRequestHead.IsGet() && !mRequestHead.IsHead()) { + // don't use the cache for other types of requests + return NS_OK; + } + + if (mResuming) { + // We don't support caching for requests initiated + // via nsIResumableChannel. + return NS_OK; + } + + // Don't cache byte range requests which are subranges, only cache 0- + // byte range requests. + if (IsSubRangeRequest(mRequestHead)) + return NS_OK; + + // Pick up an application cache from the notification + // callbacks if available and if we are not an intercepted channel. + if (!PossiblyIntercepted() && !mApplicationCache && + mInheritApplicationCache) { + nsCOMPtr<nsIApplicationCacheContainer> appCacheContainer; + GetCallback(appCacheContainer); + + if (appCacheContainer) { + appCacheContainer->GetApplicationCache(getter_AddRefs(mApplicationCache)); + } + } + + nsCOMPtr<nsICacheStorageService> cacheStorageService = + do_GetService("@mozilla.org/netwerk/cache-storage-service;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsICacheStorage> cacheStorage; + nsCOMPtr<nsIURI> openURI; + if (!mFallbackKey.IsEmpty() && mFallbackChannel) { + // This is a fallback channel, open fallback URI instead + rv = NS_NewURI(getter_AddRefs(openURI), mFallbackKey); + NS_ENSURE_SUCCESS(rv, rv); + } + else { + // In the case of intercepted channels, we need to construct the cache + // entry key based on the original URI, so that in case the intercepted + // channel is redirected, the cache entry key before and after the + // redirect is the same. + if (PossiblyIntercepted()) { + openURI = mOriginalURI; + } else { + openURI = mURI; + } + } + + RefPtr<LoadContextInfo> info = GetLoadContextInfo(this); + if (!info) { + return NS_ERROR_FAILURE; + } + + uint32_t cacheEntryOpenFlags; + bool offline = gIOService->IsOffline(); + + nsAutoCString cacheControlRequestHeader; + mRequestHead.GetHeader(nsHttp::Cache_Control, cacheControlRequestHeader); + CacheControlParser cacheControlRequest(cacheControlRequestHeader); + if (cacheControlRequest.NoStore() && !PossiblyIntercepted()) { + goto bypassCacheEntryOpen; + } + + if (offline || (mLoadFlags & INHIBIT_CACHING)) { + if (BYPASS_LOCAL_CACHE(mLoadFlags) && !offline && !PossiblyIntercepted()) { + goto bypassCacheEntryOpen; + } + cacheEntryOpenFlags = nsICacheStorage::OPEN_READONLY; + mCacheEntryIsReadOnly = true; + } + else if (BYPASS_LOCAL_CACHE(mLoadFlags) && !mApplicationCache) { + cacheEntryOpenFlags = nsICacheStorage::OPEN_TRUNCATE; + } + else { + cacheEntryOpenFlags = nsICacheStorage::OPEN_NORMALLY + | nsICacheStorage::CHECK_MULTITHREADED; + } + + if (!mPostID && mApplicationCache) { + rv = cacheStorageService->AppCacheStorage(info, + mApplicationCache, + getter_AddRefs(cacheStorage)); + } else if (PossiblyIntercepted()) { + // The synthesized cache has less restrictions on file size and so on. + rv = cacheStorageService->SynthesizedCacheStorage(info, + getter_AddRefs(cacheStorage)); + } else if (mLoadFlags & INHIBIT_PERSISTENT_CACHING) { + rv = cacheStorageService->MemoryCacheStorage(info, // ? choose app cache as well... + getter_AddRefs(cacheStorage)); + } + else if (mPinCacheContent) { + rv = cacheStorageService->PinningCacheStorage(info, + getter_AddRefs(cacheStorage)); + } + else { + rv = cacheStorageService->DiskCacheStorage(info, + !mPostID && (mChooseApplicationCache || (mLoadFlags & LOAD_CHECK_OFFLINE_CACHE)), + getter_AddRefs(cacheStorage)); + } + NS_ENSURE_SUCCESS(rv, rv); + + if ((mClassOfService & nsIClassOfService::Leader) || + (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI)) + cacheEntryOpenFlags |= nsICacheStorage::OPEN_PRIORITY; + + // Only for backward compatibility with the old cache back end. + // When removed, remove the flags and related code snippets. + if (mLoadFlags & LOAD_BYPASS_LOCAL_CACHE_IF_BUSY) + cacheEntryOpenFlags |= nsICacheStorage::OPEN_BYPASS_IF_BUSY; + + if (PossiblyIntercepted()) { + extension.Append(nsPrintfCString("u%lld", mInterceptionID)); + } else if (mPostID) { + extension.Append(nsPrintfCString("%d", mPostID)); + } + + // If this channel should be intercepted, we do not open a cache entry for this channel + // until the interception process is complete and the consumer decides what to do with it. + if (mInterceptCache == MAYBE_INTERCEPT) { + DebugOnly<bool> exists; + MOZ_ASSERT(NS_FAILED(cacheStorage->Exists(openURI, extension, &exists)) || !exists, + "The entry must not exist in the cache before we create it here"); + + nsCOMPtr<nsICacheEntry> entry; + rv = cacheStorage->OpenTruncate(openURI, extension, getter_AddRefs(entry)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsINetworkInterceptController> controller; + GetCallback(controller); + + RefPtr<InterceptedChannelChrome> intercepted = + new InterceptedChannelChrome(this, controller, entry); + intercepted->NotifyController(); + } else { + if (mInterceptCache == INTERCEPTED) { + cacheEntryOpenFlags |= nsICacheStorage::OPEN_INTERCEPTED; + // Clear OPEN_TRUNCATE for the fake cache entry, since otherwise + // cache storage will close the current entry which breaks the + // response synthesis. + cacheEntryOpenFlags &= ~nsICacheStorage::OPEN_TRUNCATE; + DebugOnly<bool> exists; + MOZ_ASSERT(NS_SUCCEEDED(cacheStorage->Exists(openURI, extension, &exists)) && exists, + "The entry must exist in the cache after we create it here"); + } + + mCacheOpenWithPriority = cacheEntryOpenFlags & nsICacheStorage::OPEN_PRIORITY; + mCacheQueueSizeWhenOpen = CacheStorageService::CacheQueueSize(mCacheOpenWithPriority); + + rv = cacheStorage->AsyncOpenURI(openURI, extension, cacheEntryOpenFlags, this); + NS_ENSURE_SUCCESS(rv, rv); + } + + waitFlags.Keep(WAIT_FOR_CACHE_ENTRY); + +bypassCacheEntryOpen: + if (!mApplicationCacheForWrite) + return NS_OK; + + // If there is an app cache to write to, open the entry right now in parallel. + + // make sure we're not abusing this function + NS_PRECONDITION(!mOfflineCacheEntry, "cache entry already open"); + + if (offline) { + // only put things in the offline cache while online + return NS_OK; + } + + if (mLoadFlags & INHIBIT_CACHING) { + // respect demand not to cache + return NS_OK; + } + + if (!mRequestHead.IsGet()) { + // only cache complete documents offline + return NS_OK; + } + + rv = cacheStorageService->AppCacheStorage(info, mApplicationCacheForWrite, + getter_AddRefs(cacheStorage)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = cacheStorage->AsyncOpenURI( + mURI, EmptyCString(), nsICacheStorage::OPEN_TRUNCATE, this); + NS_ENSURE_SUCCESS(rv, rv); + + waitFlags.Keep(WAIT_FOR_OFFLINE_CACHE_ENTRY); + + return NS_OK; +} + +nsresult +nsHttpChannel::CheckPartial(nsICacheEntry* aEntry, int64_t *aSize, int64_t *aContentLength) +{ + nsresult rv; + + rv = aEntry->GetDataSize(aSize); + + if (NS_ERROR_IN_PROGRESS == rv) { + *aSize = -1; + rv = NS_OK; + } + + NS_ENSURE_SUCCESS(rv, rv); + + nsHttpResponseHead* responseHead = mCachedResponseHead + ? mCachedResponseHead + : mResponseHead; + + if (!responseHead) + return NS_ERROR_UNEXPECTED; + + *aContentLength = responseHead->ContentLength(); + + return NS_OK; +} + +void +nsHttpChannel::UntieValidationRequest() +{ + // Make the request unconditional again. + mRequestHead.ClearHeader(nsHttp::If_Modified_Since); + mRequestHead.ClearHeader(nsHttp::If_None_Match); + mRequestHead.ClearHeader(nsHttp::ETag); +} + +NS_IMETHODIMP +nsHttpChannel::OnCacheEntryCheck(nsICacheEntry* entry, nsIApplicationCache* appCache, + uint32_t* aResult) +{ + nsresult rv = NS_OK; + + LOG(("nsHttpChannel::OnCacheEntryCheck enter [channel=%p entry=%p]", + this, entry)); + + nsAutoCString cacheControlRequestHeader; + mRequestHead.GetHeader(nsHttp::Cache_Control, cacheControlRequestHeader); + CacheControlParser cacheControlRequest(cacheControlRequestHeader); + + if (cacheControlRequest.NoStore()) { + LOG(("Not using cached response based on no-store request cache directive\n")); + *aResult = ENTRY_NOT_WANTED; + return NS_OK; + } + + // Remember the request is a custom conditional request so that we can + // process any 304 response correctly. + mCustomConditionalRequest = + mRequestHead.HasHeader(nsHttp::If_Modified_Since) || + mRequestHead.HasHeader(nsHttp::If_None_Match) || + mRequestHead.HasHeader(nsHttp::If_Unmodified_Since) || + mRequestHead.HasHeader(nsHttp::If_Match) || + mRequestHead.HasHeader(nsHttp::If_Range); + + // Be pessimistic: assume the cache entry has no useful data. + *aResult = ENTRY_WANTED; + mCachedContentIsValid = false; + + nsXPIDLCString buf; + + // Get the method that was used to generate the cached response + rv = entry->GetMetaDataElement("request-method", getter_Copies(buf)); + NS_ENSURE_SUCCESS(rv, rv); + + bool methodWasHead = buf.EqualsLiteral("HEAD"); + bool methodWasGet = buf.EqualsLiteral("GET"); + + if (methodWasHead) { + // The cached response does not contain an entity. We can only reuse + // the response if the current request is also HEAD. + if (!mRequestHead.IsHead()) { + return NS_OK; + } + } + buf.Adopt(0); + + // We'll need this value in later computations... + uint32_t lastModifiedTime; + rv = entry->GetLastModified(&lastModifiedTime); + NS_ENSURE_SUCCESS(rv, rv); + + // Determine if this is the first time that this cache entry + // has been accessed during this session. + bool fromPreviousSession = + (gHttpHandler->SessionStartTime() > lastModifiedTime); + + // Get the cached HTTP response headers + mCachedResponseHead = new nsHttpResponseHead(); + + // A "original-response-headers" metadata element holds network original headers, + // i.e. the headers in the form as they arrieved from the network. + // We need to get the network original headers first, because we need to keep them + // in order. + rv = entry->GetMetaDataElement("original-response-headers", getter_Copies(buf)); + if (NS_SUCCEEDED(rv)) { + mCachedResponseHead->ParseCachedOriginalHeaders((char *) buf.get()); + } + + buf.Adopt(0); + // A "response-head" metadata element holds response head, e.g. response status + // line and headers in the form Firefox uses them internally (no dupicate + // headers, etc.). + rv = entry->GetMetaDataElement("response-head", getter_Copies(buf)); + NS_ENSURE_SUCCESS(rv, rv); + + // Parse string stored in a "response-head" metadata element. + // These response headers will be merged with the orignal headers (i.e. the + // headers stored in a "original-response-headers" metadata element). + rv = mCachedResponseHead->ParseCachedHead(buf.get()); + NS_ENSURE_SUCCESS(rv, rv); + buf.Adopt(0); + + bool isCachedRedirect = WillRedirect(mCachedResponseHead); + + // Do not return 304 responses from the cache, and also do not return + // any other non-redirect 3xx responses from the cache (see bug 759043). + NS_ENSURE_TRUE((mCachedResponseHead->Status() / 100 != 3) || + isCachedRedirect, NS_ERROR_ABORT); + + if (mCachedResponseHead->NoStore() && mCacheEntryIsReadOnly) { + // This prevents loading no-store responses when navigating back + // while the browser is set to work offline. + LOG((" entry loading as read-only but is no-store, set INHIBIT_CACHING")); + mLoadFlags |= nsIRequest::INHIBIT_CACHING; + } + + // Don't bother to validate items that are read-only, + // unless they are read-only because of INHIBIT_CACHING or because + // we're updating the offline cache. + // Don't bother to validate if this is a fallback entry. + if (!mApplicationCacheForWrite && + (appCache || + (mCacheEntryIsReadOnly && !(mLoadFlags & nsIRequest::INHIBIT_CACHING)) || + mFallbackChannel)) { + rv = OpenCacheInputStream(entry, true, !!appCache); + if (NS_SUCCEEDED(rv)) { + mCachedContentIsValid = true; + entry->MaybeMarkValid(); + } + return rv; + } + + bool wantCompleteEntry = false; + + if (!methodWasHead && !isCachedRedirect) { + // If the cached content-length is set and it does not match the data + // size of the cached content, then the cached response is partial... + // either we need to issue a byte range request or we need to refetch + // the entire document. + // + // We exclude redirects from this check because we (usually) strip the + // entity when we store the cache entry, and even if we didn't, we + // always ignore a cached redirect's entity anyway. See bug 759043. + int64_t size, contentLength; + rv = CheckPartial(entry, &size, &contentLength); + NS_ENSURE_SUCCESS(rv,rv); + + if (size == int64_t(-1)) { + LOG((" write is in progress")); + if (mLoadFlags & LOAD_BYPASS_LOCAL_CACHE_IF_BUSY) { + LOG((" not interested in the entry, " + "LOAD_BYPASS_LOCAL_CACHE_IF_BUSY specified")); + + *aResult = ENTRY_NOT_WANTED; + return NS_OK; + } + + // Ignore !(size > 0) from the resumability condition + if (!IsResumable(size, contentLength, true)) { + LOG((" wait for entry completion, " + "response is not resumable")); + + wantCompleteEntry = true; + } + else { + mConcurrentCacheAccess = 1; + } + } + else if (contentLength != int64_t(-1) && contentLength != size) { + LOG(("Cached data size does not match the Content-Length header " + "[content-length=%lld size=%lld]\n", contentLength, size)); + + rv = MaybeSetupByteRangeRequest(size, contentLength); + mCachedContentIsPartial = NS_SUCCEEDED(rv) && mIsPartialRequest; + if (mCachedContentIsPartial) { + rv = OpenCacheInputStream(entry, false, !!appCache); + if (NS_FAILED(rv)) { + UntieByteRangeRequest(); + return rv; + } + + *aResult = ENTRY_NEEDS_REVALIDATION; + return NS_OK; + } + + if (size == 0 && mCacheOnlyMetadata) { + // Don't break cache entry load when the entry's data size + // is 0 and mCacheOnlyMetadata flag is set. In that case we + // want to proceed since the LOAD_ONLY_IF_MODIFIED flag is + // also set. + MOZ_ASSERT(mLoadFlags & LOAD_ONLY_IF_MODIFIED); + } else if (mInterceptCache != INTERCEPTED) { + return rv; + } + } + } + + bool isHttps = false; + rv = mURI->SchemeIs("https", &isHttps); + NS_ENSURE_SUCCESS(rv,rv); + + bool doValidation = false; + bool canAddImsHeader = true; + + bool isForcedValid = false; + entry->GetIsForcedValid(&isForcedValid); + + nsXPIDLCString framedBuf; + rv = entry->GetMetaDataElement("strongly-framed", getter_Copies(framedBuf)); + // describe this in terms of explicitly weakly framed so as to be backwards + // compatible with old cache contents which dont have strongly-framed makers + bool weaklyFramed = NS_SUCCEEDED(rv) && framedBuf.EqualsLiteral("0"); + bool isImmutable = !weaklyFramed && isHttps && mCachedResponseHead->Immutable(); + + // Cached entry is not the entity we request (see bug #633743) + if (ResponseWouldVary(entry)) { + LOG(("Validating based on Vary headers returning TRUE\n")); + canAddImsHeader = false; + doValidation = true; + } + // Check isForcedValid to see if it is possible to skip validation. + // Don't skip validation if we have serious reason to believe that this + // content is invalid (it's expired). + // See netwerk/cache2/nsICacheEntry.idl for details + else if (isForcedValid && + (!mCachedResponseHead->ExpiresInPast() || + !mCachedResponseHead->MustValidateIfExpired())) { + LOG(("NOT validating based on isForcedValid being true.\n")); + Telemetry::AutoCounter<Telemetry::PREDICTOR_TOTAL_PREFETCHES_USED> used; + ++used; + doValidation = false; + } + // If the LOAD_FROM_CACHE flag is set, any cached data can simply be used + else if (mLoadFlags & nsIRequest::LOAD_FROM_CACHE || mAllowStaleCacheContent) { + LOG(("NOT validating based on LOAD_FROM_CACHE load flag\n")); + doValidation = false; + } + // If the VALIDATE_ALWAYS flag is set, any cached data won't be used until + // it's revalidated with the server. + else if ((mLoadFlags & nsIRequest::VALIDATE_ALWAYS) && !isImmutable) { + LOG(("Validating based on VALIDATE_ALWAYS load flag\n")); + doValidation = true; + } + // Even if the VALIDATE_NEVER flag is set, there are still some cases in + // which we must validate the cached response with the server. + else if (mLoadFlags & nsIRequest::VALIDATE_NEVER) { + LOG(("VALIDATE_NEVER set\n")); + // if no-store validate cached response (see bug 112564) + if (mCachedResponseHead->NoStore()) { + LOG(("Validating based on no-store logic\n")); + doValidation = true; + } + else { + LOG(("NOT validating based on VALIDATE_NEVER load flag\n")); + doValidation = false; + } + } + // check if validation is strictly required... + else if (mCachedResponseHead->MustValidate()) { + LOG(("Validating based on MustValidate() returning TRUE\n")); + doValidation = true; + } else { + // previously we also checked for a query-url w/out expiration + // and didn't do heuristic on it. but defacto that is allowed now. + // + // Check if the cache entry has expired... + + uint32_t now = NowInSeconds(); + + uint32_t age = 0; + rv = mCachedResponseHead->ComputeCurrentAge(now, now, &age); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t freshness = 0; + rv = mCachedResponseHead->ComputeFreshnessLifetime(&freshness); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t expiration = 0; + rv = entry->GetExpirationTime(&expiration); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t maxAgeRequest, maxStaleRequest, minFreshRequest; + + LOG((" NowInSeconds()=%u, expiration time=%u, freshness lifetime=%u, age=%u", + now, expiration, freshness, age)); + + if (cacheControlRequest.NoCache()) { + LOG((" validating, no-cache request")); + doValidation = true; + } else if (cacheControlRequest.MaxStale(&maxStaleRequest)) { + uint32_t staleTime = age > freshness ? age - freshness : 0; + doValidation = staleTime > maxStaleRequest; + LOG((" validating=%d, max-stale=%u requested", doValidation, maxStaleRequest)); + } else if (cacheControlRequest.MaxAge(&maxAgeRequest)) { + doValidation = age > maxAgeRequest; + LOG((" validating=%d, max-age=%u requested", doValidation, maxAgeRequest)); + } else if (cacheControlRequest.MinFresh(&minFreshRequest)) { + uint32_t freshTime = freshness > age ? freshness - age : 0; + doValidation = freshTime < minFreshRequest; + LOG((" validating=%d, min-fresh=%u requested", doValidation, minFreshRequest)); + } else if (now <= expiration) { + doValidation = false; + LOG((" not validating, expire time not in the past")); + } else if (mCachedResponseHead->MustValidateIfExpired()) { + doValidation = true; + } else if (mLoadFlags & nsIRequest::VALIDATE_ONCE_PER_SESSION) { + // If the cached response does not include expiration infor- + // mation, then we must validate the response, despite whether + // or not this is the first access this session. This behavior + // is consistent with existing browsers and is generally expected + // by web authors. + if (freshness == 0) + doValidation = true; + else + doValidation = fromPreviousSession; + } + else + doValidation = true; + + LOG(("%salidating based on expiration time\n", doValidation ? "V" : "Not v")); + } + + + // If a content signature is expected to be valid in this load, + // set doValidation to force a signature check. + if (!doValidation && + mLoadInfo && mLoadInfo->GetVerifySignedContent()) { + doValidation = true; + } + + nsAutoCString requestedETag; + if (!doValidation && + NS_SUCCEEDED(mRequestHead.GetHeader(nsHttp::If_Match, requestedETag)) && + (methodWasGet || methodWasHead)) { + nsAutoCString cachedETag; + mCachedResponseHead->GetHeader(nsHttp::ETag, cachedETag); + if (!cachedETag.IsEmpty() && + (StringBeginsWith(cachedETag, NS_LITERAL_CSTRING("W/")) || + !requestedETag.Equals(cachedETag))) { + // User has defined If-Match header, if the cached entry is not + // matching the provided header value or the cached ETag is weak, + // force validation. + doValidation = true; + } + } + + if (!doValidation) { + // + // Check the authorization headers used to generate the cache entry. + // We must validate the cache entry if: + // + // 1) the cache entry was generated prior to this session w/ + // credentials (see bug 103402). + // 2) the cache entry was generated w/o credentials, but would now + // require credentials (see bug 96705). + // + // NOTE: this does not apply to proxy authentication. + // + entry->GetMetaDataElement("auth", getter_Copies(buf)); + doValidation = + (fromPreviousSession && !buf.IsEmpty()) || + (buf.IsEmpty() && mRequestHead.HasHeader(nsHttp::Authorization)); + } + + // Bug #561276: We maintain a chain of cache-keys which returns cached + // 3xx-responses (redirects) in order to detect cycles. If a cycle is + // found, ignore the cached response and hit the net. Otherwise, use + // the cached response and add the cache-key to the chain. Note that + // a limited number of redirects (cached or not) is allowed and is + // enforced independently of this mechanism + if (!doValidation && isCachedRedirect) { + nsAutoCString cacheKey; + GenerateCacheKey(mPostID, cacheKey); + + if (!mRedirectedCachekeys) + mRedirectedCachekeys = new nsTArray<nsCString>(); + else if (mRedirectedCachekeys->Contains(cacheKey)) + doValidation = true; + + LOG(("Redirection-chain %s key %s\n", + doValidation ? "contains" : "does not contain", cacheKey.get())); + + // Append cacheKey if not in the chain already + if (!doValidation) + mRedirectedCachekeys->AppendElement(cacheKey); + } + + if (doValidation && mInterceptCache == INTERCEPTED) { + doValidation = false; + } + + mCachedContentIsValid = !doValidation; + + if (doValidation) { + // + // now, we are definitely going to issue a HTTP request to the server. + // make it conditional if possible. + // + // do not attempt to validate no-store content, since servers will not + // expect it to be cached. (we only keep it in our cache for the + // purposes of back/forward, etc.) + // + // the request method MUST be either GET or HEAD (see bug 175641) and + // the cached response code must be < 400 + // + // the cached content must not be weakly framed or marked immutable + // + // do not override conditional headers when consumer has defined its own + if (!mCachedResponseHead->NoStore() && + (mRequestHead.IsGet() || mRequestHead.IsHead()) && + !mCustomConditionalRequest && !weaklyFramed && !isImmutable && + (mCachedResponseHead->Status() < 400)) { + + if (mConcurrentCacheAccess) { + // In case of concurrent read and also validation request we + // must wait for the current writer to close the output stream + // first. Otherwise, when the writer's job would have been interrupted + // before all the data were downloaded, we'd have to do a range request + // which would be a second request in line during this channel's + // life-time. nsHttpChannel is not designed to do that, so rather + // turn off concurrent read and wait for entry's completion. + // Then only re-validation or range-re-validation request will go out. + mConcurrentCacheAccess = 0; + // This will cause that OnCacheEntryCheck is called again with the same + // entry after the writer is done. + wantCompleteEntry = true; + } else { + nsAutoCString val; + // Add If-Modified-Since header if a Last-Modified was given + // and we are allowed to do this (see bugs 510359 and 269303) + if (canAddImsHeader) { + mCachedResponseHead->GetHeader(nsHttp::Last_Modified, val); + if (!val.IsEmpty()) + mRequestHead.SetHeader(nsHttp::If_Modified_Since, val); + } + // Add If-None-Match header if an ETag was given in the response + mCachedResponseHead->GetHeader(nsHttp::ETag, val); + if (!val.IsEmpty()) + mRequestHead.SetHeader(nsHttp::If_None_Match, val); + mDidReval = true; + } + } + } + + if (mCachedContentIsValid || mDidReval) { + rv = OpenCacheInputStream(entry, mCachedContentIsValid, !!appCache); + if (NS_FAILED(rv)) { + // If we can't get the entity then we have to act as though we + // don't have the cache entry. + if (mDidReval) { + UntieValidationRequest(); + mDidReval = false; + } + mCachedContentIsValid = false; + } + } + + if (mDidReval) + *aResult = ENTRY_NEEDS_REVALIDATION; + else if (wantCompleteEntry) + *aResult = RECHECK_AFTER_WRITE_FINISHED; + else + *aResult = ENTRY_WANTED; + + if (mCachedContentIsValid) { + entry->MaybeMarkValid(); + } + + LOG(("nsHTTPChannel::OnCacheEntryCheck exit [this=%p doValidation=%d result=%d]\n", + this, doValidation, *aResult)); + return rv; +} + +NS_IMETHODIMP +nsHttpChannel::OnCacheEntryAvailable(nsICacheEntry *entry, + bool aNew, + nsIApplicationCache* aAppCache, + nsresult status) +{ + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv; + + LOG(("nsHttpChannel::OnCacheEntryAvailable [this=%p entry=%p " + "new=%d appcache=%p status=%x mAppCache=%p mAppCacheForWrite=%p]\n", + this, entry, aNew, aAppCache, status, + mApplicationCache.get(), mApplicationCacheForWrite.get())); + + // if the channel's already fired onStopRequest, then we should ignore + // this event. + if (!mIsPending) { + mCacheInputStream.CloseAndRelease(); + return NS_OK; + } + + rv = OnCacheEntryAvailableInternal(entry, aNew, aAppCache, status); + if (NS_FAILED(rv)) { + CloseCacheEntry(false); + AsyncAbort(rv); + } + + return NS_OK; +} + +nsresult +nsHttpChannel::OnCacheEntryAvailableInternal(nsICacheEntry *entry, + bool aNew, + nsIApplicationCache* aAppCache, + nsresult status) +{ + nsresult rv; + + if (mCanceled) { + LOG(("channel was canceled [this=%p status=%x]\n", this, mStatus)); + return mStatus; + } + + if (aAppCache) { + if (mApplicationCache == aAppCache && !mCacheEntry) { + rv = OnOfflineCacheEntryAvailable(entry, aNew, aAppCache, status); + } + else if (mApplicationCacheForWrite == aAppCache && aNew && !mOfflineCacheEntry) { + rv = OnOfflineCacheEntryForWritingAvailable(entry, aAppCache, status); + } + else { + rv = OnOfflineCacheEntryAvailable(entry, aNew, aAppCache, status); + } + } + else { + rv = OnNormalCacheEntryAvailable(entry, aNew, status); + } + + if (NS_FAILED(rv) && (mLoadFlags & LOAD_ONLY_FROM_CACHE)) { + // If we have a fallback URI (and we're not already + // falling back), process the fallback asynchronously. + if (!mFallbackChannel && !mFallbackKey.IsEmpty()) { + return AsyncCall(&nsHttpChannel::HandleAsyncFallback); + } + + return NS_ERROR_DOCUMENT_NOT_CACHED; + } + + if (NS_FAILED(rv)) { + return rv; + } + + // We may be waiting for more callbacks... + if (AwaitingCacheCallbacks()) { + return NS_OK; + } + + return TryHSTSPriming(); +} + +nsresult +nsHttpChannel::OnNormalCacheEntryAvailable(nsICacheEntry *aEntry, + bool aNew, + nsresult aEntryStatus) +{ + mCacheEntriesToWaitFor &= ~WAIT_FOR_CACHE_ENTRY; + + if (NS_FAILED(aEntryStatus) || aNew) { + // Make sure this flag is dropped. It may happen the entry is doomed + // between OnCacheEntryCheck and OnCacheEntryAvailable. + mCachedContentIsValid = false; + + // From the same reason remove any conditional headers added + // in OnCacheEntryCheck. + if (mDidReval) { + LOG((" Removing conditional request headers")); + UntieValidationRequest(); + mDidReval = false; + } + + if (mLoadFlags & LOAD_ONLY_FROM_CACHE) { + // if this channel is only allowed to pull from the cache, then + // we must fail if we were unable to open a cache entry for read. + return NS_ERROR_DOCUMENT_NOT_CACHED; + } + } + + if (NS_SUCCEEDED(aEntryStatus)) { + mCacheEntry = aEntry; + mCacheEntryIsWriteOnly = aNew; + + if (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI) { + Telemetry::Accumulate(Telemetry::HTTP_OFFLINE_CACHE_DOCUMENT_LOAD, + false); + } + } + + return NS_OK; +} + +nsresult +nsHttpChannel::OnOfflineCacheEntryAvailable(nsICacheEntry *aEntry, + bool aNew, + nsIApplicationCache* aAppCache, + nsresult aEntryStatus) +{ + MOZ_ASSERT(!mApplicationCache || aAppCache == mApplicationCache); + MOZ_ASSERT(!aNew || !aEntry || mApplicationCacheForWrite); + + mCacheEntriesToWaitFor &= ~WAIT_FOR_CACHE_ENTRY; + + nsresult rv; + + if (NS_SUCCEEDED(aEntryStatus)) { + if (!mApplicationCache) { + mApplicationCache = aAppCache; + } + + // We successfully opened an offline cache session and the entry, + // so indicate we will load from the offline cache. + mLoadedFromApplicationCache = true; + mCacheEntryIsReadOnly = true; + mCacheEntry = aEntry; + mCacheEntryIsWriteOnly = false; + + if (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI && !mApplicationCacheForWrite) { + MaybeWarnAboutAppCache(); + } + + return NS_OK; + } + + if (!mApplicationCacheForWrite && !mFallbackChannel) { + if (!mApplicationCache) { + mApplicationCache = aAppCache; + } + + // Check for namespace match. + nsCOMPtr<nsIApplicationCacheNamespace> namespaceEntry; + rv = mApplicationCache->GetMatchingNamespace(mSpec, + getter_AddRefs(namespaceEntry)); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t namespaceType = 0; + if (!namespaceEntry || + NS_FAILED(namespaceEntry->GetItemType(&namespaceType)) || + (namespaceType & + (nsIApplicationCacheNamespace::NAMESPACE_FALLBACK | + nsIApplicationCacheNamespace::NAMESPACE_BYPASS)) == 0) { + // When loading from an application cache, only items + // on the whitelist or matching a + // fallback namespace should hit the network... + mLoadFlags |= LOAD_ONLY_FROM_CACHE; + + // ... and if there were an application cache entry, + // we would have found it earlier. + return NS_ERROR_CACHE_KEY_NOT_FOUND; + } + + if (namespaceType & + nsIApplicationCacheNamespace::NAMESPACE_FALLBACK) { + + nsAutoCString namespaceSpec; + rv = namespaceEntry->GetNamespaceSpec(namespaceSpec); + NS_ENSURE_SUCCESS(rv, rv); + + // This prevents fallback attacks injected by an insecure subdirectory + // for the whole origin (or a parent directory). + if (!IsInSubpathOfAppCacheManifest(mApplicationCache, namespaceSpec)) { + return NS_OK; + } + + rv = namespaceEntry->GetData(mFallbackKey); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return NS_OK; +} + +nsresult +nsHttpChannel::OnOfflineCacheEntryForWritingAvailable(nsICacheEntry *aEntry, + nsIApplicationCache* aAppCache, + nsresult aEntryStatus) +{ + MOZ_ASSERT(mApplicationCacheForWrite && aAppCache == mApplicationCacheForWrite); + + mCacheEntriesToWaitFor &= ~WAIT_FOR_OFFLINE_CACHE_ENTRY; + + if (NS_SUCCEEDED(aEntryStatus)) { + mOfflineCacheEntry = aEntry; + if (NS_FAILED(aEntry->GetLastModified(&mOfflineCacheLastModifiedTime))) { + mOfflineCacheLastModifiedTime = 0; + } + } + + return aEntryStatus; +} + +// Generates the proper cache-key for this instance of nsHttpChannel +nsresult +nsHttpChannel::GenerateCacheKey(uint32_t postID, nsACString &cacheKey) +{ + AssembleCacheKey(mFallbackChannel ? mFallbackKey.get() : mSpec.get(), + postID, cacheKey); + return NS_OK; +} + +// Assembles a cache-key from the given pieces of information and |mLoadFlags| +void +nsHttpChannel::AssembleCacheKey(const char *spec, uint32_t postID, + nsACString &cacheKey) +{ + cacheKey.Truncate(); + + if (mLoadFlags & LOAD_ANONYMOUS) { + cacheKey.AssignLiteral("anon&"); + } + + if (postID) { + char buf[32]; + SprintfLiteral(buf, "id=%x&", postID); + cacheKey.Append(buf); + } + + if (!cacheKey.IsEmpty()) { + cacheKey.AppendLiteral("uri="); + } + + // Strip any trailing #ref from the URL before using it as the key + const char *p = strchr(spec, '#'); + if (p) + cacheKey.Append(spec, p - spec); + else + cacheKey.Append(spec); +} + +nsresult +DoUpdateExpirationTime(nsHttpChannel* aSelf, + nsICacheEntry* aCacheEntry, + nsHttpResponseHead* aResponseHead, + uint32_t& aExpirationTime) +{ + MOZ_ASSERT(aExpirationTime == 0); + NS_ENSURE_TRUE(aResponseHead, NS_ERROR_FAILURE); + + nsresult rv; + + if (!aResponseHead->MustValidate()) { + uint32_t freshnessLifetime = 0; + + rv = aResponseHead->ComputeFreshnessLifetime(&freshnessLifetime); + if (NS_FAILED(rv)) return rv; + + if (freshnessLifetime > 0) { + uint32_t now = NowInSeconds(), currentAge = 0; + + rv = aResponseHead->ComputeCurrentAge(now, aSelf->GetRequestTime(), ¤tAge); + if (NS_FAILED(rv)) return rv; + + LOG(("freshnessLifetime = %u, currentAge = %u\n", + freshnessLifetime, currentAge)); + + if (freshnessLifetime > currentAge) { + uint32_t timeRemaining = freshnessLifetime - currentAge; + // be careful... now + timeRemaining may overflow + if (now + timeRemaining < now) + aExpirationTime = uint32_t(-1); + else + aExpirationTime = now + timeRemaining; + } + else + aExpirationTime = now; + } + } + + rv = aCacheEntry->SetExpirationTime(aExpirationTime); + NS_ENSURE_SUCCESS(rv, rv); + + return rv; +} + +// UpdateExpirationTime is called when a new response comes in from the server. +// It updates the stored response-time and sets the expiration time on the +// cache entry. +// +// From section 13.2.4 of RFC2616, we compute expiration time as follows: +// +// timeRemaining = freshnessLifetime - currentAge +// expirationTime = now + timeRemaining +// +nsresult +nsHttpChannel::UpdateExpirationTime() +{ + uint32_t expirationTime = 0; + nsresult rv = DoUpdateExpirationTime(this, mCacheEntry, mResponseHead, expirationTime); + NS_ENSURE_SUCCESS(rv, rv); + + if (mOfflineCacheEntry) { + rv = mOfflineCacheEntry->SetExpirationTime(expirationTime); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +/*static*/ inline bool +nsHttpChannel::HasQueryString(nsHttpRequestHead::ParsedMethodType method, nsIURI * uri) +{ + // Must be called on the main thread because nsIURI does not implement + // thread-safe QueryInterface. + MOZ_ASSERT(NS_IsMainThread()); + + if (method != nsHttpRequestHead::kMethod_Get && + method != nsHttpRequestHead::kMethod_Head) + return false; + + nsAutoCString query; + nsCOMPtr<nsIURL> url = do_QueryInterface(uri); + nsresult rv = url->GetQuery(query); + return NS_SUCCEEDED(rv) && !query.IsEmpty(); +} + +bool +nsHttpChannel::ShouldUpdateOfflineCacheEntry() +{ + if (!mApplicationCacheForWrite || !mOfflineCacheEntry) { + return false; + } + + // if we're updating the cache entry, update the offline cache entry too + if (mCacheEntry && mCacheEntryIsWriteOnly) { + return true; + } + + // if there's nothing in the offline cache, add it + if (mOfflineCacheEntry) { + return true; + } + + // if the document is newer than the offline entry, update it + uint32_t docLastModifiedTime; + nsresult rv = mResponseHead->GetLastModifiedValue(&docLastModifiedTime); + if (NS_FAILED(rv)) { + return true; + } + + if (mOfflineCacheLastModifiedTime == 0) { + return false; + } + + if (docLastModifiedTime > mOfflineCacheLastModifiedTime) { + return true; + } + + return false; +} + +nsresult +nsHttpChannel::OpenCacheInputStream(nsICacheEntry* cacheEntry, bool startBuffering, + bool checkingAppCacheEntry) +{ + nsresult rv; + + bool isHttps = false; + rv = mURI->SchemeIs("https", &isHttps); + NS_ENSURE_SUCCESS(rv,rv); + + if (isHttps) { + rv = cacheEntry->GetSecurityInfo( + getter_AddRefs(mCachedSecurityInfo)); + if (NS_FAILED(rv)) { + LOG(("failed to parse security-info [channel=%p, entry=%p]", + this, cacheEntry)); + NS_WARNING("failed to parse security-info"); + cacheEntry->AsyncDoom(nullptr); + return rv; + } + + // XXX: We should not be skilling this check in the offline cache + // case, but we have to do so now to work around bug 794507. + bool mustHaveSecurityInfo = !mLoadedFromApplicationCache && !checkingAppCacheEntry; + MOZ_ASSERT(mCachedSecurityInfo || !mustHaveSecurityInfo); + if (!mCachedSecurityInfo && mustHaveSecurityInfo) { + LOG(("mCacheEntry->GetSecurityInfo returned success but did not " + "return the security info [channel=%p, entry=%p]", + this, cacheEntry)); + cacheEntry->AsyncDoom(nullptr); + return NS_ERROR_UNEXPECTED; // XXX error code + } + } + + // Keep the conditions below in sync with the conditions in ReadFromCache. + + rv = NS_OK; + + if (WillRedirect(mCachedResponseHead)) { + // Do not even try to read the entity for a redirect because we do not + // return an entity to the application when we process redirects. + LOG(("Will skip read of cached redirect entity\n")); + return NS_OK; + } + + if ((mLoadFlags & nsICachingChannel::LOAD_ONLY_IF_MODIFIED) && + !mCachedContentIsPartial) { + // For LOAD_ONLY_IF_MODIFIED, we usually don't have to deal with the + // cached entity. + if (!mApplicationCacheForWrite) { + LOG(("Will skip read from cache based on LOAD_ONLY_IF_MODIFIED " + "load flag\n")); + return NS_OK; + } + + // If offline caching has been requested and the offline cache needs + // updating, we must complete the call even if the main cache entry + // is up to date. We don't know yet for sure whether the offline + // cache needs updating because at this point we haven't opened it + // for writing yet, so we have to start reading the cached entity now + // just in case. + LOG(("May skip read from cache based on LOAD_ONLY_IF_MODIFIED " + "load flag\n")); + } + + // Open an input stream for the entity, so that the call to OpenInputStream + // happens off the main thread. + nsCOMPtr<nsIInputStream> stream; + + // If an alternate representation was requested, try to open the alt + // input stream. + if (!mPreferredCachedAltDataType.IsEmpty()) { + rv = cacheEntry->OpenAlternativeInputStream(mPreferredCachedAltDataType, + getter_AddRefs(stream)); + if (NS_SUCCEEDED(rv)) { + // We have succeeded. + mAvailableCachedAltDataType = mPreferredCachedAltDataType; + // Clear the header. + mCachedResponseHead->SetContentLength(-1); + // Set the correct data size on the channel. + int64_t altDataSize; + if (NS_SUCCEEDED(cacheEntry->GetAltDataSize(&altDataSize))) { + mCachedResponseHead->SetContentLength(altDataSize); + } + } + } + + if (!stream) { + rv = cacheEntry->OpenInputStream(0, getter_AddRefs(stream)); + } + + if (NS_FAILED(rv)) { + LOG(("Failed to open cache input stream [channel=%p, " + "mCacheEntry=%p]", this, cacheEntry)); + return rv; + } + + if (startBuffering) { + bool nonBlocking; + rv = stream->IsNonBlocking(&nonBlocking); + if (NS_SUCCEEDED(rv) && nonBlocking) + startBuffering = false; + } + + if (!startBuffering) { + // Bypass wrapping the input stream for the new cache back-end since + // nsIStreamTransportService expects a blocking stream. Preloading of + // the data must be done on the level of the cache backend, internally. + // + // We do not connect the stream to the stream transport service if we + // have to validate the entry with the server. If we did, we would get + // into a race condition between the stream transport service reading + // the existing contents and the opening of the cache entry's output + // stream to write the new contents in the case where we get a non-304 + // response. + LOG(("Opened cache input stream without buffering [channel=%p, " + "mCacheEntry=%p, stream=%p]", this, + cacheEntry, stream.get())); + mCacheInputStream.takeOver(stream); + return rv; + } + + // Have the stream transport service start reading the entity on one of its + // background threads. + + nsCOMPtr<nsITransport> transport; + nsCOMPtr<nsIInputStream> wrapper; + + nsCOMPtr<nsIStreamTransportService> sts = + do_GetService(kStreamTransportServiceCID, &rv); + if (NS_SUCCEEDED(rv)) { + rv = sts->CreateInputTransport(stream, int64_t(-1), int64_t(-1), + true, getter_AddRefs(transport)); + } + if (NS_SUCCEEDED(rv)) { + rv = transport->OpenInputStream(0, 0, 0, getter_AddRefs(wrapper)); + } + if (NS_SUCCEEDED(rv)) { + LOG(("Opened cache input stream [channel=%p, wrapper=%p, " + "transport=%p, stream=%p]", this, wrapper.get(), + transport.get(), stream.get())); + } else { + LOG(("Failed to open cache input stream [channel=%p, " + "wrapper=%p, transport=%p, stream=%p]", this, + wrapper.get(), transport.get(), stream.get())); + + stream->Close(); + return rv; + } + + mCacheInputStream.takeOver(wrapper); + + return NS_OK; +} + +// Actually process the cached response that we started to handle in CheckCache +// and/or StartBufferingCachedEntity. +nsresult +nsHttpChannel::ReadFromCache(bool alreadyMarkedValid) +{ + NS_ENSURE_TRUE(mCacheEntry, NS_ERROR_FAILURE); + NS_ENSURE_TRUE(mCachedContentIsValid, NS_ERROR_FAILURE); + + LOG(("nsHttpChannel::ReadFromCache [this=%p] " + "Using cached copy of: %s\n", this, mSpec.get())); + + if (mCachedResponseHead) + mResponseHead = Move(mCachedResponseHead); + + UpdateInhibitPersistentCachingFlag(); + + // if we don't already have security info, try to get it from the cache + // entry. there are two cases to consider here: 1) we are just reading + // from the cache, or 2) this may be due to a 304 not modified response, + // in which case we could have security info from a socket transport. + if (!mSecurityInfo) + mSecurityInfo = mCachedSecurityInfo; + + if (!alreadyMarkedValid && !mCachedContentIsPartial) { + // We validated the entry, and we have write access to the cache, so + // mark the cache entry as valid in order to allow others access to + // this cache entry. + // + // TODO: This should be done asynchronously so we don't take the cache + // service lock on the main thread. + mCacheEntry->MaybeMarkValid(); + } + + nsresult rv; + + // Keep the conditions below in sync with the conditions in + // StartBufferingCachedEntity. + + if (WillRedirect(mResponseHead)) { + // TODO: Bug 759040 - We should call HandleAsyncRedirect directly here, + // to avoid event dispatching latency. + MOZ_ASSERT(!mCacheInputStream); + LOG(("Skipping skip read of cached redirect entity\n")); + return AsyncCall(&nsHttpChannel::HandleAsyncRedirect); + } + + if ((mLoadFlags & LOAD_ONLY_IF_MODIFIED) && !mCachedContentIsPartial) { + if (!mApplicationCacheForWrite) { + LOG(("Skipping read from cache based on LOAD_ONLY_IF_MODIFIED " + "load flag\n")); + MOZ_ASSERT(!mCacheInputStream); + // TODO: Bug 759040 - We should call HandleAsyncNotModified directly + // here, to avoid event dispatching latency. + return AsyncCall(&nsHttpChannel::HandleAsyncNotModified); + } + + if (!ShouldUpdateOfflineCacheEntry()) { + LOG(("Skipping read from cache based on LOAD_ONLY_IF_MODIFIED " + "load flag (mApplicationCacheForWrite not null case)\n")); + mCacheInputStream.CloseAndRelease(); + // TODO: Bug 759040 - We should call HandleAsyncNotModified directly + // here, to avoid event dispatching latency. + return AsyncCall(&nsHttpChannel::HandleAsyncNotModified); + } + } + + MOZ_ASSERT(mCacheInputStream); + if (!mCacheInputStream) { + NS_ERROR("mCacheInputStream is null but we're expecting to " + "be able to read from it."); + return NS_ERROR_UNEXPECTED; + } + + + nsCOMPtr<nsIInputStream> inputStream = mCacheInputStream.forget(); + + rv = nsInputStreamPump::Create(getter_AddRefs(mCachePump), inputStream, + int64_t(-1), int64_t(-1), 0, 0, true); + if (NS_FAILED(rv)) { + inputStream->Close(); + return rv; + } + + rv = mCachePump->AsyncRead(this, mListenerContext); + if (NS_FAILED(rv)) return rv; + + if (mTimingEnabled) + mCacheReadStart = TimeStamp::Now(); + + uint32_t suspendCount = mSuspendCount; + while (suspendCount--) + mCachePump->Suspend(); + + return NS_OK; +} + +void +nsHttpChannel::CloseCacheEntry(bool doomOnFailure) +{ + mCacheInputStream.CloseAndRelease(); + + if (!mCacheEntry) + return; + + LOG(("nsHttpChannel::CloseCacheEntry [this=%p] mStatus=%x mCacheEntryIsWriteOnly=%x", + this, mStatus, mCacheEntryIsWriteOnly)); + + // If we have begun to create or replace a cache entry, and that cache + // entry is not complete and not resumable, then it needs to be doomed. + // Otherwise, CheckCache will make the mistake of thinking that the + // partial cache entry is complete. + + bool doom = false; + if (mInitedCacheEntry) { + MOZ_ASSERT(mResponseHead, "oops"); + if (NS_FAILED(mStatus) && doomOnFailure && + mCacheEntryIsWriteOnly && !mResponseHead->IsResumable()) + doom = true; + } + else if (mCacheEntryIsWriteOnly) + doom = true; + + if (doom) { + LOG((" dooming cache entry!!")); + mCacheEntry->AsyncDoom(nullptr); + } else { + // Store updated security info, makes cached EV status race less likely + // (see bug 1040086) + if (mSecurityInfo) + mCacheEntry->SetSecurityInfo(mSecurityInfo); + } + + mCachedResponseHead = nullptr; + + mCachePump = nullptr; + mCacheEntry = nullptr; + mCacheEntryIsWriteOnly = false; + mInitedCacheEntry = false; +} + + +void +nsHttpChannel::CloseOfflineCacheEntry() +{ + if (!mOfflineCacheEntry) + return; + + LOG(("nsHttpChannel::CloseOfflineCacheEntry [this=%p]", this)); + + if (NS_FAILED(mStatus)) { + mOfflineCacheEntry->AsyncDoom(nullptr); + } + else { + bool succeeded; + if (NS_SUCCEEDED(GetRequestSucceeded(&succeeded)) && !succeeded) + mOfflineCacheEntry->AsyncDoom(nullptr); + } + + mOfflineCacheEntry = nullptr; +} + + +// Initialize the cache entry for writing. +// - finalize storage policy +// - store security info +// - update expiration time +// - store headers and other meta data +nsresult +nsHttpChannel::InitCacheEntry() +{ + nsresult rv; + + NS_ENSURE_TRUE(mCacheEntry, NS_ERROR_UNEXPECTED); + // if only reading, nothing to be done here. + if (mCacheEntryIsReadOnly) + return NS_OK; + + // Don't cache the response again if already cached... + if (mCachedContentIsValid) + return NS_OK; + + LOG(("nsHttpChannel::InitCacheEntry [this=%p entry=%p]\n", + this, mCacheEntry.get())); + + bool recreate = !mCacheEntryIsWriteOnly; + bool dontPersist = mLoadFlags & INHIBIT_PERSISTENT_CACHING; + + if (!recreate && dontPersist) { + // If the current entry is persistent but we inhibit peristence + // then force recreation of the entry as memory/only. + rv = mCacheEntry->GetPersistent(&recreate); + if (NS_FAILED(rv)) + return rv; + } + + if (recreate) { + LOG((" we have a ready entry, but reading it again from the server -> recreating cache entry\n")); + nsCOMPtr<nsICacheEntry> currentEntry; + currentEntry.swap(mCacheEntry); + rv = currentEntry->Recreate(dontPersist, getter_AddRefs(mCacheEntry)); + if (NS_FAILED(rv)) { + LOG((" recreation failed, the response will not be cached")); + return NS_OK; + } + + mCacheEntryIsWriteOnly = true; + } + + // Set the expiration time for this cache entry + rv = UpdateExpirationTime(); + if (NS_FAILED(rv)) return rv; + + // mark this weakly framed until a response body is seen + mCacheEntry->SetMetaDataElement("strongly-framed", "0"); + + rv = AddCacheEntryHeaders(mCacheEntry); + if (NS_FAILED(rv)) return rv; + + mInitedCacheEntry = true; + + // Don't perform the check when writing (doesn't make sense) + mConcurrentCacheAccess = 0; + + return NS_OK; +} + +void +nsHttpChannel::UpdateInhibitPersistentCachingFlag() +{ + // The no-store directive within the 'Cache-Control:' header indicates + // that we must not store the response in a persistent cache. + if (mResponseHead->NoStore()) + mLoadFlags |= INHIBIT_PERSISTENT_CACHING; + + // Only cache SSL content on disk if the pref is set + bool isHttps; + if (!gHttpHandler->IsPersistentHttpsCachingEnabled() && + NS_SUCCEEDED(mURI->SchemeIs("https", &isHttps)) && isHttps) { + mLoadFlags |= INHIBIT_PERSISTENT_CACHING; + } +} + +nsresult +nsHttpChannel::InitOfflineCacheEntry() +{ + // This function can be called even when we fail to connect (bug 551990) + + if (!mOfflineCacheEntry) { + return NS_OK; + } + + if (!mResponseHead || mResponseHead->NoStore()) { + if (mResponseHead && mResponseHead->NoStore()) { + mOfflineCacheEntry->AsyncDoom(nullptr); + } + + CloseOfflineCacheEntry(); + + if (mResponseHead && mResponseHead->NoStore()) { + return NS_ERROR_NOT_AVAILABLE; + } + + return NS_OK; + } + + // This entry's expiration time should match the main entry's expiration + // time. UpdateExpirationTime() will keep it in sync once the offline + // cache entry has been created. + if (mCacheEntry) { + uint32_t expirationTime; + nsresult rv = mCacheEntry->GetExpirationTime(&expirationTime); + NS_ENSURE_SUCCESS(rv, rv); + + mOfflineCacheEntry->SetExpirationTime(expirationTime); + } + + return AddCacheEntryHeaders(mOfflineCacheEntry); +} + + +nsresult +DoAddCacheEntryHeaders(nsHttpChannel *self, + nsICacheEntry *entry, + nsHttpRequestHead *requestHead, + nsHttpResponseHead *responseHead, + nsISupports *securityInfo) +{ + nsresult rv; + + LOG(("nsHttpChannel::AddCacheEntryHeaders [this=%p] begin", self)); + // Store secure data in memory only + if (securityInfo) + entry->SetSecurityInfo(securityInfo); + + // Store the HTTP request method with the cache entry so we can distinguish + // for example GET and HEAD responses. + nsAutoCString method; + requestHead->Method(method); + rv = entry->SetMetaDataElement("request-method", method.get()); + if (NS_FAILED(rv)) return rv; + + // Store the HTTP authorization scheme used if any... + rv = StoreAuthorizationMetaData(entry, requestHead); + if (NS_FAILED(rv)) return rv; + + // Iterate over the headers listed in the Vary response header, and + // store the value of the corresponding request header so we can verify + // that it has not varied when we try to re-use the cached response at + // a later time. Take care to store "Cookie" headers only as hashes + // due to security considerations and the fact that they can be pretty + // large (bug 468426). We take care of "Vary: cookie" in ResponseWouldVary. + // + // NOTE: if "Vary: accept, cookie", then we will store the "accept" header + // in the cache. we could try to avoid needlessly storing the "accept" + // header in this case, but it doesn't seem worth the extra code to perform + // the check. + { + nsAutoCString buf, metaKey; + responseHead->GetHeader(nsHttp::Vary, buf); + if (!buf.IsEmpty()) { + NS_NAMED_LITERAL_CSTRING(prefix, "request-"); + + char *bufData = buf.BeginWriting(); // going to munge buf + char *token = nsCRT::strtok(bufData, NS_HTTP_HEADER_SEPS, &bufData); + while (token) { + LOG(("nsHttpChannel::AddCacheEntryHeaders [this=%p] " \ + "processing %s", self, token)); + if (*token != '*') { + nsHttpAtom atom = nsHttp::ResolveAtom(token); + nsAutoCString val; + nsAutoCString hash; + if (NS_SUCCEEDED(requestHead->GetHeader(atom, val))) { + // If cookie-header, store a hash of the value + if (atom == nsHttp::Cookie) { + LOG(("nsHttpChannel::AddCacheEntryHeaders [this=%p] " \ + "cookie-value %s", self, val.get())); + rv = Hash(val.get(), hash); + // If hash failed, store a string not very likely + // to be the result of subsequent hashes + if (NS_FAILED(rv)) { + val = NS_LITERAL_CSTRING("<hash failed>"); + } else { + val = hash; + } + + LOG((" hashed to %s\n", val.get())); + } + + // build cache meta data key and set meta data element... + metaKey = prefix + nsDependentCString(token); + entry->SetMetaDataElement(metaKey.get(), val.get()); + } else { + LOG(("nsHttpChannel::AddCacheEntryHeaders [this=%p] " \ + "clearing metadata for %s", self, token)); + metaKey = prefix + nsDependentCString(token); + entry->SetMetaDataElement(metaKey.get(), nullptr); + } + } + token = nsCRT::strtok(bufData, NS_HTTP_HEADER_SEPS, &bufData); + } + } + } + + // Store the received HTTP head with the cache entry as an element of + // the meta data. + nsAutoCString head; + responseHead->Flatten(head, true); + rv = entry->SetMetaDataElement("response-head", head.get()); + if (NS_FAILED(rv)) return rv; + head.Truncate(); + responseHead->FlattenNetworkOriginalHeaders(head); + rv = entry->SetMetaDataElement("original-response-headers", head.get()); + if (NS_FAILED(rv)) return rv; + + // Indicate we have successfully finished setting metadata on the cache entry. + rv = entry->MetaDataReady(); + + return rv; +} + +nsresult +nsHttpChannel::AddCacheEntryHeaders(nsICacheEntry *entry) +{ + return DoAddCacheEntryHeaders(this, entry, &mRequestHead, mResponseHead, mSecurityInfo); +} + +inline void +GetAuthType(const char *challenge, nsCString &authType) +{ + const char *p; + + // get the challenge type + if ((p = strchr(challenge, ' ')) != nullptr) + authType.Assign(challenge, p - challenge); + else + authType.Assign(challenge); +} + +nsresult +StoreAuthorizationMetaData(nsICacheEntry *entry, nsHttpRequestHead *requestHead) +{ + // Not applicable to proxy authorization... + nsAutoCString val; + if (NS_FAILED(requestHead->GetHeader(nsHttp::Authorization, val))) { + return NS_OK; + } + + // eg. [Basic realm="wally world"] + nsAutoCString buf; + GetAuthType(val.get(), buf); + return entry->SetMetaDataElement("auth", buf.get()); +} + +// Finalize the cache entry +// - may need to rewrite response headers if any headers changed +// - may need to recalculate the expiration time if any headers changed +// - called only for freshly written cache entries +nsresult +nsHttpChannel::FinalizeCacheEntry() +{ + LOG(("nsHttpChannel::FinalizeCacheEntry [this=%p]\n", this)); + + // Don't update this meta-data on 304 + if (mStronglyFramed && !mCachedContentIsValid && mCacheEntry) { + LOG(("nsHttpChannel::FinalizeCacheEntry [this=%p] Is Strongly Framed\n", this)); + mCacheEntry->SetMetaDataElement("strongly-framed", "1"); + } + + if (mResponseHead && mResponseHeadersModified) { + // Set the expiration time for this cache entry + nsresult rv = UpdateExpirationTime(); + if (NS_FAILED(rv)) return rv; + } + return NS_OK; +} + +// Open an output stream to the cache entry and insert a listener tee into +// the chain of response listeners. +nsresult +nsHttpChannel::InstallCacheListener(int64_t offset) +{ + nsresult rv; + + LOG(("Preparing to write data into the cache [uri=%s]\n", mSpec.get())); + + MOZ_ASSERT(mCacheEntry); + MOZ_ASSERT(mCacheEntryIsWriteOnly || mCachedContentIsPartial); + MOZ_ASSERT(mListener); + + nsAutoCString contentEncoding, contentType; + mResponseHead->GetHeader(nsHttp::Content_Encoding, contentEncoding); + mResponseHead->ContentType(contentType); + // If the content is compressible and the server has not compressed it, + // mark the cache entry for compression. + if (contentEncoding.IsEmpty() && + (contentType.EqualsLiteral(TEXT_HTML) || + contentType.EqualsLiteral(TEXT_PLAIN) || + contentType.EqualsLiteral(TEXT_CSS) || + contentType.EqualsLiteral(TEXT_JAVASCRIPT) || + contentType.EqualsLiteral(TEXT_ECMASCRIPT) || + contentType.EqualsLiteral(TEXT_XML) || + contentType.EqualsLiteral(APPLICATION_JAVASCRIPT) || + contentType.EqualsLiteral(APPLICATION_ECMASCRIPT) || + contentType.EqualsLiteral(APPLICATION_XJAVASCRIPT) || + contentType.EqualsLiteral(APPLICATION_XHTML_XML))) { + rv = mCacheEntry->SetMetaDataElement("uncompressed-len", "0"); + if (NS_FAILED(rv)) { + LOG(("unable to mark cache entry for compression")); + } + } + + LOG(("Trading cache input stream for output stream [channel=%p]", this)); + + // We must close the input stream first because cache entries do not + // correctly handle having an output stream and input streams open at + // the same time. + mCacheInputStream.CloseAndRelease(); + + nsCOMPtr<nsIOutputStream> out; + rv = mCacheEntry->OpenOutputStream(offset, getter_AddRefs(out)); + if (rv == NS_ERROR_NOT_AVAILABLE) { + LOG((" entry doomed, not writing it [channel=%p]", this)); + // Entry is already doomed. + // This may happen when expiration time is set to past and the entry + // has been removed by the background eviction logic. + return NS_OK; + } + if (NS_FAILED(rv)) return rv; + + if (mCacheOnlyMetadata) { + LOG(("Not storing content, cacheOnlyMetadata set")); + // We must open and then close the output stream of the cache entry. + // This way we indicate the content has been written (despite with zero + // length) and the entry is now in the ready state with "having data". + + out->Close(); + return NS_OK; + } + + // XXX disk cache does not support overlapped i/o yet +#if 0 + // Mark entry valid inorder to allow simultaneous reading... + rv = mCacheEntry->MarkValid(); + if (NS_FAILED(rv)) return rv; +#endif + + nsCOMPtr<nsIStreamListenerTee> tee = + do_CreateInstance(kStreamListenerTeeCID, &rv); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIEventTarget> cacheIOTarget; + if (!CacheObserver::UseNewCache()) { + nsCOMPtr<nsICacheStorageService> serv = + do_GetService("@mozilla.org/netwerk/cache-storage-service;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + serv->GetIoTarget(getter_AddRefs(cacheIOTarget)); + } + + if (!cacheIOTarget) { + LOG(("nsHttpChannel::InstallCacheListener sync tee %p rv=%x " + "cacheIOTarget=%p", tee.get(), rv, cacheIOTarget.get())); + rv = tee->Init(mListener, out, nullptr); + } else { + LOG(("nsHttpChannel::InstallCacheListener async tee %p", tee.get())); + rv = tee->InitAsync(mListener, cacheIOTarget, out, nullptr); + } + + if (NS_FAILED(rv)) return rv; + mListener = tee; + return NS_OK; +} + +nsresult +nsHttpChannel::InstallOfflineCacheListener(int64_t offset) +{ + nsresult rv; + + LOG(("Preparing to write data into the offline cache [uri=%s]\n", + mSpec.get())); + + MOZ_ASSERT(mOfflineCacheEntry); + MOZ_ASSERT(mListener); + + nsCOMPtr<nsIOutputStream> out; + rv = mOfflineCacheEntry->OpenOutputStream(offset, getter_AddRefs(out)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIStreamListenerTee> tee = + do_CreateInstance(kStreamListenerTeeCID, &rv); + if (NS_FAILED(rv)) return rv; + + rv = tee->Init(mListener, out, nullptr); + if (NS_FAILED(rv)) return rv; + + mListener = tee; + + return NS_OK; +} + +void +nsHttpChannel::ClearBogusContentEncodingIfNeeded() +{ + // For .gz files, apache sends both a Content-Type: application/x-gzip + // as well as Content-Encoding: gzip, which is completely wrong. In + // this case, we choose to ignore the rogue Content-Encoding header. We + // must do this early on so as to prevent it from being seen up stream. + // The same problem exists for Content-Encoding: compress in default + // Apache installs. + nsAutoCString contentType; + mResponseHead->ContentType(contentType); + if (mResponseHead->HasHeaderValue(nsHttp::Content_Encoding, "gzip") && ( + contentType.EqualsLiteral(APPLICATION_GZIP) || + contentType.EqualsLiteral(APPLICATION_GZIP2) || + contentType.EqualsLiteral(APPLICATION_GZIP3))) { + // clear the Content-Encoding header + mResponseHead->ClearHeader(nsHttp::Content_Encoding); + } + else if (mResponseHead->HasHeaderValue(nsHttp::Content_Encoding, "compress") && ( + contentType.EqualsLiteral(APPLICATION_COMPRESS) || + contentType.EqualsLiteral(APPLICATION_COMPRESS2))) { + // clear the Content-Encoding header + mResponseHead->ClearHeader(nsHttp::Content_Encoding); + } +} + +//----------------------------------------------------------------------------- +// nsHttpChannel <redirect> +//----------------------------------------------------------------------------- + +nsresult +nsHttpChannel::SetupReplacementChannel(nsIURI *newURI, + nsIChannel *newChannel, + bool preserveMethod, + uint32_t redirectFlags) +{ + LOG(("nsHttpChannel::SetupReplacementChannel " + "[this=%p newChannel=%p preserveMethod=%d]", + this, newChannel, preserveMethod)); + + nsresult rv = + HttpBaseChannel::SetupReplacementChannel(newURI, newChannel, + preserveMethod, redirectFlags); + if (NS_FAILED(rv)) + return rv; + + nsCOMPtr<nsIHttpChannel> httpChannel = do_QueryInterface(newChannel); + if (!httpChannel) + return NS_OK; // no other options to set + + // convey the mApplyConversion flag (bug 91862) + nsCOMPtr<nsIEncodedChannel> encodedChannel = do_QueryInterface(httpChannel); + if (encodedChannel) + encodedChannel->SetApplyConversion(mApplyConversion); + + // transfer the resume information + if (mResuming) { + nsCOMPtr<nsIResumableChannel> resumableChannel(do_QueryInterface(newChannel)); + if (!resumableChannel) { + NS_WARNING("Got asked to resume, but redirected to non-resumable channel!"); + return NS_ERROR_NOT_RESUMABLE; + } + resumableChannel->ResumeAt(mStartPos, mEntityID); + } + + if (!(redirectFlags & nsIChannelEventSink::REDIRECT_STS_UPGRADE) && + mInterceptCache != INTERCEPTED) { + // Ensure that internally-redirected channels, or loads with manual + // redirect mode cannot be intercepted, which would look like two + // separate requests to the nsINetworkInterceptController. + if (mRedirectMode != nsIHttpChannelInternal::REDIRECT_MODE_MANUAL || + (redirectFlags & (nsIChannelEventSink::REDIRECT_TEMPORARY | + nsIChannelEventSink::REDIRECT_PERMANENT)) == 0) { + nsLoadFlags loadFlags = nsIRequest::LOAD_NORMAL; + rv = newChannel->GetLoadFlags(&loadFlags); + NS_ENSURE_SUCCESS(rv, rv); + loadFlags |= nsIChannel::LOAD_BYPASS_SERVICE_WORKER; + rv = newChannel->SetLoadFlags(loadFlags); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return NS_OK; +} + +nsresult +nsHttpChannel::AsyncProcessRedirection(uint32_t redirectType) +{ + LOG(("nsHttpChannel::AsyncProcessRedirection [this=%p type=%u]\n", + this, redirectType)); + + nsAutoCString location; + + // if a location header was not given, then we can't perform the redirect, + // so just carry on as though this were a normal response. + if (NS_FAILED(mResponseHead->GetHeader(nsHttp::Location, location))) + return NS_ERROR_FAILURE; + + // make sure non-ASCII characters in the location header are escaped. + nsAutoCString locationBuf; + if (NS_EscapeURL(location.get(), -1, esc_OnlyNonASCII, locationBuf)) + location = locationBuf; + + if (mRedirectionLimit == 0) { + LOG(("redirection limit reached!\n")); + return NS_ERROR_REDIRECT_LOOP; + } + + mRedirectType = redirectType; + + LOG(("redirecting to: %s [redirection-limit=%u]\n", + location.get(), uint32_t(mRedirectionLimit))); + + nsresult rv = CreateNewURI(location.get(), getter_AddRefs(mRedirectURI)); + + if (NS_FAILED(rv)) { + LOG(("Invalid URI for redirect: Location: %s\n", location.get())); + return NS_ERROR_CORRUPTED_CONTENT; + } + + if (mApplicationCache) { + // if we are redirected to a different origin check if there is a fallback + // cache entry to fall back to. we don't care about file strict + // checking, at least mURI is not a file URI. + if (!NS_SecurityCompareURIs(mURI, mRedirectURI, false)) { + PushRedirectAsyncFunc(&nsHttpChannel::ContinueProcessRedirectionAfterFallback); + bool waitingForRedirectCallback; + (void)ProcessFallback(&waitingForRedirectCallback); + if (waitingForRedirectCallback) + return NS_OK; + PopRedirectAsyncFunc(&nsHttpChannel::ContinueProcessRedirectionAfterFallback); + } + } + + return ContinueProcessRedirectionAfterFallback(NS_OK); +} + +nsresult +nsHttpChannel::ContinueProcessRedirectionAfterFallback(nsresult rv) +{ + if (NS_SUCCEEDED(rv) && mFallingBack) { + // do not continue with redirect processing, fallback is in + // progress now. + return NS_OK; + } + + // Kill the current cache entry if we are redirecting + // back to ourself. + bool redirectingBackToSameURI = false; + if (mCacheEntry && mCacheEntryIsWriteOnly && + NS_SUCCEEDED(mURI->Equals(mRedirectURI, &redirectingBackToSameURI)) && + redirectingBackToSameURI) + mCacheEntry->AsyncDoom(nullptr); + + bool hasRef = false; + rv = mRedirectURI->GetHasRef(&hasRef); + + // move the reference of the old location to the new one if the new + // one has none. + if (NS_SUCCEEDED(rv) && !hasRef) { + nsAutoCString ref; + mURI->GetRef(ref); + if (!ref.IsEmpty()) { + // NOTE: SetRef will fail if mRedirectURI is immutable + // (e.g. an about: URI)... Oh well. + mRedirectURI->SetRef(ref); + } + } + + bool rewriteToGET = ShouldRewriteRedirectToGET(mRedirectType, + mRequestHead.ParsedMethod()); + + // prompt if the method is not safe (such as POST, PUT, DELETE, ...) + if (!rewriteToGET && !mRequestHead.IsSafeMethod()) { + rv = PromptTempRedirect(); + if (NS_FAILED(rv)) return rv; + } + + nsCOMPtr<nsIIOService> ioService; + rv = gHttpHandler->GetIOService(getter_AddRefs(ioService)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIChannel> newChannel; + rv = NS_NewChannelInternal(getter_AddRefs(newChannel), + mRedirectURI, + mLoadInfo, + nullptr, // aLoadGroup + nullptr, // aCallbacks + nsIRequest::LOAD_NORMAL, + ioService); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t redirectFlags; + if (nsHttp::IsPermanentRedirect(mRedirectType)) + redirectFlags = nsIChannelEventSink::REDIRECT_PERMANENT; + else + redirectFlags = nsIChannelEventSink::REDIRECT_TEMPORARY; + + rv = SetupReplacementChannel(mRedirectURI, newChannel, + !rewriteToGET, redirectFlags); + if (NS_FAILED(rv)) return rv; + + // verify that this is a legal redirect + mRedirectChannel = newChannel; + + PushRedirectAsyncFunc(&nsHttpChannel::ContinueProcessRedirection); + rv = gHttpHandler->AsyncOnChannelRedirect(this, newChannel, redirectFlags); + + if (NS_SUCCEEDED(rv)) + rv = WaitForRedirectCallback(); + + if (NS_FAILED(rv)) { + AutoRedirectVetoNotifier notifier(this); + PopRedirectAsyncFunc(&nsHttpChannel::ContinueProcessRedirection); + } + + return rv; +} + +nsresult +nsHttpChannel::ContinueProcessRedirection(nsresult rv) +{ + AutoRedirectVetoNotifier notifier(this); + + LOG(("nsHttpChannel::ContinueProcessRedirection [rv=%x,this=%p]\n", rv, + this)); + if (NS_FAILED(rv)) + return rv; + + NS_PRECONDITION(mRedirectChannel, "No redirect channel?"); + + // Make sure to do this after we received redirect veto answer, + // i.e. after all sinks had been notified + mRedirectChannel->SetOriginalURI(mOriginalURI); + + // And now, the deprecated way + nsCOMPtr<nsIHttpEventSink> httpEventSink; + GetCallback(httpEventSink); + if (httpEventSink) { + // NOTE: nsIHttpEventSink is only used for compatibility with pre-1.8 + // versions. + rv = httpEventSink->OnRedirect(this, mRedirectChannel); + if (NS_FAILED(rv)) + return rv; + } + // XXX we used to talk directly with the script security manager, but that + // should really be handled by the event sink implementation. + + // begin loading the new channel + if (mLoadInfo && mLoadInfo->GetEnforceSecurity()) { + MOZ_ASSERT(!mListenerContext, "mListenerContext should be null!"); + rv = mRedirectChannel->AsyncOpen2(mListener); + } + else { + rv = mRedirectChannel->AsyncOpen(mListener, mListenerContext); + } + NS_ENSURE_SUCCESS(rv, rv); + + // close down this channel + Cancel(NS_BINDING_REDIRECTED); + + notifier.RedirectSucceeded(); + + ReleaseListeners(); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel <auth> +//----------------------------------------------------------------------------- + +NS_IMETHODIMP nsHttpChannel::OnAuthAvailable() +{ + LOG(("nsHttpChannel::OnAuthAvailable [this=%p]", this)); + + // setting mAuthRetryPending flag and resuming the transaction + // triggers process of throwing away the unauthenticated data already + // coming from the network + mAuthRetryPending = true; + mProxyAuthPending = false; + LOG(("Resuming the transaction, we got credentials from user")); + mTransactionPump->Resume(); + + return NS_OK; +} + +NS_IMETHODIMP nsHttpChannel::OnAuthCancelled(bool userCancel) +{ + LOG(("nsHttpChannel::OnAuthCancelled [this=%p]", this)); + + if (mTransactionPump) { + // If the channel is trying to authenticate to a proxy and + // that was canceled we cannot show the http response body + // from the 40x as that might mislead the user into thinking + // it was a end host response instead of a proxy reponse. + // This must check explicitly whether a proxy auth was being done + // because we do want to show the content if this is an error from + // the origin server. + if (mProxyAuthPending) + Cancel(NS_ERROR_PROXY_CONNECTION_REFUSED); + + // ensure call of OnStartRequest of the current listener here, + // it would not be called otherwise at all + nsresult rv = CallOnStartRequest(); + + // drop mAuthRetryPending flag and resume the transaction + // this resumes load of the unauthenticated content data (which + // may have been canceled if we don't want to show it) + mAuthRetryPending = false; + LOG(("Resuming the transaction, user cancelled the auth dialog")); + mTransactionPump->Resume(); + + if (NS_FAILED(rv)) + mTransactionPump->Cancel(rv); + } + + mProxyAuthPending = false; + return NS_OK; +} + +NS_IMETHODIMP nsHttpChannel::CloseStickyConnection() +{ + LOG(("nsHttpChannel::CloseStickyConnection this=%p", this)); + + // Require we are between OnStartRequest and OnStopRequest, because + // what we do here takes effect in OnStopRequest (not reusing the + // connection for next authentication round). + if (!mIsPending) { + LOG((" channel not pending")); + NS_ERROR("CloseStickyConnection not called before OnStopRequest, won't have any effect"); + return NS_ERROR_UNEXPECTED; + } + + MOZ_ASSERT(mTransaction); + if (!mTransaction) { + return NS_ERROR_UNEXPECTED; + } + + if (!(mCaps & NS_HTTP_STICKY_CONNECTION || + mTransaction->Caps() & NS_HTTP_STICKY_CONNECTION)) { + LOG((" not sticky")); + return NS_OK; + } + + RefPtr<nsAHttpConnection> conn = mTransaction->GetConnectionReference(); + if (!conn) { + LOG((" no connection")); + return NS_OK; + } + + // This turns the IsPersistent() indicator on the connection to false, + // and makes us throw it away in OnStopRequest. + conn->DontReuse(); + return NS_OK; +} + +NS_IMETHODIMP nsHttpChannel::ConnectionRestartable(bool aRestartable) +{ + LOG(("nsHttpChannel::ConnectionRestartable this=%p, restartable=%d", + this, aRestartable)); + mAuthConnectionRestartable = aRestartable; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsISupports +//----------------------------------------------------------------------------- + +NS_IMPL_ADDREF_INHERITED(nsHttpChannel, HttpBaseChannel) +NS_IMPL_RELEASE_INHERITED(nsHttpChannel, HttpBaseChannel) + +NS_INTERFACE_MAP_BEGIN(nsHttpChannel) + NS_INTERFACE_MAP_ENTRY(nsIRequest) + NS_INTERFACE_MAP_ENTRY(nsIChannel) + NS_INTERFACE_MAP_ENTRY(nsIRequestObserver) + NS_INTERFACE_MAP_ENTRY(nsIStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIHttpChannel) + NS_INTERFACE_MAP_ENTRY(nsICacheInfoChannel) + NS_INTERFACE_MAP_ENTRY(nsICachingChannel) + NS_INTERFACE_MAP_ENTRY(nsIClassOfService) + NS_INTERFACE_MAP_ENTRY(nsIUploadChannel) + NS_INTERFACE_MAP_ENTRY(nsIFormPOSTActionChannel) + NS_INTERFACE_MAP_ENTRY(nsIUploadChannel2) + NS_INTERFACE_MAP_ENTRY(nsICacheEntryOpenCallback) + NS_INTERFACE_MAP_ENTRY(nsIHttpChannelInternal) + NS_INTERFACE_MAP_ENTRY(nsIResumableChannel) + NS_INTERFACE_MAP_ENTRY(nsITransportEventSink) + NS_INTERFACE_MAP_ENTRY(nsISupportsPriority) + NS_INTERFACE_MAP_ENTRY(nsIProtocolProxyCallback) + NS_INTERFACE_MAP_ENTRY(nsIProxiedChannel) + NS_INTERFACE_MAP_ENTRY(nsIHttpAuthenticableChannel) + NS_INTERFACE_MAP_ENTRY(nsIApplicationCacheContainer) + NS_INTERFACE_MAP_ENTRY(nsIApplicationCacheChannel) + NS_INTERFACE_MAP_ENTRY(nsIAsyncVerifyRedirectCallback) + NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableRequest) + NS_INTERFACE_MAP_ENTRY(nsIThreadRetargetableStreamListener) + NS_INTERFACE_MAP_ENTRY(nsIDNSListener) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsICorsPreflightCallback) + NS_INTERFACE_MAP_ENTRY(nsIHstsPrimingCallback) + NS_INTERFACE_MAP_ENTRY(nsIChannelWithDivertableParentListener) + // we have no macro that covers this case. + if (aIID.Equals(NS_GET_IID(nsHttpChannel)) ) { + AddRef(); + *aInstancePtr = this; + return NS_OK; + } else +NS_INTERFACE_MAP_END_INHERITING(HttpBaseChannel) + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIRequest +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::Cancel(nsresult status) +{ + MOZ_ASSERT(NS_IsMainThread()); + // We should never have a pump open while a CORS preflight is in progress. + MOZ_ASSERT_IF(mPreflightChannel, !mCachePump); + + LOG(("nsHttpChannel::Cancel [this=%p status=%x]\n", this, status)); + if (mCanceled) { + LOG((" ignoring; already canceled\n")); + return NS_OK; + } + if (mWaitingForRedirectCallback) { + LOG(("channel canceled during wait for redirect callback")); + } + mCanceled = true; + mStatus = status; + if (mProxyRequest) + mProxyRequest->Cancel(status); + if (mTransaction) + gHttpHandler->CancelTransaction(mTransaction, status); + if (mTransactionPump) + mTransactionPump->Cancel(status); + mCacheInputStream.CloseAndRelease(); + if (mCachePump) + mCachePump->Cancel(status); + if (mAuthProvider) + mAuthProvider->Cancel(status); + if (mPreflightChannel) + mPreflightChannel->Cancel(status); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::Suspend() +{ + nsresult rv = SuspendInternal(); + + nsresult rvParentChannel = NS_OK; + if (mParentChannel) { + rvParentChannel = mParentChannel->SuspendMessageDiversion(); + } + + return NS_FAILED(rv) ? rv : rvParentChannel; +} + +NS_IMETHODIMP +nsHttpChannel::Resume() +{ + nsresult rv = ResumeInternal(); + + nsresult rvParentChannel = NS_OK; + if (mParentChannel) { + rvParentChannel = mParentChannel->ResumeMessageDiversion(); + } + + return NS_FAILED(rv) ? rv : rvParentChannel; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::GetSecurityInfo(nsISupports **securityInfo) +{ + NS_ENSURE_ARG_POINTER(securityInfo); + *securityInfo = mSecurityInfo; + NS_IF_ADDREF(*securityInfo); + return NS_OK; +} + +// If any of the functions that AsyncOpen calls returns immediately an error +// AsyncAbort(which calls onStart/onStopRequest) does not need to be call. +// To be sure that they are not call ReleaseListeners() is called. +// If AsyncOpen returns NS_OK, after that point AsyncAbort must be called on +// any error. +NS_IMETHODIMP +nsHttpChannel::AsyncOpen(nsIStreamListener *listener, nsISupports *context) +{ + MOZ_ASSERT(!mLoadInfo || + mLoadInfo->GetSecurityMode() == 0 || + mLoadInfo->GetInitialSecurityCheckDone() || + (mLoadInfo->GetSecurityMode() == nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL && + nsContentUtils::IsSystemPrincipal(mLoadInfo->LoadingPrincipal())), + "security flags in loadInfo but asyncOpen2() not called"); + + LOG(("nsHttpChannel::AsyncOpen [this=%p]\n", this)); + + NS_CompareLoadInfoAndLoadContext(this); + +#ifdef DEBUG + AssertPrivateBrowsingId(); +#endif + + NS_ENSURE_ARG_POINTER(listener); + NS_ENSURE_TRUE(!mIsPending, NS_ERROR_IN_PROGRESS); + NS_ENSURE_TRUE(!mWasOpened, NS_ERROR_ALREADY_OPENED); + + nsresult rv; + + MOZ_ASSERT(NS_IsMainThread()); + + if (!gHttpHandler->Active()) { + LOG((" after HTTP shutdown...")); + ReleaseListeners(); + return NS_ERROR_NOT_AVAILABLE; + } + + rv = NS_CheckPortSafety(mURI); + if (NS_FAILED(rv)) { + ReleaseListeners(); + return rv; + } + + if (mInterceptCache != INTERCEPTED && ShouldIntercept()) { + mInterceptCache = MAYBE_INTERCEPT; + SetCouldBeSynthesized(); + } + + // Remember the cookie header that was set, if any + nsAutoCString cookieHeader; + if (NS_SUCCEEDED(mRequestHead.GetHeader(nsHttp::Cookie, cookieHeader))) { + mUserSetCookieHeader = cookieHeader; + } + + AddCookiesToRequest(); + + // After we notify any observers (on-opening-request, loadGroup, etc) we + // must return NS_OK and return any errors asynchronously via + // OnStart/OnStopRequest. Observers may add a reference to the channel + // and expect to get OnStopRequest so they know when to drop the reference, + // etc. + + // notify "http-on-opening-request" observers, but not if this is a redirect + if (!(mLoadFlags & LOAD_REPLACE)) { + gHttpHandler->OnOpeningRequest(this); + } + + // Set user agent override + HttpBaseChannel::SetDocshellUserAgentOverride(); + + mIsPending = true; + mWasOpened = true; + + mListener = listener; + mListenerContext = context; + + if (mLoadGroup) + mLoadGroup->AddRequest(this, nullptr); + + // record asyncopen time unconditionally and clear it if we + // don't want it after OnModifyRequest() weighs in. But waiting for + // that to complete would mean we don't include proxy resolution in the + // timing. + mAsyncOpenTime = TimeStamp::Now(); + + // Remember we have Authorization header set here. We need to check on it + // just once and early, AsyncOpen is the best place. + mCustomAuthHeader = mRequestHead.HasHeader(nsHttp::Authorization); + + // The common case for HTTP channels is to begin proxy resolution and return + // at this point. The only time we know mProxyInfo already is if we're + // proxying a non-http protocol like ftp. + if (!mProxyInfo && NS_SUCCEEDED(ResolveProxy())) { + return NS_OK; + } + + rv = BeginConnect(); + if (NS_FAILED(rv)) { + CloseCacheEntry(false); + AsyncAbort(rv); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::AsyncOpen2(nsIStreamListener *aListener) +{ + nsCOMPtr<nsIStreamListener> listener = aListener; + nsresult rv = nsContentSecurityManager::doContentSecurityCheck(this, listener); + if (NS_WARN_IF(NS_FAILED(rv))) { + ReleaseListeners(); + return rv; + } + return AsyncOpen(listener, nullptr); +} + +// BeginConnect() SHOULD NOT call AsyncAbort(). AsyncAbort will be called by +// functions that called BeginConnect if needed. Only AsyncOpen and +// OnProxyAvailable ever call BeginConnect. +nsresult +nsHttpChannel::BeginConnect() +{ + LOG(("nsHttpChannel::BeginConnect [this=%p]\n", this)); + nsresult rv; + + // Construct connection info object + nsAutoCString host; + nsAutoCString scheme; + int32_t port = -1; + bool isHttps = false; + + rv = mURI->GetScheme(scheme); + if (NS_SUCCEEDED(rv)) + rv = mURI->SchemeIs("https", &isHttps); + if (NS_SUCCEEDED(rv)) + rv = mURI->GetAsciiHost(host); + if (NS_SUCCEEDED(rv)) + rv = mURI->GetPort(&port); + if (NS_SUCCEEDED(rv)) + mURI->GetUsername(mUsername); + if (NS_SUCCEEDED(rv)) + rv = mURI->GetAsciiSpec(mSpec); + if (NS_FAILED(rv)) { + return rv; + } + + // Reject the URL if it doesn't specify a host + if (host.IsEmpty()) { + rv = NS_ERROR_MALFORMED_URI; + return rv; + } + LOG(("host=%s port=%d\n", host.get(), port)); + LOG(("uri=%s\n", mSpec.get())); + + nsCOMPtr<nsProxyInfo> proxyInfo; + if (mProxyInfo) + proxyInfo = do_QueryInterface(mProxyInfo); + + mRequestHead.SetHTTPS(isHttps); + mRequestHead.SetOrigin(scheme, host, port); + + SetDoNotTrack(); + + NeckoOriginAttributes originAttributes; + NS_GetOriginAttributes(this, originAttributes); + + RefPtr<AltSvcMapping> mapping; + if (!mConnectionInfo && mAllowAltSvc && // per channel + !(mLoadFlags & LOAD_FRESH_CONNECTION) && + (scheme.Equals(NS_LITERAL_CSTRING("http")) || + scheme.Equals(NS_LITERAL_CSTRING("https"))) && + (!proxyInfo || proxyInfo->IsDirect()) && + (mapping = gHttpHandler->GetAltServiceMapping(scheme, + host, port, + mPrivateBrowsing))) { + LOG(("nsHttpChannel %p Alt Service Mapping Found %s://%s:%d [%s]\n", + this, scheme.get(), mapping->AlternateHost().get(), + mapping->AlternatePort(), mapping->HashKey().get())); + + if (!(mLoadFlags & LOAD_ANONYMOUS) && !mPrivateBrowsing) { + nsAutoCString altUsedLine(mapping->AlternateHost()); + bool defaultPort = mapping->AlternatePort() == + (isHttps ? NS_HTTPS_DEFAULT_PORT : NS_HTTP_DEFAULT_PORT); + if (!defaultPort) { + altUsedLine.AppendLiteral(":"); + altUsedLine.AppendInt(mapping->AlternatePort()); + } + mRequestHead.SetHeader(nsHttp::Alternate_Service_Used, altUsedLine); + } + + nsCOMPtr<nsIConsoleService> consoleService = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + if (consoleService) { + nsAutoString message(NS_LITERAL_STRING("Alternate Service Mapping found: ")); + AppendASCIItoUTF16(scheme.get(), message); + message.Append(NS_LITERAL_STRING("://")); + AppendASCIItoUTF16(host.get(), message); + message.Append(NS_LITERAL_STRING(":")); + message.AppendInt(port); + message.Append(NS_LITERAL_STRING(" to ")); + AppendASCIItoUTF16(scheme.get(), message); + message.Append(NS_LITERAL_STRING("://")); + AppendASCIItoUTF16(mapping->AlternateHost().get(), message); + message.Append(NS_LITERAL_STRING(":")); + message.AppendInt(mapping->AlternatePort()); + consoleService->LogStringMessage(message.get()); + } + + LOG(("nsHttpChannel %p Using connection info from altsvc mapping", this)); + mapping->GetConnectionInfo(getter_AddRefs(mConnectionInfo), proxyInfo, originAttributes); + Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_USE_ALTSVC, true); + Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_USE_ALTSVC_OE, !isHttps); + } else if (mConnectionInfo) { + LOG(("nsHttpChannel %p Using channel supplied connection info", this)); + Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_USE_ALTSVC, false); + } else { + LOG(("nsHttpChannel %p Using default connection info", this)); + + mConnectionInfo = new nsHttpConnectionInfo(host, port, EmptyCString(), mUsername, proxyInfo, + originAttributes, isHttps); + Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_USE_ALTSVC, false); + } + + // Set network interface id only when it's not empty to avoid + // rebuilding hash key. + if (!mNetworkInterfaceId.IsEmpty()) { + mConnectionInfo->SetNetworkInterfaceId(mNetworkInterfaceId); + } + + mAuthProvider = + do_CreateInstance("@mozilla.org/network/http-channel-auth-provider;1", + &rv); + if (NS_SUCCEEDED(rv)) + rv = mAuthProvider->Init(this); + if (NS_FAILED(rv)) { + return rv; + } + + // check to see if authorization headers should be included + // mCustomAuthHeader is set in AsyncOpen if we find Authorization header + mAuthProvider->AddAuthorizationHeaders(mCustomAuthHeader); + + // notify "http-on-modify-request" observers + CallOnModifyRequestObservers(); + + SetLoadGroupUserAgentOverride(); + + // Check if request was cancelled during on-modify-request or on-useragent. + if (mCanceled) { + return mStatus; + } + + if (mSuspendCount) { + LOG(("Waiting until resume BeginConnect [this=%p]\n", this)); + MOZ_ASSERT(!mCallOnResume); + mCallOnResume = &nsHttpChannel::HandleBeginConnectContinue; + return NS_OK; + } + + return BeginConnectContinue(); +} + +void +nsHttpChannel::HandleBeginConnectContinue() +{ + NS_PRECONDITION(!mCallOnResume, "How did that happen?"); + nsresult rv; + + if (mSuspendCount) { + LOG(("Waiting until resume BeginConnect [this=%p]\n", this)); + mCallOnResume = &nsHttpChannel::HandleBeginConnectContinue; + return; + } + + LOG(("nsHttpChannel::HandleBeginConnectContinue [this=%p]\n", this)); + rv = BeginConnectContinue(); + if (NS_FAILED(rv)) { + CloseCacheEntry(false); + Unused << AsyncAbort(rv); + } +} + +nsresult +nsHttpChannel::BeginConnectContinue() +{ + nsresult rv; + + // Check if request was cancelled during suspend AFTER on-modify-request or + // on-useragent. + if (mCanceled) { + return mStatus; + } + + // Check to see if we should redirect this channel elsewhere by + // nsIHttpChannel.redirectTo API request + if (mAPIRedirectToURI) { + return AsyncCall(&nsHttpChannel::HandleAsyncAPIRedirect); + } + // Check to see if this principal exists on local blocklists. + RefPtr<nsChannelClassifier> channelClassifier = new nsChannelClassifier(); + if (mLoadFlags & LOAD_CLASSIFY_URI) { + nsCOMPtr<nsIURIClassifier> classifier = do_GetService(NS_URICLASSIFIERSERVICE_CONTRACTID); + bool tpEnabled = false; + channelClassifier->ShouldEnableTrackingProtection(this, &tpEnabled); + if (classifier && tpEnabled) { + // We skip speculative connections by setting mLocalBlocklist only + // when tracking protection is enabled. Though we could do this for + // both phishing and malware, it is not necessary for correctness, + // since no network events will be received while the + // nsChannelClassifier is in progress. See bug 1122691. + nsCOMPtr<nsIURI> uri; + rv = GetURI(getter_AddRefs(uri)); + if (NS_SUCCEEDED(rv) && uri) { + nsAutoCString tables; + Preferences::GetCString("urlclassifier.trackingTable", &tables); + nsAutoCString results; + rv = classifier->ClassifyLocalWithTables(uri, tables, results); + if (NS_SUCCEEDED(rv) && !results.IsEmpty()) { + LOG(("nsHttpChannel::ClassifyLocalWithTables found " + "uri on local tracking blocklist [this=%p]", + this)); + mLocalBlocklist = true; + } else { + LOG(("nsHttpChannel::ClassifyLocalWithTables no result " + "found [this=%p]", this)); + } + } + } + } + + // If mTimingEnabled flag is not set after OnModifyRequest() then + // clear the already recorded AsyncOpen value for consistency. + if (!mTimingEnabled) + mAsyncOpenTime = TimeStamp(); + + // when proxying only use the pipeline bit if ProxyPipelining() allows it. + if (!mConnectionInfo->UsingConnect() && mConnectionInfo->UsingHttpProxy()) { + mCaps &= ~NS_HTTP_ALLOW_PIPELINING; + if (gHttpHandler->ProxyPipelining()) + mCaps |= NS_HTTP_ALLOW_PIPELINING; + } + + // if this somehow fails we can go on without it + gHttpHandler->AddConnectionHeader(&mRequestHead, mCaps); + + if (mLoadFlags & VALIDATE_ALWAYS || BYPASS_LOCAL_CACHE(mLoadFlags)) + mCaps |= NS_HTTP_REFRESH_DNS; + + if (!mLocalBlocklist && !mConnectionInfo->UsingHttpProxy() && + !(mLoadFlags & (LOAD_NO_NETWORK_IO | LOAD_ONLY_FROM_CACHE))) { + // Start a DNS lookup very early in case the real open is queued the DNS can + // happen in parallel. Do not do so in the presence of an HTTP proxy as + // all lookups other than for the proxy itself are done by the proxy. + // Also we don't do a lookup if the LOAD_NO_NETWORK_IO or + // LOAD_ONLY_FROM_CACHE flags are set. + // + // We keep the DNS prefetch object around so that we can retrieve + // timing information from it. There is no guarantee that we actually + // use the DNS prefetch data for the real connection, but as we keep + // this data around for 3 minutes by default, this should almost always + // be correct, and even when it isn't, the timing still represents _a_ + // valid DNS lookup timing for the site, even if it is not _the_ + // timing we used. + LOG(("nsHttpChannel::BeginConnect [this=%p] prefetching%s\n", + this, mCaps & NS_HTTP_REFRESH_DNS ? ", refresh requested" : "")); + mDNSPrefetch = new nsDNSPrefetch(mURI, this, mTimingEnabled); + mDNSPrefetch->PrefetchHigh(mCaps & NS_HTTP_REFRESH_DNS); + } + + // Adjust mCaps according to our request headers: + // - If "Connection: close" is set as a request header, then do not bother + // trying to establish a keep-alive connection. + if (mRequestHead.HasHeaderValue(nsHttp::Connection, "close")) + mCaps &= ~(NS_HTTP_ALLOW_KEEPALIVE | NS_HTTP_ALLOW_PIPELINING); + + if (gHttpHandler->CriticalRequestPrioritization()) { + if (mClassOfService & nsIClassOfService::Leader) { + mCaps |= NS_HTTP_LOAD_AS_BLOCKING; + } + if (mClassOfService & nsIClassOfService::Unblocked) { + mCaps |= NS_HTTP_LOAD_UNBLOCKED; + } + } + + // Force-Reload should reset the persistent connection pool for this host + if (mLoadFlags & LOAD_FRESH_CONNECTION) { + // just the initial document resets the whole pool + if (mLoadFlags & LOAD_INITIAL_DOCUMENT_URI) { + gHttpHandler->ConnMgr()->ClearAltServiceMappings(); + gHttpHandler->ConnMgr()->DoShiftReloadConnectionCleanup(mConnectionInfo); + } + mCaps &= ~NS_HTTP_ALLOW_PIPELINING; + } + + // We may have been cancelled already, either by on-modify-request + // listeners or load group observers; in that case, we should not send the + // request to the server + if (mCanceled) { + return mStatus; + } + + if (!(mLoadFlags & LOAD_CLASSIFY_URI)) { + return ContinueBeginConnectWithResult(); + } + + // mLocalBlocklist is true only if tracking protection is enabled and the + // URI is a tracking domain, it makes no guarantees about phishing or + // malware, so if LOAD_CLASSIFY_URI is true we must call + // nsChannelClassifier to catch phishing and malware URIs. + bool callContinueBeginConnect = true; + if (!mLocalBlocklist) { + // Here we call ContinueBeginConnectWithResult and not + // ContinueBeginConnect so that in the case of an error we do not start + // channelClassifier. + rv = ContinueBeginConnectWithResult(); + if (NS_FAILED(rv)) { + return rv; + } + callContinueBeginConnect = false; + } + // nsChannelClassifier calls ContinueBeginConnect if it has not already + // been called, after optionally cancelling the channel once we have a + // remote verdict. We call a concrete class instead of an nsI* that might + // be overridden. + LOG(("nsHttpChannel::Starting nsChannelClassifier %p [this=%p]", + channelClassifier.get(), this)); + channelClassifier->Start(this); + if (callContinueBeginConnect) { + return ContinueBeginConnectWithResult(); + } + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetEncodedBodySize(uint64_t *aEncodedBodySize) +{ + if (mCacheEntry && !mCacheEntryIsWriteOnly) { + int64_t dataSize = 0; + mCacheEntry->GetDataSize(&dataSize); + *aEncodedBodySize = dataSize; + } else { + *aEncodedBodySize = mLogicalOffset; + } + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIHttpChannelInternal +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::SetupFallbackChannel(const char *aFallbackKey) +{ + ENSURE_CALLED_BEFORE_CONNECT(); + + LOG(("nsHttpChannel::SetupFallbackChannel [this=%p, key=%s]\n", + this, aFallbackKey)); + mFallbackChannel = true; + mFallbackKey = aFallbackKey; + + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::ForceIntercepted(uint64_t aInterceptionID) +{ + ENSURE_CALLED_BEFORE_ASYNC_OPEN(); + + if (NS_WARN_IF(mLoadFlags & LOAD_BYPASS_SERVICE_WORKER)) { + return NS_ERROR_NOT_AVAILABLE; + } + + MarkIntercepted(); + mResponseCouldBeSynthesized = true; + mInterceptionID = aInterceptionID; + return NS_OK; +} + +mozilla::net::nsHttpChannel* +nsHttpChannel::QueryHttpChannelImpl(void) +{ + return this; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsISupportsPriority +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::SetPriority(int32_t value) +{ + int16_t newValue = clamped<int32_t>(value, INT16_MIN, INT16_MAX); + if (mPriority == newValue) + return NS_OK; + mPriority = newValue; + if (mTransaction) + gHttpHandler->RescheduleTransaction(mTransaction, mPriority); + return NS_OK; +} + +nsresult +nsHttpChannel::ContinueBeginConnectWithResult() +{ + LOG(("nsHttpChannel::ContinueBeginConnectWithResult [this=%p]", this)); + NS_PRECONDITION(!mCallOnResume, "How did that happen?"); + + nsresult rv; + + if (mSuspendCount) { + LOG(("Waiting until resume to do async connect [this=%p]\n", this)); + mCallOnResume = &nsHttpChannel::ContinueBeginConnect; + rv = NS_OK; + } else if (mCanceled) { + // We may have been cancelled already, by nsChannelClassifier in that + // case, we should not send the request to the server + rv = mStatus; + } else { + rv = Connect(); + } + + LOG(("nsHttpChannel::ContinueBeginConnectWithResult result [this=%p rv=%x " + "mCanceled=%i]\n", this, rv, mCanceled)); + return rv; +} + +void +nsHttpChannel::ContinueBeginConnect() +{ + nsresult rv = ContinueBeginConnectWithResult(); + if (NS_FAILED(rv)) { + CloseCacheEntry(false); + AsyncAbort(rv); + } +} + +//----------------------------------------------------------------------------- +// HttpChannel::nsIClassOfService +//----------------------------------------------------------------------------- +NS_IMETHODIMP +nsHttpChannel::SetClassFlags(uint32_t inFlags) +{ + mClassOfService = inFlags; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::AddClassFlags(uint32_t inFlags) +{ + mClassOfService |= inFlags; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::ClearClassFlags(uint32_t inFlags) +{ + mClassOfService &= ~inFlags; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIProtocolProxyCallback +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::OnProxyAvailable(nsICancelable *request, nsIChannel *channel, + nsIProxyInfo *pi, nsresult status) +{ + LOG(("nsHttpChannel::OnProxyAvailable [this=%p pi=%p status=%x mStatus=%x]\n", + this, pi, status, mStatus)); + mProxyRequest = nullptr; + + nsresult rv; + + // If status is a failure code, then it means that we failed to resolve + // proxy info. That is a non-fatal error assuming it wasn't because the + // request was canceled. We just failover to DIRECT when proxy resolution + // fails (failure can mean that the PAC URL could not be loaded). + + if (NS_SUCCEEDED(status)) + mProxyInfo = pi; + + if (!gHttpHandler->Active()) { + LOG(("nsHttpChannel::OnProxyAvailable [this=%p] " + "Handler no longer active.\n", this)); + rv = NS_ERROR_NOT_AVAILABLE; + } + else { + rv = BeginConnect(); + } + + if (NS_FAILED(rv)) { + CloseCacheEntry(false); + AsyncAbort(rv); + } + return rv; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIProxiedChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::GetProxyInfo(nsIProxyInfo **result) +{ + if (!mConnectionInfo) + *result = mProxyInfo; + else + *result = mConnectionInfo->ProxyInfo(); + NS_IF_ADDREF(*result); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsITimedChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::GetDomainLookupStart(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetDomainLookupStart(); + else + *_retval = mTransactionTimings.domainLookupStart; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetDomainLookupEnd(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetDomainLookupEnd(); + else + *_retval = mTransactionTimings.domainLookupEnd; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetConnectStart(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetConnectStart(); + else + *_retval = mTransactionTimings.connectStart; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetConnectEnd(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetConnectEnd(); + else + *_retval = mTransactionTimings.connectEnd; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetRequestStart(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetRequestStart(); + else + *_retval = mTransactionTimings.requestStart; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetResponseStart(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetResponseStart(); + else + *_retval = mTransactionTimings.responseStart; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetResponseEnd(TimeStamp* _retval) { + if (mTransaction) + *_retval = mTransaction->GetResponseEnd(); + else + *_retval = mTransactionTimings.responseEnd; + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIHttpAuthenticableChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::GetIsSSL(bool *aIsSSL) +{ + // this attribute is really misnamed - it wants to know if + // https:// is being used. SSL might be used to cover http:// + // in some circumstances (proxies, http/2, etc..) + return mURI->SchemeIs("https", aIsSSL); +} + +NS_IMETHODIMP +nsHttpChannel::GetProxyMethodIsConnect(bool *aProxyMethodIsConnect) +{ + *aProxyMethodIsConnect = mConnectionInfo->UsingConnect(); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetServerResponseHeader(nsACString &value) +{ + if (!mResponseHead) + return NS_ERROR_NOT_AVAILABLE; + return mResponseHead->GetHeader(nsHttp::Server, value); +} + +NS_IMETHODIMP +nsHttpChannel::GetProxyChallenges(nsACString &value) +{ + if (!mResponseHead) + return NS_ERROR_UNEXPECTED; + return mResponseHead->GetHeader(nsHttp::Proxy_Authenticate, value); +} + +NS_IMETHODIMP +nsHttpChannel::GetWWWChallenges(nsACString &value) +{ + if (!mResponseHead) + return NS_ERROR_UNEXPECTED; + return mResponseHead->GetHeader(nsHttp::WWW_Authenticate, value); +} + +NS_IMETHODIMP +nsHttpChannel::SetProxyCredentials(const nsACString &value) +{ + return mRequestHead.SetHeader(nsHttp::Proxy_Authorization, value); +} + +NS_IMETHODIMP +nsHttpChannel::SetWWWCredentials(const nsACString &value) +{ + return mRequestHead.SetHeader(nsHttp::Authorization, value); +} + +//----------------------------------------------------------------------------- +// Methods that nsIHttpAuthenticableChannel dupes from other IDLs, which we +// get from HttpBaseChannel, must be explicitly forwarded, because C++ sucks. +// + +NS_IMETHODIMP +nsHttpChannel::GetLoadFlags(nsLoadFlags *aLoadFlags) +{ + return HttpBaseChannel::GetLoadFlags(aLoadFlags); +} + +NS_IMETHODIMP +nsHttpChannel::GetURI(nsIURI **aURI) +{ + return HttpBaseChannel::GetURI(aURI); +} + +NS_IMETHODIMP +nsHttpChannel::GetNotificationCallbacks(nsIInterfaceRequestor **aCallbacks) +{ + return HttpBaseChannel::GetNotificationCallbacks(aCallbacks); +} + +NS_IMETHODIMP +nsHttpChannel::GetLoadGroup(nsILoadGroup **aLoadGroup) +{ + return HttpBaseChannel::GetLoadGroup(aLoadGroup); +} + +NS_IMETHODIMP +nsHttpChannel::GetRequestMethod(nsACString& aMethod) +{ + return HttpBaseChannel::GetRequestMethod(aMethod); +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIRequestObserver +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::OnStartRequest(nsIRequest *request, nsISupports *ctxt) +{ + nsresult rv; + + PROFILER_LABEL("nsHttpChannel", "OnStartRequest", + js::ProfileEntry::Category::NETWORK); + + if (!(mCanceled || NS_FAILED(mStatus))) { + // capture the request's status, so our consumers will know ASAP of any + // connection failures, etc - bug 93581 + request->GetStatus(&mStatus); + } + + LOG(("nsHttpChannel::OnStartRequest [this=%p request=%p status=%x]\n", + this, request, mStatus)); + + // Make sure things are what we expect them to be... + MOZ_ASSERT(request == mCachePump || request == mTransactionPump, + "Unexpected request"); + MOZ_ASSERT(!(mTransactionPump && mCachePump) || mCachedContentIsPartial, + "If we have both pumps, the cache content must be partial"); + + mAfterOnStartRequestBegun = true; + mOnStartRequestTimestamp = TimeStamp::Now(); + + if (!mSecurityInfo && !mCachePump && mTransaction) { + // grab the security info from the connection object; the transaction + // is guaranteed to own a reference to the connection. + mSecurityInfo = mTransaction->SecurityInfo(); + } + + // don't enter this block if we're reading from the cache... + if (NS_SUCCEEDED(mStatus) && !mCachePump && mTransaction) { + // mTransactionPump doesn't hit OnInputStreamReady and call this until + // all of the response headers have been acquired, so we can take ownership + // of them from the transaction. + mResponseHead = mTransaction->TakeResponseHead(); + // the response head may be null if the transaction was cancelled. in + // which case we just need to call OnStartRequest/OnStopRequest. + if (mResponseHead) + return ProcessResponse(); + + NS_WARNING("No response head in OnStartRequest"); + } + + // cache file could be deleted on our behalf, it could contain errors or + // it failed to allocate memory, reload from network here. + if (mCacheEntry && mCachePump && RECOVER_FROM_CACHE_FILE_ERROR(mStatus)) { + LOG((" cache file error, reloading from server")); + mCacheEntry->AsyncDoom(nullptr); + rv = StartRedirectChannelToURI(mURI, nsIChannelEventSink::REDIRECT_INTERNAL); + if (NS_SUCCEEDED(rv)) + return NS_OK; + } + + // avoid crashing if mListener happens to be null... + if (!mListener) { + NS_NOTREACHED("mListener is null"); + return NS_OK; + } + + // before we start any content load, check for redirectTo being called + // this code is executed mainly before we start load from the cache + if (mAPIRedirectToURI && !mCanceled) { + nsAutoCString redirectToSpec; + mAPIRedirectToURI->GetAsciiSpec(redirectToSpec); + LOG((" redirectTo called with uri=%s", redirectToSpec.BeginReading())); + + MOZ_ASSERT(!mOnStartRequestCalled); + + nsCOMPtr<nsIURI> redirectTo; + mAPIRedirectToURI.swap(redirectTo); + + PushRedirectAsyncFunc(&nsHttpChannel::ContinueOnStartRequest1); + rv = StartRedirectChannelToURI(redirectTo, nsIChannelEventSink::REDIRECT_TEMPORARY); + if (NS_SUCCEEDED(rv)) { + return NS_OK; + } + PopRedirectAsyncFunc(&nsHttpChannel::ContinueOnStartRequest1); + } + + // Hack: ContinueOnStartRequest1 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 ContinueOnStartRequest1(NS_BINDING_FAILED); +} + +nsresult +nsHttpChannel::ContinueOnStartRequest1(nsresult result) +{ + if (NS_SUCCEEDED(result)) { + // Redirect has passed through, we don't want to go on with this + // channel. It will now be canceled by the redirect handling code + // that called this function. + return NS_OK; + } + + // on proxy errors, try to failover + if (mConnectionInfo->ProxyInfo() && + (mStatus == NS_ERROR_PROXY_CONNECTION_REFUSED || + mStatus == NS_ERROR_UNKNOWN_PROXY_HOST || + mStatus == NS_ERROR_NET_TIMEOUT)) { + + PushRedirectAsyncFunc(&nsHttpChannel::ContinueOnStartRequest2); + if (NS_SUCCEEDED(ProxyFailover())) + return NS_OK; + PopRedirectAsyncFunc(&nsHttpChannel::ContinueOnStartRequest2); + } + + // Hack: ContinueOnStartRequest2 uses NS_OK to detect successful redirects, + // so we distinguish this codepath (a non-redirect that's processing + // normally) by passing in a bogus error code. + return ContinueOnStartRequest2(NS_BINDING_FAILED); +} + +nsresult +nsHttpChannel::ContinueOnStartRequest2(nsresult result) +{ + if (NS_SUCCEEDED(result)) { + // Redirect has passed through, we don't want to go on with this + // channel. It will now be canceled by the redirect handling code + // that called this function. + return NS_OK; + } + + // on other request errors, try to fall back + if (NS_FAILED(mStatus)) { + PushRedirectAsyncFunc(&nsHttpChannel::ContinueOnStartRequest3); + bool waitingForRedirectCallback; + ProcessFallback(&waitingForRedirectCallback); + if (waitingForRedirectCallback) + return NS_OK; + PopRedirectAsyncFunc(&nsHttpChannel::ContinueOnStartRequest3); + } + + return ContinueOnStartRequest3(NS_OK); +} + +nsresult +nsHttpChannel::ContinueOnStartRequest3(nsresult result) +{ + if (mFallingBack) + return NS_OK; + + return CallOnStartRequest(); +} + +NS_IMETHODIMP +nsHttpChannel::OnStopRequest(nsIRequest *request, nsISupports *ctxt, nsresult status) +{ + PROFILER_LABEL("nsHttpChannel", "OnStopRequest", + js::ProfileEntry::Category::NETWORK); + + LOG(("nsHttpChannel::OnStopRequest [this=%p request=%p status=%x]\n", + this, request, status)); + + MOZ_ASSERT(NS_IsMainThread(), + "OnStopRequest should only be called from the main thread"); + + if (NS_FAILED(status)) { + ProcessSecurityReport(status); + } + + // If this load failed because of a security error, it may be because we + // are in a captive portal - trigger an async check to make sure. + int32_t nsprError = -1 * NS_ERROR_GET_CODE(status); + if (mozilla::psm::IsNSSErrorCode(nsprError)) { + gIOService->RecheckCaptivePortal(); + } + + if (mTimingEnabled && request == mCachePump) { + mCacheReadEnd = TimeStamp::Now(); + + ReportNetVSCacheTelemetry(); + } + + // allow content to be cached if it was loaded successfully (bug #482935) + bool contentComplete = NS_SUCCEEDED(status); + + // honor the cancelation status even if the underlying transaction completed. + if (mCanceled || NS_FAILED(mStatus)) + status = mStatus; + + if (mCachedContentIsPartial) { + if (NS_SUCCEEDED(status)) { + // mTransactionPump should be suspended + MOZ_ASSERT(request != mTransactionPump, + "byte-range transaction finished prematurely"); + + if (request == mCachePump) { + bool streamDone; + status = OnDoneReadingPartialCacheEntry(&streamDone); + if (NS_SUCCEEDED(status) && !streamDone) + return status; + // otherwise, fall through and fire OnStopRequest... + } + else if (request == mTransactionPump) { + MOZ_ASSERT(mConcurrentCacheAccess); + } + else + NS_NOTREACHED("unexpected request"); + } + // Do not to leave the transaction in a suspended state in error cases. + if (NS_FAILED(status) && mTransaction) + gHttpHandler->CancelTransaction(mTransaction, status); + } + + nsCOMPtr<nsICompressConvStats> conv = do_QueryInterface(mCompressListener); + if (conv) { + conv->GetDecodedDataLength(&mDecodedBodySize); + } + + if (mTransaction) { + // determine if we should call DoAuthRetry + bool authRetry = mAuthRetryPending && NS_SUCCEEDED(status); + mStronglyFramed = mTransaction->ResponseIsComplete(); + LOG(("nsHttpChannel %p has a strongly framed transaction: %d", + this, mStronglyFramed)); + + // + // grab references to connection in case we need to retry an + // authentication request over it or use it for an upgrade + // to another protocol. + // + // this code relies on the code in nsHttpTransaction::Close, which + // tests for NS_HTTP_STICKY_CONNECTION to determine whether or not to + // keep the connection around after the transaction is finished. + // + RefPtr<nsAHttpConnection> conn; + LOG((" authRetry=%d, sticky conn cap=%d", authRetry, mCaps & NS_HTTP_STICKY_CONNECTION)); + // We must check caps for stickinness also on the transaction because it + // might have been updated by the transaction itself during inspection of + // the reposnse headers yet on the socket thread (found connection based + // auth schema). + if (authRetry && (mCaps & NS_HTTP_STICKY_CONNECTION || + mTransaction->Caps() & NS_HTTP_STICKY_CONNECTION)) { + conn = mTransaction->GetConnectionReference(); + LOG((" transaction %p provides connection %p", mTransaction.get(), conn.get())); + // This is so far a workaround to fix leak when reusing unpersistent + // connection for authentication retry. See bug 459620 comment 4 + // for details. + if (conn && !conn->IsPersistent()) { + LOG((" connection is not persistent, not reusing it")); + conn = nullptr; + } + // We do not use a sticky connection in case of a nsHttpPipeline as + // well (see bug 1337826). This is a quick fix, because + // nsHttpPipeline is turned off by default. + RefPtr<nsAHttpTransaction> tranConn = do_QueryObject(conn); + if (tranConn && tranConn->QueryPipeline()) { + LOG(("Do not use this connection, it is a nsHttpPipeline.")); + conn = nullptr; + } + } + + RefPtr<nsAHttpConnection> stickyConn; + if (mCaps & NS_HTTP_STICKY_CONNECTION) { + stickyConn = mTransaction->GetConnectionReference(); + } + + mTransferSize = mTransaction->GetTransferSize(); + + // If we are using the transaction to serve content, we also save the + // time since async open in the cache entry so we can compare telemetry + // between cache and net response. + if (request == mTransactionPump && mCacheEntry && + !mAsyncOpenTime.IsNull() && !mOnStartRequestTimestamp.IsNull()) { + nsAutoCString onStartTime; + onStartTime.AppendInt( (uint64_t) (mOnStartRequestTimestamp - mAsyncOpenTime).ToMilliseconds()); + mCacheEntry->SetMetaDataElement("net-response-time-onstart", onStartTime.get()); + + nsAutoCString responseTime; + responseTime.AppendInt( (uint64_t) (TimeStamp::Now() - mAsyncOpenTime).ToMilliseconds()); + mCacheEntry->SetMetaDataElement("net-response-time-onstop", responseTime.get()); + } + + // at this point, we're done with the transaction + mTransactionTimings = mTransaction->Timings(); + mTransaction = nullptr; + mTransactionPump = nullptr; + + // We no longer need the dns prefetch object + if (mDNSPrefetch && mDNSPrefetch->TimingsValid() + && !mTransactionTimings.requestStart.IsNull() + && !mTransactionTimings.connectStart.IsNull() + && mDNSPrefetch->EndTimestamp() <= mTransactionTimings.connectStart) { + // We only need the domainLookup timestamps when not using a + // persistent connection, meaning if the endTimestamp < connectStart + mTransactionTimings.domainLookupStart = + mDNSPrefetch->StartTimestamp(); + mTransactionTimings.domainLookupEnd = + mDNSPrefetch->EndTimestamp(); + } + mDNSPrefetch = nullptr; + + // handle auth retry... + if (authRetry) { + mAuthRetryPending = false; + status = DoAuthRetry(conn); + if (NS_SUCCEEDED(status)) + return NS_OK; + } + + // If DoAuthRetry failed, or if we have been cancelled since showing + // the auth. dialog, then we need to send OnStartRequest now + if (authRetry || (mAuthRetryPending && NS_FAILED(status))) { + MOZ_ASSERT(NS_FAILED(status), "should have a failure code here"); + // NOTE: since we have a failure status, we can ignore the return + // value from onStartRequest. + if (mListener) { + MOZ_ASSERT(!mOnStartRequestCalled, + "We should not call OnStartRequest twice."); + mListener->OnStartRequest(this, mListenerContext); + mOnStartRequestCalled = true; + } else { + NS_WARNING("OnStartRequest skipped because of null listener"); + } + } + + // if this transaction has been replaced, then bail. + if (mTransactionReplaced) + return NS_OK; + + if (mUpgradeProtocolCallback && stickyConn && + mResponseHead && mResponseHead->Status() == 101) { + gHttpHandler->ConnMgr()->CompleteUpgrade(stickyConn, + mUpgradeProtocolCallback); + } + } + + // HTTP_CHANNEL_DISPOSITION TELEMETRY + enum ChannelDisposition + { + kHttpCanceled = 0, + kHttpDisk = 1, + kHttpNetOK = 2, + kHttpNetEarlyFail = 3, + kHttpNetLateFail = 4, + kHttpsCanceled = 8, + kHttpsDisk = 9, + kHttpsNetOK = 10, + kHttpsNetEarlyFail = 11, + kHttpsNetLateFail = 12 + } chanDisposition = kHttpCanceled; + + // HTTP 0.9 is more likely to be an error than really 0.9, so count it that way + if (mCanceled) { + chanDisposition = kHttpCanceled; + } else if (!mUsedNetwork) { + chanDisposition = kHttpDisk; + } else if (NS_SUCCEEDED(status) && + mResponseHead && + mResponseHead->Version() != NS_HTTP_VERSION_0_9) { + chanDisposition = kHttpNetOK; + } else if (!mTransferSize) { + chanDisposition = kHttpNetEarlyFail; + } else { + chanDisposition = kHttpNetLateFail; + } + if (IsHTTPS()) { + // shift http to https disposition enums + chanDisposition = static_cast<ChannelDisposition>(chanDisposition + kHttpsCanceled); + } + LOG((" nsHttpChannel::OnStopRequest ChannelDisposition %d\n", chanDisposition)); + Telemetry::Accumulate(Telemetry::HTTP_CHANNEL_DISPOSITION, chanDisposition); + + // if needed, check cache entry has all data we expect + if (mCacheEntry && mCachePump && + mConcurrentCacheAccess && contentComplete) { + int64_t size, contentLength; + nsresult rv = CheckPartial(mCacheEntry, &size, &contentLength); + if (NS_SUCCEEDED(rv)) { + if (size == int64_t(-1)) { + // mayhemer TODO - we have to restart read from cache here at the size offset + MOZ_ASSERT(false); + LOG((" cache entry write is still in progress, but we just " + "finished reading the cache entry")); + } + else if (contentLength != int64_t(-1) && contentLength != size) { + LOG((" concurrent cache entry write has been interrupted")); + mCachedResponseHead = Move(mResponseHead); + // Ignore zero partial length because we also want to resume when + // no data at all has been read from the cache. + rv = MaybeSetupByteRangeRequest(size, contentLength, true); + if (NS_SUCCEEDED(rv) && mIsPartialRequest) { + // Prevent read from cache again + mCachedContentIsValid = 0; + mCachedContentIsPartial = 1; + + // Perform the range request + rv = ContinueConnect(); + if (NS_SUCCEEDED(rv)) { + LOG((" performing range request")); + mCachePump = nullptr; + return NS_OK; + } else { + LOG((" but range request perform failed 0x%08x", rv)); + status = NS_ERROR_NET_INTERRUPT; + } + } + else { + LOG((" but range request setup failed rv=0x%08x, failing load", rv)); + } + } + } + } + + mIsPending = false; + mStatus = status; + + // perform any final cache operations before we close the cache entry. + if (mCacheEntry && mRequestTimeInitialized) { + bool writeAccess; + // New implementation just returns value of the !mCacheEntryIsReadOnly flag passed in. + // Old implementation checks on nsICache::ACCESS_WRITE flag. + mCacheEntry->HasWriteAccess(!mCacheEntryIsReadOnly, &writeAccess); + if (writeAccess) { + FinalizeCacheEntry(); + } + } + + // Register entry to the Performance resource timing + mozilla::dom::Performance* documentPerformance = GetPerformance(); + if (documentPerformance) { + documentPerformance->AddEntry(this, this); + } + + if (mListener) { + LOG((" calling OnStopRequest\n")); + MOZ_ASSERT(!mOnStopRequestCalled, + "We should not call OnStopRequest twice"); + mListener->OnStopRequest(this, mListenerContext, status); + mOnStopRequestCalled = true; + } + + // If a preferred alt-data type was set, this signals the consumer is + // interested in reading and/or writing the alt-data representation. + // We need to hold a reference to the cache entry in case the listener calls + // openAlternativeOutputStream() after CloseCacheEntry() clears mCacheEntry. + if (!mPreferredCachedAltDataType.IsEmpty()) { + mAltDataCacheEntry = mCacheEntry; + } + + CloseCacheEntry(!contentComplete); + + if (mOfflineCacheEntry) + CloseOfflineCacheEntry(); + + if (mLoadGroup) + mLoadGroup->RemoveRequest(this, nullptr, status); + + // We don't need this info anymore + CleanRedirectCacheChainIfNecessary(); + + ReleaseListeners(); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIStreamListener +//----------------------------------------------------------------------------- + +class OnTransportStatusAsyncEvent : public Runnable +{ +public: + OnTransportStatusAsyncEvent(nsITransportEventSink* aEventSink, + nsresult aTransportStatus, + int64_t aProgress, + int64_t aProgressMax) + : mEventSink(aEventSink) + , mTransportStatus(aTransportStatus) + , mProgress(aProgress) + , mProgressMax(aProgressMax) + { + MOZ_ASSERT(!NS_IsMainThread(), "Shouldn't be created on main thread"); + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(NS_IsMainThread(), "Should run on main thread"); + if (mEventSink) { + mEventSink->OnTransportStatus(nullptr, mTransportStatus, + mProgress, mProgressMax); + } + return NS_OK; + } +private: + nsCOMPtr<nsITransportEventSink> mEventSink; + nsresult mTransportStatus; + int64_t mProgress; + int64_t mProgressMax; +}; + +NS_IMETHODIMP +nsHttpChannel::OnDataAvailable(nsIRequest *request, nsISupports *ctxt, + nsIInputStream *input, + uint64_t offset, uint32_t count) +{ + PROFILER_LABEL("nsHttpChannel", "OnDataAvailable", + js::ProfileEntry::Category::NETWORK); + + LOG(("nsHttpChannel::OnDataAvailable [this=%p request=%p offset=%llu count=%u]\n", + this, request, offset, count)); + + // don't send out OnDataAvailable notifications if we've been canceled. + if (mCanceled) + return mStatus; + + MOZ_ASSERT(mResponseHead, "No response head in ODA!!"); + + MOZ_ASSERT(!(mCachedContentIsPartial && (request == mTransactionPump)), + "transaction pump not suspended"); + + if (mAuthRetryPending || (request == mTransactionPump && mTransactionReplaced)) { + uint32_t n; + return input->ReadSegments(NS_DiscardSegment, nullptr, count, &n); + } + + if (mListener) { + // + // synthesize transport progress event. we do this here since we want + // to delay OnProgress events until we start streaming data. this is + // crucially important since it impacts the lock icon (see bug 240053). + // + nsresult transportStatus; + if (request == mCachePump) + transportStatus = NS_NET_STATUS_READING; + else + transportStatus = NS_NET_STATUS_RECEIVING_FROM; + + // mResponseHead may reference new or cached headers, but either way it + // holds our best estimate of the total content length. Even in the case + // of a byte range request, the content length stored in the cached + // response headers is what we want to use here. + + int64_t progressMax(mResponseHead->ContentLength()); + int64_t progress = mLogicalOffset + count; + + if ((progress > progressMax) && (progressMax != -1)) { + NS_WARNING("unexpected progress values - " + "is server exceeding content length?"); + } + + // make sure params are in range for js + if (!InScriptableRange(progressMax)) { + progressMax = -1; + } + + if (!InScriptableRange(progress)) { + progress = -1; + } + + if (NS_IsMainThread()) { + OnTransportStatus(nullptr, transportStatus, progress, progressMax); + } else { + nsresult rv = NS_DispatchToMainThread( + new OnTransportStatusAsyncEvent(this, transportStatus, + progress, progressMax)); + NS_ENSURE_SUCCESS(rv, rv); + } + + // + // we have to manually keep the logical offset of the stream up-to-date. + // we cannot depend solely on the offset provided, since we may have + // already streamed some data from another source (see, for example, + // OnDoneReadingPartialCacheEntry). + // + int64_t offsetBefore = 0; + nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(input); + if (seekable && NS_FAILED(seekable->Tell(&offsetBefore))) { + seekable = nullptr; + } + + nsresult rv = mListener->OnDataAvailable(this, + mListenerContext, + input, + mLogicalOffset, + count); + if (NS_SUCCEEDED(rv)) { + // by contract mListener must read all of "count" bytes, but + // nsInputStreamPump is tolerant to seekable streams that violate that + // and it will redeliver incompletely read data. So we need to do + // the same thing when updating the progress counter to stay in sync. + int64_t offsetAfter, delta; + if (seekable && NS_SUCCEEDED(seekable->Tell(&offsetAfter))) { + delta = offsetAfter - offsetBefore; + if (delta != count) { + count = delta; + + NS_WARNING("Listener OnDataAvailable contract violation"); + nsCOMPtr<nsIConsoleService> consoleService = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + nsAutoString message + (NS_LITERAL_STRING( + "http channel Listener OnDataAvailable contract violation")); + if (consoleService) { + consoleService->LogStringMessage(message.get()); + } + } + } + mLogicalOffset += count; + } + + return rv; + } + + return NS_ERROR_ABORT; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIThreadRetargetableRequest +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::RetargetDeliveryTo(nsIEventTarget* aNewTarget) +{ + MOZ_ASSERT(NS_IsMainThread(), "Should be called on main thread only"); + + NS_ENSURE_ARG(aNewTarget); + if (aNewTarget == NS_GetCurrentThread()) { + NS_WARNING("Retargeting delivery to same thread"); + return NS_OK; + } + if (!mTransactionPump && !mCachePump) { + LOG(("nsHttpChannel::RetargetDeliveryTo %p %p no pump available\n", + this, aNewTarget)); + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv = NS_OK; + // If both cache pump and transaction pump exist, we're probably dealing + // with partially cached content. So, we must be able to retarget both. + nsCOMPtr<nsIThreadRetargetableRequest> retargetableCachePump; + nsCOMPtr<nsIThreadRetargetableRequest> retargetableTransactionPump; + if (mCachePump) { + retargetableCachePump = do_QueryObject(mCachePump); + // nsInputStreamPump should implement this interface. + MOZ_ASSERT(retargetableCachePump); + rv = retargetableCachePump->RetargetDeliveryTo(aNewTarget); + } + if (NS_SUCCEEDED(rv) && mTransactionPump) { + retargetableTransactionPump = do_QueryObject(mTransactionPump); + // nsInputStreamPump should implement this interface. + MOZ_ASSERT(retargetableTransactionPump); + rv = retargetableTransactionPump->RetargetDeliveryTo(aNewTarget); + + // If retarget fails for transaction pump, we must restore mCachePump. + if (NS_FAILED(rv) && retargetableCachePump) { + nsCOMPtr<nsIThread> mainThread; + rv = NS_GetMainThread(getter_AddRefs(mainThread)); + NS_ENSURE_SUCCESS(rv, rv); + rv = retargetableCachePump->RetargetDeliveryTo(mainThread); + } + } + return rv; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsThreadRetargetableStreamListener +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::CheckListenerChain() +{ + NS_ASSERTION(NS_IsMainThread(), "Should be on main thread!"); + nsresult rv = NS_OK; + nsCOMPtr<nsIThreadRetargetableStreamListener> retargetableListener = + do_QueryInterface(mListener, &rv); + if (retargetableListener) { + rv = retargetableListener->CheckListenerChain(); + } + return rv; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsITransportEventSink +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::OnTransportStatus(nsITransport *trans, nsresult status, + int64_t progress, int64_t progressMax) +{ + MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread only"); + // cache the progress sink so we don't have to query for it each time. + if (!mProgressSink) + GetCallback(mProgressSink); + + if (status == NS_NET_STATUS_CONNECTED_TO || + status == NS_NET_STATUS_WAITING_FOR) { + if (mTransaction) { + mTransaction->GetNetworkAddresses(mSelfAddr, mPeerAddr); + } else { + nsCOMPtr<nsISocketTransport> socketTransport = + do_QueryInterface(trans); + if (socketTransport) { + socketTransport->GetSelfAddr(&mSelfAddr); + socketTransport->GetPeerAddr(&mPeerAddr); + } + } + } + + // block socket status event after Cancel or OnStopRequest has been called. + if (mProgressSink && NS_SUCCEEDED(mStatus) && mIsPending) { + LOG(("sending progress%s notification [this=%p status=%x" + " progress=%lld/%lld]\n", + (mLoadFlags & LOAD_BACKGROUND)? "" : " and status", + this, status, progress, progressMax)); + + if (!(mLoadFlags & LOAD_BACKGROUND)) { + nsAutoCString host; + mURI->GetHost(host); + mProgressSink->OnStatus(this, nullptr, status, + NS_ConvertUTF8toUTF16(host).get()); + } + + if (progress > 0) { + if ((progress > progressMax) && (progressMax != -1)) { + NS_WARNING("unexpected progress values"); + } + + // Try to get mProgressSink if it was nulled out during OnStatus. + if (!mProgressSink) { + GetCallback(mProgressSink); + } + if (mProgressSink) { + mProgressSink->OnProgress(this, nullptr, progress, progressMax); + } + } + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsICacheInfoChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::IsFromCache(bool *value) +{ + if (!mIsPending) + return NS_ERROR_NOT_AVAILABLE; + + // return false if reading a partial cache entry; the data isn't entirely + // from the cache! + + *value = (mCachePump || (mLoadFlags & LOAD_ONLY_IF_MODIFIED)) && + mCachedContentIsValid && !mCachedContentIsPartial; + + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetCacheTokenExpirationTime(uint32_t *_retval) +{ + NS_ENSURE_ARG_POINTER(_retval); + if (!mCacheEntry) + return NS_ERROR_NOT_AVAILABLE; + + return mCacheEntry->GetExpirationTime(_retval); +} + +NS_IMETHODIMP +nsHttpChannel::GetCacheTokenCachedCharset(nsACString &_retval) +{ + nsresult rv; + + if (!mCacheEntry) + return NS_ERROR_NOT_AVAILABLE; + + nsXPIDLCString cachedCharset; + rv = mCacheEntry->GetMetaDataElement("charset", + getter_Copies(cachedCharset)); + if (NS_SUCCEEDED(rv)) + _retval = cachedCharset; + + return rv; +} + +NS_IMETHODIMP +nsHttpChannel::SetCacheTokenCachedCharset(const nsACString &aCharset) +{ + if (!mCacheEntry) + return NS_ERROR_NOT_AVAILABLE; + + return mCacheEntry->SetMetaDataElement("charset", + PromiseFlatCString(aCharset).get()); +} + +NS_IMETHODIMP +nsHttpChannel::SetAllowStaleCacheContent(bool aAllowStaleCacheContent) +{ + LOG(("nsHttpChannel::SetAllowStaleCacheContent [this=%p, allow=%d]", + this, aAllowStaleCacheContent)); + mAllowStaleCacheContent = aAllowStaleCacheContent; + return NS_OK; +} +NS_IMETHODIMP +nsHttpChannel::GetAllowStaleCacheContent(bool *aAllowStaleCacheContent) +{ + NS_ENSURE_ARG(aAllowStaleCacheContent); + *aAllowStaleCacheContent = mAllowStaleCacheContent; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::PreferAlternativeDataType(const nsACString & aType) +{ + ENSURE_CALLED_BEFORE_ASYNC_OPEN(); + mPreferredCachedAltDataType = aType; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetAlternativeDataType(nsACString & aType) +{ + // must be called during or after OnStartRequest + if (!mAfterOnStartRequestBegun) { + return NS_ERROR_NOT_AVAILABLE; + } + aType = mAvailableCachedAltDataType; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::OpenAlternativeOutputStream(const nsACString & type, nsIOutputStream * *_retval) +{ + // OnStopRequest will clear mCacheEntry, but we may use mAltDataCacheEntry + // if the consumer called PreferAlternativeDataType() + nsCOMPtr<nsICacheEntry> cacheEntry = mCacheEntry ? mCacheEntry : mAltDataCacheEntry; + if (!cacheEntry) { + return NS_ERROR_NOT_AVAILABLE; + } + return cacheEntry->OpenAlternativeOutputStream(type, _retval); +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsICachingChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::GetCacheToken(nsISupports **token) +{ + NS_ENSURE_ARG_POINTER(token); + if (!mCacheEntry) + return NS_ERROR_NOT_AVAILABLE; + return CallQueryInterface(mCacheEntry, token); +} + +NS_IMETHODIMP +nsHttpChannel::SetCacheToken(nsISupports *token) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsHttpChannel::GetOfflineCacheToken(nsISupports **token) +{ + NS_ENSURE_ARG_POINTER(token); + if (!mOfflineCacheEntry) + return NS_ERROR_NOT_AVAILABLE; + return CallQueryInterface(mOfflineCacheEntry, token); +} + +NS_IMETHODIMP +nsHttpChannel::SetOfflineCacheToken(nsISupports *token) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsHttpChannel::GetCacheKey(nsISupports **key) +{ + nsresult rv; + NS_ENSURE_ARG_POINTER(key); + + LOG(("nsHttpChannel::GetCacheKey [this=%p]\n", this)); + + *key = nullptr; + + nsCOMPtr<nsISupportsPRUint32> container = + do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID, &rv); + + if (!container) + return NS_ERROR_OUT_OF_MEMORY; + + rv = container->SetData(mPostID); + if (NS_FAILED(rv)) return rv; + + return CallQueryInterface(container.get(), key); +} + +NS_IMETHODIMP +nsHttpChannel::SetCacheKey(nsISupports *key) +{ + nsresult rv; + + LOG(("nsHttpChannel::SetCacheKey [this=%p key=%p]\n", this, key)); + + ENSURE_CALLED_BEFORE_CONNECT(); + + if (!key) + mPostID = 0; + else { + // extract the post id + nsCOMPtr<nsISupportsPRUint32> container = do_QueryInterface(key, &rv); + if (NS_FAILED(rv)) return rv; + + rv = container->GetData(&mPostID); + if (NS_FAILED(rv)) return rv; + } + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetCacheOnlyMetadata(bool *aOnlyMetadata) +{ + NS_ENSURE_ARG(aOnlyMetadata); + *aOnlyMetadata = mCacheOnlyMetadata; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::SetCacheOnlyMetadata(bool aOnlyMetadata) +{ + LOG(("nsHttpChannel::SetCacheOnlyMetadata [this=%p only-metadata=%d]\n", + this, aOnlyMetadata)); + + ENSURE_CALLED_BEFORE_ASYNC_OPEN(); + + mCacheOnlyMetadata = aOnlyMetadata; + if (aOnlyMetadata) { + mLoadFlags |= LOAD_ONLY_IF_MODIFIED; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetPin(bool *aPin) +{ + NS_ENSURE_ARG(aPin); + *aPin = mPinCacheContent; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::SetPin(bool aPin) +{ + LOG(("nsHttpChannel::SetPin [this=%p pin=%d]\n", + this, aPin)); + + ENSURE_CALLED_BEFORE_CONNECT(); + + mPinCacheContent = aPin; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::ForceCacheEntryValidFor(uint32_t aSecondsToTheFuture) +{ + if (!mCacheEntry) { + LOG(("nsHttpChannel::ForceCacheEntryValidFor found no cache entry " + "for this channel [this=%p].", this)); + } else { + mCacheEntry->ForceValidFor(aSecondsToTheFuture); + + nsAutoCString key; + mCacheEntry->GetKey(key); + + LOG(("nsHttpChannel::ForceCacheEntryValidFor successfully forced valid " + "entry with key %s for %d seconds. [this=%p]", key.get(), + aSecondsToTheFuture, this)); + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIResumableChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::ResumeAt(uint64_t aStartPos, + const nsACString& aEntityID) +{ + LOG(("nsHttpChannel::ResumeAt [this=%p startPos=%llu id='%s']\n", + this, aStartPos, PromiseFlatCString(aEntityID).get())); + mEntityID = aEntityID; + mStartPos = aStartPos; + mResuming = true; + return NS_OK; +} + +nsresult +nsHttpChannel::DoAuthRetry(nsAHttpConnection *conn) +{ + LOG(("nsHttpChannel::DoAuthRetry [this=%p]\n", this)); + + MOZ_ASSERT(!mTransaction, "should not have a transaction"); + nsresult rv; + + // toggle mIsPending to allow nsIObserver implementations to modify + // the request headers (bug 95044). + mIsPending = false; + + // fetch cookies, and add them to the request header. + // the server response could have included cookies that must be sent with + // this authentication attempt (bug 84794). + // TODO: save cookies from auth response and send them here (bug 572151). + AddCookiesToRequest(); + + // notify "http-on-modify-request" observers + CallOnModifyRequestObservers(); + + mIsPending = true; + + // get rid of the old response headers + mResponseHead = nullptr; + + // rewind the upload stream + if (mUploadStream) { + nsCOMPtr<nsISeekableStream> seekable = do_QueryInterface(mUploadStream); + if (seekable) + seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0); + } + + // always set sticky connection flag + mCaps |= NS_HTTP_STICKY_CONNECTION; + // and when needed, allow restart regardless the sticky flag + if (mAuthConnectionRestartable) { + LOG((" connection made restartable")); + mCaps |= NS_HTTP_CONNECTION_RESTARTABLE; + mAuthConnectionRestartable = false; + } else { + LOG((" connection made non-restartable")); + mCaps &= ~NS_HTTP_CONNECTION_RESTARTABLE; + } + + // and create a new one... + rv = SetupTransaction(); + if (NS_FAILED(rv)) return rv; + + // transfer ownership of connection to transaction + if (conn) + mTransaction->SetConnection(conn); + + rv = gHttpHandler->InitiateTransaction(mTransaction, mPriority); + if (NS_FAILED(rv)) return rv; + + rv = mTransactionPump->AsyncRead(this, nullptr); + if (NS_FAILED(rv)) return rv; + + uint32_t suspendCount = mSuspendCount; + while (suspendCount--) + mTransactionPump->Suspend(); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIApplicationCacheChannel +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::GetApplicationCache(nsIApplicationCache **out) +{ + NS_IF_ADDREF(*out = mApplicationCache); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::SetApplicationCache(nsIApplicationCache *appCache) +{ + ENSURE_CALLED_BEFORE_CONNECT(); + + mApplicationCache = appCache; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetApplicationCacheForWrite(nsIApplicationCache **out) +{ + NS_IF_ADDREF(*out = mApplicationCacheForWrite); + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::SetApplicationCacheForWrite(nsIApplicationCache *appCache) +{ + ENSURE_CALLED_BEFORE_CONNECT(); + + mApplicationCacheForWrite = appCache; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetLoadedFromApplicationCache(bool *aLoadedFromApplicationCache) +{ + *aLoadedFromApplicationCache = mLoadedFromApplicationCache; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetInheritApplicationCache(bool *aInherit) +{ + *aInherit = mInheritApplicationCache; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::SetInheritApplicationCache(bool aInherit) +{ + ENSURE_CALLED_BEFORE_CONNECT(); + + mInheritApplicationCache = aInherit; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::GetChooseApplicationCache(bool *aChoose) +{ + *aChoose = mChooseApplicationCache; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::SetChooseApplicationCache(bool aChoose) +{ + ENSURE_CALLED_BEFORE_CONNECT(); + + mChooseApplicationCache = aChoose; + return NS_OK; +} + +nsHttpChannel::OfflineCacheEntryAsForeignMarker* +nsHttpChannel::GetOfflineCacheEntryAsForeignMarker() +{ + if (!mApplicationCache) + return nullptr; + + return new OfflineCacheEntryAsForeignMarker(mApplicationCache, mURI); +} + +nsresult +nsHttpChannel::OfflineCacheEntryAsForeignMarker::MarkAsForeign() +{ + nsresult rv; + + nsCOMPtr<nsIURI> noRefURI; + rv = mCacheURI->CloneIgnoringRef(getter_AddRefs(noRefURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString spec; + rv = noRefURI->GetAsciiSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + return mApplicationCache->MarkEntry(spec, + nsIApplicationCache::ITEM_FOREIGN); +} + +NS_IMETHODIMP +nsHttpChannel::MarkOfflineCacheEntryAsForeign() +{ + nsresult rv; + + nsAutoPtr<OfflineCacheEntryAsForeignMarker> marker( + GetOfflineCacheEntryAsForeignMarker()); + + if (!marker) + return NS_ERROR_NOT_AVAILABLE; + + rv = marker->MarkAsForeign(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel::nsIAsyncVerifyRedirectCallback +//----------------------------------------------------------------------------- + +nsresult +nsHttpChannel::WaitForRedirectCallback() +{ + nsresult rv; + LOG(("nsHttpChannel::WaitForRedirectCallback [this=%p]\n", this)); + + if (mTransactionPump) { + rv = mTransactionPump->Suspend(); + NS_ENSURE_SUCCESS(rv, rv); + } + if (mCachePump) { + rv = mCachePump->Suspend(); + if (NS_FAILED(rv) && mTransactionPump) { +#ifdef DEBUG + nsresult resume = +#endif + mTransactionPump->Resume(); + MOZ_ASSERT(NS_SUCCEEDED(resume), + "Failed to resume transaction pump"); + } + NS_ENSURE_SUCCESS(rv, rv); + } + + mWaitingForRedirectCallback = true; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::OnRedirectVerifyCallback(nsresult result) +{ + LOG(("nsHttpChannel::OnRedirectVerifyCallback [this=%p] " + "result=%x stack=%d mWaitingForRedirectCallback=%u\n", + this, result, mRedirectFuncStack.Length(), mWaitingForRedirectCallback)); + MOZ_ASSERT(mWaitingForRedirectCallback, + "Someone forgot to call WaitForRedirectCallback() ?!"); + mWaitingForRedirectCallback = false; + + if (mCanceled && NS_SUCCEEDED(result)) + result = NS_BINDING_ABORTED; + + for (uint32_t i = mRedirectFuncStack.Length(); i > 0;) { + --i; + // Pop the last function pushed to the stack + nsContinueRedirectionFunc func = mRedirectFuncStack[i]; + mRedirectFuncStack.RemoveElementAt(mRedirectFuncStack.Length() - 1); + + // Call it with the result we got from the callback or the deeper + // function call. + result = (this->*func)(result); + + // If a new function has been pushed to the stack and placed us in the + // waiting state, we need to break the chain and wait for the callback + // again. + if (mWaitingForRedirectCallback) + break; + } + + if (NS_FAILED(result) && !mCanceled) { + // First, cancel this channel if we are in failure state to set mStatus + // and let it be propagated to pumps. + Cancel(result); + } + + if (!mWaitingForRedirectCallback) { + // We are not waiting for the callback. At this moment we must release + // reference to the redirect target channel, otherwise we may leak. + mRedirectChannel = nullptr; + } + + // We always resume the pumps here. If all functions on stack have been + // called we need OnStopRequest to be triggered, and if we broke out of the + // loop above (and are thus waiting for a new callback) the suspension + // count must be balanced in the pumps. + if (mTransactionPump) + mTransactionPump->Resume(); + if (mCachePump) + mCachePump->Resume(); + + return result; +} + +void +nsHttpChannel::PushRedirectAsyncFunc(nsContinueRedirectionFunc func) +{ + mRedirectFuncStack.AppendElement(func); +} + +void +nsHttpChannel::PopRedirectAsyncFunc(nsContinueRedirectionFunc func) +{ + MOZ_ASSERT(func == mRedirectFuncStack[mRedirectFuncStack.Length() - 1], + "Trying to pop wrong method from redirect async stack!"); + + mRedirectFuncStack.TruncateLength(mRedirectFuncStack.Length() - 1); +} + +//----------------------------------------------------------------------------- +// nsIDNSListener functions +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::OnLookupComplete(nsICancelable *request, + nsIDNSRecord *rec, + nsresult status) +{ + MOZ_ASSERT(NS_IsMainThread(), "Expecting DNS callback on main thread."); + + LOG(("nsHttpChannel::OnLookupComplete [this=%p] prefetch complete%s: " + "%s status[0x%x]\n", + this, mCaps & NS_HTTP_REFRESH_DNS ? ", refresh requested" : "", + NS_SUCCEEDED(status) ? "success" : "failure", status)); + + // We no longer need the dns prefetch object. Note: mDNSPrefetch could be + // validly null if OnStopRequest has already been called. + // We only need the domainLookup timestamps when not loading from cache + if (mDNSPrefetch && mDNSPrefetch->TimingsValid() && mTransaction) { + TimeStamp connectStart = mTransaction->GetConnectStart(); + TimeStamp requestStart = mTransaction->GetRequestStart(); + // We only set the domainLookup timestamps if we're not using a + // persistent connection. + if (requestStart.IsNull() && connectStart.IsNull()) { + mTransaction->SetDomainLookupStart(mDNSPrefetch->StartTimestamp()); + mTransaction->SetDomainLookupEnd(mDNSPrefetch->EndTimestamp()); + } + } + mDNSPrefetch = nullptr; + + // Unset DNS cache refresh if it was requested, + if (mCaps & NS_HTTP_REFRESH_DNS) { + mCaps &= ~NS_HTTP_REFRESH_DNS; + if (mTransaction) { + mTransaction->SetDNSWasRefreshed(); + } + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpChannel internal functions +//----------------------------------------------------------------------------- + +// Creates an URI to the given location using current URI for base and charset +nsresult +nsHttpChannel::CreateNewURI(const char *loc, nsIURI **newURI) +{ + nsCOMPtr<nsIIOService> ioService; + nsresult rv = gHttpHandler->GetIOService(getter_AddRefs(ioService)); + if (NS_FAILED(rv)) return rv; + + // the new uri should inherit the origin charset of the current uri + nsAutoCString originCharset; + rv = mURI->GetOriginCharset(originCharset); + if (NS_FAILED(rv)) + originCharset.Truncate(); + + return ioService->NewURI(nsDependentCString(loc), + originCharset.get(), + mURI, + newURI); +} + +void +nsHttpChannel::MaybeInvalidateCacheEntryForSubsequentGet() +{ + // See RFC 2616 section 5.1.1. These are considered valid + // methods which DO NOT invalidate cache-entries for the + // referred resource. POST, PUT and DELETE as well as any + // other method not listed here will potentially invalidate + // any cached copy of the resource + if (mRequestHead.IsGet() || mRequestHead.IsOptions() || + mRequestHead.IsHead() || mRequestHead.IsTrace() || + mRequestHead.IsConnect()) { + return; + } + + // Invalidate the request-uri. + if (LOG_ENABLED()) { + nsAutoCString key; + mURI->GetAsciiSpec(key); + LOG(("MaybeInvalidateCacheEntryForSubsequentGet [this=%p uri=%s]\n", + this, key.get())); + } + + DoInvalidateCacheEntry(mURI); + + // Invalidate Location-header if set + nsAutoCString location; + mResponseHead->GetHeader(nsHttp::Location, location); + if (!location.IsEmpty()) { + LOG((" Location-header=%s\n", location.get())); + InvalidateCacheEntryForLocation(location.get()); + } + + // Invalidate Content-Location-header if set + mResponseHead->GetHeader(nsHttp::Content_Location, location); + if (!location.IsEmpty()) { + LOG((" Content-Location-header=%s\n", location.get())); + InvalidateCacheEntryForLocation(location.get()); + } +} + +void +nsHttpChannel::InvalidateCacheEntryForLocation(const char *location) +{ + nsAutoCString tmpCacheKey, tmpSpec; + nsCOMPtr<nsIURI> resultingURI; + nsresult rv = CreateNewURI(location, getter_AddRefs(resultingURI)); + if (NS_SUCCEEDED(rv) && HostPartIsTheSame(resultingURI)) { + DoInvalidateCacheEntry(resultingURI); + } else { + LOG((" hosts not matching\n")); + } +} + +void +nsHttpChannel::DoInvalidateCacheEntry(nsIURI* aURI) +{ + // NOTE: + // Following comments 24,32 and 33 in bug #327765, we only care about + // the cache in the protocol-handler, not the application cache. + // The logic below deviates from the original logic in OpenCacheEntry on + // one point by using only READ_ONLY access-policy. I think this is safe. + + nsresult rv; + + nsAutoCString key; + if (LOG_ENABLED()) { + aURI->GetAsciiSpec(key); + } + + LOG(("DoInvalidateCacheEntry [channel=%p key=%s]", this, key.get())); + + nsCOMPtr<nsICacheStorageService> cacheStorageService = + do_GetService("@mozilla.org/netwerk/cache-storage-service;1", &rv); + + nsCOMPtr<nsICacheStorage> cacheStorage; + if (NS_SUCCEEDED(rv)) { + RefPtr<LoadContextInfo> info = GetLoadContextInfo(this); + rv = cacheStorageService->DiskCacheStorage(info, false, getter_AddRefs(cacheStorage)); + } + + if (NS_SUCCEEDED(rv)) { + rv = cacheStorage->AsyncDoomURI(aURI, EmptyCString(), nullptr); + } + + LOG(("DoInvalidateCacheEntry [channel=%p key=%s rv=%d]", this, key.get(), int(rv))); +} + +void +nsHttpChannel::AsyncOnExamineCachedResponse() +{ + gHttpHandler->OnExamineCachedResponse(this); + +} + +void +nsHttpChannel::UpdateAggregateCallbacks() +{ + if (!mTransaction) { + return; + } + nsCOMPtr<nsIInterfaceRequestor> callbacks; + NS_NewNotificationCallbacksAggregation(mCallbacks, mLoadGroup, + NS_GetCurrentThread(), + getter_AddRefs(callbacks)); + mTransaction->SetSecurityCallbacks(callbacks); +} + +NS_IMETHODIMP +nsHttpChannel::SetLoadGroup(nsILoadGroup *aLoadGroup) +{ + MOZ_ASSERT(NS_IsMainThread(), "Wrong thread."); + + nsresult rv = HttpBaseChannel::SetLoadGroup(aLoadGroup); + if (NS_SUCCEEDED(rv)) { + UpdateAggregateCallbacks(); + } + return rv; +} + +NS_IMETHODIMP +nsHttpChannel::SetNotificationCallbacks(nsIInterfaceRequestor *aCallbacks) +{ + MOZ_ASSERT(NS_IsMainThread(), "Wrong thread."); + + nsresult rv = HttpBaseChannel::SetNotificationCallbacks(aCallbacks); + if (NS_SUCCEEDED(rv)) { + UpdateAggregateCallbacks(); + } + return rv; +} + +void +nsHttpChannel::MarkIntercepted() +{ + mInterceptCache = INTERCEPTED; +} + +NS_IMETHODIMP +nsHttpChannel::GetResponseSynthesized(bool* aSynthesized) +{ + NS_ENSURE_ARG_POINTER(aSynthesized); + *aSynthesized = (mInterceptCache == INTERCEPTED); + return NS_OK; +} + +bool +nsHttpChannel::AwaitingCacheCallbacks() +{ + return mCacheEntriesToWaitFor != 0; +} + +void +nsHttpChannel::SetPushedStream(Http2PushedStream *stream) +{ + MOZ_ASSERT(stream); + MOZ_ASSERT(!mPushedStream); + mPushedStream = stream; +} + +nsresult +nsHttpChannel::OnPush(const nsACString &url, Http2PushedStream *pushedStream) +{ + MOZ_ASSERT(NS_IsMainThread()); + LOG(("nsHttpChannel::OnPush [this=%p]\n", this)); + + MOZ_ASSERT(mCaps & NS_HTTP_ONPUSH_LISTENER); + nsCOMPtr<nsIHttpPushListener> pushListener; + NS_QueryNotificationCallbacks(mCallbacks, + mLoadGroup, + NS_GET_IID(nsIHttpPushListener), + getter_AddRefs(pushListener)); + + MOZ_ASSERT(pushListener); + if (!pushListener) { + LOG(("nsHttpChannel::OnPush [this=%p] notification callbacks do not " + "implement nsIHttpPushListener\n", this)); + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr<nsIURI> pushResource; + nsresult rv; + + // Create a Channel for the Push Resource + rv = NS_NewURI(getter_AddRefs(pushResource), url); + if (NS_FAILED(rv)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIIOService> ioService; + rv = gHttpHandler->GetIOService(getter_AddRefs(ioService)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIChannel> pushChannel; + rv = NS_NewChannelInternal(getter_AddRefs(pushChannel), + pushResource, + mLoadInfo, + nullptr, // aLoadGroup + nullptr, // aCallbacks + nsIRequest::LOAD_NORMAL, + ioService); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIHttpChannel> pushHttpChannel = do_QueryInterface(pushChannel); + MOZ_ASSERT(pushHttpChannel); + if (!pushHttpChannel) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<nsHttpChannel> channel; + CallQueryInterface(pushHttpChannel, channel.StartAssignment()); + MOZ_ASSERT(channel); + if (!channel) { + return NS_ERROR_UNEXPECTED; + } + + // new channel needs mrqeuesthead and headers from pushedStream + channel->mRequestHead.ParseHeaderSet( + pushedStream->GetRequestString().BeginWriting()); + + channel->mLoadGroup = mLoadGroup; + channel->mLoadInfo = mLoadInfo; + channel->mCallbacks = mCallbacks; + + // Link the pushed stream with the new channel and call listener + channel->SetPushedStream(pushedStream); + rv = pushListener->OnPush(this, pushHttpChannel); + return rv; +} + +// static +bool nsHttpChannel::IsRedirectStatus(uint32_t status) +{ + // 305 disabled as a security measure (see bug 187996). + return status == 300 || status == 301 || status == 302 || status == 303 || + status == 307 || status == 308; +} + +void +nsHttpChannel::SetCouldBeSynthesized() +{ + MOZ_ASSERT(!BypassServiceWorker()); + mResponseCouldBeSynthesized = true; +} + +void +nsHttpChannel::SetConnectionInfo(nsHttpConnectionInfo *aCI) +{ + mConnectionInfo = aCI ? aCI->Clone() : nullptr; +} + +NS_IMETHODIMP +nsHttpChannel::OnPreflightSucceeded() +{ + MOZ_ASSERT(mRequireCORSPreflight, "Why did a preflight happen?"); + mIsCorsPreflightDone = 1; + mPreflightChannel = nullptr; + + return ContinueConnect(); +} + +NS_IMETHODIMP +nsHttpChannel::OnPreflightFailed(nsresult aError) +{ + MOZ_ASSERT(mRequireCORSPreflight, "Why did a preflight happen?"); + mIsCorsPreflightDone = 1; + mPreflightChannel = nullptr; + + CloseCacheEntry(false); + AsyncAbort(aError); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsIHstsPrimingCallback functions +//----------------------------------------------------------------------------- + +/* + * May be invoked synchronously if HSTS priming has already been performed + * for the host. + */ +nsresult +nsHttpChannel::OnHSTSPrimingSucceeded(bool aCached) +{ + if (nsMixedContentBlocker::sUseHSTS) { + // redirect the channel to HTTPS if the pref + // "security.mixed_content.use_hsts" is true + LOG(("HSTS Priming succeeded, redirecting to HTTPS [this=%p]", this)); + Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS_PRIMING_RESULT, + (aCached) ? HSTSPrimingResult::eHSTS_PRIMING_CACHED_DO_UPGRADE : + HSTSPrimingResult::eHSTS_PRIMING_SUCCEEDED); + return AsyncCall(&nsHttpChannel::HandleAsyncRedirectChannelToHttps); + } + + // If "security.mixed_content.use_hsts" is false, record the result of + // HSTS priming and block or proceed with the load as required by + // mixed-content blocking + bool wouldBlock = mLoadInfo->GetMixedContentWouldBlock(); + + // preserve the mixed-content-before-hsts order and block if required + if (wouldBlock) { + LOG(("HSTS Priming succeeded, blocking for mixed-content [this=%p]", + this)); + Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS_PRIMING_RESULT, + HSTSPrimingResult::eHSTS_PRIMING_SUCCEEDED_BLOCK); + CloseCacheEntry(false); + return AsyncAbort(NS_ERROR_CONTENT_BLOCKED); + } + + LOG(("HSTS Priming succeeded, loading insecure: [this=%p]", this)); + Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS_PRIMING_RESULT, + HSTSPrimingResult::eHSTS_PRIMING_SUCCEEDED_HTTP); + + nsresult rv = ContinueConnect(); + if (NS_FAILED(rv)) { + CloseCacheEntry(false); + return AsyncAbort(rv); + } + + return NS_OK; +} + +/* + * May be invoked synchronously if HSTS priming has already been performed + * for the host. + */ +nsresult +nsHttpChannel::OnHSTSPrimingFailed(nsresult aError, bool aCached) +{ + bool wouldBlock = mLoadInfo->GetMixedContentWouldBlock(); + + LOG(("HSTS Priming Failed [this=%p], %s the load", this, + (wouldBlock) ? "blocking" : "allowing")); + if (aCached) { + // Between the time we marked for priming and started the priming request, + // the host was found to not allow the upgrade, probably from another + // priming request. + Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS_PRIMING_RESULT, + (wouldBlock) ? HSTSPrimingResult::eHSTS_PRIMING_CACHED_BLOCK : + HSTSPrimingResult::eHSTS_PRIMING_CACHED_NO_UPGRADE); + } else { + // A priming request was sent, and no HSTS header was found that allows + // the upgrade. + Telemetry::Accumulate(Telemetry::MIXED_CONTENT_HSTS_PRIMING_RESULT, + (wouldBlock) ? HSTSPrimingResult::eHSTS_PRIMING_FAILED_BLOCK : + HSTSPrimingResult::eHSTS_PRIMING_FAILED_ACCEPT); + } + + // Don't visit again for at least + // security.mixed_content.hsts_priming_cache_timeout seconds. + nsISiteSecurityService* sss = gHttpHandler->GetSSService(); + NS_ENSURE_TRUE(sss, NS_ERROR_OUT_OF_MEMORY); + nsresult rv = sss->CacheNegativeHSTSResult(mURI, + nsMixedContentBlocker::sHSTSPrimingCacheTimeout); + if (NS_FAILED(rv)) { + NS_ERROR("nsISiteSecurityService::CacheNegativeHSTSResult failed"); + } + + // If we would block, go ahead and abort with the error provided + if (wouldBlock) { + CloseCacheEntry(false); + return AsyncAbort(aError); + } + + // we can continue the load and the UI has been updated as mixed content + rv = ContinueConnect(); + if (NS_FAILED(rv)) { + CloseCacheEntry(false); + return AsyncAbort(rv); + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// AChannelHasDivertableParentChannelAsListener internal functions +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpChannel::MessageDiversionStarted(ADivertableParentChannel *aParentChannel) +{ + LOG(("nsHttpChannel::MessageDiversionStarted [this=%p]", this)); + MOZ_ASSERT(!mParentChannel); + mParentChannel = aParentChannel; + // If the channel is suspended, propagate that info to the parent's mEventQ. + uint32_t suspendCount = mSuspendCount; + while (suspendCount--) { + mParentChannel->SuspendMessageDiversion(); + } + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::MessageDiversionStop() +{ + LOG(("nsHttpChannel::MessageDiversionStop [this=%p]", this)); + MOZ_ASSERT(mParentChannel); + mParentChannel = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +nsHttpChannel::SuspendInternal() +{ + NS_ENSURE_TRUE(mIsPending, NS_ERROR_NOT_AVAILABLE); + + LOG(("nsHttpChannel::SuspendInternal [this=%p]\n", this)); + + ++mSuspendCount; + + nsresult rvTransaction = NS_OK; + if (mTransactionPump) { + rvTransaction = mTransactionPump->Suspend(); + } + nsresult rvCache = NS_OK; + if (mCachePump) { + rvCache = mCachePump->Suspend(); + } + + return NS_FAILED(rvTransaction) ? rvTransaction : rvCache; +} + +NS_IMETHODIMP +nsHttpChannel::ResumeInternal() +{ + NS_ENSURE_TRUE(mSuspendCount > 0, NS_ERROR_UNEXPECTED); + + LOG(("nsHttpChannel::ResumeInternal [this=%p]\n", this)); + + if (--mSuspendCount == 0 && mCallOnResume) { + nsresult rv = AsyncCall(mCallOnResume); + mCallOnResume = nullptr; + NS_ENSURE_SUCCESS(rv, rv); + } + + nsresult rvTransaction = NS_OK; + if (mTransactionPump) { + rvTransaction = mTransactionPump->Resume(); + } + + nsresult rvCache = NS_OK; + if (mCachePump) { + rvCache = mCachePump->Resume(); + } + + return NS_FAILED(rvTransaction) ? rvTransaction : rvCache; +} + +void +nsHttpChannel::MaybeWarnAboutAppCache() +{ + // First, accumulate a telemetry ping about appcache usage. + Telemetry::Accumulate(Telemetry::HTTP_OFFLINE_CACHE_DOCUMENT_LOAD, + true); + + // Then, issue a deprecation warning. + nsCOMPtr<nsIDeprecationWarner> warner; + GetCallback(warner); + if (warner) { + warner->IssueWarning(nsIDocument::eAppCache, false); + } +} + +void +nsHttpChannel::SetLoadGroupUserAgentOverride() +{ + nsCOMPtr<nsIURI> uri; + GetURI(getter_AddRefs(uri)); + nsAutoCString uriScheme; + if (uri) { + uri->GetScheme(uriScheme); + } + + // We don't need a UA for file: protocols. + if (uriScheme.EqualsLiteral("file")) { + gHttpHandler->OnUserAgentRequest(this); + return; + } + + nsIRequestContextService* rcsvc = gHttpHandler->GetRequestContextService(); + nsCOMPtr<nsIRequestContext> rc; + if (rcsvc) { + rcsvc->GetRequestContext(mRequestContextID, + getter_AddRefs(rc)); + } + + nsAutoCString ua; + if (nsContentUtils::IsNonSubresourceRequest(this)) { + gHttpHandler->OnUserAgentRequest(this); + if (rc) { + GetRequestHeader(NS_LITERAL_CSTRING("User-Agent"), ua); + rc->SetUserAgentOverride(ua); + } + } else { + GetRequestHeader(NS_LITERAL_CSTRING("User-Agent"), ua); + // Don't overwrite the UA if it is already set (eg by an XHR with explicit UA). + if (ua.IsEmpty()) { + if (rc) { + rc->GetUserAgentOverride(ua); + SetRequestHeader(NS_LITERAL_CSTRING("User-Agent"), ua, false); + } else { + gHttpHandler->OnUserAgentRequest(this); + } + } + } +} + +void +nsHttpChannel::SetDoNotTrack() +{ + /** + * 'DoNotTrack' header should be added if 'privacy.donottrackheader.enabled' + * is true or tracking protection is enabled. See bug 1258033. + */ + nsCOMPtr<nsILoadContext> loadContext; + NS_QueryNotificationCallbacks(this, loadContext); + + if ((loadContext && loadContext->UseTrackingProtection()) || + nsContentUtils::DoNotTrackEnabled()) { + mRequestHead.SetHeader(nsHttp::DoNotTrack, + NS_LITERAL_CSTRING("1"), + false); + } +} + + +void +nsHttpChannel::ReportNetVSCacheTelemetry() +{ + nsresult rv; + if (!mCacheEntry) { + return; + } + + // We only report telemetry if the entry is persistent (on disk) + bool persistent; + rv = mCacheEntry->GetPersistent(&persistent); + if (NS_FAILED(rv) || !persistent) { + return; + } + + nsXPIDLCString tmpStr; + rv = mCacheEntry->GetMetaDataElement("net-response-time-onstart", + getter_Copies(tmpStr)); + if (NS_FAILED(rv)) { + return; + } + uint64_t onStartNetTime = tmpStr.ToInteger64(&rv); + if (NS_FAILED(rv)) { + return; + } + + tmpStr.Truncate(); + rv = mCacheEntry->GetMetaDataElement("net-response-time-onstop", + getter_Copies(tmpStr)); + if (NS_FAILED(rv)) { + return; + } + uint64_t onStopNetTime = tmpStr.ToInteger64(&rv); + if (NS_FAILED(rv)) { + return; + } + + uint64_t onStartCacheTime = (mOnStartRequestTimestamp - mAsyncOpenTime).ToMilliseconds(); + int64_t onStartDiff = onStartNetTime - onStartCacheTime; + onStartDiff += 500; // We offset the difference by 500 ms to report positive values in telemetry + + uint64_t onStopCacheTime = (mCacheReadEnd - mAsyncOpenTime).ToMilliseconds(); + int64_t onStopDiff = onStopNetTime - onStopCacheTime; + onStopDiff += 500; // We offset the difference by 500 ms + + if (mDidReval) { + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTART_REVALIDATED, onStartDiff); + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_REVALIDATED, onStopDiff); + } else { + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTART_NOTREVALIDATED, onStartDiff); + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_NOTREVALIDATED, onStopDiff); + } + + if (mDidReval) { + // We don't report revalidated probes as the data would be skewed. + return; + } + + uint32_t diskStorageSizeK = 0; + rv = mCacheEntry->GetDiskStorageSizeInKB(&diskStorageSizeK); + if (NS_FAILED(rv)) { + return; + } + + nsAutoCString contentType; + if (mResponseHead && mResponseHead->HasContentType()) { + mResponseHead->ContentType(contentType); + } + bool isImage = StringBeginsWith(contentType, NS_LITERAL_CSTRING("image/")); + if (isImage) { + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTART_ISIMG, onStartDiff); + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_ISIMG, onStopDiff); + } else { + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTART_NOTIMG, onStartDiff); + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_NOTIMG, onStopDiff); + } + + if (mCacheOpenWithPriority) { + if (mCacheQueueSizeWhenOpen < 5) { + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTART_QSMALL_HIGHPRI, onStartDiff); + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_QSMALL_HIGHPRI, onStopDiff); + } else if (mCacheQueueSizeWhenOpen < 10) { + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTART_QMED_HIGHPRI, onStartDiff); + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_QMED_HIGHPRI, onStopDiff); + } else { + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTART_QBIG_HIGHPRI, onStartDiff); + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_QBIG_HIGHPRI, onStopDiff); + } + } else { // The limits are higher for normal priority cache queues + if (mCacheQueueSizeWhenOpen < 10) { + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTART_QSMALL_NORMALPRI, onStartDiff); + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_QSMALL_NORMALPRI, onStopDiff); + } else if (mCacheQueueSizeWhenOpen < 50) { + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTART_QMED_NORMALPRI, onStartDiff); + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_QMED_NORMALPRI, onStopDiff); + } else { + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTART_QBIG_NORMALPRI, onStartDiff); + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_QBIG_NORMALPRI, onStopDiff); + } + } + + if (diskStorageSizeK < 32) { + if (mCacheOpenWithPriority) { + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTART_SMALL_HIGHPRI, onStartDiff); + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_SMALL_HIGHPRI, onStopDiff); + } else { + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTART_SMALL_NORMALPRI, onStartDiff); + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_SMALL_NORMALPRI, onStopDiff); + } + } else if (diskStorageSizeK < 256) { + if (mCacheOpenWithPriority) { + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTART_MED_HIGHPRI, onStartDiff); + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_MED_HIGHPRI, onStopDiff); + } else { + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTART_MED_NORMALPRI, onStartDiff); + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_MED_NORMALPRI, onStopDiff); + } + } else { + if (mCacheOpenWithPriority) { + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTART_LARGE_HIGHPRI, onStartDiff); + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_LARGE_HIGHPRI, onStopDiff); + } else { + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTART_LARGE_NORMALPRI, onStartDiff); + Telemetry::Accumulate(Telemetry::HTTP_NET_VS_CACHE_ONSTOP_LARGE_NORMALPRI, onStopDiff); + } + } +} + +} // namespace net +} // namespace mozilla |