/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set ts=4 sw=2 sts=2 et 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 "nsHttpTransaction.h" #include #include #include "HttpLog.h" #include "HTTPSRecordResolver.h" #include "NSSErrorsService.h" #include "base/basictypes.h" #include "mozilla/Components.h" #include "mozilla/net/SSLTokensCache.h" #include "mozilla/ScopeExit.h" #include "mozilla/Tokenizer.h" #include "mozilla/StaticPrefs_network.h" #include "MockHttpAuth.h" #include "nsCRT.h" #include "nsComponentManagerUtils.h" // do_CreateInstance #include "nsHttpBasicAuth.h" #include "nsHttpChannel.h" #include "nsHttpChunkedDecoder.h" #include "nsHttpDigestAuth.h" #include "nsHttpHandler.h" #include "nsHttpNTLMAuth.h" #ifdef MOZ_AUTH_EXTENSION # include "nsHttpNegotiateAuth.h" #endif #include "nsHttpRequestHead.h" #include "nsHttpResponseHead.h" #include "nsICancelable.h" #include "nsIClassOfService.h" #include "nsIDNSByTypeRecord.h" #include "nsIDNSRecord.h" #include "nsIDNSService.h" #include "nsIEventTarget.h" #include "nsIHttpActivityObserver.h" #include "nsIHttpAuthenticator.h" #include "nsIInputStream.h" #include "nsIInputStreamPriority.h" #include "nsIMultiplexInputStream.h" #include "nsIOService.h" #include "nsIPipe.h" #include "nsIRequestContext.h" #include "nsISeekableStream.h" #include "nsITLSSocketControl.h" #include "nsIThrottledInputChannel.h" #include "nsITransport.h" #include "nsMultiplexInputStream.h" #include "nsNetCID.h" #include "nsNetUtil.h" #include "nsQueryObject.h" #include "nsSocketTransportService2.h" #include "nsStringStream.h" #include "nsTransportUtils.h" #include "sslerr.h" #include "SpeculativeTransaction.h" //----------------------------------------------------------------------------- // Place a limit on how much non-compliant HTTP can be skipped while // looking for a response header #define MAX_INVALID_RESPONSE_BODY_SIZE (1024 * 128) using namespace mozilla::net; namespace mozilla::net { //----------------------------------------------------------------------------- // nsHttpTransaction //----------------------------------------------------------------------------- nsHttpTransaction::nsHttpTransaction() { LOG(("Creating nsHttpTransaction @%p\n", this)); #ifdef MOZ_VALGRIND memset(&mSelfAddr, 0, sizeof(NetAddr)); memset(&mPeerAddr, 0, sizeof(NetAddr)); #endif mSelfAddr.raw.family = PR_AF_UNSPEC; mPeerAddr.raw.family = PR_AF_UNSPEC; mThroughCaptivePortal = gHttpHandler->GetThroughCaptivePortal(); } void nsHttpTransaction::ResumeReading() { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); if (!mReadingStopped) { return; } LOG(("nsHttpTransaction::ResumeReading %p", this)); mReadingStopped = false; // This with either reengage the limit when still throttled in WriteSegments // or simply reset to allow unlimeted reading again. mThrottlingReadAllowance = THROTTLE_NO_LIMIT; if (mConnection) { mConnection->TransactionHasDataToRecv(this); nsresult rv = mConnection->ResumeRecv(); if (NS_FAILED(rv)) { LOG((" resume failed with rv=%" PRIx32, static_cast(rv))); } } } bool nsHttpTransaction::EligibleForThrottling() const { return (mClassOfServiceFlags & (nsIClassOfService::Throttleable | nsIClassOfService::DontThrottle | nsIClassOfService::Leader | nsIClassOfService::Unblocked)) == nsIClassOfService::Throttleable; } void nsHttpTransaction::SetClassOfService(ClassOfService cos) { if (mClosed) { return; } bool wasThrottling = EligibleForThrottling(); mClassOfServiceFlags = cos.Flags(); mClassOfServiceIncremental = cos.Incremental(); bool isThrottling = EligibleForThrottling(); if (mConnection && wasThrottling != isThrottling) { // Do nothing until we are actually activated. For now // only remember the throttle flag. Call to UpdateActiveTransaction // would add this transaction to the list too early. gHttpHandler->ConnMgr()->UpdateActiveTransaction(this); if (mReadingStopped && !isThrottling) { ResumeReading(); } } } class ReleaseOnSocketThread final : public mozilla::Runnable { public: explicit ReleaseOnSocketThread(nsTArray>&& aDoomed) : Runnable("ReleaseOnSocketThread"), mDoomed(std::move(aDoomed)) {} NS_IMETHOD Run() override { mDoomed.Clear(); return NS_OK; } void Dispatch() { nsCOMPtr sts = do_GetService("@mozilla.org/network/socket-transport-service;1"); Unused << sts->Dispatch(this, nsIEventTarget::DISPATCH_NORMAL); } private: virtual ~ReleaseOnSocketThread() = default; nsTArray> mDoomed; }; nsHttpTransaction::~nsHttpTransaction() { LOG(("Destroying nsHttpTransaction @%p\n", this)); if (mPushedStream) { mPushedStream->OnPushFailed(); mPushedStream = nullptr; } if (mTokenBucketCancel) { mTokenBucketCancel->Cancel(NS_ERROR_ABORT); mTokenBucketCancel = nullptr; } // Force the callbacks and connection to be released right now mCallbacks = nullptr; mEarlyHintObserver = nullptr; delete mResponseHead; delete mChunkedDecoder; ReleaseBlockingTransaction(); nsTArray> arrayToRelease; if (mConnection) { arrayToRelease.AppendElement(mConnection.forget()); } if (!arrayToRelease.IsEmpty()) { RefPtr r = new ReleaseOnSocketThread(std::move(arrayToRelease)); r->Dispatch(); } } nsresult nsHttpTransaction::Init( uint32_t caps, nsHttpConnectionInfo* cinfo, nsHttpRequestHead* requestHead, nsIInputStream* requestBody, uint64_t requestContentLength, bool requestBodyHasHeaders, nsIEventTarget* target, nsIInterfaceRequestor* callbacks, nsITransportEventSink* eventsink, uint64_t browserId, HttpTrafficCategory trafficCategory, nsIRequestContext* requestContext, ClassOfService classOfService, uint32_t initialRwin, bool responseTimeoutEnabled, uint64_t channelId, TransactionObserverFunc&& transactionObserver, OnPushCallback&& aOnPushCallback, HttpTransactionShell* transWithPushedStream, uint32_t aPushedStreamId) { nsresult rv; LOG1(("nsHttpTransaction::Init [this=%p caps=%x]\n", this, caps)); MOZ_ASSERT(cinfo); MOZ_ASSERT(requestHead); MOZ_ASSERT(target); MOZ_ASSERT(target->IsOnCurrentThread()); mChannelId = channelId; mTransactionObserver = std::move(transactionObserver); mOnPushCallback = std::move(aOnPushCallback); mBrowserId = browserId; mTrafficCategory = trafficCategory; LOG1(("nsHttpTransaction %p SetRequestContext %p\n", this, requestContext)); mRequestContext = requestContext; SetClassOfService(classOfService); mResponseTimeoutEnabled = responseTimeoutEnabled; mInitialRwin = initialRwin; // create transport event sink proxy. it coalesces consecutive // events of the same status type. rv = net_NewTransportEventSinkProxy(getter_AddRefs(mTransportSink), eventsink, target); if (NS_FAILED(rv)) return rv; mConnInfo = cinfo; mCallbacks = callbacks; mConsumerTarget = target; mCaps = caps; // eventsink is a nsHttpChannel when we expect "103 Early Hints" responses. // We expect it in document requests and not e.g. in TRR requests. mEarlyHintObserver = do_QueryInterface(eventsink); if (requestHead->IsHead()) { mNoContent = true; } // grab a weak reference to the request head mRequestHead = requestHead; mReqHeaderBuf = nsHttp::ConvertRequestHeadToString( *requestHead, !!requestBody, requestBodyHasHeaders, cinfo->UsingConnect()); if (LOG1_ENABLED()) { LOG1(("http request [\n")); LogHeaders(mReqHeaderBuf.get()); LOG1(("]\n")); } // report the request header if (gHttpHandler->HttpActivityDistributorActivated()) { nsCString requestBuf(mReqHeaderBuf); NS_DispatchToMainThread(NS_NewRunnableFunction( "ObserveHttpActivityWithArgs", [channelId(mChannelId), requestBuf]() { if (!gHttpHandler) { return; } gHttpHandler->ObserveHttpActivityWithArgs( HttpActivityArgs(channelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, NS_HTTP_ACTIVITY_SUBTYPE_REQUEST_HEADER, PR_Now(), 0, requestBuf); })); } // Create a string stream for the request header buf (the stream holds // a non-owning reference to the request header data, so we MUST keep // mReqHeaderBuf around). nsCOMPtr headers; rv = NS_NewByteInputStream(getter_AddRefs(headers), mReqHeaderBuf, NS_ASSIGNMENT_DEPEND); if (NS_FAILED(rv)) return rv; mHasRequestBody = !!requestBody; if (mHasRequestBody && !requestContentLength) { mHasRequestBody = false; } requestContentLength += mReqHeaderBuf.Length(); if (mHasRequestBody) { // wrap the headers and request body in a multiplexed input stream. nsCOMPtr multi; rv = nsMultiplexInputStreamConstructor(NS_GET_IID(nsIMultiplexInputStream), getter_AddRefs(multi)); if (NS_FAILED(rv)) return rv; rv = multi->AppendStream(headers); if (NS_FAILED(rv)) return rv; rv = multi->AppendStream(requestBody); if (NS_FAILED(rv)) return rv; // wrap the multiplexed input stream with a buffered input stream, so // that we write data in the largest chunks possible. this is actually // necessary to workaround some common server bugs (see bug 137155). nsCOMPtr stream(do_QueryInterface(multi)); rv = NS_NewBufferedInputStream(getter_AddRefs(mRequestStream), stream.forget(), nsIOService::gDefaultSegmentSize); if (NS_FAILED(rv)) return rv; } else { mRequestStream = headers; } nsCOMPtr throttled = do_QueryInterface(eventsink); if (throttled) { nsCOMPtr queue; rv = throttled->GetThrottleQueue(getter_AddRefs(queue)); // In case of failure, just carry on without throttling. if (NS_SUCCEEDED(rv) && queue) { nsCOMPtr wrappedStream; rv = queue->WrapStream(mRequestStream, getter_AddRefs(wrappedStream)); // Failure to throttle isn't sufficient reason to fail // initialization if (NS_SUCCEEDED(rv)) { MOZ_ASSERT(wrappedStream != nullptr); LOG( ("nsHttpTransaction::Init %p wrapping input stream using throttle " "queue %p\n", this, queue.get())); mRequestStream = wrappedStream; } } } // make sure request content-length fits within js MAX_SAFE_INTEGER mRequestSize = InScriptableRange(requestContentLength) ? static_cast(requestContentLength) : -1; // create pipe for response stream NS_NewPipe2(getter_AddRefs(mPipeIn), getter_AddRefs(mPipeOut), true, true, nsIOService::gDefaultSegmentSize, nsIOService::gDefaultSegmentCount); if (transWithPushedStream && aPushedStreamId) { RefPtr trans = transWithPushedStream->AsHttpTransaction(); MOZ_ASSERT(trans); mPushedStream = trans->TakePushedStreamById(aPushedStreamId); } bool forceUseHTTPSRR = StaticPrefs::network_dns_force_use_https_rr(); if ((gHttpHandler->UseHTTPSRRAsAltSvcEnabled() && !(mCaps & NS_HTTP_DISALLOW_HTTPS_RR)) || forceUseHTTPSRR) { nsCOMPtr target; Unused << gHttpHandler->GetSocketThreadTarget(getter_AddRefs(target)); if (target) { if (StaticPrefs::network_dns_force_waiting_https_rr() || StaticPrefs::network_dns_echconfig_enabled() || forceUseHTTPSRR) { mCaps |= NS_HTTP_FORCE_WAIT_HTTP_RR; } mResolver = new HTTPSRecordResolver(this); nsCOMPtr dnsRequest; rv = mResolver->FetchHTTPSRRInternal(target, getter_AddRefs(dnsRequest)); if (NS_SUCCEEDED(rv)) { mHTTPSSVCReceivedStage = HTTPSSVC_NOT_PRESENT; } { MutexAutoLock lock(mLock); mDNSRequest.swap(dnsRequest); if (NS_FAILED(rv)) { MakeDontWaitHTTPSRR(); } } } } RefPtr httpChannel = do_QueryObject(eventsink); RefPtr listener = httpChannel ? httpChannel->GetWebTransportSessionEventListener() : nullptr; if (listener) { mWebTransportSessionEventListener = std::move(listener); } return NS_OK; } static inline void CreateAndStartTimer(nsCOMPtr& aTimer, nsITimerCallback* aCallback, uint32_t aTimeout) { MOZ_DIAGNOSTIC_ASSERT(OnSocketThread(), "not on socket thread"); MOZ_ASSERT(!aTimer); if (!aTimeout) { return; } NS_NewTimerWithCallback(getter_AddRefs(aTimer), aCallback, aTimeout, nsITimer::TYPE_ONE_SHOT); } void nsHttpTransaction::OnPendingQueueInserted( const nsACString& aConnectionHashKey) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); { MutexAutoLock lock(mLock); mHashKeyOfConnectionEntry.Assign(aConnectionHashKey); } // Don't create mHttp3BackupTimer if HTTPS RR is in play. if (mConnInfo->IsHttp3() && !mOrigConnInfo && !mConnInfo->GetWebTransport()) { // Backup timer should only be created once. if (!mHttp3BackupTimerCreated) { CreateAndStartTimer(mHttp3BackupTimer, this, StaticPrefs::network_http_http3_backup_timer_delay()); mHttp3BackupTimerCreated = true; } } } nsresult nsHttpTransaction::AsyncRead(nsIStreamListener* listener, nsIRequest** pump) { RefPtr transactionPump; nsresult rv = nsInputStreamPump::Create(getter_AddRefs(transactionPump), mPipeIn); NS_ENSURE_SUCCESS(rv, rv); rv = transactionPump->AsyncRead(listener); NS_ENSURE_SUCCESS(rv, rv); transactionPump.forget(pump); return NS_OK; } // This method should only be used on the socket thread nsAHttpConnection* nsHttpTransaction::Connection() { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); return mConnection.get(); } void nsHttpTransaction::SetH2WSConnRefTaken() { if (!OnSocketThread()) { nsCOMPtr event = NewRunnableMethod("nsHttpTransaction::SetH2WSConnRefTaken", this, &nsHttpTransaction::SetH2WSConnRefTaken); gSocketTransportService->Dispatch(event, NS_DISPATCH_NORMAL); return; } } UniquePtr nsHttpTransaction::TakeResponseHead() { MOZ_ASSERT(!mResponseHeadTaken, "TakeResponseHead called 2x"); // Lock TakeResponseHead() against main thread MutexAutoLock lock(mLock); mResponseHeadTaken = true; // Even in OnStartRequest() the headers won't be available if we were // canceled if (!mHaveAllHeaders) { NS_WARNING("response headers not available or incomplete"); return nullptr; } return WrapUnique(std::exchange(mResponseHead, nullptr)); } UniquePtr nsHttpTransaction::TakeResponseTrailers() { MOZ_ASSERT(!mResponseTrailersTaken, "TakeResponseTrailers called 2x"); // Lock TakeResponseTrailers() against main thread MutexAutoLock lock(mLock); mResponseTrailersTaken = true; return std::move(mForTakeResponseTrailers); } void nsHttpTransaction::SetProxyConnectFailed() { mProxyConnectFailed = true; } nsHttpRequestHead* nsHttpTransaction::RequestHead() { return mRequestHead; } uint32_t nsHttpTransaction::Http1xTransactionCount() { return 1; } nsresult nsHttpTransaction::TakeSubTransactions( nsTArray>& outTransactions) { return NS_ERROR_NOT_IMPLEMENTED; } //---------------------------------------------------------------------------- // nsHttpTransaction::nsAHttpTransaction //---------------------------------------------------------------------------- void nsHttpTransaction::SetConnection(nsAHttpConnection* conn) { { MutexAutoLock lock(mLock); mConnection = conn; if (mConnection) { mIsHttp3Used = mConnection->Version() == HttpVersion::v3_0; } } } void nsHttpTransaction::OnActivated() { MOZ_ASSERT(OnSocketThread()); if (mActivated) { return; } if (mTrafficCategory != HttpTrafficCategory::eInvalid) { HttpTrafficAnalyzer* hta = gHttpHandler->GetHttpTrafficAnalyzer(); if (hta) { hta->IncrementHttpTransaction(mTrafficCategory); } if (mConnection) { mConnection->SetTrafficCategory(mTrafficCategory); } } if (mConnection && mRequestHead && mConnection->Version() >= HttpVersion::v2_0) { // So this is fun. On http/2, we want to send TE: trailers, to be // spec-compliant. So we add it to the request head here. The fun part // is that adding a header to the request head at this point has no // effect on what we send on the wire, as the headers are already // flattened (in Init()) by the time we get here. So the *real* adding // of the header happens in the h2 compression code. We still have to // add the header to the request head here, though, so that devtools can // show that we sent the header. FUN! Unused << mRequestHead->SetHeader(nsHttp::TE, "trailers"_ns); } mActivated = true; gHttpHandler->ConnMgr()->AddActiveTransaction(this); } void nsHttpTransaction::GetSecurityCallbacks(nsIInterfaceRequestor** cb) { MutexAutoLock lock(mLock); nsCOMPtr tmp(mCallbacks); tmp.forget(cb); } void nsHttpTransaction::SetSecurityCallbacks( nsIInterfaceRequestor* aCallbacks) { { MutexAutoLock lock(mLock); mCallbacks = aCallbacks; } if (gSocketTransportService) { RefPtr event = new UpdateSecurityCallbacks(this, aCallbacks); gSocketTransportService->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL); } } void nsHttpTransaction::OnTransportStatus(nsITransport* transport, nsresult status, int64_t progress) { LOG1(("nsHttpTransaction::OnSocketStatus [this=%p status=%" PRIx32 " progress=%" PRId64 "]\n", this, static_cast(status), progress)); if (status == NS_NET_STATUS_CONNECTED_TO || status == NS_NET_STATUS_WAITING_FOR) { if (mConnection) { MutexAutoLock lock(mLock); mConnection->GetSelfAddr(&mSelfAddr); mConnection->GetPeerAddr(&mPeerAddr); mResolvedByTRR = mConnection->ResolvedByTRR(); mEffectiveTRRMode = mConnection->EffectiveTRRMode(); mTRRSkipReason = mConnection->TRRSkipReason(); mEchConfigUsed = mConnection->GetEchConfigUsed(); } } // If the timing is enabled, and we are not using a persistent connection // then the requestStart timestamp will be null, so we mark the timestamps // for domainLookupStart/End and connectStart/End // If we are using a persistent connection they will remain null, // and the correct value will be returned in Performance. if (TimingEnabled() && GetRequestStart().IsNull()) { if (status == NS_NET_STATUS_RESOLVING_HOST) { SetDomainLookupStart(TimeStamp::Now(), true); } else if (status == NS_NET_STATUS_RESOLVED_HOST) { SetDomainLookupEnd(TimeStamp::Now()); } else if (status == NS_NET_STATUS_CONNECTING_TO) { SetConnectStart(TimeStamp::Now()); } else if (status == NS_NET_STATUS_CONNECTED_TO) { TimeStamp tnow = TimeStamp::Now(); SetConnectEnd(tnow, true); { MutexAutoLock lock(mLock); mTimings.tcpConnectEnd = tnow; } } else if (status == NS_NET_STATUS_TLS_HANDSHAKE_STARTING) { { MutexAutoLock lock(mLock); mTimings.secureConnectionStart = TimeStamp::Now(); } } else if (status == NS_NET_STATUS_TLS_HANDSHAKE_ENDED) { SetConnectEnd(TimeStamp::Now(), false); } else if (status == NS_NET_STATUS_SENDING_TO) { // Set the timestamp to Now(), only if it null SetRequestStart(TimeStamp::Now(), true); } } if (!mTransportSink) return; MOZ_ASSERT(OnSocketThread(), "not on socket thread"); // Need to do this before the STATUS_RECEIVING_FROM check below, to make // sure that the activity distributor gets told about all status events. // upon STATUS_WAITING_FOR; report request body sent if ((mHasRequestBody) && (status == NS_NET_STATUS_WAITING_FOR)) { gHttpHandler->ObserveHttpActivityWithArgs( HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, NS_HTTP_ACTIVITY_SUBTYPE_REQUEST_BODY_SENT, PR_Now(), 0, ""_ns); } // report the status and progress gHttpHandler->ObserveHttpActivityWithArgs( HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_SOCKET_TRANSPORT, static_cast(status), PR_Now(), progress, ""_ns); // nsHttpChannel synthesizes progress events in OnDataAvailable if (status == NS_NET_STATUS_RECEIVING_FROM) return; int64_t progressMax; if (status == NS_NET_STATUS_SENDING_TO) { // suppress progress when only writing request headers if (!mHasRequestBody) { LOG1( ("nsHttpTransaction::OnTransportStatus %p " "SENDING_TO without request body\n", this)); return; } if (mReader) { // A mRequestStream method is on the stack - wait. LOG( ("nsHttpTransaction::OnSocketStatus [this=%p] " "Skipping Re-Entrant NS_NET_STATUS_SENDING_TO\n", this)); // its ok to coalesce several of these into one deferred event mDeferredSendProgress = true; return; } nsCOMPtr tellable = do_QueryInterface(mRequestStream); if (!tellable) { LOG1( ("nsHttpTransaction::OnTransportStatus %p " "SENDING_TO without tellable request stream\n", this)); MOZ_ASSERT( !mRequestStream, "mRequestStream should be tellable as it was wrapped in " "nsBufferedInputStream, which provides the tellable interface even " "when wrapping non-tellable streams."); progress = 0; } else { int64_t prog = 0; tellable->Tell(&prog); progress = prog; } // when uploading, we include the request headers in the progress // notifications. progressMax = mRequestSize; } else { progress = 0; progressMax = 0; } mTransportSink->OnTransportStatus(transport, status, progress, progressMax); } bool nsHttpTransaction::IsDone() { return mTransactionDone; } nsresult nsHttpTransaction::Status() { return mStatus; } uint32_t nsHttpTransaction::Caps() { return mCaps & ~mCapsToClear; } void nsHttpTransaction::SetDNSWasRefreshed() { MOZ_ASSERT(mConsumerTarget->IsOnCurrentThread(), "SetDNSWasRefreshed on target thread only!"); mCapsToClear |= NS_HTTP_REFRESH_DNS; } nsresult nsHttpTransaction::ReadRequestSegment(nsIInputStream* stream, void* closure, const char* buf, uint32_t offset, uint32_t count, uint32_t* countRead) { // For the tracking of sent bytes that we used to do for the networkstats // API, please see bug 1318883 where it was removed. nsHttpTransaction* trans = (nsHttpTransaction*)closure; nsresult rv = trans->mReader->OnReadSegment(buf, count, countRead); if (NS_FAILED(rv)) { trans->MaybeRefreshSecurityInfo(); return rv; } LOG(("nsHttpTransaction::ReadRequestSegment %p read=%u", trans, *countRead)); trans->mSentData = true; return NS_OK; } nsresult nsHttpTransaction::ReadSegments(nsAHttpSegmentReader* reader, uint32_t count, uint32_t* countRead) { LOG(("nsHttpTransaction::ReadSegments %p", this)); MOZ_ASSERT(OnSocketThread(), "not on socket thread"); if (mTransactionDone) { *countRead = 0; return mStatus; } if (!m0RTTInProgress) { MaybeCancelFallbackTimer(); } if (!mConnected && !m0RTTInProgress) { mConnected = true; MaybeRefreshSecurityInfo(); } mDeferredSendProgress = false; mReader = reader; nsresult rv = mRequestStream->ReadSegments(ReadRequestSegment, this, count, countRead); mReader = nullptr; if (m0RTTInProgress && (mEarlyDataDisposition == EARLY_NONE) && NS_SUCCEEDED(rv) && (*countRead > 0)) { LOG(("mEarlyDataDisposition = EARLY_SENT")); mEarlyDataDisposition = EARLY_SENT; } if (mDeferredSendProgress && mConnection) { // to avoid using mRequestStream concurrently, OnTransportStatus() // did not report upload status off the ReadSegments() stack from // nsSocketTransport do it now. OnTransportStatus(mConnection->Transport(), NS_NET_STATUS_SENDING_TO, 0); } mDeferredSendProgress = false; if (mForceRestart) { // The forceRestart condition was dealt with on the stack, but it did not // clear the flag because nsPipe in the readsegment stack clears out // return codes, so we need to use the flag here as a cue to return // ERETARGETED if (NS_SUCCEEDED(rv)) { rv = NS_BINDING_RETARGETED; } mForceRestart = false; } // if read would block then we need to AsyncWait on the request stream. // have callback occur on socket thread so we stay synchronized. if (rv == NS_BASE_STREAM_WOULD_BLOCK) { nsCOMPtr asyncIn = do_QueryInterface(mRequestStream); if (asyncIn) { nsCOMPtr target; Unused << gHttpHandler->GetSocketThreadTarget(getter_AddRefs(target)); if (target) { asyncIn->AsyncWait(this, 0, 0, target); } else { NS_ERROR("no socket thread event target"); rv = NS_ERROR_UNEXPECTED; } } } return rv; } nsresult nsHttpTransaction::WritePipeSegment(nsIOutputStream* stream, void* closure, char* buf, uint32_t offset, uint32_t count, uint32_t* countWritten) { nsHttpTransaction* trans = (nsHttpTransaction*)closure; if (trans->mTransactionDone) return NS_BASE_STREAM_CLOSED; // stop iterating if (trans->TimingEnabled()) { // Set the timestamp to Now(), only if it null trans->SetResponseStart(TimeStamp::Now(), true); } // Bug 1153929 - add checks to fix windows crash MOZ_ASSERT(trans->mWriter); if (!trans->mWriter) { return NS_ERROR_UNEXPECTED; } nsresult rv; // // OK, now let the caller fill this segment with data. // rv = trans->mWriter->OnWriteSegment(buf, count, countWritten); if (NS_FAILED(rv)) { trans->MaybeRefreshSecurityInfo(); return rv; // caller didn't want to write anything } LOG(("nsHttpTransaction::WritePipeSegment %p written=%u", trans, *countWritten)); MOZ_ASSERT(*countWritten > 0, "bad writer"); trans->mReceivedData = true; trans->mTransferSize += *countWritten; // Let the transaction "play" with the buffer. It is free to modify // the contents of the buffer and/or modify countWritten. // - Bytes in HTTP headers don't count towards countWritten, so the input // side of pipe (aka nsHttpChannel's mTransactionPump) won't hit // OnInputStreamReady until all headers have been parsed. // rv = trans->ProcessData(buf, *countWritten, countWritten); if (NS_FAILED(rv)) trans->Close(rv); return rv; // failure code only stops WriteSegments; it is not propagated. } bool nsHttpTransaction::ShouldThrottle() { if (mClassOfServiceFlags & nsIClassOfService::DontThrottle) { // We deliberately don't touch the throttling window here since // DontThrottle requests are expected to be long-standing media // streams and would just unnecessarily block running downloads. // If we want to ballance bandwidth for media responses against // running downloads, we need to find something smarter like // changing the suspend/resume throttling intervals at-runtime. return false; } if (!gHttpHandler->ConnMgr()->ShouldThrottle(this)) { // We are not obligated to throttle return false; } if (mContentRead < 16000) { // Let the first bytes go, it may also well be all the content we get LOG(("nsHttpTransaction::ShouldThrottle too few content (%" PRIi64 ") this=%p", mContentRead, this)); return false; } if (!(mClassOfServiceFlags & nsIClassOfService::Throttleable) && gHttpHandler->ConnMgr()->IsConnEntryUnderPressure(mConnInfo)) { LOG(("nsHttpTransaction::ShouldThrottle entry pressure this=%p", this)); // This is expensive to check (two hashtable lookups) but may help // freeing connections for active tab transactions. // Checking this only for transactions that are not explicitly marked // as throttleable because trackers and (specially) downloads should // keep throttling even under pressure. return false; } return true; } void nsHttpTransaction::DontReuseConnection() { LOG(("nsHttpTransaction::DontReuseConnection %p\n", this)); if (!OnSocketThread()) { LOG(("DontReuseConnection %p not on socket thread\n", this)); nsCOMPtr event = NewRunnableMethod("nsHttpTransaction::DontReuseConnection", this, &nsHttpTransaction::DontReuseConnection); gSocketTransportService->Dispatch(event, NS_DISPATCH_NORMAL); return; } if (mConnection) { mConnection->DontReuse(); } } nsresult nsHttpTransaction::WriteSegments(nsAHttpSegmentWriter* writer, uint32_t count, uint32_t* countWritten) { LOG(("nsHttpTransaction::WriteSegments %p", this)); MOZ_ASSERT(OnSocketThread(), "not on socket thread"); if (mTransactionDone) { return NS_SUCCEEDED(mStatus) ? NS_BASE_STREAM_CLOSED : mStatus; } if (ShouldThrottle()) { if (mThrottlingReadAllowance == THROTTLE_NO_LIMIT) { // no limit set // V1: ThrottlingReadLimit() returns 0 mThrottlingReadAllowance = gHttpHandler->ThrottlingReadLimit(); } } else { mThrottlingReadAllowance = THROTTLE_NO_LIMIT; // don't limit } if (mThrottlingReadAllowance == 0) { // depleted if (gHttpHandler->ConnMgr()->CurrentBrowserId() != mBrowserId) { nsHttp::NotifyActiveTabLoadOptimization(); } // Must remember that we have to call ResumeRecv() on our connection when // called back by the conn manager to resume reading. LOG(("nsHttpTransaction::WriteSegments %p response throttled", this)); mReadingStopped = true; // This makes the underlaying connection or stream wait for explicit resume. // For h1 this means we stop reading from the socket. // For h2 this means we stop updating recv window for the stream. return NS_BASE_STREAM_WOULD_BLOCK; } mWriter = writer; if (!mPipeOut) { return NS_ERROR_UNEXPECTED; } if (mThrottlingReadAllowance > 0) { LOG(("nsHttpTransaction::WriteSegments %p limiting read from %u to %d", this, count, mThrottlingReadAllowance)); count = std::min(count, static_cast(mThrottlingReadAllowance)); } nsresult rv = mPipeOut->WriteSegments(WritePipeSegment, this, count, countWritten); mWriter = nullptr; if (mForceRestart) { // The forceRestart condition was dealt with on the stack, but it did not // clear the flag because nsPipe in the writesegment stack clears out // return codes, so we need to use the flag here as a cue to return // ERETARGETED if (NS_SUCCEEDED(rv)) { rv = NS_BINDING_RETARGETED; } mForceRestart = false; } // if pipe would block then we need to AsyncWait on it. have callback // occur on socket thread so we stay synchronized. if (rv == NS_BASE_STREAM_WOULD_BLOCK) { nsCOMPtr target; Unused << gHttpHandler->GetSocketThreadTarget(getter_AddRefs(target)); if (target) { mPipeOut->AsyncWait(this, 0, 0, target); mWaitingOnPipeOut = true; } else { NS_ERROR("no socket thread event target"); rv = NS_ERROR_UNEXPECTED; } } else if (mThrottlingReadAllowance > 0 && NS_SUCCEEDED(rv)) { MOZ_ASSERT(count >= *countWritten); mThrottlingReadAllowance -= *countWritten; } return rv; } bool nsHttpTransaction::ProxyConnectFailed() { return mProxyConnectFailed; } bool nsHttpTransaction::DataSentToChildProcess() { return false; } already_AddRefed nsHttpTransaction::SecurityInfo() { MutexAutoLock lock(mLock); return do_AddRef(mSecurityInfo); } bool nsHttpTransaction::HasStickyConnection() const { return mCaps & NS_HTTP_STICKY_CONNECTION; } bool nsHttpTransaction::ResponseIsComplete() { return mResponseIsComplete; } int64_t nsHttpTransaction::GetTransferSize() { return mTransferSize; } int64_t nsHttpTransaction::GetRequestSize() { return mRequestSize; } bool nsHttpTransaction::IsHttp3Used() { return mIsHttp3Used; } bool nsHttpTransaction::Http2Disabled() const { return mCaps & NS_HTTP_DISALLOW_SPDY; } bool nsHttpTransaction::Http3Disabled() const { return mCaps & NS_HTTP_DISALLOW_HTTP3; } already_AddRefed nsHttpTransaction::GetConnInfo() const { RefPtr connInfo = mConnInfo->Clone(); return connInfo.forget(); } already_AddRefed nsHttpTransaction::TakePushedStreamById(uint32_t aStreamId) { MOZ_ASSERT(mConsumerTarget->IsOnCurrentThread()); MOZ_ASSERT(aStreamId); auto entry = mIDToStreamMap.Lookup(aStreamId); if (entry) { RefPtr stream = entry.Data(); entry.Remove(); return stream.forget(); } return nullptr; } void nsHttpTransaction::OnPush(Http2PushedStreamWrapper* aStream) { LOG(("nsHttpTransaction::OnPush %p aStream=%p", this, aStream)); MOZ_ASSERT(aStream); MOZ_ASSERT(mOnPushCallback); MOZ_ASSERT(mConsumerTarget); RefPtr stream = aStream; if (!mConsumerTarget->IsOnCurrentThread()) { RefPtr self = this; if (NS_FAILED(mConsumerTarget->Dispatch( NS_NewRunnableFunction("nsHttpTransaction::OnPush", [self, stream]() { self->OnPush(stream); }), NS_DISPATCH_NORMAL))) { stream->OnPushFailed(); } return; } mIDToStreamMap.WithEntryHandle(stream->StreamID(), [&](auto&& entry) { MOZ_ASSERT(!entry); entry.OrInsert(stream); }); if (NS_FAILED(mOnPushCallback(stream->StreamID(), stream->GetResourceUrl(), stream->GetRequestString(), this))) { stream->OnPushFailed(); mIDToStreamMap.Remove(stream->StreamID()); } } nsHttpTransaction* nsHttpTransaction::AsHttpTransaction() { return this; } HttpTransactionParent* nsHttpTransaction::AsHttpTransactionParent() { return nullptr; } nsHttpTransaction::HTTPSSVC_CONNECTION_FAILED_REASON nsHttpTransaction::ErrorCodeToFailedReason(nsresult aErrorCode) { HTTPSSVC_CONNECTION_FAILED_REASON reason = HTTPSSVC_CONNECTION_OTHERS; switch (aErrorCode) { case NS_ERROR_UNKNOWN_HOST: reason = HTTPSSVC_CONNECTION_UNKNOWN_HOST; break; case NS_ERROR_CONNECTION_REFUSED: reason = HTTPSSVC_CONNECTION_UNREACHABLE; break; default: if (m421Received) { reason = HTTPSSVC_CONNECTION_421_RECEIVED; } else if (NS_ERROR_GET_MODULE(aErrorCode) == NS_ERROR_MODULE_SECURITY) { reason = HTTPSSVC_CONNECTION_SECURITY_ERROR; } break; } return reason; } bool nsHttpTransaction::PrepareSVCBRecordsForRetry( const nsACString& aFailedDomainName, const nsACString& aFailedAlpn, bool& aAllRecordsHaveEchConfig) { MOZ_ASSERT(mRecordsForRetry.IsEmpty()); if (!mHTTPSSVCRecord) { return false; } // If we already failed to connect with h3, don't select records that supports // h3. bool noHttp3 = mCaps & NS_HTTP_DISALLOW_HTTP3; bool unused; nsTArray> records; Unused << mHTTPSSVCRecord->GetAllRecordsWithEchConfig( mCaps & NS_HTTP_DISALLOW_SPDY, noHttp3, &aAllRecordsHaveEchConfig, &unused, records); // Note that it's possible that we can't get any usable record here. For // example, when http3 connection is failed, we won't select records with // http3 alpn. // If not all records have echConfig, we'll directly fallback to the origin // server. if (!aAllRecordsHaveEchConfig) { return false; } // Take the records behind the failed one and put them into mRecordsForRetry. for (const auto& record : records) { nsAutoCString name; record->GetName(name); nsAutoCString alpn; nsresult rv = record->GetSelectedAlpn(alpn); if (name == aFailedDomainName) { // If the record has no alpn or the alpn is already tried, we skip this // record. if (NS_FAILED(rv) || alpn == aFailedAlpn) { continue; } } mRecordsForRetry.InsertElementAt(0, record); } // Set mHTTPSSVCRecord to null to avoid this function being executed twice. mHTTPSSVCRecord = nullptr; return !mRecordsForRetry.IsEmpty(); } already_AddRefed nsHttpTransaction::PrepareFastFallbackConnInfo(bool aEchConfigUsed) { MOZ_ASSERT(mHTTPSSVCRecord && mOrigConnInfo); RefPtr fallbackConnInfo; nsCOMPtr fastFallbackRecord; Unused << mHTTPSSVCRecord->GetServiceModeRecord( mCaps & NS_HTTP_DISALLOW_SPDY, true, getter_AddRefs(fastFallbackRecord)); if (fastFallbackRecord && aEchConfigUsed) { nsAutoCString echConfig; Unused << fastFallbackRecord->GetEchConfig(echConfig); if (echConfig.IsEmpty()) { fastFallbackRecord = nullptr; } } if (!fastFallbackRecord) { if (aEchConfigUsed) { LOG( ("nsHttpTransaction::PrepareFastFallbackConnInfo [this=%p] no record " "can be used", this)); return nullptr; } if (mOrigConnInfo->IsHttp3()) { mOrigConnInfo->CloneAsDirectRoute(getter_AddRefs(fallbackConnInfo)); } else { fallbackConnInfo = mOrigConnInfo; } return fallbackConnInfo.forget(); } fallbackConnInfo = mOrigConnInfo->CloneAndAdoptHTTPSSVCRecord(fastFallbackRecord); return fallbackConnInfo.forget(); } void nsHttpTransaction::PrepareConnInfoForRetry(nsresult aReason) { LOG(("nsHttpTransaction::PrepareConnInfoForRetry [this=%p reason=%" PRIx32 "]", this, static_cast(aReason))); RefPtr failedConnInfo = mConnInfo->Clone(); mConnInfo = nullptr; bool echConfigUsed = gHttpHandler->EchConfigEnabled(failedConnInfo->IsHttp3()) && !failedConnInfo->GetEchConfig().IsEmpty(); if (mFastFallbackTriggered) { mFastFallbackTriggered = false; MOZ_ASSERT(mBackupConnInfo); mConnInfo.swap(mBackupConnInfo); return; } auto useOrigConnInfoToRetry = [&]() { mOrigConnInfo.swap(mConnInfo); if (mConnInfo->IsHttp3() && ((mCaps & NS_HTTP_DISALLOW_HTTP3) || gHttpHandler->IsHttp3Excluded(mConnInfo->GetRoutedHost().IsEmpty() ? mConnInfo->GetOrigin() : mConnInfo->GetRoutedHost()))) { RefPtr ci; mConnInfo->CloneAsDirectRoute(getter_AddRefs(ci)); mConnInfo = ci; } }; if (!echConfigUsed) { LOG((" echConfig is not used, fallback to origin conn info")); useOrigConnInfoToRetry(); return; } Telemetry::HistogramID id = Telemetry::TRANSACTION_ECH_RETRY_OTHERS_COUNT; auto updateCount = MakeScopeExit([&] { auto entry = mEchRetryCounterMap.Lookup(id); MOZ_ASSERT(entry, "table not initialized"); if (entry) { *entry += 1; } }); if (aReason == psm::GetXPCOMFromNSSError(SSL_ERROR_ECH_RETRY_WITHOUT_ECH)) { LOG((" Got SSL_ERROR_ECH_RETRY_WITHOUT_ECH, use empty echConfig to retry")); failedConnInfo->SetEchConfig(EmptyCString()); failedConnInfo.swap(mConnInfo); id = Telemetry::TRANSACTION_ECH_RETRY_WITHOUT_ECH_COUNT; return; } if (aReason == psm::GetXPCOMFromNSSError(SSL_ERROR_ECH_RETRY_WITH_ECH)) { LOG((" Got SSL_ERROR_ECH_RETRY_WITH_ECH, use retry echConfig")); MOZ_ASSERT(mConnection); nsCOMPtr socketControl; if (mConnection) { mConnection->GetTLSSocketControl(getter_AddRefs(socketControl)); } MOZ_ASSERT(socketControl); nsAutoCString retryEchConfig; if (socketControl && NS_SUCCEEDED(socketControl->GetRetryEchConfig(retryEchConfig))) { MOZ_ASSERT(!retryEchConfig.IsEmpty()); failedConnInfo->SetEchConfig(retryEchConfig); failedConnInfo.swap(mConnInfo); } id = Telemetry::TRANSACTION_ECH_RETRY_WITH_ECH_COUNT; return; } // Note that we retry the connection not only for SSL_ERROR_ECH_FAILED, but // also for all failure cases. if (aReason == psm::GetXPCOMFromNSSError(SSL_ERROR_ECH_FAILED) || NS_FAILED(aReason)) { LOG((" Got SSL_ERROR_ECH_FAILED, try other records")); if (aReason == psm::GetXPCOMFromNSSError(SSL_ERROR_ECH_FAILED)) { id = Telemetry::TRANSACTION_ECH_RETRY_ECH_FAILED_COUNT; } if (mRecordsForRetry.IsEmpty()) { if (mHTTPSSVCRecord) { bool allRecordsHaveEchConfig = true; if (!PrepareSVCBRecordsForRetry(failedConnInfo->GetRoutedHost(), failedConnInfo->GetNPNToken(), allRecordsHaveEchConfig)) { LOG( (" Can't find other records with echConfig, " "allRecordsHaveEchConfig=%d", allRecordsHaveEchConfig)); if (gHttpHandler->FallbackToOriginIfConfigsAreECHAndAllFailed() || !allRecordsHaveEchConfig) { useOrigConnInfoToRetry(); } return; } } else { LOG((" No available records to retry")); if (gHttpHandler->FallbackToOriginIfConfigsAreECHAndAllFailed()) { useOrigConnInfoToRetry(); } return; } } if (LOG5_ENABLED()) { LOG(("SvcDomainName to retry: [")); for (const auto& r : mRecordsForRetry) { nsAutoCString name; r->GetName(name); nsAutoCString alpn; r->GetSelectedAlpn(alpn); LOG((" name=%s alpn=%s", name.get(), alpn.get())); } LOG(("]")); } RefPtr recordsForRetry = mRecordsForRetry.PopLastElement().forget(); mConnInfo = mOrigConnInfo->CloneAndAdoptHTTPSSVCRecord(recordsForRetry); } } void nsHttpTransaction::MaybeReportFailedSVCDomain( nsresult aReason, nsHttpConnectionInfo* aFailedConnInfo) { if (aReason == psm::GetXPCOMFromNSSError(SSL_ERROR_ECH_RETRY_WITHOUT_ECH) || aReason != psm::GetXPCOMFromNSSError(SSL_ERROR_ECH_RETRY_WITH_ECH)) { return; } Telemetry::Accumulate(Telemetry::DNS_HTTPSSVC_CONNECTION_FAILED_REASON, ErrorCodeToFailedReason(aReason)); nsCOMPtr dns = do_GetService(NS_DNSSERVICE_CONTRACTID); if (dns) { const nsCString& failedHost = aFailedConnInfo->GetRoutedHost().IsEmpty() ? aFailedConnInfo->GetOrigin() : aFailedConnInfo->GetRoutedHost(); LOG(("add failed domain name [%s] -> [%s] to exclusion list", aFailedConnInfo->GetOrigin().get(), failedHost.get())); Unused << dns->ReportFailedSVCDomainName(aFailedConnInfo->GetOrigin(), failedHost); } } bool nsHttpTransaction::ShouldRestartOn0RttError(nsresult reason) { LOG( ("nsHttpTransaction::ShouldRestartOn0RttError [this=%p, " "mEarlyDataWasAvailable=%d error=%" PRIx32 "]\n", this, mEarlyDataWasAvailable, static_cast(reason))); return StaticPrefs::network_http_early_data_disable_on_error() && mEarlyDataWasAvailable && SecurityErrorThatMayNeedRestart(reason); } static void MaybeRemoveSSLToken(nsITransportSecurityInfo* aSecurityInfo) { if (!StaticPrefs:: network_http_remove_resumption_token_when_early_data_failed()) { return; } if (!aSecurityInfo) { return; } nsAutoCString key; aSecurityInfo->GetPeerId(key); nsresult rv = SSLTokensCache::RemoveAll(key); LOG(("RemoveSSLToken [key=%s, rv=%" PRIx32 "]", key.get(), static_cast(rv))); } void nsHttpTransaction::Close(nsresult reason) { LOG(("nsHttpTransaction::Close [this=%p reason=%" PRIx32 "]\n", this, static_cast(reason))); { MutexAutoLock lock(mLock); mEarlyHintObserver = nullptr; mWebTransportSessionEventListener = nullptr; } if (!mClosed) { gHttpHandler->ConnMgr()->RemoveActiveTransaction(this); mActivated = false; } if (mDNSRequest) { mDNSRequest->Cancel(NS_ERROR_ABORT); mDNSRequest = nullptr; } MaybeCancelFallbackTimer(); MOZ_ASSERT(OnSocketThread(), "not on socket thread"); if (reason == NS_BINDING_RETARGETED) { LOG((" close %p skipped due to ERETARGETED\n", this)); return; } if (mClosed) { LOG((" already closed\n")); return; } NotifyTransactionObserver(reason); if (mTokenBucketCancel) { mTokenBucketCancel->Cancel(reason); mTokenBucketCancel = nullptr; } // report the reponse is complete if not already reported if (!mResponseIsComplete) { gHttpHandler->ObserveHttpActivityWithArgs( HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, NS_HTTP_ACTIVITY_SUBTYPE_RESPONSE_COMPLETE, PR_Now(), static_cast(mContentRead), ""_ns); } // report that this transaction is closing gHttpHandler->ObserveHttpActivityWithArgs( HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, NS_HTTP_ACTIVITY_SUBTYPE_TRANSACTION_CLOSE, PR_Now(), 0, ""_ns); // we must no longer reference the connection! find out if the // connection was being reused before letting it go. bool connReused = false; bool isHttp2or3 = false; if (mConnection) { connReused = mConnection->IsReused(); isHttp2or3 = mConnection->Version() >= HttpVersion::v2_0; if (!mConnected) { MaybeRefreshSecurityInfo(); } } mConnected = false; bool shouldRestartTransactionForHTTPSRR = mOrigConnInfo && AllowedErrorForHTTPSRRFallback(reason); // // if the connection was reset or closed before we wrote any part of the // request or if we wrote the request but didn't receive any part of the // response and the connection was being reused, then we can (and really // should) assume that we wrote to a stale connection and we must therefore // repeat the request over a new connection. // // We have decided to retry not only in case of the reused connections, but // all safe methods(bug 1236277). // // NOTE: the conditions under which we will automatically retry the HTTP // request have to be carefully selected to avoid duplication of the // request from the point-of-view of the server. such duplication could // have dire consequences including repeated purchases, etc. // // NOTE: because of the way SSL proxy CONNECT is implemented, it is // possible that the transaction may have received data without having // sent any data. for this reason, mSendData == FALSE does not imply // mReceivedData == FALSE. (see bug 203057 for more info.) // // Never restart transactions that are marked as sticky to their conenction. // We use that capability to identify transactions bound to connection based // authentication. Reissuing them on a different connections will break // this bondage. Major issue may arise when there is an NTLM message auth // header on the transaction and we send it to a different NTLM authenticated // connection. It will break that connection and also confuse the channel's // auth provider, beliving the cached credentials are wrong and asking for // the password mistakenly again from the user. if ((reason == NS_ERROR_NET_RESET || reason == NS_OK || reason == psm::GetXPCOMFromNSSError(SSL_ERROR_DOWNGRADE_WITH_EARLY_DATA) || ShouldRestartOn0RttError(reason) || shouldRestartTransactionForHTTPSRR) && (!(mCaps & NS_HTTP_STICKY_CONNECTION) || (mCaps & NS_HTTP_CONNECTION_RESTARTABLE) || (mEarlyDataDisposition == EARLY_425))) { if (mForceRestart) { SetRestartReason(TRANSACTION_RESTART_FORCED); if (NS_SUCCEEDED(Restart())) { if (mResponseHead) { mResponseHead->Reset(); } mContentRead = 0; mContentLength = -1; delete mChunkedDecoder; mChunkedDecoder = nullptr; mHaveStatusLine = false; mHaveAllHeaders = false; mHttpResponseMatched = false; mResponseIsComplete = false; mDidContentStart = false; mNoContent = false; mSentData = false; mReceivedData = false; mSupportsHTTP3 = false; LOG(("transaction force restarted\n")); return; } } mDoNotTryEarlyData = true; // reallySentData is meant to separate the instances where data has // been sent by this transaction but buffered at a higher level while // a TLS session (perhaps via a tunnel) is setup. bool reallySentData = mSentData && (!mConnection || mConnection->BytesWritten()); // If this is true, it means we failed to use the HTTPSSVC connection info // to connect to the server. We need to retry with the original connection // info. shouldRestartTransactionForHTTPSRR &= !reallySentData; if (reason == psm::GetXPCOMFromNSSError(SSL_ERROR_DOWNGRADE_WITH_EARLY_DATA) || reason == psm::GetXPCOMFromNSSError(SSL_ERROR_PROTOCOL_VERSION_ALERT) || (!mReceivedData && ((mRequestHead && mRequestHead->IsSafeMethod()) || !reallySentData || connReused)) || shouldRestartTransactionForHTTPSRR) { if (shouldRestartTransactionForHTTPSRR) { MaybeReportFailedSVCDomain(reason, mConnInfo); PrepareConnInfoForRetry(reason); mDontRetryWithDirectRoute = true; LOG( ("transaction will be restarted with the fallback connection info " "key=%s", mConnInfo ? mConnInfo->HashKey().get() : "None")); } if (shouldRestartTransactionForHTTPSRR) { auto toRestartReason = [](nsresult aStatus) -> TRANSACTION_RESTART_REASON { if (aStatus == NS_ERROR_NET_RESET) { return TRANSACTION_RESTART_HTTPS_RR_NET_RESET; } if (aStatus == NS_ERROR_CONNECTION_REFUSED) { return TRANSACTION_RESTART_HTTPS_RR_CONNECTION_REFUSED; } if (aStatus == NS_ERROR_UNKNOWN_HOST) { return TRANSACTION_RESTART_HTTPS_RR_UNKNOWN_HOST; } if (aStatus == NS_ERROR_NET_TIMEOUT) { return TRANSACTION_RESTART_HTTPS_RR_NET_TIMEOUT; } if (psm::IsNSSErrorCode(-1 * NS_ERROR_GET_CODE(aStatus))) { return TRANSACTION_RESTART_HTTPS_RR_SEC_ERROR; } MOZ_ASSERT_UNREACHABLE("Unexpected reason"); return TRANSACTION_RESTART_OTHERS; }; SetRestartReason(toRestartReason(reason)); } else if (!reallySentData) { SetRestartReason(TRANSACTION_RESTART_NO_DATA_SENT); } else if (reason == psm::GetXPCOMFromNSSError( SSL_ERROR_DOWNGRADE_WITH_EARLY_DATA)) { SetRestartReason(TRANSACTION_RESTART_DOWNGRADE_WITH_EARLY_DATA); } else if (reason == psm::GetXPCOMFromNSSError(SSL_ERROR_PROTOCOL_VERSION_ALERT)) { SetRestartReason(TRANSACTION_RESTART_PROTOCOL_VERSION_ALERT); } // if restarting fails, then we must proceed to close the pipe, // which will notify the channel that the transaction failed. // Note that when echConfig is enabled, it's possible that we don't have a // usable connection info to retry. if (mConnInfo && NS_SUCCEEDED(Restart())) { return; } // mConnInfo could be set to null in PrepareConnInfoForRetry() when we // can't find an available https rr to retry. We have to set mConnInfo // back to mOrigConnInfo to make sure no crash when mConnInfo being // accessed again. if (!mConnInfo) { mConnInfo.swap(mOrigConnInfo); MOZ_ASSERT(mConnInfo); } } } Telemetry::Accumulate(Telemetry::HTTP_TRANSACTION_RESTART_REASON, mRestartReason); if (!mResponseIsComplete && NS_SUCCEEDED(reason) && isHttp2or3) { // Responses without content-length header field are still complete if // they are transfered over http2 or http3 and the stream is properly // closed. mResponseIsComplete = true; } if ((mChunkedDecoder || (mContentLength >= int64_t(0))) && (NS_SUCCEEDED(reason) && !mResponseIsComplete)) { NS_WARNING("Partial transfer, incomplete HTTP response received"); if ((mHttpResponseCode / 100 == 2) && (mHttpVersion >= HttpVersion::v1_1)) { FrameCheckLevel clevel = gHttpHandler->GetEnforceH1Framing(); if (clevel >= FRAMECHECK_BARELY) { // If clevel == FRAMECHECK_STRICT mark any incomplete response as // partial. // if clevel == FRAMECHECK_BARELY: 1) mark a chunked-encoded response // that do not ends on exactly a chunk boundary as partial; We are not // strict about the last 0-size chunk and do not mark as parial // responses that do not have the last 0-size chunk but do end on a // chunk boundary. (check mChunkedDecoder->GetChunkRemaining() != 0) // 2) mark a transfer that is partial and it is not chunk-encoded or // gzip-encoded or other content-encoding as partial. (check // !mChunkedDecoder && !mContentDecoding && mContentDecodingCheck)) // if clevel == FRAMECHECK_STRICT_CHUNKED mark a chunked-encoded // response that ends on exactly a chunk boundary also as partial. // Here a response must have the last 0-size chunk. if ((clevel == FRAMECHECK_STRICT) || (mChunkedDecoder && (mChunkedDecoder->GetChunkRemaining() || (clevel == FRAMECHECK_STRICT_CHUNKED))) || (!mChunkedDecoder && !mContentDecoding && mContentDecodingCheck)) { reason = NS_ERROR_NET_PARTIAL_TRANSFER; LOG(("Partial transfer, incomplete HTTP response received: %s", mChunkedDecoder ? "broken chunk" : "c-l underrun")); } } } if (mConnection) { // whether or not we generate an error for the transaction // bad framing means we don't want a pconn mConnection->DontReuse(); } } bool relConn = true; if (NS_SUCCEEDED(reason)) { // the server has not sent the final \r\n terminating the header // section, and there may still be a header line unparsed. let's make // sure we parse the remaining header line, and then hopefully, the // response will be usable (see bug 88792). if (!mHaveAllHeaders) { char data[] = "\n\n"; uint32_t unused = 0; // If we have a partial line already, we actually need two \ns to finish // the headers section. Unused << ParseHead(data, mLineBuf.IsEmpty() ? 1 : 2, &unused); if (mResponseHead->Version() == HttpVersion::v0_9) { // Reject 0 byte HTTP/0.9 Responses - bug 423506 LOG(("nsHttpTransaction::Close %p 0 Byte 0.9 Response", this)); reason = NS_ERROR_NET_RESET; } } // honor the sticky connection flag... if (mCaps & NS_HTTP_STICKY_CONNECTION) { LOG((" keeping the connection because of STICKY_CONNECTION flag")); relConn = false; } // if the proxy connection has failed, we want the connection be held // to allow the upper layers (think nsHttpChannel) to close it when // the failure is unrecoverable. // we can't just close it here, because mProxyConnectFailed is to a general // flag and is also set for e.g. 407 which doesn't mean to kill the // connection, specifically when connection oriented auth may be involved. if (mProxyConnectFailed) { LOG((" keeping the connection because of mProxyConnectFailed")); relConn = false; } // Use mOrigConnInfo as an indicator that this transaction is completed // successfully with an HTTPSSVC record. if (mOrigConnInfo) { Telemetry::Accumulate(Telemetry::DNS_HTTPSSVC_CONNECTION_FAILED_REASON, HTTPSSVC_CONNECTION_OK); } } // mTimings.responseEnd is normally recorded based on the end of a // HTTP delimiter such as chunked-encodings or content-length. However, // EOF or an error still require an end time be recorded. if (TimingEnabled()) { const TimingStruct timings = Timings(); if (timings.responseEnd.IsNull() && !timings.responseStart.IsNull()) { SetResponseEnd(TimeStamp::Now()); } } if (mTrafficCategory != HttpTrafficCategory::eInvalid) { HttpTrafficAnalyzer* hta = gHttpHandler->GetHttpTrafficAnalyzer(); if (hta) { hta->AccumulateHttpTransferredSize(mTrafficCategory, mTransferSize, mContentRead); } } if (mThroughCaptivePortal) { Telemetry::ScalarAdd( Telemetry::ScalarID::NETWORKING_HTTP_TRANSACTIONS_CAPTIVE_PORTAL, 1); } if (relConn && mConnection) { MutexAutoLock lock(mLock); mConnection = nullptr; } if (isHttp2or3 && reason == psm::GetXPCOMFromNSSError(SSL_ERROR_PROTOCOL_VERSION_ALERT)) { // Change reason to NS_ERROR_ABORT, so we avoid showing a missleading // error page tthat TLS1.0 is disabled. H2 or H3 is used here so the // TLS version is not a problem. reason = NS_ERROR_ABORT; } mStatus = reason; mTransactionDone = true; // forcibly flag the transaction as complete mClosed = true; if (mResolver) { mResolver->Close(); mResolver = nullptr; } ReleaseBlockingTransaction(); // release some resources that we no longer need mRequestStream = nullptr; mReqHeaderBuf.Truncate(); mLineBuf.Truncate(); if (mChunkedDecoder) { delete mChunkedDecoder; mChunkedDecoder = nullptr; } for (const auto& entry : mEchRetryCounterMap) { Telemetry::Accumulate(static_cast(entry.GetKey()), entry.GetData()); } // closing this pipe triggers the channel's OnStopRequest method. mPipeOut->CloseWithStatus(reason); } nsHttpConnectionInfo* nsHttpTransaction::ConnectionInfo() { return mConnInfo.get(); } bool // NOTE BASE CLASS nsAHttpTransaction::ResponseTimeoutEnabled() const { return false; } PRIntervalTime // NOTE BASE CLASS nsAHttpTransaction::ResponseTimeout() { return gHttpHandler->ResponseTimeout(); } bool nsHttpTransaction::ResponseTimeoutEnabled() const { return mResponseTimeoutEnabled; } //----------------------------------------------------------------------------- // nsHttpTransaction //----------------------------------------------------------------------------- static inline void RemoveAlternateServiceUsedHeader( nsHttpRequestHead* aRequestHead) { if (aRequestHead) { DebugOnly rv = aRequestHead->SetHeader(nsHttp::Alternate_Service_Used, "0"_ns); MOZ_ASSERT(NS_SUCCEEDED(rv)); } } void nsHttpTransaction::SetRestartReason(TRANSACTION_RESTART_REASON aReason) { if (mRestartReason == TRANSACTION_RESTART_NONE) { mRestartReason = aReason; } } nsresult nsHttpTransaction::Restart() { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); // limit the number of restart attempts - bug 92224 if (++mRestartCount >= gHttpHandler->MaxRequestAttempts()) { LOG(("reached max request attempts, failing transaction @%p\n", this)); return NS_ERROR_NET_RESET; } LOG(("restarting transaction @%p\n", this)); if (mRequestHead) { // Dispatching on a new connection better w/o an ambient connection proxy // auth request header to not confuse the proxy authenticator. nsAutoCString proxyAuth; if (NS_SUCCEEDED( mRequestHead->GetHeader(nsHttp::Proxy_Authorization, proxyAuth)) && IsStickyAuthSchemeAt(proxyAuth)) { Unused << mRequestHead->ClearHeader(nsHttp::Proxy_Authorization); } } // rewind streams in case we already wrote out the request nsCOMPtr seekable = do_QueryInterface(mRequestStream); if (seekable) seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0); if (mDoNotTryEarlyData) { MutexAutoLock lock(mLock); MaybeRemoveSSLToken(mSecurityInfo); } // clear old connection state... { MutexAutoLock lock(mLock); mSecurityInfo = nullptr; } if (mConnection) { if (!mReuseOnRestart) { mConnection->DontReuse(); } MutexAutoLock lock(mLock); mConnection = nullptr; } // Reset this to our default state, since this may change from one restart // to the next mReuseOnRestart = false; if (!mDoNotRemoveAltSvc && (!mConnInfo->GetRoutedHost().IsEmpty() || mConnInfo->IsHttp3()) && !mDontRetryWithDirectRoute) { RefPtr ci; mConnInfo->CloneAsDirectRoute(getter_AddRefs(ci)); mConnInfo = ci; RemoveAlternateServiceUsedHeader(mRequestHead); } // Reset mDoNotRemoveAltSvc for the next try. mDoNotRemoveAltSvc = false; mEarlyDataWasAvailable = false; mRestarted = true; // If we weren't trying to do 'proper' ECH, disable ECH GREASE when retrying. if (mConnInfo->GetEchConfig().IsEmpty() && StaticPrefs::security_tls_ech_disable_grease_on_fallback()) { mCaps |= NS_HTTP_DISALLOW_ECH; } mCaps |= NS_HTTP_IS_RETRY; // Use TRANSACTION_RESTART_OTHERS as a catch-all. SetRestartReason(TRANSACTION_RESTART_OTHERS); // Reset the IP family preferences, so the new connection can try to use // another IPv4 or IPv6 address. gHttpHandler->ConnMgr()->ResetIPFamilyPreference(mConnInfo); return gHttpHandler->InitiateTransaction(this, mPriority); } bool nsHttpTransaction::TakeRestartedState() { // This return true if the transaction has been restarted internally. Used to // let the consuming nsHttpChannel reset proxy authentication. The flag is // reset to false by this method. return mRestarted.exchange(false); } char* nsHttpTransaction::LocateHttpStart(char* buf, uint32_t len, bool aAllowPartialMatch) { MOZ_ASSERT(!aAllowPartialMatch || mLineBuf.IsEmpty()); static const char HTTPHeader[] = "HTTP/1."; static const uint32_t HTTPHeaderLen = sizeof(HTTPHeader) - 1; static const char HTTP2Header[] = "HTTP/2"; static const uint32_t HTTP2HeaderLen = sizeof(HTTP2Header) - 1; static const char HTTP3Header[] = "HTTP/3"; static const uint32_t HTTP3HeaderLen = sizeof(HTTP3Header) - 1; // ShoutCast ICY is treated as HTTP/1.0 static const char ICYHeader[] = "ICY "; static const uint32_t ICYHeaderLen = sizeof(ICYHeader) - 1; if (aAllowPartialMatch && (len < HTTPHeaderLen)) { return (nsCRT::strncasecmp(buf, HTTPHeader, len) == 0) ? buf : nullptr; } // mLineBuf can contain partial match from previous search if (!mLineBuf.IsEmpty()) { MOZ_ASSERT(mLineBuf.Length() < HTTPHeaderLen); int32_t checkChars = std::min(len, HTTPHeaderLen - mLineBuf.Length()); if (nsCRT::strncasecmp(buf, HTTPHeader + mLineBuf.Length(), checkChars) == 0) { mLineBuf.Append(buf, checkChars); if (mLineBuf.Length() == HTTPHeaderLen) { // We've found whole HTTPHeader sequence. Return pointer at the // end of matched sequence since it is stored in mLineBuf. return (buf + checkChars); } // Response matches pattern but is still incomplete. return nullptr; } // Previous partial match together with new data doesn't match the // pattern. Start the search again. mLineBuf.Truncate(); } bool firstByte = true; while (len > 0) { if (nsCRT::strncasecmp(buf, HTTPHeader, std::min(len, HTTPHeaderLen)) == 0) { if (len < HTTPHeaderLen) { // partial HTTPHeader sequence found // save partial match to mLineBuf mLineBuf.Assign(buf, len); return nullptr; } // whole HTTPHeader sequence found return buf; } // At least "SmarterTools/2.0.3974.16813" generates nonsensical // HTTP/2.0 responses to our HTTP/1 requests. Treat the minimal case of // it as HTTP/1.1 to be compatible with old versions of ourselves and // other browsers if (firstByte && !mInvalidResponseBytesRead && len >= HTTP2HeaderLen && (nsCRT::strncasecmp(buf, HTTP2Header, HTTP2HeaderLen) == 0)) { LOG(("nsHttpTransaction:: Identified HTTP/2.0 treating as 1.x\n")); return buf; } // HTTP/3.0 responses to our HTTP/1 requests. Treat the minimal case of // it as HTTP/1.1 to be compatible with old versions of ourselves and // other browsers if (firstByte && !mInvalidResponseBytesRead && len >= HTTP3HeaderLen && (nsCRT::strncasecmp(buf, HTTP3Header, HTTP3HeaderLen) == 0)) { LOG(("nsHttpTransaction:: Identified HTTP/3.0 treating as 1.x\n")); return buf; } // Treat ICY (AOL/Nullsoft ShoutCast) non-standard header in same fashion // as HTTP/2.0 is treated above. This will allow "ICY " to be interpretted // as HTTP/1.0 in nsHttpResponseHead::ParseVersion if (firstByte && !mInvalidResponseBytesRead && len >= ICYHeaderLen && (nsCRT::strncasecmp(buf, ICYHeader, ICYHeaderLen) == 0)) { LOG(("nsHttpTransaction:: Identified ICY treating as HTTP/1.0\n")); return buf; } if (!nsCRT::IsAsciiSpace(*buf)) firstByte = false; buf++; len--; } return nullptr; } nsresult nsHttpTransaction::ParseLine(nsACString& line) { LOG1(("nsHttpTransaction::ParseLine [%s]\n", PromiseFlatCString(line).get())); nsresult rv = NS_OK; if (!mHaveStatusLine) { mResponseHead->ParseStatusLine(line); mHaveStatusLine = true; // XXX this should probably never happen if (mResponseHead->Version() == HttpVersion::v0_9) mHaveAllHeaders = true; } else { rv = mResponseHead->ParseHeaderLine(line); } return rv; } nsresult nsHttpTransaction::ParseLineSegment(char* segment, uint32_t len) { MOZ_ASSERT(!mHaveAllHeaders, "already have all headers"); if (!mLineBuf.IsEmpty() && mLineBuf.Last() == '\n') { // trim off the new line char, and if this segment is // not a continuation of the previous or if we haven't // parsed the status line yet, then parse the contents // of mLineBuf. mLineBuf.Truncate(mLineBuf.Length() - 1); if (!mHaveStatusLine || (*segment != ' ' && *segment != '\t')) { nsresult rv = ParseLine(mLineBuf); mLineBuf.Truncate(); if (NS_FAILED(rv)) { return rv; } } } // append segment to mLineBuf... mLineBuf.Append(segment, len); // a line buf with only a new line char signifies the end of headers. if (mLineBuf.First() == '\n') { mLineBuf.Truncate(); // discard this response if it is a 100 continue or other 1xx status. uint16_t status = mResponseHead->Status(); if (status == 103) { nsCString linkHeader; nsresult rv = mResponseHead->GetHeader(nsHttp::Link, linkHeader); nsCString referrerPolicy; Unused << mResponseHead->GetHeader(nsHttp::Referrer_Policy, referrerPolicy); if (NS_SUCCEEDED(rv) && !linkHeader.IsEmpty()) { nsCString cspHeader; Unused << mResponseHead->GetHeader(nsHttp::Content_Security_Policy, cspHeader); nsCOMPtr earlyHint; { MutexAutoLock lock(mLock); earlyHint = mEarlyHintObserver; } if (earlyHint) { DebugOnly rv = NS_DispatchToMainThread( NS_NewRunnableFunction( "nsIEarlyHintObserver->EarlyHint", [obs{std::move(earlyHint)}, header{std::move(linkHeader)}, referrerPolicy{std::move(referrerPolicy)}, cspHeader{std::move(cspHeader)}]() { obs->EarlyHint(header, referrerPolicy, cspHeader); }), NS_DISPATCH_NORMAL); MOZ_ASSERT(NS_SUCCEEDED(rv)); } } } if ((status != 101) && (status / 100 == 1)) { LOG(("ignoring 1xx response except 101 and 103\n")); mHaveStatusLine = false; mHttpResponseMatched = false; mConnection->SetLastTransactionExpectedNoContent(true); mResponseHead->Reset(); return NS_OK; } if (!mConnection->IsProxyConnectInProgress()) { MutexAutoLock lock(mLock); mEarlyHintObserver = nullptr; } mHaveAllHeaders = true; } return NS_OK; } nsresult nsHttpTransaction::ParseHead(char* buf, uint32_t count, uint32_t* countRead) { nsresult rv; uint32_t len; char* eol; LOG(("nsHttpTransaction::ParseHead [count=%u]\n", count)); *countRead = 0; MOZ_ASSERT(!mHaveAllHeaders, "oops"); // allocate the response head object if necessary if (!mResponseHead) { mResponseHead = new nsHttpResponseHead(); if (!mResponseHead) return NS_ERROR_OUT_OF_MEMORY; // report that we have a least some of the response if (!mReportedStart) { mReportedStart = true; gHttpHandler->ObserveHttpActivityWithArgs( HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, NS_HTTP_ACTIVITY_SUBTYPE_RESPONSE_START, PR_Now(), 0, ""_ns); } } if (!mHttpResponseMatched) { // Normally we insist on seeing HTTP/1.x in the first few bytes, // but if we are on a persistent connection and the previous transaction // was not supposed to have any content then we need to be prepared // to skip over a response body that the server may have sent even // though it wasn't allowed. if (!mConnection || !mConnection->LastTransactionExpectedNoContent()) { // tolerate only minor junk before the status line mHttpResponseMatched = true; char* p = LocateHttpStart(buf, std::min(count, 11), true); if (!p) { // Treat any 0.9 style response of a put as a failure. if (mRequestHead->IsPut()) return NS_ERROR_ABORT; mResponseHead->ParseStatusLine(""_ns); mHaveStatusLine = true; mHaveAllHeaders = true; return NS_OK; } if (p > buf) { // skip over the junk mInvalidResponseBytesRead += p - buf; *countRead = p - buf; buf = p; } } else { char* p = LocateHttpStart(buf, count, false); if (p) { mInvalidResponseBytesRead += p - buf; *countRead = p - buf; buf = p; mHttpResponseMatched = true; } else { mInvalidResponseBytesRead += count; *countRead = count; if (mInvalidResponseBytesRead > MAX_INVALID_RESPONSE_BODY_SIZE) { LOG( ("nsHttpTransaction::ParseHead() " "Cannot find Response Header\n")); // cannot go back and call this 0.9 anymore as we // have thrown away a lot of the leading junk return NS_ERROR_ABORT; } return NS_OK; } } } // otherwise we can assume that we don't have a HTTP/0.9 response. MOZ_ASSERT(mHttpResponseMatched); while ((eol = static_cast(memchr(buf, '\n', count - *countRead))) != nullptr) { // found line in range [buf:eol] len = eol - buf + 1; *countRead += len; // actually, the line is in the range [buf:eol-1] if ((eol > buf) && (*(eol - 1) == '\r')) len--; buf[len - 1] = '\n'; rv = ParseLineSegment(buf, len); if (NS_FAILED(rv)) return rv; if (mHaveAllHeaders) return NS_OK; // skip over line buf = eol + 1; if (!mHttpResponseMatched) { // a 100 class response has caused us to throw away that set of // response headers and look for the next response return NS_ERROR_NET_INTERRUPT; } } // do something about a partial header line if (!mHaveAllHeaders && (len = count - *countRead)) { *countRead = count; // ignore a trailing carriage return, and don't bother calling // ParseLineSegment if buf only contains a carriage return. if ((buf[len - 1] == '\r') && (--len == 0)) return NS_OK; rv = ParseLineSegment(buf, len); if (NS_FAILED(rv)) return rv; } return NS_OK; } bool nsHttpTransaction::HandleWebTransportResponse(uint16_t aStatus) { MOZ_ASSERT(mIsForWebTransport); if (!(aStatus >= 200 && aStatus < 300)) { return false; } RefPtr wtSession = mConnection->GetWebTransportSession(this); if (!wtSession) { return false; } nsCOMPtr webTransportListener; { MutexAutoLock lock(mLock); webTransportListener = mWebTransportSessionEventListener; mWebTransportSessionEventListener = nullptr; } if (webTransportListener) { webTransportListener->OnSessionReadyInternal(wtSession); wtSession->SetWebTransportSessionEventListener(webTransportListener); } return true; } nsresult nsHttpTransaction::HandleContentStart() { LOG(("nsHttpTransaction::HandleContentStart [this=%p]\n", this)); MOZ_ASSERT(OnSocketThread(), "not on socket thread"); if (mResponseHead) { if (mEarlyDataDisposition == EARLY_ACCEPTED) { if (mResponseHead->Status() == 425) { // We will report this state when the final responce arrives. mEarlyDataDisposition = EARLY_425; } else { Unused << mResponseHead->SetHeader(nsHttp::X_Firefox_Early_Data, "accepted"_ns); } } else if (mEarlyDataDisposition == EARLY_SENT) { Unused << mResponseHead->SetHeader(nsHttp::X_Firefox_Early_Data, "sent"_ns); } else if (mEarlyDataDisposition == EARLY_425) { Unused << mResponseHead->SetHeader(nsHttp::X_Firefox_Early_Data, "received 425"_ns); mEarlyDataDisposition = EARLY_NONE; } // no header on NONE case if (LOG3_ENABLED()) { LOG3(("http response [\n")); nsAutoCString headers; mResponseHead->Flatten(headers, false); headers.AppendLiteral(" OriginalHeaders"); headers.AppendLiteral("\r\n"); mResponseHead->FlattenNetworkOriginalHeaders(headers); LogHeaders(headers.get()); LOG3(("]\n")); } CheckForStickyAuthScheme(); // Save http version, mResponseHead isn't available anymore after // TakeResponseHead() is called mHttpVersion = mResponseHead->Version(); mHttpResponseCode = mResponseHead->Status(); // notify the connection, give it a chance to cause a reset. bool reset = false; nsresult rv = mConnection->OnHeadersAvailable(this, mRequestHead, mResponseHead, &reset); NS_ENSURE_SUCCESS(rv, rv); // looks like we should ignore this response, resetting... if (reset) { LOG(("resetting transaction's response head\n")); mHaveAllHeaders = false; mHaveStatusLine = false; mReceivedData = false; mSentData = false; mHttpResponseMatched = false; mResponseHead->Reset(); // wait to be called again... return NS_OK; } bool responseChecked = false; if (mIsForWebTransport) { responseChecked = HandleWebTransportResponse(mResponseHead->Status()); LOG(("HandleWebTransportResponse res=%d", responseChecked)); if (responseChecked) { mNoContent = true; mPreserveStream = true; } } if (!responseChecked) { // check if this is a no-content response switch (mResponseHead->Status()) { case 101: mPreserveStream = true; [[fallthrough]]; // to other no content cases: case 204: case 205: case 304: mNoContent = true; LOG(("this response should not contain a body.\n")); break; case 408: LOG(("408 Server Timeouts")); if (mConnection->Version() >= HttpVersion::v2_0) { mForceRestart = true; return NS_ERROR_NET_RESET; } // If this error could be due to a persistent connection // reuse then we pass an error code of NS_ERROR_NET_RESET // to trigger the transaction 'restart' mechanism. We // tell it to reset its response headers so that it will // be ready to receive the new response. LOG(("408 Server Timeouts now=%d lastWrite=%d", PR_IntervalNow(), mConnection->LastWriteTime())); if ((PR_IntervalNow() - mConnection->LastWriteTime()) >= PR_MillisecondsToInterval(1000)) { mForceRestart = true; return NS_ERROR_NET_RESET; } break; case 421: LOG(("Misdirected Request.\n")); gHttpHandler->ClearHostMapping(mConnInfo); m421Received = true; mCaps |= NS_HTTP_REFRESH_DNS; // retry on a new connection - just in case // See bug 1609410, we can't restart the transaction when // NS_HTTP_STICKY_CONNECTION is set. In the case that a connection // already passed NTLM authentication, restarting the transaction will // cause the connection to be closed. if (!mRestartCount && !(mCaps & NS_HTTP_STICKY_CONNECTION)) { mCaps &= ~NS_HTTP_ALLOW_KEEPALIVE; mForceRestart = true; // force restart has built in loop protection return NS_ERROR_NET_RESET; } break; case 425: LOG(("Too Early.")); if ((mEarlyDataDisposition == EARLY_425) && !mDoNotTryEarlyData) { mDoNotTryEarlyData = true; mForceRestart = true; // force restart has built in loop protection if (mConnection->Version() >= HttpVersion::v2_0) { mReuseOnRestart = true; } return NS_ERROR_NET_RESET; } break; } } // Remember whether HTTP3 is supported mSupportsHTTP3 = nsHttpHandler::IsHttp3SupportedByServer(mResponseHead); CollectTelemetryForUploads(); // Report telemetry if (mSupportsHTTP3) { Accumulate(Telemetry::TRANSACTION_WAIT_TIME_HTTP2_SUP_HTTP3, mPendingDurationTime.ToMilliseconds()); } // If we're only connecting then we're going to be upgrading this // connection since we were successful. Any data from now on belongs to // the upgrade handler. If we're not successful the content body doesn't // matter. Proxy http errors are treated as network errors. This // connection won't be reused since it's marked sticky and no // keep-alive. if (mCaps & NS_HTTP_CONNECT_ONLY) { MOZ_ASSERT(!(mCaps & NS_HTTP_ALLOW_KEEPALIVE) && (mCaps & NS_HTTP_STICKY_CONNECTION), "connection should be sticky and no keep-alive"); // The transaction will expect the server to close the socket if // there's no content length instead of doing the upgrade. mNoContent = true; } // preserve connection for tunnel setup - h2 websocket upgrade only if (mIsHttp2Websocket && mResponseHead->Status() == 200) { LOG(("nsHttpTransaction::HandleContentStart websocket upgrade resp 200")); mNoContent = true; } if (mResponseHead->Status() == 200 && mConnection->IsProxyConnectInProgress()) { // successful CONNECTs do not have response bodies mNoContent = true; } mConnection->SetLastTransactionExpectedNoContent(mNoContent); if (mNoContent) { mContentLength = 0; } else { // grab the content-length from the response headers mContentLength = mResponseHead->ContentLength(); // handle chunked encoding here, so we'll know immediately when // we're done with the socket. please note that _all_ other // decoding is done when the channel receives the content data // so as not to block the socket transport thread too much. if (mResponseHead->Version() >= HttpVersion::v1_0 && mResponseHead->HasHeaderValue(nsHttp::Transfer_Encoding, "chunked")) { // we only support the "chunked" transfer encoding right now. mChunkedDecoder = new nsHttpChunkedDecoder(); LOG(("nsHttpTransaction %p chunked decoder created\n", this)); // Ignore server specified Content-Length. if (mContentLength != int64_t(-1)) { LOG(("nsHttpTransaction %p chunked with C-L ignores C-L\n", this)); mContentLength = -1; if (mConnection) { mConnection->DontReuse(); } } } else if (mContentLength == int64_t(-1)) { LOG(("waiting for the server to close the connection.\n")); } } } mDidContentStart = true; return NS_OK; } // called on the socket thread nsresult nsHttpTransaction::HandleContent(char* buf, uint32_t count, uint32_t* contentRead, uint32_t* contentRemaining) { nsresult rv; LOG(("nsHttpTransaction::HandleContent [this=%p count=%u]\n", this, count)); *contentRead = 0; *contentRemaining = 0; MOZ_ASSERT(mConnection); if (!mDidContentStart) { rv = HandleContentStart(); if (NS_FAILED(rv)) return rv; // Do not write content to the pipe if we haven't started streaming yet if (!mDidContentStart) return NS_OK; } if (mChunkedDecoder) { // give the buf over to the chunked decoder so it can reformat the // data and tell us how much is really there. rv = mChunkedDecoder->HandleChunkedContent(buf, count, contentRead, contentRemaining); if (NS_FAILED(rv)) return rv; } else if (mContentLength >= int64_t(0)) { // HTTP/1.0 servers have been known to send erroneous Content-Length // headers. So, unless the connection is persistent, we must make // allowances for a possibly invalid Content-Length header. Thus, if // NOT persistent, we simply accept everything in |buf|. if (mConnection->IsPersistent() || mPreserveStream || mHttpVersion >= HttpVersion::v1_1) { int64_t remaining = mContentLength - mContentRead; *contentRead = uint32_t(std::min(count, remaining)); *contentRemaining = count - *contentRead; } else { *contentRead = count; // mContentLength might need to be increased... int64_t position = mContentRead + int64_t(count); if (position > mContentLength) { mContentLength = position; // mResponseHead->SetContentLength(mContentLength); } } } else { // when we are just waiting for the server to close the connection... // (no explicit content-length given) *contentRead = count; } if (*contentRead) { // update count of content bytes read and report progress... mContentRead += *contentRead; } LOG1( ("nsHttpTransaction::HandleContent [this=%p count=%u read=%u " "mContentRead=%" PRId64 " mContentLength=%" PRId64 "]\n", this, count, *contentRead, mContentRead, mContentLength)); // check for end-of-file if ((mContentRead == mContentLength) || (mChunkedDecoder && mChunkedDecoder->ReachedEOF())) { { MutexAutoLock lock(mLock); if (mChunkedDecoder) { mForTakeResponseTrailers = mChunkedDecoder->TakeTrailers(); } // the transaction is done with a complete response. mTransactionDone = true; mResponseIsComplete = true; } ReleaseBlockingTransaction(); if (TimingEnabled()) { SetResponseEnd(TimeStamp::Now()); } // report the entire response has arrived gHttpHandler->ObserveHttpActivityWithArgs( HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, NS_HTTP_ACTIVITY_SUBTYPE_RESPONSE_COMPLETE, PR_Now(), static_cast(mContentRead), ""_ns); } return NS_OK; } nsresult nsHttpTransaction::ProcessData(char* buf, uint32_t count, uint32_t* countRead) { nsresult rv; LOG1(("nsHttpTransaction::ProcessData [this=%p count=%u]\n", this, count)); *countRead = 0; // we may not have read all of the headers yet... if (!mHaveAllHeaders) { uint32_t bytesConsumed = 0; do { uint32_t localBytesConsumed = 0; char* localBuf = buf + bytesConsumed; uint32_t localCount = count - bytesConsumed; rv = ParseHead(localBuf, localCount, &localBytesConsumed); if (NS_FAILED(rv) && rv != NS_ERROR_NET_INTERRUPT) return rv; bytesConsumed += localBytesConsumed; } while (rv == NS_ERROR_NET_INTERRUPT); mCurrentHttpResponseHeaderSize += bytesConsumed; if (mCurrentHttpResponseHeaderSize > gHttpHandler->MaxHttpResponseHeaderSize()) { LOG(("nsHttpTransaction %p The response header exceeds the limit.\n", this)); return NS_ERROR_FILE_TOO_BIG; } count -= bytesConsumed; // if buf has some content in it, shift bytes to top of buf. if (count && bytesConsumed) memmove(buf, buf + bytesConsumed, count); if (mResponseHead && mHaveAllHeaders) { auto reportResponseHeader = [&](uint32_t aSubType) { nsAutoCString completeResponseHeaders; mResponseHead->Flatten(completeResponseHeaders, false); completeResponseHeaders.AppendLiteral("\r\n"); gHttpHandler->ObserveHttpActivityWithArgs( HttpActivityArgs(mChannelId), NS_HTTP_ACTIVITY_TYPE_HTTP_TRANSACTION, aSubType, PR_Now(), 0, completeResponseHeaders); }; if (mConnection->IsProxyConnectInProgress()) { reportResponseHeader(NS_HTTP_ACTIVITY_SUBTYPE_PROXY_RESPONSE_HEADER); } else if (!mReportedResponseHeader) { mReportedResponseHeader = true; reportResponseHeader(NS_HTTP_ACTIVITY_SUBTYPE_RESPONSE_HEADER); } } } // even though count may be 0, we still want to call HandleContent // so it can complete the transaction if this is a "no-content" response. if (mHaveAllHeaders) { uint32_t countRemaining = 0; // // buf layout: // // +--------------------------------------+----------------+-----+ // | countRead | countRemaining | | // +--------------------------------------+----------------+-----+ // // count : bytes read from the socket // countRead : bytes corresponding to this transaction // countRemaining : bytes corresponding to next transaction on conn // // NOTE: // count > countRead + countRemaining <==> chunked transfer encoding // rv = HandleContent(buf, count, countRead, &countRemaining); if (NS_FAILED(rv)) return rv; // we may have read more than our share, in which case we must give // the excess bytes back to the connection if (mResponseIsComplete && countRemaining && (mConnection->Version() != HttpVersion::v3_0)) { MOZ_ASSERT(mConnection); rv = mConnection->PushBack(buf + *countRead, countRemaining); NS_ENSURE_SUCCESS(rv, rv); } if (!mContentDecodingCheck && mResponseHead) { mContentDecoding = mResponseHead->HasHeader(nsHttp::Content_Encoding); mContentDecodingCheck = true; } } return NS_OK; } // Called when the transaction marked for blocking is associated with a // connection (i.e. added to a new h1 conn, an idle http connection, etc..) It // is safe to call this multiple times with it only having an effect once. void nsHttpTransaction::DispatchedAsBlocking() { if (mDispatchedAsBlocking) return; LOG(("nsHttpTransaction %p dispatched as blocking\n", this)); if (!mRequestContext) return; LOG( ("nsHttpTransaction adding blocking transaction %p from " "request context %p\n", this, mRequestContext.get())); mRequestContext->AddBlockingTransaction(); mDispatchedAsBlocking = true; } void nsHttpTransaction::RemoveDispatchedAsBlocking() { if (!mRequestContext || !mDispatchedAsBlocking) { LOG(("nsHttpTransaction::RemoveDispatchedAsBlocking this=%p not blocking", this)); return; } uint32_t blockers = 0; nsresult rv = mRequestContext->RemoveBlockingTransaction(&blockers); LOG( ("nsHttpTransaction removing blocking transaction %p from " "request context %p. %d blockers remain.\n", this, mRequestContext.get(), blockers)); if (NS_SUCCEEDED(rv) && !blockers) { LOG( ("nsHttpTransaction %p triggering release of blocked channels " " with request context=%p\n", this, mRequestContext.get())); rv = gHttpHandler->ConnMgr()->ProcessPendingQ(); if (NS_FAILED(rv)) { LOG( ("nsHttpTransaction::RemoveDispatchedAsBlocking\n" " failed to process pending queue\n")); } } mDispatchedAsBlocking = false; } void nsHttpTransaction::ReleaseBlockingTransaction() { RemoveDispatchedAsBlocking(); LOG( ("nsHttpTransaction %p request context set to null " "in ReleaseBlockingTransaction() - was %p\n", this, mRequestContext.get())); mRequestContext = nullptr; } void nsHttpTransaction::DisableSpdy() { mCaps |= NS_HTTP_DISALLOW_SPDY; if (mConnInfo) { // This is our clone of the connection info, not the persistent one that // is owned by the connection manager, so we're safe to change this here mConnInfo->SetNoSpdy(true); } } void nsHttpTransaction::DisableHttp2ForProxy() { mCaps |= NS_HTTP_DISALLOW_HTTP2_PROXY; } void nsHttpTransaction::DisableHttp3(bool aAllowRetryHTTPSRR) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); // mOrigConnInfo is an indicator that HTTPS RR is used, so don't mess up the // connection info. // When HTTPS RR is used, PrepareConnInfoForRetry() could select other h3 // record to connect. if (mOrigConnInfo) { LOG( ("nsHttpTransaction::DisableHttp3 this=%p mOrigConnInfo=%s " "aAllowRetryHTTPSRR=%d", this, mOrigConnInfo->HashKey().get(), aAllowRetryHTTPSRR)); if (!aAllowRetryHTTPSRR) { mCaps |= NS_HTTP_DISALLOW_HTTP3; } return; } mCaps |= NS_HTTP_DISALLOW_HTTP3; MOZ_ASSERT(mConnInfo); if (mConnInfo) { // After CloneAsDirectRoute(), http3 will be disabled. RefPtr connInfo; mConnInfo->CloneAsDirectRoute(getter_AddRefs(connInfo)); RemoveAlternateServiceUsedHeader(mRequestHead); MOZ_ASSERT(!connInfo->IsHttp3()); mConnInfo.swap(connInfo); } } void nsHttpTransaction::CheckForStickyAuthScheme() { LOG(("nsHttpTransaction::CheckForStickyAuthScheme this=%p", this)); MOZ_ASSERT(mHaveAllHeaders); MOZ_ASSERT(mResponseHead); MOZ_ASSERT(OnSocketThread(), "not on socket thread"); CheckForStickyAuthSchemeAt(nsHttp::WWW_Authenticate); CheckForStickyAuthSchemeAt(nsHttp::Proxy_Authenticate); } void nsHttpTransaction::CheckForStickyAuthSchemeAt(nsHttpAtom const& header) { if (mCaps & NS_HTTP_STICKY_CONNECTION) { LOG((" already sticky")); return; } nsAutoCString auth; if (NS_FAILED(mResponseHead->GetHeader(header, auth))) { return; } if (IsStickyAuthSchemeAt(auth)) { LOG((" connection made sticky")); // This is enough to make this transaction keep it's current connection, // prevents the connection from being released back to the pool. mCaps |= NS_HTTP_STICKY_CONNECTION; } } bool nsHttpTransaction::IsStickyAuthSchemeAt(nsACString const& auth) { Tokenizer p(auth); nsAutoCString schema; while (p.ReadWord(schema)) { ToLowerCase(schema); // using a new instance because of thread safety of auth modules refcnt nsCOMPtr authenticator; if (schema.EqualsLiteral("negotiate")) { #ifdef MOZ_AUTH_EXTENSION authenticator = new nsHttpNegotiateAuth(); #endif } else if (schema.EqualsLiteral("basic")) { authenticator = new nsHttpBasicAuth(); } else if (schema.EqualsLiteral("digest")) { authenticator = new nsHttpDigestAuth(); } else if (schema.EqualsLiteral("ntlm")) { authenticator = new nsHttpNTLMAuth(); } else if (schema.EqualsLiteral("mock_auth") && PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR")) { authenticator = new MockHttpAuth(); } if (authenticator) { uint32_t flags; nsresult rv = authenticator->GetAuthFlags(&flags); if (NS_SUCCEEDED(rv) && (flags & nsIHttpAuthenticator::CONNECTION_BASED)) { return true; } } // schemes are separated with LFs, nsHttpHeaderArray::MergeHeader p.SkipUntil(Tokenizer::Token::NewLine()); p.SkipWhites(Tokenizer::INCLUDE_NEW_LINE); } return false; } TimingStruct nsHttpTransaction::Timings() { mozilla::MutexAutoLock lock(mLock); TimingStruct timings = mTimings; return timings; } void nsHttpTransaction::BootstrapTimings(TimingStruct times) { mozilla::MutexAutoLock lock(mLock); mTimings = times; } void nsHttpTransaction::SetDomainLookupStart(mozilla::TimeStamp timeStamp, bool onlyIfNull) { mozilla::MutexAutoLock lock(mLock); if (onlyIfNull && !mTimings.domainLookupStart.IsNull()) { return; // We only set the timestamp if it was previously null } mTimings.domainLookupStart = timeStamp; } void nsHttpTransaction::SetDomainLookupEnd(mozilla::TimeStamp timeStamp, bool onlyIfNull) { mozilla::MutexAutoLock lock(mLock); if (onlyIfNull && !mTimings.domainLookupEnd.IsNull()) { return; // We only set the timestamp if it was previously null } mTimings.domainLookupEnd = timeStamp; } void nsHttpTransaction::SetConnectStart(mozilla::TimeStamp timeStamp, bool onlyIfNull) { mozilla::MutexAutoLock lock(mLock); if (onlyIfNull && !mTimings.connectStart.IsNull()) { return; // We only set the timestamp if it was previously null } mTimings.connectStart = timeStamp; } void nsHttpTransaction::SetConnectEnd(mozilla::TimeStamp timeStamp, bool onlyIfNull) { mozilla::MutexAutoLock lock(mLock); if (onlyIfNull && !mTimings.connectEnd.IsNull()) { return; // We only set the timestamp if it was previously null } mTimings.connectEnd = timeStamp; } void nsHttpTransaction::SetRequestStart(mozilla::TimeStamp timeStamp, bool onlyIfNull) { mozilla::MutexAutoLock lock(mLock); if (onlyIfNull && !mTimings.requestStart.IsNull()) { return; // We only set the timestamp if it was previously null } mTimings.requestStart = timeStamp; } void nsHttpTransaction::SetResponseStart(mozilla::TimeStamp timeStamp, bool onlyIfNull) { mozilla::MutexAutoLock lock(mLock); if (onlyIfNull && !mTimings.responseStart.IsNull()) { return; // We only set the timestamp if it was previously null } mTimings.responseStart = timeStamp; } void nsHttpTransaction::SetResponseEnd(mozilla::TimeStamp timeStamp, bool onlyIfNull) { mozilla::MutexAutoLock lock(mLock); if (onlyIfNull && !mTimings.responseEnd.IsNull()) { return; // We only set the timestamp if it was previously null } mTimings.responseEnd = timeStamp; } mozilla::TimeStamp nsHttpTransaction::GetDomainLookupStart() { mozilla::MutexAutoLock lock(mLock); return mTimings.domainLookupStart; } mozilla::TimeStamp nsHttpTransaction::GetDomainLookupEnd() { mozilla::MutexAutoLock lock(mLock); return mTimings.domainLookupEnd; } mozilla::TimeStamp nsHttpTransaction::GetConnectStart() { mozilla::MutexAutoLock lock(mLock); return mTimings.connectStart; } mozilla::TimeStamp nsHttpTransaction::GetTcpConnectEnd() { mozilla::MutexAutoLock lock(mLock); return mTimings.tcpConnectEnd; } mozilla::TimeStamp nsHttpTransaction::GetSecureConnectionStart() { mozilla::MutexAutoLock lock(mLock); return mTimings.secureConnectionStart; } mozilla::TimeStamp nsHttpTransaction::GetConnectEnd() { mozilla::MutexAutoLock lock(mLock); return mTimings.connectEnd; } mozilla::TimeStamp nsHttpTransaction::GetRequestStart() { mozilla::MutexAutoLock lock(mLock); return mTimings.requestStart; } mozilla::TimeStamp nsHttpTransaction::GetResponseStart() { mozilla::MutexAutoLock lock(mLock); return mTimings.responseStart; } mozilla::TimeStamp nsHttpTransaction::GetResponseEnd() { mozilla::MutexAutoLock lock(mLock); return mTimings.responseEnd; } //----------------------------------------------------------------------------- // nsHttpTransaction deletion event //----------------------------------------------------------------------------- class DeleteHttpTransaction : public Runnable { public: explicit DeleteHttpTransaction(nsHttpTransaction* trans) : Runnable("net::DeleteHttpTransaction"), mTrans(trans) {} NS_IMETHOD Run() override { delete mTrans; return NS_OK; } private: nsHttpTransaction* mTrans; }; void nsHttpTransaction::DeleteSelfOnConsumerThread() { LOG(("nsHttpTransaction::DeleteSelfOnConsumerThread [this=%p]\n", this)); bool val; if (!mConsumerTarget || (NS_SUCCEEDED(mConsumerTarget->IsOnCurrentThread(&val)) && val)) { delete this; } else { LOG(("proxying delete to consumer thread...\n")); nsCOMPtr event = new DeleteHttpTransaction(this); if (NS_FAILED(mConsumerTarget->Dispatch(event, NS_DISPATCH_NORMAL))) { NS_WARNING("failed to dispatch nsHttpDeleteTransaction event"); } } } bool nsHttpTransaction::TryToRunPacedRequest() { if (mSubmittedRatePacing) return mPassedRatePacing; mSubmittedRatePacing = true; mSynchronousRatePaceRequest = true; Unused << gHttpHandler->SubmitPacedRequest( this, getter_AddRefs(mTokenBucketCancel)); mSynchronousRatePaceRequest = false; return mPassedRatePacing; } void nsHttpTransaction::OnTokenBucketAdmitted() { mPassedRatePacing = true; mTokenBucketCancel = nullptr; if (!mSynchronousRatePaceRequest) { nsresult rv = gHttpHandler->ConnMgr()->ProcessPendingQ(mConnInfo); if (NS_FAILED(rv)) { LOG( ("nsHttpTransaction::OnTokenBucketAdmitted\n" " failed to process pending queue\n")); } } } void nsHttpTransaction::CancelPacing(nsresult reason) { if (mTokenBucketCancel) { mTokenBucketCancel->Cancel(reason); mTokenBucketCancel = nullptr; } } //----------------------------------------------------------------------------- // nsHttpTransaction::nsISupports //----------------------------------------------------------------------------- NS_IMPL_ADDREF(nsHttpTransaction) NS_IMETHODIMP_(MozExternalRefCountType) nsHttpTransaction::Release() { nsrefcnt count; MOZ_ASSERT(0 != mRefCnt, "dup release"); count = --mRefCnt; NS_LOG_RELEASE(this, count, "nsHttpTransaction"); if (0 == count) { mRefCnt = 1; /* stablize */ // it is essential that the transaction be destroyed on the consumer // thread (we could be holding the last reference to our consumer). DeleteSelfOnConsumerThread(); return 0; } return count; } NS_IMPL_QUERY_INTERFACE(nsHttpTransaction, nsIInputStreamCallback, nsIOutputStreamCallback, nsITimerCallback, nsINamed) //----------------------------------------------------------------------------- // nsHttpTransaction::nsIInputStreamCallback //----------------------------------------------------------------------------- // called on the socket thread NS_IMETHODIMP nsHttpTransaction::OnInputStreamReady(nsIAsyncInputStream* out) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); if (mConnection) { mConnection->TransactionHasDataToWrite(this); nsresult rv = mConnection->ResumeSend(); if (NS_FAILED(rv)) NS_ERROR("ResumeSend failed"); } return NS_OK; } //----------------------------------------------------------------------------- // nsHttpTransaction::nsIOutputStreamCallback //----------------------------------------------------------------------------- // called on the socket thread NS_IMETHODIMP nsHttpTransaction::OnOutputStreamReady(nsIAsyncOutputStream* out) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); mWaitingOnPipeOut = false; if (mConnection) { mConnection->TransactionHasDataToRecv(this); nsresult rv = mConnection->ResumeRecv(); if (NS_FAILED(rv) && rv != NS_BASE_STREAM_WOULD_BLOCK) { NS_ERROR("ResumeRecv failed"); } } return NS_OK; } void nsHttpTransaction::GetNetworkAddresses( NetAddr& self, NetAddr& peer, bool& aResolvedByTRR, nsIRequest::TRRMode& aEffectiveTRRMode, TRRSkippedReason& aSkipReason, bool& aEchConfigUsed) { MutexAutoLock lock(mLock); self = mSelfAddr; peer = mPeerAddr; aResolvedByTRR = mResolvedByTRR; aEffectiveTRRMode = mEffectiveTRRMode; aSkipReason = mTRRSkipReason; aEchConfigUsed = mEchConfigUsed; } bool nsHttpTransaction::Do0RTT() { LOG(("nsHttpTransaction::Do0RTT")); mEarlyDataWasAvailable = true; if (mRequestHead->IsSafeMethod() && !mDoNotTryEarlyData && (!mConnection || !mConnection->IsProxyConnectInProgress())) { m0RTTInProgress = true; } return m0RTTInProgress; } nsresult nsHttpTransaction::Finish0RTT(bool aRestart, bool aAlpnChanged /* ignored */) { LOG(("nsHttpTransaction::Finish0RTT %p %d %d\n", this, aRestart, aAlpnChanged)); MOZ_ASSERT(m0RTTInProgress); m0RTTInProgress = false; MaybeCancelFallbackTimer(); if (!aRestart && (mEarlyDataDisposition == EARLY_SENT)) { // note that if this is invoked by a 3 param version of finish0rtt this // disposition might be reverted mEarlyDataDisposition = EARLY_ACCEPTED; } if (aRestart) { // Not to use 0RTT when this transaction is restarted next time. mDoNotTryEarlyData = true; // Reset request headers to be sent again. nsCOMPtr seekable = do_QueryInterface(mRequestStream); if (seekable) { seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0); } else { return NS_ERROR_FAILURE; } } else if (!mConnected) { // this is code that was skipped in ::ReadSegments while in 0RTT mConnected = true; MaybeRefreshSecurityInfo(); } return NS_OK; } void nsHttpTransaction::Refused0RTT() { LOG(("nsHttpTransaction::Refused0RTT %p\n", this)); if (mEarlyDataDisposition == EARLY_ACCEPTED) { mEarlyDataDisposition = EARLY_SENT; // undo accepted state } } void nsHttpTransaction::SetHttpTrailers(nsCString& aTrailers) { LOG(("nsHttpTransaction::SetHttpTrailers %p", this)); LOG(("[\n %s\n]", aTrailers.BeginReading())); // Introduce a local variable to minimize the critical section. UniquePtr httpTrailers(new nsHttpHeaderArray()); // Given it's usually null, use double-check locking for performance. if (mForTakeResponseTrailers) { MutexAutoLock lock(mLock); if (mForTakeResponseTrailers) { // Copy the trailer. |TakeResponseTrailers| gets the original trailer // until the final swap. *httpTrailers = *mForTakeResponseTrailers; } } int32_t cur = 0; int32_t len = aTrailers.Length(); while (cur < len) { int32_t newline = aTrailers.FindCharInSet("\n", cur); if (newline == -1) { newline = len; } int32_t end = (newline && aTrailers[newline - 1] == '\r') ? newline - 1 : newline; nsDependentCSubstring line(aTrailers, cur, end); nsHttpAtom hdr; nsAutoCString hdrNameOriginal; nsAutoCString val; if (NS_SUCCEEDED(httpTrailers->ParseHeaderLine(line, &hdr, &hdrNameOriginal, &val))) { if (hdr == nsHttp::Server_Timing) { Unused << httpTrailers->SetHeaderFromNet(hdr, hdrNameOriginal, val, true); } } cur = newline + 1; } if (httpTrailers->Count() == 0) { // Didn't find a Server-Timing header, so get rid of this. httpTrailers = nullptr; } MutexAutoLock lock(mLock); std::swap(mForTakeResponseTrailers, httpTrailers); } bool nsHttpTransaction::IsWebsocketUpgrade() { if (mRequestHead) { nsAutoCString upgradeHeader; if (NS_SUCCEEDED(mRequestHead->GetHeader(nsHttp::Upgrade, upgradeHeader)) && upgradeHeader.LowerCaseEqualsLiteral("websocket")) { return true; } } return false; } void nsHttpTransaction::OnProxyConnectComplete(int32_t aResponseCode) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); MOZ_ASSERT(mConnInfo->UsingConnect()); LOG(("nsHttpTransaction::OnProxyConnectComplete %p aResponseCode=%d", this, aResponseCode)); mProxyConnectResponseCode = aResponseCode; } int32_t nsHttpTransaction::GetProxyConnectResponseCode() { return mProxyConnectResponseCode; } void nsHttpTransaction::SetFlat407Headers(const nsACString& aHeaders) { MOZ_ASSERT(mProxyConnectResponseCode == 407); MOZ_ASSERT(!mResponseHead); LOG(("nsHttpTransaction::SetFlat407Headers %p", this)); mFlat407Headers = aHeaders; } void nsHttpTransaction::NotifyTransactionObserver(nsresult reason) { MOZ_ASSERT(OnSocketThread()); if (!mTransactionObserver) { return; } bool versionOk = false; bool authOk = false; LOG(("nsHttpTransaction::NotifyTransactionObserver %p reason %" PRIx32 " conn %p\n", this, static_cast(reason), mConnection.get())); if (mConnection) { HttpVersion version = mConnection->Version(); versionOk = (((reason == NS_BASE_STREAM_CLOSED) || (reason == NS_OK)) && ((mConnection->Version() == HttpVersion::v2_0) || (mConnection->Version() == HttpVersion::v3_0))); nsCOMPtr socketControl; mConnection->GetTLSSocketControl(getter_AddRefs(socketControl)); LOG( ("nsHttpTransaction::NotifyTransactionObserver" " version %u socketControl %p\n", static_cast(version), socketControl.get())); if (socketControl) { authOk = !socketControl->GetFailedVerification(); } } TransactionObserverResult result; result.versionOk() = versionOk; result.authOk() = authOk; result.closeReason() = reason; TransactionObserverFunc obs = nullptr; std::swap(obs, mTransactionObserver); obs(std::move(result)); } void nsHttpTransaction::UpdateConnectionInfo(nsHttpConnectionInfo* aConnInfo) { MOZ_ASSERT(aConnInfo); if (mActivated) { MOZ_ASSERT(false, "Should not update conn info after activated"); return; } mOrigConnInfo = mConnInfo->Clone(); mConnInfo = aConnInfo; } nsresult nsHttpTransaction::OnHTTPSRRAvailable( nsIDNSHTTPSSVCRecord* aHTTPSSVCRecord, nsISVCBRecord* aHighestPriorityRecord) { LOG(("nsHttpTransaction::OnHTTPSRRAvailable [this=%p] mActivated=%d", this, mActivated)); MOZ_ASSERT(OnSocketThread(), "not on socket thread"); { MutexAutoLock lock(mLock); MakeDontWaitHTTPSRR(); mDNSRequest = nullptr; } if (!mResolver) { LOG(("The transaction is not interested in HTTPS record anymore.")); return NS_OK; } RefPtr deleteProtector(this); uint32_t receivedStage = HTTPSSVC_NO_USABLE_RECORD; // Make sure we set the correct value to |mHTTPSSVCReceivedStage|, since we // also use this value to indicate whether HTTPS RR is used or not. auto updateHTTPSSVCReceivedStage = MakeScopeExit([&] { mHTTPSSVCReceivedStage = receivedStage; // In the case that an HTTPS RR is unavailable, we should call // ProcessPendingQ to make sure this transition to be processed soon. if (!mHTTPSSVCRecord) { gHttpHandler->ConnMgr()->ProcessPendingQ(mConnInfo); } }); nsCOMPtr record = aHTTPSSVCRecord; if (!record) { return NS_ERROR_FAILURE; } bool hasIPAddress = false; Unused << record->GetHasIPAddresses(&hasIPAddress); if (mActivated) { receivedStage = hasIPAddress ? HTTPSSVC_WITH_IPHINT_RECEIVED_STAGE_2 : HTTPSSVC_WITHOUT_IPHINT_RECEIVED_STAGE_2; return NS_OK; } receivedStage = hasIPAddress ? HTTPSSVC_WITH_IPHINT_RECEIVED_STAGE_1 : HTTPSSVC_WITHOUT_IPHINT_RECEIVED_STAGE_1; nsCOMPtr svcbRecord = aHighestPriorityRecord; if (!svcbRecord) { LOG((" no usable record!")); nsCOMPtr dns = do_GetService(NS_DNSSERVICE_CONTRACTID); bool allRecordsExcluded = false; Unused << record->GetAllRecordsExcluded(&allRecordsExcluded); Telemetry::Accumulate(Telemetry::DNS_HTTPSSVC_CONNECTION_FAILED_REASON, allRecordsExcluded ? HTTPSSVC_CONNECTION_ALL_RECORDS_EXCLUDED : HTTPSSVC_CONNECTION_NO_USABLE_RECORD); if (allRecordsExcluded && StaticPrefs::network_dns_httpssvc_reset_exclustion_list() && dns) { Unused << dns->ResetExcludedSVCDomainName(mConnInfo->GetOrigin()); if (NS_FAILED(record->GetServiceModeRecord(mCaps & NS_HTTP_DISALLOW_SPDY, mCaps & NS_HTTP_DISALLOW_HTTP3, getter_AddRefs(svcbRecord)))) { return NS_ERROR_FAILURE; } } else { return NS_ERROR_FAILURE; } } // Remember this RR set. In the case that the connection establishment failed, // we will use other records to retry. mHTTPSSVCRecord = record; RefPtr newInfo = mConnInfo->CloneAndAdoptHTTPSSVCRecord(svcbRecord); bool needFastFallback = newInfo->IsHttp3(); bool foundInPendingQ = gHttpHandler->ConnMgr()->RemoveTransFromConnEntry( this, mHashKeyOfConnectionEntry); // Adopt the new connection info, so this transaction will be added into the // new connection entry. UpdateConnectionInfo(newInfo); // If this transaction is sucessfully removed from a connection entry, we call // ProcessNewTransaction to process it immediately. // If not, this means that nsHttpTransaction::OnHTTPSRRAvailable happens // before ProcessNewTransaction and this transaction will be processed later. if (foundInPendingQ) { if (NS_FAILED(gHttpHandler->ConnMgr()->ProcessNewTransaction(this))) { LOG(("Failed to process this transaction.")); return NS_ERROR_FAILURE; } } // In case we already have mHttp3BackupTimer, cancel it. MaybeCancelFallbackTimer(); if (needFastFallback) { CreateAndStartTimer( mFastFallbackTimer, this, StaticPrefs::network_dns_httpssvc_http3_fast_fallback_timeout()); } // Prefetch the A/AAAA records of the target name. nsAutoCString targetName; Unused << svcbRecord->GetName(targetName); if (mResolver) { mResolver->PrefetchAddrRecord(targetName, mCaps & NS_HTTP_REFRESH_DNS); } // echConfig is used, so initialize the retry counters to 0. if (!mConnInfo->GetEchConfig().IsEmpty()) { mEchRetryCounterMap.InsertOrUpdate( Telemetry::TRANSACTION_ECH_RETRY_WITH_ECH_COUNT, 0); mEchRetryCounterMap.InsertOrUpdate( Telemetry::TRANSACTION_ECH_RETRY_WITHOUT_ECH_COUNT, 0); mEchRetryCounterMap.InsertOrUpdate( Telemetry::TRANSACTION_ECH_RETRY_ECH_FAILED_COUNT, 0); mEchRetryCounterMap.InsertOrUpdate( Telemetry::TRANSACTION_ECH_RETRY_OTHERS_COUNT, 0); } return NS_OK; } uint32_t nsHttpTransaction::HTTPSSVCReceivedStage() { return mHTTPSSVCReceivedStage; } void nsHttpTransaction::MaybeCancelFallbackTimer() { MOZ_DIAGNOSTIC_ASSERT(OnSocketThread(), "not on socket thread"); if (mFastFallbackTimer) { mFastFallbackTimer->Cancel(); mFastFallbackTimer = nullptr; } if (mHttp3BackupTimer) { mHttp3BackupTimer->Cancel(); mHttp3BackupTimer = nullptr; } } void nsHttpTransaction::OnBackupConnectionReady(bool aTriggeredByHTTPSRR) { LOG( ("nsHttpTransaction::OnBackupConnectionReady [%p] mConnected=%d " "aTriggeredByHTTPSRR=%d", this, mConnected, aTriggeredByHTTPSRR)); if (mConnected || mClosed || mRestarted) { return; } // If HTTPS RR is in play, don't mess up the fallback mechansim of HTTPS RR. if (!aTriggeredByHTTPSRR && mOrigConnInfo) { return; } if (mConnection) { // The transaction will only be restarted when we already have a connection. // When there is no connection, this transaction will be moved to another // connection entry. SetRestartReason(aTriggeredByHTTPSRR ? TRANSACTION_RESTART_HTTPS_RR_FAST_FALLBACK : TRANSACTION_RESTART_HTTP3_FAST_FALLBACK); } mCaps |= NS_HTTP_DISALLOW_HTTP3; // Need to backup the origin conn info, since UpdateConnectionInfo() will be // called in HandleFallback() and mOrigConnInfo will be // replaced. RefPtr backup = mOrigConnInfo; HandleFallback(mBackupConnInfo); mOrigConnInfo.swap(backup); RemoveAlternateServiceUsedHeader(mRequestHead); if (mResolver) { if (mBackupConnInfo) { const nsCString& host = mBackupConnInfo->GetRoutedHost().IsEmpty() ? mBackupConnInfo->GetOrigin() : mBackupConnInfo->GetRoutedHost(); mResolver->PrefetchAddrRecord(host, Caps() & NS_HTTP_REFRESH_DNS); } if (!aTriggeredByHTTPSRR) { // We are about to use this backup connection. We shoud not try to use // HTTPS RR at this point. mResolver->Close(); mResolver = nullptr; } } } static void CreateBackupConnection( nsHttpConnectionInfo* aBackupConnInfo, nsIInterfaceRequestor* aCallbacks, uint32_t aCaps, std::function&& aResultCallback) { aBackupConnInfo->SetFallbackConnection(true); RefPtr trans = new SpeculativeTransaction( aBackupConnInfo, aCallbacks, aCaps | NS_HTTP_DISALLOW_HTTP3, std::move(aResultCallback)); uint32_t limit = StaticPrefs::network_http_http3_parallel_fallback_conn_limit(); if (limit) { trans->SetParallelSpeculativeConnectLimit(limit); trans->SetIgnoreIdle(true); } gHttpHandler->ConnMgr()->DoFallbackConnection(trans, false); } void nsHttpTransaction::OnHttp3BackupTimer() { LOG(("nsHttpTransaction::OnHttp3BackupTimer [%p]", this)); MOZ_ASSERT(OnSocketThread(), "not on socket thread"); MOZ_ASSERT(mConnInfo->IsHttp3()); mHttp3BackupTimer = nullptr; mConnInfo->CloneAsDirectRoute(getter_AddRefs(mBackupConnInfo)); MOZ_ASSERT(!mBackupConnInfo->IsHttp3()); RefPtr self = this; auto callback = [self](bool aSucceded) { if (aSucceded) { self->OnBackupConnectionReady(false); } }; CreateBackupConnection(mBackupConnInfo, mCallbacks, mCaps, std::move(callback)); } void nsHttpTransaction::OnFastFallbackTimer() { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); LOG(("nsHttpTransaction::OnFastFallbackTimer [%p] mConnected=%d", this, mConnected)); mFastFallbackTimer = nullptr; MOZ_ASSERT(mHTTPSSVCRecord && mOrigConnInfo); if (!mHTTPSSVCRecord || !mOrigConnInfo) { return; } bool echConfigUsed = gHttpHandler->EchConfigEnabled(mConnInfo->IsHttp3()) && !mConnInfo->GetEchConfig().IsEmpty(); mBackupConnInfo = PrepareFastFallbackConnInfo(echConfigUsed); if (!mBackupConnInfo) { return; } MOZ_ASSERT(!mBackupConnInfo->IsHttp3()); RefPtr self = this; auto callback = [self](bool aSucceded) { if (!aSucceded) { return; } self->mFastFallbackTriggered = true; self->OnBackupConnectionReady(true); }; CreateBackupConnection(mBackupConnInfo, mCallbacks, mCaps, std::move(callback)); } void nsHttpTransaction::HandleFallback( nsHttpConnectionInfo* aFallbackConnInfo) { if (mConnection) { MOZ_ASSERT(mActivated); // Close the transaction with NS_ERROR_NET_RESET, since we know doing this // will make transaction to be restarted. mConnection->CloseTransaction(this, NS_ERROR_NET_RESET); return; } if (!aFallbackConnInfo) { // Nothing to do here. return; } LOG(("nsHttpTransaction %p HandleFallback to connInfo[%s]", this, aFallbackConnInfo->HashKey().get())); bool foundInPendingQ = gHttpHandler->ConnMgr()->RemoveTransFromConnEntry( this, mHashKeyOfConnectionEntry); if (!foundInPendingQ) { MOZ_ASSERT(false, "transaction not in entry"); return; } // rewind streams in case we already wrote out the request nsCOMPtr seekable = do_QueryInterface(mRequestStream); if (seekable) { seekable->Seek(nsISeekableStream::NS_SEEK_SET, 0); } UpdateConnectionInfo(aFallbackConnInfo); Unused << gHttpHandler->ConnMgr()->ProcessNewTransaction(this); } NS_IMETHODIMP nsHttpTransaction::Notify(nsITimer* aTimer) { MOZ_DIAGNOSTIC_ASSERT(OnSocketThread(), "not on socket thread"); if (!gHttpHandler || !gHttpHandler->ConnMgr()) { return NS_OK; } if (aTimer == mFastFallbackTimer) { OnFastFallbackTimer(); } else if (aTimer == mHttp3BackupTimer) { OnHttp3BackupTimer(); } return NS_OK; } NS_IMETHODIMP nsHttpTransaction::GetName(nsACString& aName) { aName.AssignLiteral("nsHttpTransaction"); return NS_OK; } bool nsHttpTransaction::GetSupportsHTTP3() { return mSupportsHTTP3; } const int64_t TELEMETRY_REQUEST_SIZE_10M = (int64_t)10 * (int64_t)(1 << 20); const int64_t TELEMETRY_REQUEST_SIZE_50M = (int64_t)50 * (int64_t)(1 << 20); const int64_t TELEMETRY_REQUEST_SIZE_100M = (int64_t)100 * (int64_t)(1 << 20); void nsHttpTransaction::CollectTelemetryForUploads() { if ((mRequestSize < TELEMETRY_REQUEST_SIZE_10M) || mTimings.requestStart.IsNull() || mTimings.responseStart.IsNull()) { return; } nsAutoCString protocolVersion(nsHttp::GetProtocolVersion(mHttpVersion)); TimeDuration sendTime = mTimings.responseStart - mTimings.requestStart; double megabits = static_cast(mRequestSize) * 8.0 / 1000000.0; uint32_t mpbs = static_cast(megabits / sendTime.ToSeconds()); Telemetry::Accumulate(Telemetry::HTTP_UPLOAD_BANDWIDTH_MBPS, protocolVersion, mpbs); if ((mHttpVersion == HttpVersion::v3_0) || mSupportsHTTP3) { nsAutoCString key((mHttpVersion == HttpVersion::v3_0) ? "uses_http3" : "supports_http3"); auto hist = Telemetry::HTTP3_UPLOAD_TIME_10M_100M; if (mRequestSize <= TELEMETRY_REQUEST_SIZE_50M) { key.Append("_10_50"_ns); } else if (mRequestSize <= TELEMETRY_REQUEST_SIZE_100M) { key.Append("_50_100"_ns); } else { hist = Telemetry::HTTP3_UPLOAD_TIME_GT_100M; } Telemetry::AccumulateTimeDelta(hist, key, mTimings.requestStart, mTimings.responseStart); } } void nsHttpTransaction::GetHashKeyOfConnectionEntry(nsACString& aResult) { MutexAutoLock lock(mLock); aResult.Assign(mHashKeyOfConnectionEntry); } void nsHttpTransaction::SetIsForWebTransport(bool aIsForWebTransport) { mIsForWebTransport = aIsForWebTransport; } void nsHttpTransaction::RemoveConnection() { MutexAutoLock lock(mLock); mConnection = nullptr; } } // namespace mozilla::net