diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /netwerk/protocol/http/nsHttpConnectionMgr.cpp | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'netwerk/protocol/http/nsHttpConnectionMgr.cpp')
-rw-r--r-- | netwerk/protocol/http/nsHttpConnectionMgr.cpp | 3863 |
1 files changed, 3863 insertions, 0 deletions
diff --git a/netwerk/protocol/http/nsHttpConnectionMgr.cpp b/netwerk/protocol/http/nsHttpConnectionMgr.cpp new file mode 100644 index 0000000000..2e937d0f2a --- /dev/null +++ b/netwerk/protocol/http/nsHttpConnectionMgr.cpp @@ -0,0 +1,3863 @@ +/* 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 "HttpLog.h" + +// Log on level :5, instead of default :4. +#undef LOG +#define LOG(args) LOG5(args) +#undef LOG_ENABLED +#define LOG_ENABLED() LOG5_ENABLED() + +#include <algorithm> +#include <utility> + +#include "ConnectionHandle.h" +#include "HttpConnectionUDP.h" +#include "NullHttpTransaction.h" +#include "SpeculativeTransaction.h" +#include "mozilla/Components.h" +#include "mozilla/PerfStats.h" +#include "mozilla/ProfilerMarkers.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Unused.h" +#include "mozilla/glean/GleanMetrics.h" +#include "mozilla/net/DNS.h" +#include "mozilla/net/DashboardTypes.h" +#include "nsCOMPtr.h" +#include "nsHttpConnectionMgr.h" +#include "nsHttpHandler.h" +#include "nsIClassOfService.h" +#include "nsIDNSByTypeRecord.h" +#include "nsIDNSListener.h" +#include "nsIDNSRecord.h" +#include "nsIDNSService.h" +#include "nsIHttpChannelInternal.h" +#include "nsIPipe.h" +#include "nsIRequestContext.h" +#include "nsISocketTransport.h" +#include "nsISocketTransportService.h" +#include "nsITransport.h" +#include "nsIXPConnect.h" +#include "nsInterfaceRequestorAgg.h" +#include "nsNetCID.h" +#include "nsNetSegmentUtils.h" +#include "nsNetUtil.h" +#include "nsQueryObject.h" +#include "nsSocketTransportService2.h" +#include "nsStreamUtils.h" + +using namespace mozilla; + +namespace geckoprofiler::markers { + +struct UrlMarker { + static constexpr Span<const char> MarkerTypeName() { + return MakeStringSpan("Url"); + } + static void StreamJSONMarkerData( + mozilla::baseprofiler::SpliceableJSONWriter& aWriter, + const mozilla::ProfilerString8View& aURL) { + if (aURL.Length() != 0) { + aWriter.StringProperty("url", aURL); + } + } + static MarkerSchema MarkerTypeDisplay() { + using MS = MarkerSchema; + MS schema(MS::Location::MarkerChart, MS::Location::MarkerTable); + schema.SetTableLabel("{marker.name} - {marker.data.url}"); + schema.AddKeyFormat("url", MS::Format::Url); + return schema; + } +}; + +} // namespace geckoprofiler::markers + +namespace mozilla::net { + +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS(nsHttpConnectionMgr, nsIObserver, nsINamed) + +//----------------------------------------------------------------------------- + +nsHttpConnectionMgr::nsHttpConnectionMgr() { + LOG(("Creating nsHttpConnectionMgr @%p\n", this)); +} + +nsHttpConnectionMgr::~nsHttpConnectionMgr() { + LOG(("Destroying nsHttpConnectionMgr @%p\n", this)); + MOZ_ASSERT(mCoalescingHash.Count() == 0); + if (mTimeoutTick) mTimeoutTick->Cancel(); +} + +nsresult nsHttpConnectionMgr::EnsureSocketThreadTarget() { + nsCOMPtr<nsIEventTarget> sts; + nsCOMPtr<nsIIOService> ioService = components::IO::Service(); + if (ioService) { + nsCOMPtr<nsISocketTransportService> realSTS = + components::SocketTransport::Service(); + sts = do_QueryInterface(realSTS); + } + + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + + // do nothing if already initialized or if we've shut down + if (mSocketThreadTarget || mIsShuttingDown) return NS_OK; + + mSocketThreadTarget = sts; + + return sts ? NS_OK : NS_ERROR_NOT_AVAILABLE; +} + +nsresult nsHttpConnectionMgr::Init( + uint16_t maxUrgentExcessiveConns, uint16_t maxConns, + uint16_t maxPersistConnsPerHost, uint16_t maxPersistConnsPerProxy, + uint16_t maxRequestDelay, bool throttleEnabled, uint32_t throttleVersion, + uint32_t throttleSuspendFor, uint32_t throttleResumeFor, + uint32_t throttleReadLimit, uint32_t throttleReadInterval, + uint32_t throttleHoldTime, uint32_t throttleMaxTime, + bool beConservativeForProxy) { + LOG(("nsHttpConnectionMgr::Init\n")); + + { + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + + mMaxUrgentExcessiveConns = maxUrgentExcessiveConns; + mMaxConns = maxConns; + mMaxPersistConnsPerHost = maxPersistConnsPerHost; + mMaxPersistConnsPerProxy = maxPersistConnsPerProxy; + mMaxRequestDelay = maxRequestDelay; + + mThrottleEnabled = throttleEnabled; + mThrottleVersion = throttleVersion; + mThrottleSuspendFor = throttleSuspendFor; + mThrottleResumeFor = throttleResumeFor; + mThrottleReadLimit = throttleReadLimit; + mThrottleReadInterval = throttleReadInterval; + mThrottleHoldTime = throttleHoldTime; + mThrottleMaxTime = TimeDuration::FromMilliseconds(throttleMaxTime); + + mBeConservativeForProxy = beConservativeForProxy; + + mIsShuttingDown = false; + } + + return EnsureSocketThreadTarget(); +} + +class BoolWrapper : public ARefBase { + public: + BoolWrapper() = default; + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BoolWrapper, override) + + public: // intentional! + bool mBool{false}; + + private: + virtual ~BoolWrapper() = default; +}; + +nsresult nsHttpConnectionMgr::Shutdown() { + LOG(("nsHttpConnectionMgr::Shutdown\n")); + + RefPtr<BoolWrapper> shutdownWrapper = new BoolWrapper(); + { + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + + // do nothing if already shutdown + if (!mSocketThreadTarget) return NS_OK; + + nsresult rv = + PostEvent(&nsHttpConnectionMgr::OnMsgShutdown, 0, shutdownWrapper); + + // release our reference to the STS to prevent further events + // from being posted. this is how we indicate that we are + // shutting down. + mIsShuttingDown = true; + mSocketThreadTarget = nullptr; + + if (NS_FAILED(rv)) { + NS_WARNING("unable to post SHUTDOWN message"); + return rv; + } + } + + // wait for shutdown event to complete + SpinEventLoopUntil("nsHttpConnectionMgr::Shutdown"_ns, + [&, shutdownWrapper]() { return shutdownWrapper->mBool; }); + + return NS_OK; +} + +class ConnEvent : public Runnable { + public: + ConnEvent(nsHttpConnectionMgr* mgr, nsConnEventHandler handler, + int32_t iparam, ARefBase* vparam) + : Runnable("net::ConnEvent"), + mMgr(mgr), + mHandler(handler), + mIParam(iparam), + mVParam(vparam) {} + + NS_IMETHOD Run() override { + (mMgr->*mHandler)(mIParam, mVParam); + return NS_OK; + } + + private: + virtual ~ConnEvent() = default; + + RefPtr<nsHttpConnectionMgr> mMgr; + nsConnEventHandler mHandler; + int32_t mIParam; + RefPtr<ARefBase> mVParam; +}; + +nsresult nsHttpConnectionMgr::PostEvent(nsConnEventHandler handler, + int32_t iparam, ARefBase* vparam) { + Unused << EnsureSocketThreadTarget(); + + nsCOMPtr<nsIEventTarget> target; + { + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + target = mSocketThreadTarget; + } + + if (!target) { + NS_WARNING("cannot post event if not initialized"); + return NS_ERROR_NOT_INITIALIZED; + } + + nsCOMPtr<nsIRunnable> event = new ConnEvent(this, handler, iparam, vparam); + return target->Dispatch(event, NS_DISPATCH_NORMAL); +} + +void nsHttpConnectionMgr::PruneDeadConnectionsAfter(uint32_t timeInSeconds) { + LOG(("nsHttpConnectionMgr::PruneDeadConnectionsAfter\n")); + + if (!mTimer) mTimer = NS_NewTimer(); + + // failure to create a timer is not a fatal error, but idle connections + // will not be cleaned up until we try to use them. + if (mTimer) { + mTimeOfNextWakeUp = timeInSeconds + NowInSeconds(); + mTimer->Init(this, timeInSeconds * 1000, nsITimer::TYPE_ONE_SHOT); + } else { + NS_WARNING("failed to create: timer for pruning the dead connections!"); + } +} + +void nsHttpConnectionMgr::ConditionallyStopPruneDeadConnectionsTimer() { + // Leave the timer in place if there are connections that potentially + // need management + if (mNumIdleConns || + (mNumActiveConns && StaticPrefs::network_http_http2_enabled())) { + return; + } + + LOG(("nsHttpConnectionMgr::StopPruneDeadConnectionsTimer\n")); + + // Reset mTimeOfNextWakeUp so that we can find a new shortest value. + mTimeOfNextWakeUp = UINT64_MAX; + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } +} + +void nsHttpConnectionMgr::ConditionallyStopTimeoutTick() { + LOG( + ("nsHttpConnectionMgr::ConditionallyStopTimeoutTick " + "armed=%d active=%d\n", + mTimeoutTickArmed, mNumActiveConns)); + + if (!mTimeoutTickArmed) return; + + if (mNumActiveConns) return; + + LOG(("nsHttpConnectionMgr::ConditionallyStopTimeoutTick stop==true\n")); + + mTimeoutTick->Cancel(); + mTimeoutTickArmed = false; +} + +//----------------------------------------------------------------------------- +// nsHttpConnectionMgr::nsINamed +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpConnectionMgr::GetName(nsACString& aName) { + aName.AssignLiteral("nsHttpConnectionMgr"); + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsHttpConnectionMgr::nsIObserver +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +nsHttpConnectionMgr::Observe(nsISupports* subject, const char* topic, + const char16_t* data) { + LOG(("nsHttpConnectionMgr::Observe [topic=\"%s\"]\n", topic)); + + if (0 == strcmp(topic, NS_TIMER_CALLBACK_TOPIC)) { + nsCOMPtr<nsITimer> timer = do_QueryInterface(subject); + if (timer == mTimer) { + Unused << PruneDeadConnections(); + } else if (timer == mTimeoutTick) { + TimeoutTick(); + } else if (timer == mTrafficTimer) { + Unused << PruneNoTraffic(); + } else if (timer == mThrottleTicker) { + ThrottlerTick(); + } else if (timer == mDelayedResumeReadTimer) { + ResumeBackgroundThrottledTransactions(); + } else { + MOZ_ASSERT(false, "unexpected timer-callback"); + LOG(("Unexpected timer object\n")); + return NS_ERROR_UNEXPECTED; + } + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- + +nsresult nsHttpConnectionMgr::AddTransaction(HttpTransactionShell* trans, + int32_t priority) { + LOG(("nsHttpConnectionMgr::AddTransaction [trans=%p %d]\n", trans, priority)); + // Make sure a transaction is not in a pending queue. + CheckTransInPendingQueue(trans->AsHttpTransaction()); + return PostEvent(&nsHttpConnectionMgr::OnMsgNewTransaction, priority, + trans->AsHttpTransaction()); +} + +class NewTransactionData : public ARefBase { + public: + NewTransactionData(nsHttpTransaction* trans, int32_t priority, + nsHttpTransaction* transWithStickyConn) + : mTrans(trans), + mPriority(priority), + mTransWithStickyConn(transWithStickyConn) {} + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(NewTransactionData, override) + + RefPtr<nsHttpTransaction> mTrans; + int32_t mPriority; + RefPtr<nsHttpTransaction> mTransWithStickyConn; + + private: + virtual ~NewTransactionData() = default; +}; + +nsresult nsHttpConnectionMgr::AddTransactionWithStickyConn( + HttpTransactionShell* trans, int32_t priority, + HttpTransactionShell* transWithStickyConn) { + LOG( + ("nsHttpConnectionMgr::AddTransactionWithStickyConn " + "[trans=%p %d transWithStickyConn=%p]\n", + trans, priority, transWithStickyConn)); + // Make sure a transaction is not in a pending queue. + CheckTransInPendingQueue(trans->AsHttpTransaction()); + + RefPtr<NewTransactionData> data = + new NewTransactionData(trans->AsHttpTransaction(), priority, + transWithStickyConn->AsHttpTransaction()); + return PostEvent(&nsHttpConnectionMgr::OnMsgNewTransactionWithStickyConn, 0, + data); +} + +nsresult nsHttpConnectionMgr::RescheduleTransaction(HttpTransactionShell* trans, + int32_t priority) { + LOG(("nsHttpConnectionMgr::RescheduleTransaction [trans=%p %d]\n", trans, + priority)); + return PostEvent(&nsHttpConnectionMgr::OnMsgReschedTransaction, priority, + trans->AsHttpTransaction()); +} + +void nsHttpConnectionMgr::UpdateClassOfServiceOnTransaction( + HttpTransactionShell* trans, const ClassOfService& classOfService) { + LOG( + ("nsHttpConnectionMgr::UpdateClassOfServiceOnTransaction [trans=%p " + "classOfService flags=%" PRIu32 " inc=%d]\n", + trans, static_cast<uint32_t>(classOfService.Flags()), + classOfService.Incremental())); + + Unused << EnsureSocketThreadTarget(); + + nsCOMPtr<nsIEventTarget> target; + { + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + target = mSocketThreadTarget; + } + + if (!target) { + NS_WARNING("cannot post event if not initialized"); + return; + } + + RefPtr<nsHttpConnectionMgr> self(this); + Unused << target->Dispatch(NS_NewRunnableFunction( + "nsHttpConnectionMgr::CallUpdateClassOfServiceOnTransaction", + [cos{classOfService}, self{std::move(self)}, trans = RefPtr{trans}]() { + self->OnMsgUpdateClassOfServiceOnTransaction( + cos, trans->AsHttpTransaction()); + })); +} + +nsresult nsHttpConnectionMgr::CancelTransaction(HttpTransactionShell* trans, + nsresult reason) { + LOG(("nsHttpConnectionMgr::CancelTransaction [trans=%p reason=%" PRIx32 "]\n", + trans, static_cast<uint32_t>(reason))); + return PostEvent(&nsHttpConnectionMgr::OnMsgCancelTransaction, + static_cast<int32_t>(reason), trans->AsHttpTransaction()); +} + +nsresult nsHttpConnectionMgr::PruneDeadConnections() { + return PostEvent(&nsHttpConnectionMgr::OnMsgPruneDeadConnections); +} + +// +// Called after a timeout. Check for active connections that have had no +// traffic since they were "marked" and nuke them. +nsresult nsHttpConnectionMgr::PruneNoTraffic() { + LOG(("nsHttpConnectionMgr::PruneNoTraffic\n")); + mPruningNoTraffic = true; + return PostEvent(&nsHttpConnectionMgr::OnMsgPruneNoTraffic); +} + +nsresult nsHttpConnectionMgr::VerifyTraffic() { + LOG(("nsHttpConnectionMgr::VerifyTraffic\n")); + return PostEvent(&nsHttpConnectionMgr::OnMsgVerifyTraffic); +} + +nsresult nsHttpConnectionMgr::DoShiftReloadConnectionCleanup() { + return PostEvent(&nsHttpConnectionMgr::OnMsgDoShiftReloadConnectionCleanup, 0, + nullptr); +} + +nsresult nsHttpConnectionMgr::DoShiftReloadConnectionCleanupWithConnInfo( + nsHttpConnectionInfo* aCI) { + if (!aCI) { + return NS_ERROR_INVALID_ARG; + } + + RefPtr<nsHttpConnectionInfo> ci = aCI->Clone(); + return PostEvent(&nsHttpConnectionMgr::OnMsgDoShiftReloadConnectionCleanup, 0, + ci); +} + +nsresult nsHttpConnectionMgr::DoSingleConnectionCleanup( + nsHttpConnectionInfo* aCI) { + if (!aCI) { + return NS_ERROR_INVALID_ARG; + } + + RefPtr<nsHttpConnectionInfo> ci = aCI->Clone(); + return PostEvent(&nsHttpConnectionMgr::OnMsgDoSingleConnectionCleanup, 0, ci); +} + +class SpeculativeConnectArgs : public ARefBase { + public: + SpeculativeConnectArgs() = default; + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SpeculativeConnectArgs, override) + + public: // intentional! + RefPtr<SpeculativeTransaction> mTrans; + + bool mFetchHTTPSRR{false}; + + private: + virtual ~SpeculativeConnectArgs() = default; + NS_DECL_OWNINGTHREAD +}; + +nsresult nsHttpConnectionMgr::SpeculativeConnect( + nsHttpConnectionInfo* ci, nsIInterfaceRequestor* callbacks, uint32_t caps, + SpeculativeTransaction* aTransaction, bool aFetchHTTPSRR) { + if (!IsNeckoChild() && NS_IsMainThread()) { + // HACK: make sure PSM gets initialized on the main thread. + net_EnsurePSMInit(); + } + + LOG(("nsHttpConnectionMgr::SpeculativeConnect [ci=%s]\n", + ci->HashKey().get())); + + nsCOMPtr<nsISpeculativeConnectionOverrider> overrider = + do_GetInterface(callbacks); + + bool allow1918 = overrider ? overrider->GetAllow1918() : false; + + // Hosts that are Local IP Literals should not be speculatively + // connected - Bug 853423. + if ((!allow1918) && ci && ci->HostIsLocalIPLiteral()) { + LOG( + ("nsHttpConnectionMgr::SpeculativeConnect skipping RFC1918 " + "address [%s]", + ci->Origin())); + return NS_OK; + } + + nsCString url = ci->EndToEndSSL() ? "https://"_ns : "http://"_ns; + url += ci->GetOrigin(); + PROFILER_MARKER("SpeculativeConnect", NETWORK, {}, UrlMarker, url); + + RefPtr<SpeculativeConnectArgs> args = new SpeculativeConnectArgs(); + + // Wrap up the callbacks and the target to ensure they're released on the + // target thread properly. + nsCOMPtr<nsIInterfaceRequestor> wrappedCallbacks; + NS_NewInterfaceRequestorAggregation(callbacks, nullptr, + getter_AddRefs(wrappedCallbacks)); + + caps |= ci->GetAnonymous() ? NS_HTTP_LOAD_ANONYMOUS : 0; + caps |= NS_HTTP_ERROR_SOFTLY; + args->mTrans = aTransaction + ? aTransaction + : new SpeculativeTransaction(ci, wrappedCallbacks, caps); + args->mFetchHTTPSRR = aFetchHTTPSRR; + + if (overrider) { + args->mTrans->SetParallelSpeculativeConnectLimit( + overrider->GetParallelSpeculativeConnectLimit()); + args->mTrans->SetIgnoreIdle(overrider->GetIgnoreIdle()); + args->mTrans->SetIsFromPredictor(overrider->GetIsFromPredictor()); + args->mTrans->SetAllow1918(overrider->GetAllow1918()); + } + + return PostEvent(&nsHttpConnectionMgr::OnMsgSpeculativeConnect, 0, args); +} + +nsresult nsHttpConnectionMgr::GetSocketThreadTarget(nsIEventTarget** target) { + Unused << EnsureSocketThreadTarget(); + + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + nsCOMPtr<nsIEventTarget> temp(mSocketThreadTarget); + temp.forget(target); + return NS_OK; +} + +nsresult nsHttpConnectionMgr::ReclaimConnection(HttpConnectionBase* conn) { + LOG(("nsHttpConnectionMgr::ReclaimConnection [conn=%p]\n", conn)); + + Unused << EnsureSocketThreadTarget(); + + nsCOMPtr<nsIEventTarget> target; + { + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + target = mSocketThreadTarget; + } + + if (!target) { + NS_WARNING("cannot post event if not initialized"); + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr<HttpConnectionBase> connRef(conn); + RefPtr<nsHttpConnectionMgr> self(this); + return target->Dispatch(NS_NewRunnableFunction( + "nsHttpConnectionMgr::CallReclaimConnection", + [conn{std::move(connRef)}, self{std::move(self)}]() { + self->OnMsgReclaimConnection(conn); + })); +} + +// A structure used to marshall 6 pointers across the various necessary +// threads to complete an HTTP upgrade. +class nsCompleteUpgradeData : public ARefBase { + public: + nsCompleteUpgradeData(nsHttpTransaction* aTrans, + nsIHttpUpgradeListener* aListener, bool aJsWrapped) + : mTrans(aTrans), mUpgradeListener(aListener), mJsWrapped(aJsWrapped) {} + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(nsCompleteUpgradeData, override) + + RefPtr<nsHttpTransaction> mTrans; + nsCOMPtr<nsIHttpUpgradeListener> mUpgradeListener; + + nsCOMPtr<nsISocketTransport> mSocketTransport; + nsCOMPtr<nsIAsyncInputStream> mSocketIn; + nsCOMPtr<nsIAsyncOutputStream> mSocketOut; + + bool mJsWrapped; + + private: + virtual ~nsCompleteUpgradeData() { + NS_ReleaseOnMainThread("nsCompleteUpgradeData.mUpgradeListener", + mUpgradeListener.forget()); + } +}; + +nsresult nsHttpConnectionMgr::CompleteUpgrade( + HttpTransactionShell* aTrans, nsIHttpUpgradeListener* aUpgradeListener) { + // test if aUpgradeListener is a wrapped JsObject + nsCOMPtr<nsIXPConnectWrappedJS> wrapper = do_QueryInterface(aUpgradeListener); + + bool wrapped = !!wrapper; + + RefPtr<nsCompleteUpgradeData> data = new nsCompleteUpgradeData( + aTrans->AsHttpTransaction(), aUpgradeListener, wrapped); + return PostEvent(&nsHttpConnectionMgr::OnMsgCompleteUpgrade, 0, data); +} + +nsresult nsHttpConnectionMgr::UpdateParam(nsParamName name, uint16_t value) { + uint32_t param = (uint32_t(name) << 16) | uint32_t(value); + return PostEvent(&nsHttpConnectionMgr::OnMsgUpdateParam, + static_cast<int32_t>(param), nullptr); +} + +nsresult nsHttpConnectionMgr::ProcessPendingQ(nsHttpConnectionInfo* aCI) { + LOG(("nsHttpConnectionMgr::ProcessPendingQ [ci=%s]\n", aCI->HashKey().get())); + RefPtr<nsHttpConnectionInfo> ci; + if (aCI) { + ci = aCI->Clone(); + } + return PostEvent(&nsHttpConnectionMgr::OnMsgProcessPendingQ, 0, ci); +} + +nsresult nsHttpConnectionMgr::ProcessPendingQ() { + LOG(("nsHttpConnectionMgr::ProcessPendingQ [All CI]\n")); + return PostEvent(&nsHttpConnectionMgr::OnMsgProcessPendingQ, 0, nullptr); +} + +void nsHttpConnectionMgr::OnMsgUpdateRequestTokenBucket(int32_t, + ARefBase* param) { + EventTokenBucket* tokenBucket = static_cast<EventTokenBucket*>(param); + gHttpHandler->SetRequestTokenBucket(tokenBucket); +} + +nsresult nsHttpConnectionMgr::UpdateRequestTokenBucket( + EventTokenBucket* aBucket) { + // Call From main thread when a new EventTokenBucket has been made in order + // to post the new value to the socket thread. + return PostEvent(&nsHttpConnectionMgr::OnMsgUpdateRequestTokenBucket, 0, + aBucket); +} + +nsresult nsHttpConnectionMgr::ClearConnectionHistory() { + return PostEvent(&nsHttpConnectionMgr::OnMsgClearConnectionHistory, 0, + nullptr); +} + +void nsHttpConnectionMgr::OnMsgClearConnectionHistory(int32_t, + ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("nsHttpConnectionMgr::OnMsgClearConnectionHistory")); + + for (auto iter = mCT.Iter(); !iter.Done(); iter.Next()) { + RefPtr<ConnectionEntry> ent = iter.Data(); + if (ent->IdleConnectionsLength() == 0 && ent->ActiveConnsLength() == 0 && + ent->DnsAndConnectSocketsLength() == 0 && + ent->UrgentStartQueueLength() == 0 && ent->PendingQueueLength() == 0 && + !ent->mDoNotDestroy) { + iter.Remove(); + } + } +} + +nsresult nsHttpConnectionMgr::CloseIdleConnection(nsHttpConnection* conn) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnectionMgr::CloseIdleConnection %p conn=%p", this, conn)); + + if (!conn->ConnectionInfo()) { + return NS_ERROR_UNEXPECTED; + } + + ConnectionEntry* ent = mCT.GetWeak(conn->ConnectionInfo()->HashKey()); + + if (!ent || NS_FAILED(ent->CloseIdleConnection(conn))) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +nsresult nsHttpConnectionMgr::RemoveIdleConnection(nsHttpConnection* conn) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("nsHttpConnectionMgr::RemoveIdleConnection %p conn=%p", this, conn)); + + if (!conn->ConnectionInfo()) { + return NS_ERROR_UNEXPECTED; + } + + ConnectionEntry* ent = mCT.GetWeak(conn->ConnectionInfo()->HashKey()); + + if (!ent || NS_FAILED(ent->RemoveIdleConnection(conn))) { + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +HttpConnectionBase* nsHttpConnectionMgr::FindCoalescableConnectionByHashKey( + ConnectionEntry* ent, const nsCString& key, bool justKidding, bool aNoHttp2, + bool aNoHttp3) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(!aNoHttp2 || !aNoHttp3); + MOZ_ASSERT(ent->mConnInfo); + nsHttpConnectionInfo* ci = ent->mConnInfo; + + nsTArray<nsWeakPtr>* listOfWeakConns = mCoalescingHash.Get(key); + if (!listOfWeakConns) { + return nullptr; + } + + uint32_t listLen = listOfWeakConns->Length(); + for (uint32_t j = 0; j < listLen;) { + RefPtr<HttpConnectionBase> potentialMatch = + do_QueryReferent(listOfWeakConns->ElementAt(j)); + if (!potentialMatch) { + // This is a connection that needs to be removed from the list + LOG( + ("FindCoalescableConnectionByHashKey() found old conn %p that has " + "null weak ptr - removing\n", + listOfWeakConns->ElementAt(j).get())); + if (j != listLen - 1) { + listOfWeakConns->Elements()[j] = + listOfWeakConns->Elements()[listLen - 1]; + } + listOfWeakConns->RemoveLastElement(); + MOZ_ASSERT(listOfWeakConns->Length() == listLen - 1); + listLen--; + continue; // without adjusting iterator + } + + if (aNoHttp3 && potentialMatch->UsingHttp3()) { + j++; + continue; + } + if (aNoHttp2 && potentialMatch->UsingSpdy()) { + j++; + continue; + } + bool couldJoin; + if (justKidding) { + couldJoin = + potentialMatch->TestJoinConnection(ci->GetOrigin(), ci->OriginPort()); + } else { + couldJoin = + potentialMatch->JoinConnection(ci->GetOrigin(), ci->OriginPort()); + } + if (couldJoin) { + LOG( + ("FindCoalescableConnectionByHashKey() found match conn=%p key=%s " + "newCI=%s matchedCI=%s join ok\n", + potentialMatch.get(), key.get(), ci->HashKey().get(), + potentialMatch->ConnectionInfo()->HashKey().get())); + return potentialMatch.get(); + } + LOG( + ("FindCoalescableConnectionByHashKey() found match conn=%p key=%s " + "newCI=%s matchedCI=%s join failed\n", + potentialMatch.get(), key.get(), ci->HashKey().get(), + potentialMatch->ConnectionInfo()->HashKey().get())); + + ++j; // bypassed by continue when weakptr fails + } + + if (!listLen) { // shrunk to 0 while iterating + LOG(("FindCoalescableConnectionByHashKey() removing empty list element\n")); + mCoalescingHash.Remove(key); + } + return nullptr; +} + +static void BuildOriginFrameHashKey(nsACString& newKey, + nsHttpConnectionInfo* ci, + const nsACString& host, int32_t port) { + newKey.Assign(host); + if (ci->GetAnonymous()) { + newKey.AppendLiteral("~A:"); + } else { + newKey.AppendLiteral("~.:"); + } + if (ci->GetFallbackConnection()) { + newKey.AppendLiteral("~F:"); + } else { + newKey.AppendLiteral("~.:"); + } + newKey.AppendInt(port); + newKey.AppendLiteral("/["); + nsAutoCString suffix; + ci->GetOriginAttributes().CreateSuffix(suffix); + newKey.Append(suffix); + newKey.AppendLiteral("]viaORIGIN.FRAME"); +} + +HttpConnectionBase* nsHttpConnectionMgr::FindCoalescableConnection( + ConnectionEntry* ent, bool justKidding, bool aNoHttp2, bool aNoHttp3) { + MOZ_ASSERT(!aNoHttp2 || !aNoHttp3); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(ent->mConnInfo); + nsHttpConnectionInfo* ci = ent->mConnInfo; + LOG(("FindCoalescableConnection %s\n", ci->HashKey().get())); + // First try and look it up by origin frame + nsCString newKey; + BuildOriginFrameHashKey(newKey, ci, ci->GetOrigin(), ci->OriginPort()); + HttpConnectionBase* conn = FindCoalescableConnectionByHashKey( + ent, newKey, justKidding, aNoHttp2, aNoHttp3); + if (conn) { + LOG(("FindCoalescableConnection(%s) match conn %p on frame key %s\n", + ci->HashKey().get(), conn, newKey.get())); + return conn; + } + + // now check for DNS based keys + // deleted conns (null weak pointers) are removed from list + uint32_t keyLen = ent->mCoalescingKeys.Length(); + for (uint32_t i = 0; i < keyLen; ++i) { + conn = FindCoalescableConnectionByHashKey(ent, ent->mCoalescingKeys[i], + justKidding, aNoHttp2, aNoHttp3); + if (conn) { + LOG(("FindCoalescableConnection(%s) match conn %p on dns key %s\n", + ci->HashKey().get(), conn, ent->mCoalescingKeys[i].get())); + return conn; + } + } + + LOG(("FindCoalescableConnection(%s) no matching conn\n", + ci->HashKey().get())); + return nullptr; +} + +void nsHttpConnectionMgr::UpdateCoalescingForNewConn( + HttpConnectionBase* newConn, ConnectionEntry* ent, bool aNoHttp3) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(newConn); + MOZ_ASSERT(newConn->ConnectionInfo()); + MOZ_ASSERT(ent); + MOZ_ASSERT(mCT.GetWeak(newConn->ConnectionInfo()->HashKey()) == ent); + LOG(("UpdateCoalescingForNewConn newConn=%p aNoHttp3=%d", newConn, aNoHttp3)); + if (newConn->ConnectionInfo()->GetWebTransport()) { + LOG(("Don't coalesce a WebTransport conn %p", newConn)); + // TODO: implement this properly in bug 1815735. + return; + } + + HttpConnectionBase* existingConn = + FindCoalescableConnection(ent, true, false, false); + if (existingConn) { + // Prefer http3 connection, but allow an HTTP/2 connection if it is used for + // WebSocket. + if (newConn->UsingHttp3() && existingConn->UsingSpdy()) { + RefPtr<nsHttpConnection> connTCP = do_QueryObject(existingConn); + if (connTCP && !connTCP->IsForWebSocket()) { + LOG( + ("UpdateCoalescingForNewConn() found existing active H2 conn that " + "could have served newConn, but new connection is H3, therefore " + "close the H2 conncetion")); + existingConn->SetCloseReason( + ConnectionCloseReason::CLOSE_EXISTING_CONN_FOR_COALESCING); + existingConn->DontReuse(); + } + } else if (existingConn->UsingHttp3() && newConn->UsingSpdy()) { + RefPtr<nsHttpConnection> connTCP = do_QueryObject(newConn); + if (connTCP && !connTCP->IsForWebSocket() && !aNoHttp3) { + LOG( + ("UpdateCoalescingForNewConn() found existing active H3 conn that " + "could have served H2 newConn graceful close of newConn=%p to " + "migrate to existingConn %p\n", + newConn, existingConn)); + existingConn->SetCloseReason( + ConnectionCloseReason::CLOSE_NEW_CONN_FOR_COALESCING); + newConn->DontReuse(); + return; + } + } else { + LOG( + ("UpdateCoalescingForNewConn() found existing active conn that could " + "have served newConn " + "graceful close of newConn=%p to migrate to existingConn %p\n", + newConn, existingConn)); + existingConn->SetCloseReason( + ConnectionCloseReason::CLOSE_NEW_CONN_FOR_COALESCING); + newConn->DontReuse(); + return; + } + } + + // This connection might go into the mCoalescingHash for new transactions to + // be coalesced onto if it can accept new transactions + if (!newConn->CanDirectlyActivate()) { + return; + } + + uint32_t keyLen = ent->mCoalescingKeys.Length(); + for (uint32_t i = 0; i < keyLen; ++i) { + LOG(( + "UpdateCoalescingForNewConn() registering newConn %p %s under key %s\n", + newConn, newConn->ConnectionInfo()->HashKey().get(), + ent->mCoalescingKeys[i].get())); + + mCoalescingHash + .LookupOrInsertWith( + ent->mCoalescingKeys[i], + [] { + LOG(("UpdateCoalescingForNewConn() need new list element\n")); + return MakeUnique<nsTArray<nsWeakPtr>>(1); + }) + ->AppendElement(do_GetWeakReference( + static_cast<nsISupportsWeakReference*>(newConn))); + } + + // this is a new connection that can be coalesced onto. hooray! + // if there are other connection to this entry (e.g. + // some could still be handshaking, shutting down, etc..) then close + // them down after any transactions that are on them are complete. + // This probably happened due to the parallel connection algorithm + // that is used only before the host is known to speak h2. + ent->MakeAllDontReuseExcept(newConn); +} + +// This function lets a connection, after completing the NPN phase, +// report whether or not it is using spdy through the usingSpdy +// argument. It would not be necessary if NPN were driven out of +// the connection manager. The connection entry associated with the +// connection is then updated to indicate whether or not we want to use +// spdy with that host and update the coalescing hash +// entries used for de-sharding hostsnames. +void nsHttpConnectionMgr::ReportSpdyConnection(nsHttpConnection* conn, + bool usingSpdy, + bool disallowHttp3) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (!conn->ConnectionInfo()) { + return; + } + ConnectionEntry* ent = mCT.GetWeak(conn->ConnectionInfo()->HashKey()); + if (!ent || !usingSpdy) { + return; + } + + ent->mUsingSpdy = true; + mNumSpdyHttp3ActiveConns++; + + // adjust timeout timer + uint32_t ttl = conn->TimeToLive(); + uint64_t timeOfExpire = NowInSeconds() + ttl; + if (!mTimer || timeOfExpire < mTimeOfNextWakeUp) { + PruneDeadConnectionsAfter(ttl); + } + + UpdateCoalescingForNewConn(conn, ent, disallowHttp3); + + nsresult rv = ProcessPendingQ(ent->mConnInfo); + if (NS_FAILED(rv)) { + LOG( + ("ReportSpdyConnection conn=%p ent=%p " + "failed to process pending queue (%08x)\n", + conn, ent, static_cast<uint32_t>(rv))); + } + rv = PostEvent(&nsHttpConnectionMgr::OnMsgProcessAllSpdyPendingQ); + if (NS_FAILED(rv)) { + LOG( + ("ReportSpdyConnection conn=%p ent=%p " + "failed to post event (%08x)\n", + conn, ent, static_cast<uint32_t>(rv))); + } +} + +void nsHttpConnectionMgr::ReportHttp3Connection(HttpConnectionBase* conn) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (!conn->ConnectionInfo()) { + return; + } + ConnectionEntry* ent = mCT.GetWeak(conn->ConnectionInfo()->HashKey()); + if (!ent) { + return; + } + + mNumSpdyHttp3ActiveConns++; + + UpdateCoalescingForNewConn(conn, ent, false); + nsresult rv = ProcessPendingQ(ent->mConnInfo); + if (NS_FAILED(rv)) { + LOG( + ("ReportHttp3Connection conn=%p ent=%p " + "failed to process pending queue (%08x)\n", + conn, ent, static_cast<uint32_t>(rv))); + } + rv = PostEvent(&nsHttpConnectionMgr::OnMsgProcessAllSpdyPendingQ); + if (NS_FAILED(rv)) { + LOG( + ("ReportHttp3Connection conn=%p ent=%p " + "failed to post event (%08x)\n", + conn, ent, static_cast<uint32_t>(rv))); + } +} + +//----------------------------------------------------------------------------- +bool nsHttpConnectionMgr::DispatchPendingQ( + nsTArray<RefPtr<PendingTransactionInfo>>& pendingQ, ConnectionEntry* ent, + bool considerAll) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + PendingTransactionInfo* pendingTransInfo = nullptr; + nsresult rv; + bool dispatchedSuccessfully = false; + + // if !considerAll iterate the pending list until one is dispatched + // successfully. Keep iterating afterwards only until a transaction fails to + // dispatch. if considerAll == true then try and dispatch all items. + for (uint32_t i = 0; i < pendingQ.Length();) { + pendingTransInfo = pendingQ[i]; + + bool alreadyDnsAndConnectSocketOrWaitingForTLS = + pendingTransInfo->IsAlreadyClaimedInitializingConn(); + + rv = TryDispatchTransaction(ent, alreadyDnsAndConnectSocketOrWaitingForTLS, + pendingTransInfo); + if (NS_SUCCEEDED(rv) || (rv != NS_ERROR_NOT_AVAILABLE)) { + if (NS_SUCCEEDED(rv)) { + LOG((" dispatching pending transaction...\n")); + } else { + LOG( + (" removing pending transaction based on " + "TryDispatchTransaction returning hard error %" PRIx32 "\n", + static_cast<uint32_t>(rv))); + } + if (pendingQ.RemoveElement(pendingTransInfo)) { + // pendingTransInfo is now potentially destroyed + dispatchedSuccessfully = true; + continue; // dont ++i as we just made the array shorter + } + + LOG((" transaction not found in pending queue\n")); + } + + if (dispatchedSuccessfully && !considerAll) break; + + ++i; + } + return dispatchedSuccessfully; +} + +uint32_t nsHttpConnectionMgr::MaxPersistConnections( + ConnectionEntry* ent) const { + if (ent->mConnInfo->UsingHttpProxy() && !ent->mConnInfo->UsingConnect()) { + return static_cast<uint32_t>(mMaxPersistConnsPerProxy); + } + + return static_cast<uint32_t>(mMaxPersistConnsPerHost); +} + +void nsHttpConnectionMgr::PreparePendingQForDispatching( + ConnectionEntry* ent, nsTArray<RefPtr<PendingTransactionInfo>>& pendingQ, + bool considerAll) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + pendingQ.Clear(); + + uint32_t totalCount = ent->TotalActiveConnections(); + uint32_t maxPersistConns = MaxPersistConnections(ent); + uint32_t availableConnections = + maxPersistConns > totalCount ? maxPersistConns - totalCount : 0; + + // No need to try dispatching if we reach the active connection limit. + if (!availableConnections) { + return; + } + + // Only have to get transactions from the queue whose window id is 0. + if (!gHttpHandler->ActiveTabPriority()) { + ent->AppendPendingQForFocusedWindow(0, pendingQ, availableConnections); + return; + } + + uint32_t maxFocusedWindowConnections = + availableConnections * gHttpHandler->FocusedWindowTransactionRatio(); + MOZ_ASSERT(maxFocusedWindowConnections < availableConnections); + + if (!maxFocusedWindowConnections) { + maxFocusedWindowConnections = 1; + } + + // Only need to dispatch transactions for either focused or + // non-focused window because considerAll is false. + if (!considerAll) { + ent->AppendPendingQForFocusedWindow(mCurrentBrowserId, pendingQ, + maxFocusedWindowConnections); + + if (pendingQ.IsEmpty()) { + ent->AppendPendingQForNonFocusedWindows(mCurrentBrowserId, pendingQ, + availableConnections); + } + return; + } + + uint32_t maxNonFocusedWindowConnections = + availableConnections - maxFocusedWindowConnections; + nsTArray<RefPtr<PendingTransactionInfo>> remainingPendingQ; + + ent->AppendPendingQForFocusedWindow(mCurrentBrowserId, pendingQ, + maxFocusedWindowConnections); + + if (maxNonFocusedWindowConnections) { + ent->AppendPendingQForNonFocusedWindows( + mCurrentBrowserId, remainingPendingQ, maxNonFocusedWindowConnections); + } + + // If the slots for either focused or non-focused window are not filled up + // to the availability, try to use the remaining available connections + // for the other slot (with preference for the focused window). + if (remainingPendingQ.Length() < maxNonFocusedWindowConnections) { + ent->AppendPendingQForFocusedWindow( + mCurrentBrowserId, pendingQ, + maxNonFocusedWindowConnections - remainingPendingQ.Length()); + } else if (pendingQ.Length() < maxFocusedWindowConnections) { + ent->AppendPendingQForNonFocusedWindows( + mCurrentBrowserId, remainingPendingQ, + maxFocusedWindowConnections - pendingQ.Length()); + } + + MOZ_ASSERT(pendingQ.Length() + remainingPendingQ.Length() <= + availableConnections); + + LOG( + ("nsHttpConnectionMgr::PreparePendingQForDispatching " + "focused window pendingQ.Length()=%zu" + ", remainingPendingQ.Length()=%zu\n", + pendingQ.Length(), remainingPendingQ.Length())); + + // Append elements in |remainingPendingQ| to |pendingQ|. The order in + // |pendingQ| is like: [focusedWindowTrans...nonFocusedWindowTrans]. + pendingQ.AppendElements(std::move(remainingPendingQ)); +} + +bool nsHttpConnectionMgr::ProcessPendingQForEntry(ConnectionEntry* ent, + bool considerAll) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG( + ("nsHttpConnectionMgr::ProcessPendingQForEntry " + "[ci=%s ent=%p active=%zu idle=%zu urgent-start-queue=%zu" + " queued=%zu]\n", + ent->mConnInfo->HashKey().get(), ent, ent->ActiveConnsLength(), + ent->IdleConnectionsLength(), ent->UrgentStartQueueLength(), + ent->PendingQueueLength())); + + if (LOG_ENABLED()) { + ent->PrintPendingQ(); + ent->LogConnections(); + } + + if (!ent->PendingQueueLength() && !ent->UrgentStartQueueLength()) { + return false; + } + ProcessSpdyPendingQ(ent); + + bool dispatchedSuccessfully = false; + + if (ent->UrgentStartQueueLength()) { + nsTArray<RefPtr<PendingTransactionInfo>> pendingQ; + ent->AppendPendingUrgentStartQ(pendingQ); + dispatchedSuccessfully = DispatchPendingQ(pendingQ, ent, considerAll); + for (const auto& transactionInfo : Reversed(pendingQ)) { + ent->InsertTransaction(transactionInfo); + } + } + + if (dispatchedSuccessfully && !considerAll) { + return dispatchedSuccessfully; + } + + nsTArray<RefPtr<PendingTransactionInfo>> pendingQ; + PreparePendingQForDispatching(ent, pendingQ, considerAll); + + // The only case that |pendingQ| is empty is when there is no + // connection available for dispatching. + if (pendingQ.IsEmpty()) { + return dispatchedSuccessfully; + } + + dispatchedSuccessfully |= DispatchPendingQ(pendingQ, ent, considerAll); + + // Put the leftovers into connection entry, in the same order as they + // were before to keep the natural ordering. + for (const auto& transactionInfo : Reversed(pendingQ)) { + ent->InsertTransaction(transactionInfo, true); + } + + // Only remove empty pendingQ when considerAll is true. + if (considerAll) { + ent->RemoveEmptyPendingQ(); + } + + return dispatchedSuccessfully; +} + +bool nsHttpConnectionMgr::ProcessPendingQForEntry(nsHttpConnectionInfo* ci) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + ConnectionEntry* ent = mCT.GetWeak(ci->HashKey()); + if (ent) return ProcessPendingQForEntry(ent, false); + return false; +} + +// we're at the active connection limit if any one of the following conditions +// is true: +// (1) at max-connections +// (2) keep-alive enabled and at max-persistent-connections-per-server/proxy +// (3) keep-alive disabled and at max-connections-per-server +bool nsHttpConnectionMgr::AtActiveConnectionLimit(ConnectionEntry* ent, + uint32_t caps) { + nsHttpConnectionInfo* ci = ent->mConnInfo; + uint32_t totalCount = ent->TotalActiveConnections(); + + if (ci->IsHttp3()) { + if (ci->GetWebTransport()) { + // TODO: implement this properly in bug 1815735. + return false; + } + return totalCount > 0; + } + + uint32_t maxPersistConns = MaxPersistConnections(ent); + + LOG( + ("nsHttpConnectionMgr::AtActiveConnectionLimit [ci=%s caps=%x," + "totalCount=%u, maxPersistConns=%u]\n", + ci->HashKey().get(), caps, totalCount, maxPersistConns)); + + if (caps & NS_HTTP_URGENT_START) { + if (totalCount >= (mMaxUrgentExcessiveConns + maxPersistConns)) { + LOG(( + "The number of total connections are greater than or equal to sum of " + "max urgent-start queue length and the number of max persistent " + "connections.\n")); + return true; + } + return false; + } + + // update maxconns if potentially limited by the max socket count + // this requires a dynamic reduction in the max socket count to a point + // lower than the max-connections pref. + uint32_t maxSocketCount = gHttpHandler->MaxSocketCount(); + if (mMaxConns > maxSocketCount) { + mMaxConns = maxSocketCount; + LOG(("nsHttpConnectionMgr %p mMaxConns dynamically reduced to %u", this, + mMaxConns)); + } + + // If there are more active connections than the global limit, then we're + // done. Purging idle connections won't get us below it. + if (mNumActiveConns >= mMaxConns) { + LOG((" num active conns == max conns\n")); + return true; + } + + bool result = (totalCount >= maxPersistConns); + LOG(("AtActiveConnectionLimit result: %s", result ? "true" : "false")); + return result; +} + +// returns NS_OK if a connection was started +// return NS_ERROR_NOT_AVAILABLE if a new connection cannot be made due to +// ephemeral limits +// returns other NS_ERROR on hard failure conditions +nsresult nsHttpConnectionMgr::MakeNewConnection( + ConnectionEntry* ent, PendingTransactionInfo* pendingTransInfo) { + LOG(("nsHttpConnectionMgr::MakeNewConnection %p ent=%p trans=%p", this, ent, + pendingTransInfo->Transaction())); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (ent->FindConnToClaim(pendingTransInfo)) { + return NS_OK; + } + + nsHttpTransaction* trans = pendingTransInfo->Transaction(); + + // If this host is trying to negotiate a SPDY session right now, + // don't create any new connections until the result of the + // negotiation is known. + if (!(trans->Caps() & NS_HTTP_DISALLOW_SPDY) && + (trans->Caps() & NS_HTTP_ALLOW_KEEPALIVE) && ent->RestrictConnections()) { + LOG( + ("nsHttpConnectionMgr::MakeNewConnection [ci = %s] " + "Not Available Due to RestrictConnections()\n", + ent->mConnInfo->HashKey().get())); + return NS_ERROR_NOT_AVAILABLE; + } + + // We need to make a new connection. If that is going to exceed the + // global connection limit then try and free up some room by closing + // an idle connection to another host. We know it won't select "ent" + // because we have already determined there are no idle connections + // to our destination + + if ((mNumIdleConns + mNumActiveConns + 1 >= mMaxConns) && mNumIdleConns) { + // If the global number of connections is preventing the opening of new + // connections to a host without idle connections, then close them + // regardless of their TTL. + auto iter = mCT.ConstIter(); + while (mNumIdleConns + mNumActiveConns + 1 >= mMaxConns && !iter.Done()) { + RefPtr<ConnectionEntry> entry = iter.Data(); + entry->CloseIdleConnections((mNumIdleConns + mNumActiveConns + 1) - + mMaxConns); + iter.Next(); + } + } + + if ((mNumIdleConns + mNumActiveConns + 1 >= mMaxConns) && mNumActiveConns && + StaticPrefs::network_http_http2_enabled()) { + // If the global number of connections is preventing the opening of new + // connections to a host without idle connections, then close any spdy + // ASAP. + for (const RefPtr<ConnectionEntry>& entry : mCT.Values()) { + while (entry->MakeFirstActiveSpdyConnDontReuse()) { + // Stop on <= (particularly =) because this dontreuse + // causes async close. + if (mNumIdleConns + mNumActiveConns + 1 <= mMaxConns) { + goto outerLoopEnd; + } + } + } + outerLoopEnd:; + } + + if (AtActiveConnectionLimit(ent, trans->Caps())) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv = ent->CreateDnsAndConnectSocket( + trans, trans->Caps(), false, false, + trans->GetClassOfService().Flags() & nsIClassOfService::UrgentStart, true, + pendingTransInfo); + if (NS_FAILED(rv)) { + /* hard failure */ + LOG( + ("nsHttpConnectionMgr::MakeNewConnection [ci = %s trans = %p] " + "CreateDnsAndConnectSocket() hard failure.\n", + ent->mConnInfo->HashKey().get(), trans)); + trans->Close(rv); + if (rv == NS_ERROR_NOT_AVAILABLE) rv = NS_ERROR_FAILURE; + return rv; + } + + return NS_OK; +} + +// returns OK if a connection is found for the transaction +// and the transaction is started. +// returns ERROR_NOT_AVAILABLE if no connection can be found and it +// should be queued until circumstances change +// returns other ERROR when transaction has a hard failure and should +// not remain in the pending queue +nsresult nsHttpConnectionMgr::TryDispatchTransaction( + ConnectionEntry* ent, bool onlyReusedConnection, + PendingTransactionInfo* pendingTransInfo) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsHttpTransaction* trans = pendingTransInfo->Transaction(); + + LOG( + ("nsHttpConnectionMgr::TryDispatchTransaction without conn " + "[trans=%p ci=%p ci=%s caps=%x onlyreused=%d active=%zu " + "idle=%zu]\n", + trans, ent->mConnInfo.get(), ent->mConnInfo->HashKey().get(), + uint32_t(trans->Caps()), onlyReusedConnection, ent->ActiveConnsLength(), + ent->IdleConnectionsLength())); + + uint32_t caps = trans->Caps(); + + // 0 - If this should use spdy then dispatch it post haste. + // 1 - If there is connection pressure then see if we can pipeline this on + // a connection of a matching type instead of using a new conn + // 2 - If there is an idle connection, use it! + // 3 - if class == reval or script and there is an open conn of that type + // then pipeline onto shortest pipeline of that class if limits allow + // 4 - If we aren't up against our connection limit, + // then open a new one + // 5 - Try a pipeline if we haven't already - this will be unusual because + // it implies a low connection pressure situation where + // MakeNewConnection() failed.. that is possible, but unlikely, due to + // global limits + // 6 - no connection is available - queue it + + RefPtr<HttpConnectionBase> unusedSpdyPersistentConnection; + + // step 0 + // look for existing spdy connection - that's always best because it is + // essentially pipelining without head of line blocking + + RefPtr<HttpConnectionBase> conn = GetH2orH3ActiveConn( + ent, + (!StaticPrefs::network_http_http2_enabled() || + (caps & NS_HTTP_DISALLOW_SPDY)), + (!nsHttpHandler::IsHttp3Enabled() || (caps & NS_HTTP_DISALLOW_HTTP3))); + if (conn) { + LOG(("TryingDispatchTransaction: an active h2 connection exists")); + WebSocketSupport wsSupp = conn->GetWebSocketSupport(); + if (trans->IsWebsocketUpgrade()) { + LOG(("TryingDispatchTransaction: this is a websocket upgrade")); + if (wsSupp == WebSocketSupport::NO_SUPPORT) { + LOG(( + "TryingDispatchTransaction: no support for websockets over Http2")); + // This is a websocket transaction and we already have a h2 connection + // that do not support websockets, we should disable h2 for this + // transaction. + trans->DisableSpdy(); + caps &= NS_HTTP_DISALLOW_SPDY; + trans->MakeSticky(); + } else if (wsSupp == WebSocketSupport::SUPPORTED) { + RefPtr<nsHttpConnection> connTCP = do_QueryObject(conn); + LOG(("TryingDispatchTransaction: websockets over Http2")); + + // No limit for number of websockets, dispatch transaction to the tunnel + RefPtr<nsHttpConnection> connToTunnel; + connTCP->CreateTunnelStream(trans, getter_AddRefs(connToTunnel), true); + ent->InsertIntoH2WebsocketConns(connToTunnel); + trans->SetConnection(nullptr); + connToTunnel->SetInSpdyTunnel(); // tells conn it is already in tunnel + trans->SetIsHttp2Websocket(true); + nsresult rv = DispatchTransaction(ent, trans, connToTunnel); + // need to undo NonSticky bypass for transaction reset to continue + // for correct websocket upgrade handling + trans->MakeSticky(); + return rv; + } else { + // if we aren't sure that websockets are supported yet or we are + // already at the connection limit then we queue the transaction + LOG(("TryingDispatchTransaction: unsure if websockets over Http2")); + return NS_ERROR_NOT_AVAILABLE; + } + } else { + if ((caps & NS_HTTP_ALLOW_KEEPALIVE) || + (caps & NS_HTTP_ALLOW_SPDY_WITHOUT_KEEPALIVE) || + !conn->IsExperienced()) { + LOG((" dispatch to spdy: [conn=%p]\n", conn.get())); + trans->RemoveDispatchedAsBlocking(); /* just in case */ + nsresult rv = DispatchTransaction(ent, trans, conn); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; + } + unusedSpdyPersistentConnection = conn; + } + } + + // If this is not a blocking transaction and the request context for it is + // currently processing one or more blocking transactions then we + // need to just leave it in the queue until those are complete unless it is + // explicitly marked as unblocked. + if (!(caps & NS_HTTP_LOAD_AS_BLOCKING)) { + if (!(caps & NS_HTTP_LOAD_UNBLOCKED)) { + nsIRequestContext* requestContext = trans->RequestContext(); + if (requestContext) { + uint32_t blockers = 0; + if (NS_SUCCEEDED( + requestContext->GetBlockingTransactionCount(&blockers)) && + blockers) { + // need to wait for blockers to clear + LOG((" blocked by request context: [rc=%p trans=%p blockers=%d]\n", + requestContext, trans, blockers)); + return NS_ERROR_NOT_AVAILABLE; + } + } + } + } else { + // Mark the transaction and its load group as blocking right now to prevent + // other transactions from being reordered in the queue due to slow syns. + trans->DispatchedAsBlocking(); + } + + // step 1 + // If connection pressure, then we want to favor pipelining of any kind + // h1 pipelining has been removed + + // Subject most transactions at high parallelism to rate pacing. + // It will only be actually submitted to the + // token bucket once, and if possible it is granted admission synchronously. + // It is important to leave a transaction in the pending queue when blocked by + // pacing so it can be found on cancel if necessary. + // Transactions that cause blocking or bypass it (e.g. js/css) are not rate + // limited. + if (gHttpHandler->UseRequestTokenBucket()) { + // submit even whitelisted transactions to the token bucket though they will + // not be slowed by it + bool runNow = trans->TryToRunPacedRequest(); + if (!runNow) { + if ((mNumActiveConns - mNumSpdyHttp3ActiveConns) <= + gHttpHandler->RequestTokenBucketMinParallelism()) { + runNow = true; // white list it + } else if (caps & (NS_HTTP_LOAD_AS_BLOCKING | NS_HTTP_LOAD_UNBLOCKED)) { + runNow = true; // white list it + } + } + if (!runNow) { + LOG((" blocked due to rate pacing trans=%p\n", trans)); + return NS_ERROR_NOT_AVAILABLE; + } + } + + // step 2 + // consider an idle persistent connection + bool idleConnsAllUrgent = false; + if (caps & NS_HTTP_ALLOW_KEEPALIVE) { + nsresult rv = TryDispatchTransactionOnIdleConn(ent, pendingTransInfo, true, + &idleConnsAllUrgent); + if (NS_SUCCEEDED(rv)) { + LOG((" dispatched step 2 (idle) trans=%p\n", trans)); + return NS_OK; + } + } + + // step 3 + // consider pipelining scripts and revalidations + // h1 pipelining has been removed + + // Don't dispatch if this transaction is waiting for HTTPS RR. + // This usually happens when the pref "network.dns.force_waiting_https_rr" is + // true or when echConfig is enabled. + if (trans->WaitingForHTTPSRR()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // step 4 + if (!onlyReusedConnection) { + nsresult rv = MakeNewConnection(ent, pendingTransInfo); + if (NS_SUCCEEDED(rv)) { + // this function returns NOT_AVAILABLE for asynchronous connects + LOG((" dispatched step 4 (async new conn) trans=%p\n", trans)); + return NS_ERROR_NOT_AVAILABLE; + } + + if (rv != NS_ERROR_NOT_AVAILABLE) { + // not available return codes should try next step as they are + // not hard errors. Other codes should stop now + LOG((" failed step 4 (%" PRIx32 ") trans=%p\n", + static_cast<uint32_t>(rv), trans)); + return rv; + } + + // repeat step 2 when there are only idle connections and all are urgent, + // don't respect urgency so that non-urgent transaction will be allowed + // to dispatch on an urgent-start-only marked connection to avoid + // dispatch deadlocks + if (!(trans->GetClassOfService().Flags() & + nsIClassOfService::UrgentStart) && + idleConnsAllUrgent && + ent->ActiveConnsLength() < MaxPersistConnections(ent)) { + rv = TryDispatchTransactionOnIdleConn(ent, pendingTransInfo, false); + if (NS_SUCCEEDED(rv)) { + LOG((" dispatched step 2a (idle, reuse urgent) trans=%p\n", trans)); + return NS_OK; + } + } + } + + // step 5 + // previously pipelined anything here if allowed but h1 pipelining has been + // removed + + // step 6 + if (unusedSpdyPersistentConnection) { + // to avoid deadlocks, we need to throw away this perfectly valid SPDY + // connection to make room for a new one that can service a no KEEPALIVE + // request + unusedSpdyPersistentConnection->DontReuse(); + } + + LOG((" not dispatched (queued) trans=%p\n", trans)); + return NS_ERROR_NOT_AVAILABLE; /* queue it */ +} + +nsresult nsHttpConnectionMgr::TryDispatchTransactionOnIdleConn( + ConnectionEntry* ent, PendingTransactionInfo* pendingTransInfo, + bool respectUrgency, bool* allUrgent) { + bool onlyUrgent = !!ent->IdleConnectionsLength(); + + nsHttpTransaction* trans = pendingTransInfo->Transaction(); + bool urgentTrans = + trans->GetClassOfService().Flags() & nsIClassOfService::UrgentStart; + + LOG( + ("nsHttpConnectionMgr::TryDispatchTransactionOnIdleConn, ent=%p, " + "trans=%p, urgent=%d", + ent, trans, urgentTrans)); + + RefPtr<nsHttpConnection> conn = + ent->GetIdleConnection(respectUrgency, urgentTrans, &onlyUrgent); + + if (allUrgent) { + *allUrgent = onlyUrgent; + } + + if (conn) { + // This will update the class of the connection to be the class of + // the transaction dispatched on it. + ent->InsertIntoActiveConns(conn); + nsresult rv = DispatchTransaction(ent, trans, conn); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult nsHttpConnectionMgr::DispatchTransaction(ConnectionEntry* ent, + nsHttpTransaction* trans, + HttpConnectionBase* conn) { + uint32_t caps = trans->Caps(); + int32_t priority = trans->Priority(); + nsresult rv; + + LOG( + ("nsHttpConnectionMgr::DispatchTransaction " + "[ent-ci=%s %p trans=%p caps=%x conn=%p priority=%d isHttp2=%d " + "isHttp3=%d]\n", + ent->mConnInfo->HashKey().get(), ent, trans, caps, conn, priority, + conn->UsingSpdy(), conn->UsingHttp3())); + + // It is possible for a rate-paced transaction to be dispatched independent + // of the token bucket when the amount of parallelization has changed or + // when a muxed connection (e.g. h2) becomes available. + trans->CancelPacing(NS_OK); + + TimeStamp now = TimeStamp::Now(); + auto recordPendingTimeForHTTPSRR = [&](nsCString& aKey) { + uint32_t stage = trans->HTTPSSVCReceivedStage(); + TimeDuration elapsed = now - trans->GetPendingTime(); + if (HTTPS_RR_IS_USED(stage)) { + glean::networking::transaction_wait_time_https_rr.AccumulateRawDuration( + elapsed); + + } else { + glean::networking::transaction_wait_time.AccumulateRawDuration(elapsed); + } + PerfStats::RecordMeasurement(PerfStats::Metric::HttpTransactionWaitTime, + elapsed); + }; + + nsAutoCString httpVersionkey("h1"_ns); + if (conn->UsingSpdy() || conn->UsingHttp3()) { + LOG( + ("Spdy Dispatch Transaction via Activate(). Transaction host = %s, " + "Connection host = %s\n", + trans->ConnectionInfo()->Origin(), conn->ConnectionInfo()->Origin())); + rv = conn->Activate(trans, caps, priority); + if (NS_SUCCEEDED(rv) && !trans->GetPendingTime().IsNull()) { + if (conn->UsingSpdy()) { + httpVersionkey = "h2"_ns; + AccumulateTimeDelta(Telemetry::TRANSACTION_WAIT_TIME_SPDY, + trans->GetPendingTime(), now); + } else { + httpVersionkey = "h3"_ns; + AccumulateTimeDelta(Telemetry::TRANSACTION_WAIT_TIME_HTTP3, + trans->GetPendingTime(), now); + } + recordPendingTimeForHTTPSRR(httpVersionkey); + trans->SetPendingTime(false); + } + return rv; + } + + MOZ_ASSERT(conn && !conn->Transaction(), + "DispatchTranaction() on non spdy active connection"); + + rv = DispatchAbstractTransaction(ent, trans, caps, conn, priority); + + if (NS_SUCCEEDED(rv) && !trans->GetPendingTime().IsNull()) { + AccumulateTimeDelta(Telemetry::TRANSACTION_WAIT_TIME_HTTP, + trans->GetPendingTime(), now); + recordPendingTimeForHTTPSRR(httpVersionkey); + trans->SetPendingTime(false); + } + return rv; +} + +// Use this method for dispatching nsAHttpTransction's. It can only safely be +// used upon first use of a connection when NPN has not negotiated SPDY vs +// HTTP/1 yet as multiplexing onto an existing SPDY session requires a +// concrete nsHttpTransaction +nsresult nsHttpConnectionMgr::DispatchAbstractTransaction( + ConnectionEntry* ent, nsAHttpTransaction* aTrans, uint32_t caps, + HttpConnectionBase* conn, int32_t priority) { + MOZ_ASSERT(ent); + + nsresult rv; + MOZ_ASSERT(!conn->UsingSpdy(), + "Spdy Must Not Use DispatchAbstractTransaction"); + LOG( + ("nsHttpConnectionMgr::DispatchAbstractTransaction " + "[ci=%s trans=%p caps=%x conn=%p]\n", + ent->mConnInfo->HashKey().get(), aTrans, caps, conn)); + + RefPtr<nsAHttpTransaction> transaction(aTrans); + RefPtr<ConnectionHandle> handle = new ConnectionHandle(conn); + + // give the transaction the indirect reference to the connection. + transaction->SetConnection(handle); + + rv = conn->Activate(transaction, caps, priority); + if (NS_FAILED(rv)) { + LOG((" conn->Activate failed [rv=%" PRIx32 "]\n", + static_cast<uint32_t>(rv))); + DebugOnly<nsresult> rv_remove = ent->RemoveActiveConnection(conn); + MOZ_ASSERT(NS_SUCCEEDED(rv_remove)); + + // sever back references to connection, and do so without triggering + // a call to ReclaimConnection ;-) + transaction->SetConnection(nullptr); + handle->Reset(); // destroy the connection + } + + return rv; +} + +void nsHttpConnectionMgr::ReportProxyTelemetry(ConnectionEntry* ent) { + enum { PROXY_NONE = 1, PROXY_HTTP = 2, PROXY_SOCKS = 3, PROXY_HTTPS = 4 }; + + if (!ent->mConnInfo->UsingProxy()) { + Telemetry::Accumulate(Telemetry::HTTP_PROXY_TYPE, PROXY_NONE); + } else if (ent->mConnInfo->UsingHttpsProxy()) { + Telemetry::Accumulate(Telemetry::HTTP_PROXY_TYPE, PROXY_HTTPS); + } else if (ent->mConnInfo->UsingHttpProxy()) { + Telemetry::Accumulate(Telemetry::HTTP_PROXY_TYPE, PROXY_HTTP); + } else { + Telemetry::Accumulate(Telemetry::HTTP_PROXY_TYPE, PROXY_SOCKS); + } +} + +nsresult nsHttpConnectionMgr::ProcessNewTransaction(nsHttpTransaction* trans) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + // since "adds" and "cancels" are processed asynchronously and because + // various events might trigger an "add" directly on the socket thread, + // we must take care to avoid dispatching a transaction that has already + // been canceled (see bug 190001). + if (NS_FAILED(trans->Status())) { + LOG((" transaction was canceled... dropping event!\n")); + return NS_OK; + } + + // Make sure a transaction is not in a pending queue. + CheckTransInPendingQueue(trans); + + trans->SetPendingTime(); + + RefPtr<Http2PushedStreamWrapper> pushedStreamWrapper = + trans->GetPushedStream(); + if (pushedStreamWrapper) { + Http2PushedStream* pushedStream = pushedStreamWrapper->GetStream(); + if (pushedStream) { + RefPtr<Http2Session> session = pushedStream->Session(); + LOG((" ProcessNewTransaction %p tied to h2 session push %p\n", trans, + session.get())); + return session->AddStream(trans, trans->Priority(), nullptr) + ? NS_OK + : NS_ERROR_UNEXPECTED; + } + } + + nsresult rv = NS_OK; + nsHttpConnectionInfo* ci = trans->ConnectionInfo(); + MOZ_ASSERT(ci); + MOZ_ASSERT(!ci->IsHttp3() || !(trans->Caps() & NS_HTTP_DISALLOW_HTTP3)); + + bool isWildcard = false; + ConnectionEntry* ent = GetOrCreateConnectionEntry( + ci, trans->Caps() & NS_HTTP_DISALLOW_HTTP2_PROXY, + trans->Caps() & NS_HTTP_DISALLOW_SPDY, + trans->Caps() & NS_HTTP_DISALLOW_HTTP3, &isWildcard); + MOZ_ASSERT(ent); + + if (gHttpHandler->EchConfigEnabled(ci->IsHttp3())) { + ent->MaybeUpdateEchConfig(ci); + } + + ReportProxyTelemetry(ent); + + // Check if the transaction already has a sticky reference to a connection. + // If so, then we can just use it directly by transferring its reference + // to the new connection variable instead of searching for a new one + + nsAHttpConnection* wrappedConnection = trans->Connection(); + RefPtr<HttpConnectionBase> conn; + RefPtr<PendingTransactionInfo> pendingTransInfo; + if (wrappedConnection) conn = wrappedConnection->TakeHttpConnection(); + + if (conn) { + MOZ_ASSERT(trans->Caps() & NS_HTTP_STICKY_CONNECTION); + LOG( + ("nsHttpConnectionMgr::ProcessNewTransaction trans=%p " + "sticky connection=%p\n", + trans, conn.get())); + + if (!ent->IsInActiveConns(conn)) { + LOG( + ("nsHttpConnectionMgr::ProcessNewTransaction trans=%p " + "sticky connection=%p needs to go on the active list\n", + trans, conn.get())); + + // make sure it isn't on the idle list - we expect this to be an + // unknown fresh connection + MOZ_ASSERT(!ent->IsInIdleConnections(conn)); + MOZ_ASSERT(!conn->IsExperienced()); + + ent->InsertIntoActiveConns(conn); // make it active + } + + trans->SetConnection(nullptr); + rv = DispatchTransaction(ent, trans, conn); + } else if (isWildcard) { + // We have a HTTP/2 session to the proxy, create a new tunneled + // connection. + RefPtr<HttpConnectionBase> conn = GetH2orH3ActiveConn(ent, false, true); + RefPtr<nsHttpConnection> connTCP = do_QueryObject(conn); + if (ci->UsingHttpsProxy() && ci->UsingConnect()) { + LOG(("About to create new tunnel conn from [%p]", connTCP.get())); + ConnectionEntry* specificEnt = mCT.GetWeak(ci->HashKey()); + + if (!specificEnt) { + RefPtr<nsHttpConnectionInfo> clone(ci->Clone()); + specificEnt = new ConnectionEntry(clone); + mCT.InsertOrUpdate(clone->HashKey(), RefPtr{specificEnt}); + } + + ent = specificEnt; + bool atLimit = AtActiveConnectionLimit(ent, trans->Caps()); + if (atLimit) { + rv = NS_ERROR_NOT_AVAILABLE; + } else { + RefPtr<nsHttpConnection> newTunnel; + connTCP->CreateTunnelStream(trans, getter_AddRefs(newTunnel)); + + ent->InsertIntoActiveConns(newTunnel); + trans->SetConnection(nullptr); + newTunnel->SetInSpdyTunnel(); + rv = DispatchTransaction(ent, trans, newTunnel); + // need to undo the bypass for transaction reset for proxy + trans->MakeNonRestartable(); + } + } else { + rv = DispatchTransaction(ent, trans, connTCP); + } + } else { + if (!ent->AllowHttp2()) { + trans->DisableSpdy(); + } + pendingTransInfo = new PendingTransactionInfo(trans); + rv = TryDispatchTransaction(ent, false, pendingTransInfo); + } + + if (NS_SUCCEEDED(rv)) { + LOG((" ProcessNewTransaction Dispatch Immediately trans=%p\n", trans)); + return rv; + } + + if (rv == NS_ERROR_NOT_AVAILABLE) { + if (!pendingTransInfo) { + pendingTransInfo = new PendingTransactionInfo(trans); + } + + ent->InsertTransaction(pendingTransInfo); + return NS_OK; + } + + LOG((" ProcessNewTransaction Hard Error trans=%p rv=%" PRIx32 "\n", trans, + static_cast<uint32_t>(rv))); + return rv; +} + +void nsHttpConnectionMgr::IncrementActiveConnCount() { + mNumActiveConns++; + ActivateTimeoutTick(); +} + +void nsHttpConnectionMgr::DecrementActiveConnCount(HttpConnectionBase* conn) { + MOZ_DIAGNOSTIC_ASSERT(mNumActiveConns > 0); + if (mNumActiveConns > 0) { + mNumActiveConns--; + } + + RefPtr<nsHttpConnection> connTCP = do_QueryObject(conn); + if (!connTCP || connTCP->EverUsedSpdy()) mNumSpdyHttp3ActiveConns--; + ConditionallyStopTimeoutTick(); +} + +void nsHttpConnectionMgr::StartedConnect() { + mNumActiveConns++; + ActivateTimeoutTick(); // likely disabled by RecvdConnect() +} + +void nsHttpConnectionMgr::RecvdConnect() { + MOZ_DIAGNOSTIC_ASSERT(mNumActiveConns > 0); + if (mNumActiveConns > 0) { + mNumActiveConns--; + } + + ConditionallyStopTimeoutTick(); +} + +void nsHttpConnectionMgr::DispatchSpdyPendingQ( + nsTArray<RefPtr<PendingTransactionInfo>>& pendingQ, ConnectionEntry* ent, + HttpConnectionBase* connH2, HttpConnectionBase* connH3) { + if (pendingQ.Length() == 0) { + return; + } + + nsTArray<RefPtr<PendingTransactionInfo>> leftovers; + uint32_t index; + // Dispatch all the transactions we can + for (index = 0; index < pendingQ.Length() && + ((connH3 && connH3->CanDirectlyActivate()) || + (connH2 && connH2->CanDirectlyActivate())); + ++index) { + PendingTransactionInfo* pendingTransInfo = pendingQ[index]; + + // We can not dispatch NS_HTTP_ALLOW_KEEPALIVE transactions. + if (!(pendingTransInfo->Transaction()->Caps() & NS_HTTP_ALLOW_KEEPALIVE)) { + leftovers.AppendElement(pendingTransInfo); + continue; + } + + // Try dispatching on HTTP3 first + HttpConnectionBase* conn = nullptr; + if (!(pendingTransInfo->Transaction()->Caps() & NS_HTTP_DISALLOW_HTTP3) && + connH3 && connH3->CanDirectlyActivate()) { + conn = connH3; + } else if (!(pendingTransInfo->Transaction()->Caps() & + NS_HTTP_DISALLOW_SPDY) && + connH2 && connH2->CanDirectlyActivate()) { + conn = connH2; + } else { + leftovers.AppendElement(pendingTransInfo); + continue; + } + + nsresult rv = + DispatchTransaction(ent, pendingTransInfo->Transaction(), conn); + if (NS_FAILED(rv)) { + // this cannot happen, but if due to some bug it does then + // close the transaction + MOZ_ASSERT(false, "Dispatch SPDY Transaction"); + LOG(("ProcessSpdyPendingQ Dispatch Transaction failed trans=%p\n", + pendingTransInfo->Transaction())); + pendingTransInfo->Transaction()->Close(rv); + } + } + + // Slurp up the rest of the pending queue into our leftovers bucket (we + // might have some left if conn->CanDirectlyActivate returned false) + for (; index < pendingQ.Length(); ++index) { + PendingTransactionInfo* pendingTransInfo = pendingQ[index]; + leftovers.AppendElement(pendingTransInfo); + } + + // Put the leftovers back in the pending queue and get rid of the + // transactions we dispatched + pendingQ = std::move(leftovers); +} + +// This function tries to dispatch the pending h2 or h3 transactions on +// the connection entry sent in as an argument. It will do so on the +// active h2 or h3 connection either in that same entry or from the +// coalescing hash table +void nsHttpConnectionMgr::ProcessSpdyPendingQ(ConnectionEntry* ent) { + // Look for one HTTP2 and one HTTP3 connection. + // We may have transactions that have NS_HTTP_DISALLOW_SPDY/HTTP3 set + // and we may need an alternative. + HttpConnectionBase* connH3 = GetH2orH3ActiveConn(ent, true, false); + HttpConnectionBase* connH2 = GetH2orH3ActiveConn(ent, false, true); + if ((!connH3 || !connH3->CanDirectlyActivate()) && + (!connH2 || !connH2->CanDirectlyActivate())) { + return; + } + + nsTArray<RefPtr<PendingTransactionInfo>> urgentQ; + ent->AppendPendingUrgentStartQ(urgentQ); + DispatchSpdyPendingQ(urgentQ, ent, connH2, connH3); + for (const auto& transactionInfo : Reversed(urgentQ)) { + ent->InsertTransaction(transactionInfo); + } + + if ((!connH3 || !connH3->CanDirectlyActivate()) && + (!connH2 || !connH2->CanDirectlyActivate())) { + return; + } + + nsTArray<RefPtr<PendingTransactionInfo>> pendingQ; + // XXX Get all transactions for SPDY currently. + ent->AppendPendingQForNonFocusedWindows(0, pendingQ); + DispatchSpdyPendingQ(pendingQ, ent, connH2, connH3); + + // Put the leftovers back in the pending queue. + for (const auto& transactionInfo : pendingQ) { + ent->InsertTransaction(transactionInfo); + } +} + +void nsHttpConnectionMgr::OnMsgProcessAllSpdyPendingQ(int32_t, ARefBase*) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnectionMgr::OnMsgProcessAllSpdyPendingQ\n")); + for (const auto& entry : mCT.Values()) { + ProcessSpdyPendingQ(entry.get()); + } +} + +// Given a connection entry, return an active h2 or h3 connection +// that can be directly activated or null. +HttpConnectionBase* nsHttpConnectionMgr::GetH2orH3ActiveConn( + ConnectionEntry* ent, bool aNoHttp2, bool aNoHttp3) { + if (aNoHttp2 && aNoHttp3) { + return nullptr; + } + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(ent); + + // First look at ent. If protocol that ent provides is no forbidden, + // i.e. ent use HTTP3 and !aNoHttp3 or en uses HTTP over TCP and !aNoHttp2. + if ((!aNoHttp3 && ent->IsHttp3()) || (!aNoHttp2 && !ent->IsHttp3())) { + HttpConnectionBase* conn = ent->GetH2orH3ActiveConn(); + if (conn) { + return conn; + } + } + + nsHttpConnectionInfo* ci = ent->mConnInfo; + + // there was no active HTTP2/3 connection in the connection entry, but + // there might be one in the hash table for coalescing + HttpConnectionBase* existingConn = + FindCoalescableConnection(ent, false, aNoHttp2, aNoHttp3); + if (existingConn) { + LOG( + ("GetH2orH3ActiveConn() request for ent %p %s " + "found an active connection %p in the coalescing hashtable\n", + ent, ci->HashKey().get(), existingConn)); + return existingConn; + } + + LOG( + ("GetH2orH3ActiveConn() request for ent %p %s " + "did not find an active connection\n", + ent, ci->HashKey().get())); + return nullptr; +} + +//----------------------------------------------------------------------------- + +void nsHttpConnectionMgr::AbortAndCloseAllConnections(int32_t, ARefBase*) { + if (!OnSocketThread()) { + Unused << PostEvent(&nsHttpConnectionMgr::AbortAndCloseAllConnections); + return; + } + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnectionMgr::AbortAndCloseAllConnections\n")); + for (auto iter = mCT.Iter(); !iter.Done(); iter.Next()) { + RefPtr<ConnectionEntry> ent = iter.Data(); + + // Close all active connections. + ent->CloseActiveConnections(); + + // Close all idle connections. + ent->CloseIdleConnections(); + + // Close websocket "fake" connections + ent->CloseH2WebsocketConnections(); + + ent->ClosePendingConnections(); + + // Close all pending transactions. + ent->CancelAllTransactions(NS_ERROR_ABORT); + + // Close all half open tcp connections. + ent->CloseAllDnsAndConnectSockets(); + + MOZ_ASSERT(!ent->mDoNotDestroy); + iter.Remove(); + } + + mActiveTransactions[false].Clear(); + mActiveTransactions[true].Clear(); +} + +void nsHttpConnectionMgr::OnMsgShutdown(int32_t, ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnectionMgr::OnMsgShutdown\n")); + + gHttpHandler->StopRequestTokenBucket(); + AbortAndCloseAllConnections(0, nullptr); + + // If all idle connections are removed we can stop pruning dead + // connections. + ConditionallyStopPruneDeadConnectionsTimer(); + + if (mTimeoutTick) { + mTimeoutTick->Cancel(); + mTimeoutTick = nullptr; + mTimeoutTickArmed = false; + } + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + if (mTrafficTimer) { + mTrafficTimer->Cancel(); + mTrafficTimer = nullptr; + } + DestroyThrottleTicker(); + + mCoalescingHash.Clear(); + + // signal shutdown complete + nsCOMPtr<nsIRunnable> runnable = + new ConnEvent(this, &nsHttpConnectionMgr::OnMsgShutdownConfirm, 0, param); + NS_DispatchToMainThread(runnable); +} + +void nsHttpConnectionMgr::OnMsgShutdownConfirm(int32_t priority, + ARefBase* param) { + MOZ_ASSERT(NS_IsMainThread()); + LOG(("nsHttpConnectionMgr::OnMsgShutdownConfirm\n")); + + BoolWrapper* shutdown = static_cast<BoolWrapper*>(param); + shutdown->mBool = true; +} + +void nsHttpConnectionMgr::OnMsgNewTransaction(int32_t priority, + ARefBase* param) { + nsHttpTransaction* trans = static_cast<nsHttpTransaction*>(param); + + LOG(("nsHttpConnectionMgr::OnMsgNewTransaction [trans=%p]\n", trans)); + trans->SetPriority(priority); + nsresult rv = ProcessNewTransaction(trans); + if (NS_FAILED(rv)) trans->Close(rv); // for whatever its worth +} + +void nsHttpConnectionMgr::OnMsgNewTransactionWithStickyConn(int32_t priority, + ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + NewTransactionData* data = static_cast<NewTransactionData*>(param); + LOG( + ("nsHttpConnectionMgr::OnMsgNewTransactionWithStickyConn " + "[trans=%p, transWithStickyConn=%p, conn=%p]\n", + data->mTrans.get(), data->mTransWithStickyConn.get(), + data->mTransWithStickyConn->Connection())); + + MOZ_ASSERT(data->mTransWithStickyConn && + data->mTransWithStickyConn->Caps() & NS_HTTP_STICKY_CONNECTION); + + data->mTrans->SetPriority(data->mPriority); + + RefPtr<nsAHttpConnection> conn = data->mTransWithStickyConn->Connection(); + if (conn && conn->IsPersistent()) { + // This is so far a workaround to only reuse persistent + // connection for authentication retry. See bug 459620 comment 4 + // for details. + LOG((" Reuse connection [%p] for transaction [%p]", conn.get(), + data->mTrans.get())); + data->mTrans->SetConnection(conn); + } + + nsresult rv = ProcessNewTransaction(data->mTrans); + if (NS_FAILED(rv)) { + data->mTrans->Close(rv); // for whatever its worth + } +} + +void nsHttpConnectionMgr::OnMsgReschedTransaction(int32_t priority, + ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnectionMgr::OnMsgReschedTransaction [trans=%p]\n", param)); + + RefPtr<nsHttpTransaction> trans = static_cast<nsHttpTransaction*>(param); + trans->SetPriority(priority); + + if (!trans->ConnectionInfo()) { + return; + } + ConnectionEntry* ent = mCT.GetWeak(trans->ConnectionInfo()->HashKey()); + + if (ent) { + ent->ReschedTransaction(trans); + } +} + +void nsHttpConnectionMgr::OnMsgUpdateClassOfServiceOnTransaction( + ClassOfService cos, ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG( + ("nsHttpConnectionMgr::OnMsgUpdateClassOfServiceOnTransaction " + "[trans=%p]\n", + param)); + + nsHttpTransaction* trans = static_cast<nsHttpTransaction*>(param); + + ClassOfService previous = trans->GetClassOfService(); + trans->SetClassOfService(cos); + + // incremental change alone will not trigger a reschedule + if ((previous.Flags() ^ cos.Flags()) & + (NS_HTTP_LOAD_AS_BLOCKING | NS_HTTP_LOAD_UNBLOCKED)) { + Unused << RescheduleTransaction(trans, trans->Priority()); + } +} + +void nsHttpConnectionMgr::OnMsgCancelTransaction(int32_t reason, + ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnectionMgr::OnMsgCancelTransaction [trans=%p]\n", param)); + + nsresult closeCode = static_cast<nsresult>(reason); + + // caller holds a ref to param/trans on stack + nsHttpTransaction* trans = static_cast<nsHttpTransaction*>(param); + + // + // if the transaction owns a connection and the transaction is not done, + // then ask the connection to close the transaction. otherwise, close the + // transaction directly (removing it from the pending queue first). + // + RefPtr<nsAHttpConnection> conn(trans->Connection()); + if (conn && !trans->IsDone()) { + conn->CloseTransaction(trans, closeCode); + } else { + ConnectionEntry* ent = nullptr; + if (trans->ConnectionInfo()) { + ent = mCT.GetWeak(trans->ConnectionInfo()->HashKey()); + } + if (ent && ent->RemoveTransFromPendingQ(trans)) { + LOG( + ("nsHttpConnectionMgr::OnMsgCancelTransaction [trans=%p]" + " removed from pending queue\n", + trans)); + } + + trans->Close(closeCode); + + // Cancel is a pretty strong signal that things might be hanging + // so we want to cancel any null transactions related to this connection + // entry. They are just optimizations, but they aren't hooked up to + // anything that might get canceled from the rest of gecko, so best + // to assume that's what was meant by the cancel we did receive if + // it only applied to something in the queue. + if (ent) { + ent->CloseAllActiveConnsWithNullTransactcion(closeCode); + } + } +} + +void nsHttpConnectionMgr::OnMsgProcessPendingQ(int32_t, ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + nsHttpConnectionInfo* ci = static_cast<nsHttpConnectionInfo*>(param); + + if (!ci) { + LOG(("nsHttpConnectionMgr::OnMsgProcessPendingQ [ci=nullptr]\n")); + // Try and dispatch everything + for (const auto& entry : mCT.Values()) { + Unused << ProcessPendingQForEntry(entry.get(), true); + } + return; + } + + LOG(("nsHttpConnectionMgr::OnMsgProcessPendingQ [ci=%s]\n", + ci->HashKey().get())); + + // start by processing the queue identified by the given connection info. + ConnectionEntry* ent = mCT.GetWeak(ci->HashKey()); + if (!(ent && ProcessPendingQForEntry(ent, false))) { + // if we reach here, it means that we couldn't dispatch a transaction + // for the specified connection info. walk the connection table... + for (const auto& entry : mCT.Values()) { + if (ProcessPendingQForEntry(entry.get(), false)) { + break; + } + } + } +} + +nsresult nsHttpConnectionMgr::CancelTransactions(nsHttpConnectionInfo* ci, + nsresult code) { + LOG(("nsHttpConnectionMgr::CancelTransactions %s\n", ci->HashKey().get())); + + int32_t intReason = static_cast<int32_t>(code); + return PostEvent(&nsHttpConnectionMgr::OnMsgCancelTransactions, intReason, + ci); +} + +void nsHttpConnectionMgr::OnMsgCancelTransactions(int32_t code, + ARefBase* param) { + nsresult reason = static_cast<nsresult>(code); + nsHttpConnectionInfo* ci = static_cast<nsHttpConnectionInfo*>(param); + ConnectionEntry* ent = mCT.GetWeak(ci->HashKey()); + LOG(("nsHttpConnectionMgr::OnMsgCancelTransactions %s %p\n", + ci->HashKey().get(), ent)); + if (ent) { + ent->CancelAllTransactions(reason); + } +} + +void nsHttpConnectionMgr::OnMsgPruneDeadConnections(int32_t, ARefBase*) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnectionMgr::OnMsgPruneDeadConnections\n")); + + // Reset mTimeOfNextWakeUp so that we can find a new shortest value. + mTimeOfNextWakeUp = UINT64_MAX; + + // check canreuse() for all idle connections plus any active connections on + // connection entries that are using spdy. + if (mNumIdleConns || + (mNumActiveConns && StaticPrefs::network_http_http2_enabled())) { + for (auto iter = mCT.Iter(); !iter.Done(); iter.Next()) { + RefPtr<ConnectionEntry> ent = iter.Data(); + + LOG((" pruning [ci=%s]\n", ent->mConnInfo->HashKey().get())); + + // Find out how long it will take for next idle connection to not + // be reusable anymore. + uint32_t timeToNextExpire = ent->PruneDeadConnections(); + + // If time to next expire found is shorter than time to next + // wake-up, we need to change the time for next wake-up. + if (timeToNextExpire != UINT32_MAX) { + uint32_t now = NowInSeconds(); + uint64_t timeOfNextExpire = now + timeToNextExpire; + // If pruning of dead connections is not already scheduled to + // happen or time found for next connection to expire is is + // before mTimeOfNextWakeUp, we need to schedule the pruning to + // happen after timeToNextExpire. + if (!mTimer || timeOfNextExpire < mTimeOfNextWakeUp) { + PruneDeadConnectionsAfter(timeToNextExpire); + } + } else { + ConditionallyStopPruneDeadConnectionsTimer(); + } + + ent->RemoveEmptyPendingQ(); + + // If this entry is empty, we have too many entries busy then + // we can clean it up and restart + if (mCT.Count() > 125 && ent->IdleConnectionsLength() == 0 && + ent->ActiveConnsLength() == 0 && + ent->DnsAndConnectSocketsLength() == 0 && + ent->PendingQueueLength() == 0 && + ent->UrgentStartQueueLength() == 0 && !ent->mDoNotDestroy && + (!ent->mUsingSpdy || mCT.Count() > 300)) { + LOG((" removing empty connection entry\n")); + iter.Remove(); + continue; + } + + // Otherwise use this opportunity to compact our arrays... + ent->Compact(); + } + } +} + +void nsHttpConnectionMgr::OnMsgPruneNoTraffic(int32_t, ARefBase*) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnectionMgr::OnMsgPruneNoTraffic\n")); + + // Prune connections without traffic + for (const RefPtr<ConnectionEntry>& ent : mCT.Values()) { + // Close the connections with no registered traffic. + ent->PruneNoTraffic(); + } + + mPruningNoTraffic = false; // not pruning anymore +} + +void nsHttpConnectionMgr::OnMsgVerifyTraffic(int32_t, ARefBase*) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG(("nsHttpConnectionMgr::OnMsgVerifyTraffic\n")); + + if (mPruningNoTraffic) { + // Called in the time gap when the timeout to prune notraffic + // connections has triggered but the pruning hasn't happened yet. + return; + } + + mCoalescingHash.Clear(); + + // Mark connections for traffic verification + for (const auto& entry : mCT.Values()) { + entry->VerifyTraffic(); + } + + // If the timer is already there. we just re-init it + if (!mTrafficTimer) { + mTrafficTimer = NS_NewTimer(); + } + + // failure to create a timer is not a fatal error, but dead + // connections will not be cleaned up as nicely + if (mTrafficTimer) { + // Give active connections time to get more traffic before killing + // them off. Default: 5000 milliseconds + mTrafficTimer->Init(this, gHttpHandler->NetworkChangedTimeout(), + nsITimer::TYPE_ONE_SHOT); + } else { + NS_WARNING("failed to create timer for VerifyTraffic!"); + } + // Calling ActivateTimeoutTick to ensure the next timeout tick is 1s. + ActivateTimeoutTick(); +} + +void nsHttpConnectionMgr::OnMsgDoShiftReloadConnectionCleanup(int32_t, + ARefBase* param) { + LOG(("nsHttpConnectionMgr::OnMsgDoShiftReloadConnectionCleanup\n")); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + mCoalescingHash.Clear(); + + nsHttpConnectionInfo* ci = static_cast<nsHttpConnectionInfo*>(param); + + for (const auto& entry : mCT.Values()) { + entry->ClosePersistentConnections(); + } + + if (ci) ResetIPFamilyPreference(ci); +} + +void nsHttpConnectionMgr::OnMsgDoSingleConnectionCleanup(int32_t, + ARefBase* param) { + LOG(("nsHttpConnectionMgr::OnMsgDoSingleConnectionCleanup\n")); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsHttpConnectionInfo* ci = static_cast<nsHttpConnectionInfo*>(param); + + if (!ci) { + return; + } + + ConnectionEntry* entry = mCT.GetWeak(ci->HashKey()); + if (entry) { + entry->ClosePersistentConnections(); + } + + ResetIPFamilyPreference(ci); +} + +void nsHttpConnectionMgr::OnMsgReclaimConnection(HttpConnectionBase* conn) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + // + // 1) remove the connection from the active list + // 2) if keep-alive, add connection to idle list + // 3) post event to process the pending transaction queue + // + + MOZ_ASSERT(conn); + ConnectionEntry* ent = conn->ConnectionInfo() + ? mCT.GetWeak(conn->ConnectionInfo()->HashKey()) + : nullptr; + + if (!ent) { + // this can happen if the connection is made outside of the + // connection manager and is being "reclaimed" for use with + // future transactions. HTTP/2 tunnels work like this. + bool isWildcard = false; + ent = GetOrCreateConnectionEntry(conn->ConnectionInfo(), true, false, false, + &isWildcard); + LOG( + ("nsHttpConnectionMgr::OnMsgReclaimConnection conn %p " + "forced new hash entry %s\n", + conn, conn->ConnectionInfo()->HashKey().get())); + } + + MOZ_ASSERT(ent); + RefPtr<nsHttpConnectionInfo> ci(ent->mConnInfo); + + LOG(("nsHttpConnectionMgr::OnMsgReclaimConnection [ent=%p conn=%p]\n", ent, + conn)); + + // If the connection is in the active list, remove that entry + // and the reference held by the mActiveConns list. + // This is never the final reference on conn as the event context + // is also holding one that is released at the end of this function. + + RefPtr<nsHttpConnection> connTCP = do_QueryObject(conn); + if (!connTCP || connTCP->EverUsedSpdy()) { + // Spdyand Http3 connections aren't reused in the traditional HTTP way in + // the idleconns list, they are actively multplexed as active + // conns. Even when they have 0 transactions on them they are + // considered active connections. So when one is reclaimed it + // is really complete and is meant to be shut down and not + // reused. + conn->DontReuse(); + } + + // a connection that still holds a reference to a transaction was + // not closed naturally (i.e. it was reset or aborted) and is + // therefore not something that should be reused. + if (conn->Transaction()) { + conn->DontReuse(); + } + + if (NS_SUCCEEDED(ent->RemoveActiveConnection(conn)) || + NS_SUCCEEDED(ent->RemovePendingConnection(conn))) { + } else if (!connTCP || connTCP->EverUsedSpdy()) { + LOG(("HttpConnectionBase %p not found in its connection entry, try ^anon", + conn)); + // repeat for flipped anon flag as we share connection entries for spdy + // connections. + RefPtr<nsHttpConnectionInfo> anonInvertedCI(ci->Clone()); + anonInvertedCI->SetAnonymous(!ci->GetAnonymous()); + + ConnectionEntry* ent = mCT.GetWeak(anonInvertedCI->HashKey()); + if (ent) { + if (NS_SUCCEEDED(ent->RemoveActiveConnection(conn))) { + } else { + LOG( + ("HttpConnectionBase %p could not be removed from its entry's " + "active list", + conn)); + } + } + } + + if (connTCP && connTCP->CanReuse()) { + LOG((" adding connection to idle list\n")); + // Keep The idle connection list sorted with the connections that + // have moved the largest data pipelines at the front because these + // connections have the largest cwnds on the server. + + // The linear search is ok here because the number of idleconns + // in a single entry is generally limited to a small number (i.e. 6) + + ent->InsertIntoIdleConnections(connTCP); + } else { + if (ent->IsInH2WebsocketConns(conn)) { + ent->RemoveH2WebsocketConns(conn); + } + LOG((" connection cannot be reused; closing connection\n")); + conn->SetCloseReason(ConnectionCloseReason::CANT_REUSED); + conn->Close(NS_ERROR_ABORT); + } + + OnMsgProcessPendingQ(0, ci); +} + +void nsHttpConnectionMgr::OnMsgCompleteUpgrade(int32_t, ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsresult rv = NS_OK; + nsCompleteUpgradeData* data = static_cast<nsCompleteUpgradeData*>(param); + MOZ_ASSERT(data->mTrans && data->mTrans->Caps() & NS_HTTP_STICKY_CONNECTION); + + RefPtr<nsAHttpConnection> conn(data->mTrans->Connection()); + LOG( + ("nsHttpConnectionMgr::OnMsgCompleteUpgrade " + "conn=%p listener=%p wrapped=%d\n", + conn.get(), data->mUpgradeListener.get(), data->mJsWrapped)); + + if (!conn) { + // Delay any error reporting to happen in transportAvailableFunc + rv = NS_ERROR_UNEXPECTED; + } else { + MOZ_ASSERT(!data->mSocketTransport); + rv = conn->TakeTransport(getter_AddRefs(data->mSocketTransport), + getter_AddRefs(data->mSocketIn), + getter_AddRefs(data->mSocketOut)); + + if (NS_FAILED(rv)) { + LOG((" conn->TakeTransport failed with %" PRIx32, + static_cast<uint32_t>(rv))); + } + } + + RefPtr<nsCompleteUpgradeData> upgradeData(data); + + nsCOMPtr<nsIAsyncInputStream> socketIn; + nsCOMPtr<nsIAsyncOutputStream> socketOut; + + // If this is for JS, the input and output sockets need to be piped over the + // socket thread. Otherwise, the JS may attempt to read and/or write the + // sockets on the main thread, which could cause network I/O on the main + // thread. This is particularly bad in the case of TLS connections, because + // PSM and NSS rely on those connections only being used on the socket + // thread. + if (data->mJsWrapped) { + nsCOMPtr<nsIAsyncInputStream> pipeIn; + uint32_t segsize = 0; + uint32_t segcount = 0; + net_ResolveSegmentParams(segsize, segcount); + if (NS_SUCCEEDED(rv)) { + NS_NewPipe2(getter_AddRefs(pipeIn), getter_AddRefs(socketOut), true, true, + segsize, segcount); + rv = NS_AsyncCopy(pipeIn, data->mSocketOut, gSocketTransportService, + NS_ASYNCCOPY_VIA_READSEGMENTS, segsize); + } + + nsCOMPtr<nsIAsyncOutputStream> pipeOut; + if (NS_SUCCEEDED(rv)) { + NS_NewPipe2(getter_AddRefs(socketIn), getter_AddRefs(pipeOut), true, true, + segsize, segcount); + rv = NS_AsyncCopy(data->mSocketIn, pipeOut, gSocketTransportService, + NS_ASYNCCOPY_VIA_WRITESEGMENTS, segsize); + } + } else { + socketIn = upgradeData->mSocketIn; + socketOut = upgradeData->mSocketOut; + } + + auto transportAvailableFunc = [upgradeData{std::move(upgradeData)}, socketIn, + socketOut, aRv(rv)]() { + // Handle any potential previous errors first + // and call OnUpgradeFailed if necessary. + nsresult rv = aRv; + + if (NS_FAILED(rv)) { + rv = upgradeData->mUpgradeListener->OnUpgradeFailed(rv); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpConnectionMgr::OnMsgCompleteUpgrade OnUpgradeFailed failed." + " listener=%p\n", + upgradeData->mUpgradeListener.get())); + } + return; + } + + rv = upgradeData->mUpgradeListener->OnTransportAvailable( + upgradeData->mSocketTransport, socketIn, socketOut); + if (NS_FAILED(rv)) { + LOG( + ("nsHttpConnectionMgr::OnMsgCompleteUpgrade OnTransportAvailable " + "failed. listener=%p\n", + upgradeData->mUpgradeListener.get())); + } + }; + + if (data->mJsWrapped) { + LOG( + ("nsHttpConnectionMgr::OnMsgCompleteUpgrade " + "conn=%p listener=%p wrapped=%d pass to main thread\n", + conn.get(), data->mUpgradeListener.get(), data->mJsWrapped)); + NS_DispatchToMainThread( + NS_NewRunnableFunction("net::nsHttpConnectionMgr::OnMsgCompleteUpgrade", + transportAvailableFunc)); + } else { + transportAvailableFunc(); + } +} + +void nsHttpConnectionMgr::OnMsgUpdateParam(int32_t inParam, ARefBase*) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + uint32_t param = static_cast<uint32_t>(inParam); + uint16_t name = ((param) & 0xFFFF0000) >> 16; + uint16_t value = param & 0x0000FFFF; + + switch (name) { + case MAX_CONNECTIONS: + mMaxConns = value; + break; + case MAX_URGENT_START_Q: + mMaxUrgentExcessiveConns = value; + break; + case MAX_PERSISTENT_CONNECTIONS_PER_HOST: + mMaxPersistConnsPerHost = value; + break; + case MAX_PERSISTENT_CONNECTIONS_PER_PROXY: + mMaxPersistConnsPerProxy = value; + break; + case MAX_REQUEST_DELAY: + mMaxRequestDelay = value; + break; + case THROTTLING_ENABLED: + SetThrottlingEnabled(!!value); + break; + case THROTTLING_SUSPEND_FOR: + mThrottleSuspendFor = value; + break; + case THROTTLING_RESUME_FOR: + mThrottleResumeFor = value; + break; + case THROTTLING_READ_LIMIT: + mThrottleReadLimit = value; + break; + case THROTTLING_READ_INTERVAL: + mThrottleReadInterval = value; + break; + case THROTTLING_HOLD_TIME: + mThrottleHoldTime = value; + break; + case THROTTLING_MAX_TIME: + mThrottleMaxTime = TimeDuration::FromMilliseconds(value); + break; + case PROXY_BE_CONSERVATIVE: + mBeConservativeForProxy = !!value; + break; + default: + MOZ_ASSERT_UNREACHABLE("unexpected parameter name"); + } +} + +// Read Timeout Tick handlers + +void nsHttpConnectionMgr::ActivateTimeoutTick() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + LOG( + ("nsHttpConnectionMgr::ActivateTimeoutTick() " + "this=%p mTimeoutTick=%p\n", + this, mTimeoutTick.get())); + + // The timer tick should be enabled if it is not already pending. + // Upon running the tick will rearm itself if there are active + // connections available. + + if (mTimeoutTick && mTimeoutTickArmed) { + // make sure we get one iteration on a quick tick + if (mTimeoutTickNext > 1) { + mTimeoutTickNext = 1; + mTimeoutTick->SetDelay(1000); + } + return; + } + + if (!mTimeoutTick) { + mTimeoutTick = NS_NewTimer(); + if (!mTimeoutTick) { + NS_WARNING("failed to create timer for http timeout management"); + return; + } + ReentrantMonitorAutoEnter mon(mReentrantMonitor); + if (!mSocketThreadTarget) { + NS_WARNING("cannot activate timout if not initialized or shutdown"); + return; + } + mTimeoutTick->SetTarget(mSocketThreadTarget); + } + + if (mIsShuttingDown) { // Atomic + // don't set a timer to generate an event if we're shutting down + // (and mSocketThreadTarget might be null or garbage if we're shutting down) + return; + } + MOZ_ASSERT(!mTimeoutTickArmed, "timer tick armed"); + mTimeoutTickArmed = true; + mTimeoutTick->Init(this, 1000, nsITimer::TYPE_REPEATING_SLACK); +} + +class UINT64Wrapper : public ARefBase { + public: + explicit UINT64Wrapper(uint64_t aUint64) : mUint64(aUint64) {} + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(UINT64Wrapper, override) + + uint64_t GetValue() { return mUint64; } + + private: + uint64_t mUint64; + virtual ~UINT64Wrapper() = default; +}; + +nsresult nsHttpConnectionMgr::UpdateCurrentBrowserId(uint64_t aId) { + RefPtr<UINT64Wrapper> idWrapper = new UINT64Wrapper(aId); + return PostEvent(&nsHttpConnectionMgr::OnMsgUpdateCurrentBrowserId, 0, + idWrapper); +} + +void nsHttpConnectionMgr::SetThrottlingEnabled(bool aEnable) { + LOG(("nsHttpConnectionMgr::SetThrottlingEnabled enable=%d", aEnable)); + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + mThrottleEnabled = aEnable; + + if (mThrottleEnabled) { + EnsureThrottleTickerIfNeeded(); + } else { + DestroyThrottleTicker(); + ResumeReadOf(mActiveTransactions[false]); + ResumeReadOf(mActiveTransactions[true]); + } +} + +bool nsHttpConnectionMgr::InThrottlingTimeWindow() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mThrottlingWindowEndsAt.IsNull()) { + return true; + } + return TimeStamp::NowLoRes() <= mThrottlingWindowEndsAt; +} + +void nsHttpConnectionMgr::TouchThrottlingTimeWindow(bool aEnsureTicker) { + LOG(("nsHttpConnectionMgr::TouchThrottlingTimeWindow")); + + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + mThrottlingWindowEndsAt = TimeStamp::NowLoRes() + mThrottleMaxTime; + + if (!mThrottleTicker && MOZ_LIKELY(aEnsureTicker) && + MOZ_LIKELY(mThrottleEnabled)) { + EnsureThrottleTickerIfNeeded(); + } +} + +void nsHttpConnectionMgr::LogActiveTransactions(char operation) { + if (!LOG_ENABLED()) { + return; + } + + nsTArray<RefPtr<nsHttpTransaction>>* trs = nullptr; + uint32_t au, at, bu = 0, bt = 0; + + trs = mActiveTransactions[false].Get(mCurrentBrowserId); + au = trs ? trs->Length() : 0; + trs = mActiveTransactions[true].Get(mCurrentBrowserId); + at = trs ? trs->Length() : 0; + + for (const auto& data : mActiveTransactions[false].Values()) { + bu += data->Length(); + } + bu -= au; + for (const auto& data : mActiveTransactions[true].Values()) { + bt += data->Length(); + } + bt -= at; + + // Shows counts of: + // - unthrottled transaction for the active tab + // - throttled transaction for the active tab + // - unthrottled transaction for background tabs + // - throttled transaction for background tabs + LOG(("Active transactions %c[%u,%u,%u,%u]", operation, au, at, bu, bt)); +} + +void nsHttpConnectionMgr::AddActiveTransaction(nsHttpTransaction* aTrans) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + uint64_t tabId = aTrans->BrowserId(); + bool throttled = aTrans->EligibleForThrottling(); + + nsTArray<RefPtr<nsHttpTransaction>>* transactions = + mActiveTransactions[throttled].GetOrInsertNew(tabId); + + MOZ_ASSERT(!transactions->Contains(aTrans)); + + transactions->AppendElement(aTrans); + + LOG(("nsHttpConnectionMgr::AddActiveTransaction t=%p tabid=%" PRIx64 + "(%d) thr=%d", + aTrans, tabId, tabId == mCurrentBrowserId, throttled)); + LogActiveTransactions('+'); + + if (tabId == mCurrentBrowserId) { + mActiveTabTransactionsExist = true; + if (!throttled) { + mActiveTabUnthrottledTransactionsExist = true; + } + } + + // Shift the throttling window to the future (actually, makes sure + // that throttling will engage when there is anything to throttle.) + // The |false| argument means we don't need this call to ensure + // the ticker, since we do it just below. Calling + // EnsureThrottleTickerIfNeeded directly does a bit more than call + // from inside of TouchThrottlingTimeWindow. + TouchThrottlingTimeWindow(false); + + if (!mThrottleEnabled) { + return; + } + + EnsureThrottleTickerIfNeeded(); +} + +void nsHttpConnectionMgr::RemoveActiveTransaction( + nsHttpTransaction* aTrans, Maybe<bool> const& aOverride) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + uint64_t tabId = aTrans->BrowserId(); + bool forActiveTab = tabId == mCurrentBrowserId; + bool throttled = aOverride.valueOr(aTrans->EligibleForThrottling()); + + nsTArray<RefPtr<nsHttpTransaction>>* transactions = + mActiveTransactions[throttled].Get(tabId); + + if (!transactions || !transactions->RemoveElement(aTrans)) { + // Was not tracked as active, probably just ignore. + return; + } + + LOG(("nsHttpConnectionMgr::RemoveActiveTransaction t=%p tabid=%" PRIx64 + "(%d) thr=%d", + aTrans, tabId, forActiveTab, throttled)); + + if (!transactions->IsEmpty()) { + // There are still transactions of the type, hence nothing in the throttling + // conditions has changed and we don't need to update "Exists" caches nor we + // need to wake any now throttled transactions. + LogActiveTransactions('-'); + return; + } + + // To optimize the following logic, always remove the entry when the array is + // empty. + mActiveTransactions[throttled].Remove(tabId); + LogActiveTransactions('-'); + + if (forActiveTab) { + // Update caches of the active tab transaction existence, since it's now + // affected + if (!throttled) { + mActiveTabUnthrottledTransactionsExist = false; + } + if (mActiveTabTransactionsExist) { + mActiveTabTransactionsExist = + mActiveTransactions[!throttled].Contains(tabId); + } + } + + if (!mThrottleEnabled) { + return; + } + + bool unthrottledExist = !mActiveTransactions[false].IsEmpty(); + bool throttledExist = !mActiveTransactions[true].IsEmpty(); + + if (!unthrottledExist && !throttledExist) { + // Nothing active globally, just get rid of the timer completely and we are + // done. + MOZ_ASSERT(!mActiveTabUnthrottledTransactionsExist); + MOZ_ASSERT(!mActiveTabTransactionsExist); + + DestroyThrottleTicker(); + return; + } + + if (mThrottleVersion == 1) { + if (!mThrottlingInhibitsReading) { + // There is then nothing to wake up. Affected transactions will not be + // put to sleep automatically on next tick. + LOG((" reading not currently inhibited")); + return; + } + } + + if (mActiveTabUnthrottledTransactionsExist) { + // There are still unthrottled transactions for the active tab, hence the + // state is unaffected and we don't need to do anything (nothing to wake). + LOG((" there are unthrottled for the active tab")); + return; + } + + if (mActiveTabTransactionsExist) { + // There are only trottled transactions for the active tab. + // If the last transaction we just removed was a non-throttled for the + // active tab we can wake the throttled transactions for the active tab. + if (forActiveTab && !throttled) { + LOG((" resuming throttled for active tab")); + ResumeReadOf(mActiveTransactions[true].Get(mCurrentBrowserId)); + } + return; + } + + if (!unthrottledExist) { + // There are no unthrottled transactions for any tab. Resume all throttled, + // all are only for background tabs. + LOG((" delay resuming throttled for background tabs")); + DelayedResumeBackgroundThrottledTransactions(); + return; + } + + if (forActiveTab) { + // Removing the last transaction for the active tab frees up the unthrottled + // background tabs transactions. + LOG((" delay resuming unthrottled for background tabs")); + DelayedResumeBackgroundThrottledTransactions(); + return; + } + + LOG((" not resuming anything")); +} + +void nsHttpConnectionMgr::UpdateActiveTransaction(nsHttpTransaction* aTrans) { + LOG(("nsHttpConnectionMgr::UpdateActiveTransaction ENTER t=%p", aTrans)); + + // First remove then add. In case of a download that is the only active + // transaction and has just been marked as download (goes unthrottled to + // throttled), adding first would cause it to be throttled for first few + // milliseconds - becuause it would appear as if there were both throttled + // and unthrottled transactions at the time. + + Maybe<bool> reversed; + reversed.emplace(!aTrans->EligibleForThrottling()); + RemoveActiveTransaction(aTrans, reversed); + + AddActiveTransaction(aTrans); + + LOG(("nsHttpConnectionMgr::UpdateActiveTransaction EXIT t=%p", aTrans)); +} + +bool nsHttpConnectionMgr::ShouldThrottle(nsHttpTransaction* aTrans) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("nsHttpConnectionMgr::ShouldThrottle trans=%p", aTrans)); + + if (mThrottleVersion == 1) { + if (!mThrottlingInhibitsReading || !mThrottleEnabled) { + return false; + } + } else { + if (!mThrottleEnabled) { + return false; + } + } + + uint64_t tabId = aTrans->BrowserId(); + bool forActiveTab = tabId == mCurrentBrowserId; + bool throttled = aTrans->EligibleForThrottling(); + + bool stop = [=]() { + if (mActiveTabTransactionsExist) { + if (!tabId) { + // Chrome initiated and unidentified transactions just respect + // their throttle flag, when something for the active tab is happening. + // This also includes downloads. + LOG((" active tab loads, trans is tab-less, throttled=%d", throttled)); + return throttled; + } + if (!forActiveTab) { + // This is a background tab request, we want them to always throttle + // when there are transactions running for the ative tab. + LOG((" active tab loads, trans not of the active tab")); + return true; + } + + if (mActiveTabUnthrottledTransactionsExist) { + // Unthrottled transactions for the active tab take precedence + LOG((" active tab loads unthrottled, trans throttled=%d", throttled)); + return throttled; + } + + LOG((" trans for active tab, don't throttle")); + return false; + } + + MOZ_ASSERT(!forActiveTab); + + if (!mActiveTransactions[false].IsEmpty()) { + // This means there are unthrottled active transactions for background + // tabs. If we are here, there can't be any transactions for the active + // tab. (If there is no transaction for a tab id, there is no entry for it + // in the hashtable.) + LOG((" backround tab(s) load unthrottled, trans throttled=%d", + throttled)); + return throttled; + } + + // There are only unthrottled transactions for background tabs: don't + // throttle. + LOG((" backround tab(s) load throttled, don't throttle")); + return false; + }(); + + if (forActiveTab && !stop) { + // This is an active-tab transaction and is allowed to read. Hence, + // prolong the throttle time window to make sure all 'lower-decks' + // transactions will actually throttle. + TouchThrottlingTimeWindow(); + return false; + } + + // Only stop reading when in the configured throttle max-time (aka time + // window). This window is prolonged (restarted) by a call to + // TouchThrottlingTimeWindow called on new transaction activation or on + // receive of response bytes of an active tab transaction. + bool inWindow = InThrottlingTimeWindow(); + + LOG((" stop=%d, in-window=%d, delayed-bck-timer=%d", stop, inWindow, + !!mDelayedResumeReadTimer)); + + if (!forActiveTab) { + // If the delayed background resume timer exists, background transactions + // are scheduled to be woken after a delay, hence leave them throttled. + inWindow = inWindow || mDelayedResumeReadTimer; + } + + return stop && inWindow; +} + +bool nsHttpConnectionMgr::IsConnEntryUnderPressure( + nsHttpConnectionInfo* connInfo) { + ConnectionEntry* ent = mCT.GetWeak(connInfo->HashKey()); + if (!ent) { + // No entry, no pressure. + return false; + } + + return ent->PendingQueueLengthForWindow(mCurrentBrowserId) > 0; +} + +bool nsHttpConnectionMgr::IsThrottleTickerNeeded() { + LOG(("nsHttpConnectionMgr::IsThrottleTickerNeeded")); + + if (mActiveTabUnthrottledTransactionsExist && + mActiveTransactions[false].Count() > 1) { + LOG((" there are unthrottled transactions for both active and bck")); + return true; + } + + if (mActiveTabTransactionsExist && mActiveTransactions[true].Count() > 1) { + LOG((" there are throttled transactions for both active and bck")); + return true; + } + + if (!mActiveTransactions[true].IsEmpty() && + !mActiveTransactions[false].IsEmpty()) { + LOG((" there are both throttled and unthrottled transactions")); + return true; + } + + LOG((" nothing to throttle")); + return false; +} + +void nsHttpConnectionMgr::EnsureThrottleTickerIfNeeded() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("nsHttpConnectionMgr::EnsureThrottleTickerIfNeeded")); + if (!IsThrottleTickerNeeded()) { + return; + } + + // There is a new demand to throttle, hence unschedule delayed resume + // of background throttled transastions. + CancelDelayedResumeBackgroundThrottledTransactions(); + + if (mThrottleTicker) { + return; + } + + mThrottleTicker = NS_NewTimer(); + if (mThrottleTicker) { + if (mThrottleVersion == 1) { + MOZ_ASSERT(!mThrottlingInhibitsReading); + + mThrottleTicker->Init(this, mThrottleSuspendFor, nsITimer::TYPE_ONE_SHOT); + mThrottlingInhibitsReading = true; + } else { + mThrottleTicker->Init(this, mThrottleReadInterval, + nsITimer::TYPE_ONE_SHOT); + } + } + + LogActiveTransactions('^'); +} + +// Can be called with or without the monitor held +void nsHttpConnectionMgr::DestroyThrottleTicker() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + // Nothing to throttle, hence no need for this timer anymore. + CancelDelayedResumeBackgroundThrottledTransactions(); + + MOZ_ASSERT(!mThrottleEnabled || !IsThrottleTickerNeeded()); + + if (!mThrottleTicker) { + return; + } + + LOG(("nsHttpConnectionMgr::DestroyThrottleTicker")); + mThrottleTicker->Cancel(); + mThrottleTicker = nullptr; + + if (mThrottleVersion == 1) { + mThrottlingInhibitsReading = false; + } + + LogActiveTransactions('v'); +} + +void nsHttpConnectionMgr::ThrottlerTick() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mThrottleVersion == 1) { + mThrottlingInhibitsReading = !mThrottlingInhibitsReading; + + LOG(("nsHttpConnectionMgr::ThrottlerTick inhibit=%d", + mThrottlingInhibitsReading)); + + // If there are only background transactions to be woken after a delay, keep + // the ticker so that we woke them only for the resume-for interval and then + // throttle them again until the background-resume delay passes. + if (!mThrottlingInhibitsReading && !mDelayedResumeReadTimer && + (!IsThrottleTickerNeeded() || !InThrottlingTimeWindow())) { + LOG((" last tick")); + mThrottleTicker = nullptr; + } + + if (mThrottlingInhibitsReading) { + if (mThrottleTicker) { + mThrottleTicker->Init(this, mThrottleSuspendFor, + nsITimer::TYPE_ONE_SHOT); + } + } else { + if (mThrottleTicker) { + mThrottleTicker->Init(this, mThrottleResumeFor, + nsITimer::TYPE_ONE_SHOT); + } + + ResumeReadOf(mActiveTransactions[false], true); + ResumeReadOf(mActiveTransactions[true]); + } + } else { + LOG(("nsHttpConnectionMgr::ThrottlerTick")); + + // If there are only background transactions to be woken after a delay, keep + // the ticker so that we still keep the low read limit for that time. + if (!mDelayedResumeReadTimer && + (!IsThrottleTickerNeeded() || !InThrottlingTimeWindow())) { + LOG((" last tick")); + mThrottleTicker = nullptr; + } + + if (mThrottleTicker) { + mThrottleTicker->Init(this, mThrottleReadInterval, + nsITimer::TYPE_ONE_SHOT); + } + + ResumeReadOf(mActiveTransactions[false], true); + ResumeReadOf(mActiveTransactions[true]); + } +} + +void nsHttpConnectionMgr::DelayedResumeBackgroundThrottledTransactions() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + if (mThrottleVersion == 1) { + if (mDelayedResumeReadTimer) { + return; + } + } else { + // If the mThrottleTicker doesn't exist, there is nothing currently + // being throttled. Hence, don't invoke the hold time interval. + // This is called also when a single download transaction becomes + // marked as throttleable. We would otherwise block it unnecessarily. + if (mDelayedResumeReadTimer || !mThrottleTicker) { + return; + } + } + + LOG(("nsHttpConnectionMgr::DelayedResumeBackgroundThrottledTransactions")); + NS_NewTimerWithObserver(getter_AddRefs(mDelayedResumeReadTimer), this, + mThrottleHoldTime, nsITimer::TYPE_ONE_SHOT); +} + +void nsHttpConnectionMgr::CancelDelayedResumeBackgroundThrottledTransactions() { + if (!mDelayedResumeReadTimer) { + return; + } + + LOG( + ("nsHttpConnectionMgr::" + "CancelDelayedResumeBackgroundThrottledTransactions")); + mDelayedResumeReadTimer->Cancel(); + mDelayedResumeReadTimer = nullptr; +} + +void nsHttpConnectionMgr::ResumeBackgroundThrottledTransactions() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("nsHttpConnectionMgr::ResumeBackgroundThrottledTransactions")); + mDelayedResumeReadTimer = nullptr; + + if (!IsThrottleTickerNeeded()) { + DestroyThrottleTicker(); + } + + if (!mActiveTransactions[false].IsEmpty()) { + ResumeReadOf(mActiveTransactions[false], true); + } else { + ResumeReadOf(mActiveTransactions[true], true); + } +} + +void nsHttpConnectionMgr::ResumeReadOf( + nsClassHashtable<nsUint64HashKey, nsTArray<RefPtr<nsHttpTransaction>>>& + hashtable, + bool excludeForActiveTab) { + for (const auto& entry : hashtable) { + if (excludeForActiveTab && entry.GetKey() == mCurrentBrowserId) { + // These have never been throttled (never stopped reading) + continue; + } + ResumeReadOf(entry.GetWeak()); + } +} + +void nsHttpConnectionMgr::ResumeReadOf( + nsTArray<RefPtr<nsHttpTransaction>>* transactions) { + MOZ_ASSERT(transactions); + + for (const auto& trans : *transactions) { + trans->ResumeReading(); + } +} + +void nsHttpConnectionMgr::NotifyConnectionOfBrowserIdChange( + uint64_t previousId) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + nsTArray<RefPtr<nsHttpTransaction>>* transactions = nullptr; + nsTArray<RefPtr<nsAHttpConnection>> connections; + + auto addConnectionHelper = + [&connections](nsTArray<RefPtr<nsHttpTransaction>>* trans) { + if (!trans) { + return; + } + + for (const auto& t : *trans) { + RefPtr<nsAHttpConnection> conn = t->Connection(); + if (conn && !connections.Contains(conn)) { + connections.AppendElement(conn); + } + } + }; + + // Get unthrottled transactions with the previous and current window id. + transactions = mActiveTransactions[false].Get(previousId); + addConnectionHelper(transactions); + transactions = mActiveTransactions[false].Get(mCurrentBrowserId); + addConnectionHelper(transactions); + + // Get throttled transactions with the previous and current window id. + transactions = mActiveTransactions[true].Get(previousId); + addConnectionHelper(transactions); + transactions = mActiveTransactions[true].Get(mCurrentBrowserId); + addConnectionHelper(transactions); + + for (const auto& conn : connections) { + conn->CurrentBrowserIdChanged(mCurrentBrowserId); + } +} + +void nsHttpConnectionMgr::OnMsgUpdateCurrentBrowserId(int32_t aLoading, + ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + uint64_t id = static_cast<UINT64Wrapper*>(param)->GetValue(); + + if (mCurrentBrowserId == id) { + // duplicate notification + return; + } + + bool activeTabWasLoading = mActiveTabTransactionsExist; + + uint64_t previousId = mCurrentBrowserId; + mCurrentBrowserId = id; + + if (gHttpHandler->ActiveTabPriority()) { + NotifyConnectionOfBrowserIdChange(previousId); + } + + LOG( + ("nsHttpConnectionMgr::OnMsgUpdateCurrentBrowserId" + " id=%" PRIx64 "\n", + mCurrentBrowserId)); + + nsTArray<RefPtr<nsHttpTransaction>>* transactions = nullptr; + + // Update the "Exists" caches and resume any transactions that now deserve it, + // changing the active tab changes the conditions for throttling. + transactions = mActiveTransactions[false].Get(mCurrentBrowserId); + mActiveTabUnthrottledTransactionsExist = !!transactions; + + if (!mActiveTabUnthrottledTransactionsExist) { + transactions = mActiveTransactions[true].Get(mCurrentBrowserId); + } + mActiveTabTransactionsExist = !!transactions; + + if (transactions) { + // This means there are some transactions for this newly activated tab, + // resume them but anything else. + LOG((" resuming newly activated tab transactions")); + ResumeReadOf(transactions); + return; + } + + if (!activeTabWasLoading) { + // There were no transactions for the previously active tab, hence + // all remaning transactions, if there were, were all unthrottled, + // no need to wake them. + return; + } + + if (!mActiveTransactions[false].IsEmpty()) { + LOG((" resuming unthrottled background transactions")); + ResumeReadOf(mActiveTransactions[false]); + return; + } + + if (!mActiveTransactions[true].IsEmpty()) { + LOG((" resuming throttled background transactions")); + ResumeReadOf(mActiveTransactions[true]); + return; + } + + DestroyThrottleTicker(); +} + +void nsHttpConnectionMgr::TimeoutTick() { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(mTimeoutTick, "no readtimeout tick"); + + LOG(("nsHttpConnectionMgr::TimeoutTick active=%d\n", mNumActiveConns)); + // The next tick will be between 1 second and 1 hr + // Set it to the max value here, and the TimeoutTick()s can + // reduce it to their local needs. + mTimeoutTickNext = 3600; // 1hr + + for (const RefPtr<ConnectionEntry>& ent : mCT.Values()) { + uint32_t timeoutTickNext = ent->TimeoutTick(); + mTimeoutTickNext = std::min(mTimeoutTickNext, timeoutTickNext); + } + + if (mTimeoutTick) { + mTimeoutTickNext = std::max(mTimeoutTickNext, 1U); + mTimeoutTick->SetDelay(mTimeoutTickNext * 1000); + } +} + +// GetOrCreateConnectionEntry finds a ent for a particular CI for use in +// dispatching a transaction according to these rules +// 1] use an ent that matches the ci that can be dispatched immediately +// 2] otherwise use an ent of wildcard(ci) than can be dispatched immediately +// 3] otherwise create an ent that matches ci and make new conn on it + +ConnectionEntry* nsHttpConnectionMgr::GetOrCreateConnectionEntry( + nsHttpConnectionInfo* specificCI, bool prohibitWildCard, bool aNoHttp2, + bool aNoHttp3, bool* aIsWildcard, bool* aAvailableForDispatchNow) { + if (aAvailableForDispatchNow) { + *aAvailableForDispatchNow = false; + } + *aIsWildcard = false; + + // step 1 + ConnectionEntry* specificEnt = mCT.GetWeak(specificCI->HashKey()); + if (specificEnt && specificEnt->AvailableForDispatchNow()) { + if (aAvailableForDispatchNow) { + *aAvailableForDispatchNow = true; + } + return specificEnt; + } + + // step 1 repeated for an inverted anonymous flag; we return an entry + // only when it has an h2 established connection that is not authenticated + // with a client certificate. + RefPtr<nsHttpConnectionInfo> anonInvertedCI(specificCI->Clone()); + anonInvertedCI->SetAnonymous(!specificCI->GetAnonymous()); + ConnectionEntry* invertedEnt = mCT.GetWeak(anonInvertedCI->HashKey()); + if (invertedEnt) { + HttpConnectionBase* h2orh3conn = + GetH2orH3ActiveConn(invertedEnt, aNoHttp2, aNoHttp3); + if (h2orh3conn && h2orh3conn->IsExperienced() && + h2orh3conn->NoClientCertAuth()) { + MOZ_ASSERT(h2orh3conn->UsingSpdy() || h2orh3conn->UsingHttp3()); + LOG( + ("GetOrCreateConnectionEntry is coalescing h2/3 an/onymous " + "connections, ent=%p", + invertedEnt)); + return invertedEnt; + } + } + + if (!specificCI->UsingHttpsProxy()) { + prohibitWildCard = true; + } + + // step 2 + if (!prohibitWildCard && aNoHttp3) { + RefPtr<nsHttpConnectionInfo> wildCardProxyCI; + DebugOnly<nsresult> rv = + specificCI->CreateWildCard(getter_AddRefs(wildCardProxyCI)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + ConnectionEntry* wildCardEnt = mCT.GetWeak(wildCardProxyCI->HashKey()); + if (wildCardEnt && wildCardEnt->AvailableForDispatchNow()) { + if (aAvailableForDispatchNow) { + *aAvailableForDispatchNow = true; + } + *aIsWildcard = true; + return wildCardEnt; + } + } + + // step 3 + if (!specificEnt) { + RefPtr<nsHttpConnectionInfo> clone(specificCI->Clone()); + specificEnt = new ConnectionEntry(clone); + mCT.InsertOrUpdate(clone->HashKey(), RefPtr{specificEnt}); + } + return specificEnt; +} + +void nsHttpConnectionMgr::DoSpeculativeConnection( + SpeculativeTransaction* aTrans, bool aFetchHTTPSRR) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(aTrans); + + bool isWildcard = false; + ConnectionEntry* ent = GetOrCreateConnectionEntry( + aTrans->ConnectionInfo(), false, aTrans->Caps() & NS_HTTP_DISALLOW_SPDY, + aTrans->Caps() & NS_HTTP_DISALLOW_HTTP3, &isWildcard); + if (!aFetchHTTPSRR && + gHttpHandler->EchConfigEnabled(aTrans->ConnectionInfo()->IsHttp3())) { + // This happens when this is called from + // SpeculativeTransaction::OnHTTPSRRAvailable. We have to update this + // entry's echConfig so that the newly created connection can use the latest + // echConfig. + ent->MaybeUpdateEchConfig(aTrans->ConnectionInfo()); + } + DoSpeculativeConnectionInternal(ent, aTrans, aFetchHTTPSRR); +} + +void nsHttpConnectionMgr::DoSpeculativeConnectionInternal( + ConnectionEntry* aEnt, SpeculativeTransaction* aTrans, bool aFetchHTTPSRR) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(aTrans); + MOZ_ASSERT(aEnt); + if (!gHttpHandler->Active()) { + // Do nothing if we are shutting down. + return; + } + + if (aFetchHTTPSRR && NS_SUCCEEDED(aTrans->FetchHTTPSRR())) { + // nsHttpConnectionMgr::DoSpeculativeConnection will be called again when + // HTTPS RR is available. + return; + } + + uint32_t parallelSpeculativeConnectLimit = + aTrans->ParallelSpeculativeConnectLimit() + ? *aTrans->ParallelSpeculativeConnectLimit() + : gHttpHandler->ParallelSpeculativeConnectLimit(); + bool ignoreIdle = aTrans->IgnoreIdle() ? *aTrans->IgnoreIdle() : false; + bool isFromPredictor = + aTrans->IsFromPredictor() ? *aTrans->IsFromPredictor() : false; + bool allow1918 = aTrans->Allow1918() ? *aTrans->Allow1918() : false; + + bool keepAlive = aTrans->Caps() & NS_HTTP_ALLOW_KEEPALIVE; + if (mNumDnsAndConnectSockets < parallelSpeculativeConnectLimit && + ((ignoreIdle && + (aEnt->IdleConnectionsLength() < parallelSpeculativeConnectLimit)) || + !aEnt->IdleConnectionsLength()) && + !(keepAlive && aEnt->RestrictConnections()) && + !AtActiveConnectionLimit(aEnt, aTrans->Caps())) { + nsresult rv = aEnt->CreateDnsAndConnectSocket(aTrans, aTrans->Caps(), true, + isFromPredictor, false, + allow1918, nullptr); + if (NS_FAILED(rv)) { + glean::networking::speculative_connect_outcome + .Get("aborted_socket_fail"_ns) + .Add(1); + LOG( + ("DoSpeculativeConnectionInternal Transport socket creation " + "failure: %" PRIx32 "\n", + static_cast<uint32_t>(rv))); + } else { + glean::networking::speculative_connect_outcome.Get("successful"_ns) + .Add(1); + } + } else { + glean::networking::speculative_connect_outcome + .Get("aborted_socket_limit"_ns) + .Add(1); + LOG( + ("DoSpeculativeConnectionInternal Transport ci=%s " + "not created due to existing connection count:%d", + aEnt->mConnInfo->HashKey().get(), parallelSpeculativeConnectLimit)); + } +} + +void nsHttpConnectionMgr::DoFallbackConnection(SpeculativeTransaction* aTrans, + bool aFetchHTTPSRR) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(aTrans); + + LOG(("nsHttpConnectionMgr::DoFallbackConnection")); + + bool availableForDispatchNow = false; + bool aIsWildcard = false; + ConnectionEntry* ent = GetOrCreateConnectionEntry( + aTrans->ConnectionInfo(), false, aTrans->Caps() & NS_HTTP_DISALLOW_SPDY, + aTrans->Caps() & NS_HTTP_DISALLOW_HTTP3, &aIsWildcard, + &availableForDispatchNow); + + if (availableForDispatchNow) { + LOG( + ("nsHttpConnectionMgr::DoFallbackConnection fallback connection is " + "ready for dispatching ent=%p", + ent)); + aTrans->InvokeCallback(); + return; + } + + DoSpeculativeConnectionInternal(ent, aTrans, aFetchHTTPSRR); +} + +void nsHttpConnectionMgr::OnMsgSpeculativeConnect(int32_t, ARefBase* param) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + SpeculativeConnectArgs* args = static_cast<SpeculativeConnectArgs*>(param); + + LOG( + ("nsHttpConnectionMgr::OnMsgSpeculativeConnect [ci=%s, " + "mFetchHTTPSRR=%d]\n", + args->mTrans->ConnectionInfo()->HashKey().get(), args->mFetchHTTPSRR)); + DoSpeculativeConnection(args->mTrans, args->mFetchHTTPSRR); +} + +bool nsHttpConnectionMgr::BeConservativeIfProxied(nsIProxyInfo* proxy) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + if (mBeConservativeForProxy) { + // The pref says to be conservative for proxies. + return true; + } + + if (!proxy) { + // There is no proxy, so be conservative by default. + return true; + } + + // Be conservative only if there is no proxy host set either. + // This logic was copied from nsSSLIOLayerAddToSocket. + nsAutoCString proxyHost; + proxy->GetHost(proxyHost); + return proxyHost.IsEmpty(); +} + +// register a connection to receive CanJoinConnection() for particular +// origin keys +void nsHttpConnectionMgr::RegisterOriginCoalescingKey(HttpConnectionBase* conn, + const nsACString& host, + int32_t port) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + nsHttpConnectionInfo* ci = conn ? conn->ConnectionInfo() : nullptr; + if (!ci || !conn->CanDirectlyActivate()) { + return; + } + + nsCString newKey; + BuildOriginFrameHashKey(newKey, ci, host, port); + mCoalescingHash.GetOrInsertNew(newKey, 1)->AppendElement( + do_GetWeakReference(static_cast<nsISupportsWeakReference*>(conn))); + + LOG( + ("nsHttpConnectionMgr::RegisterOriginCoalescingKey " + "Established New Coalescing Key %s to %p %s\n", + newKey.get(), conn, ci->HashKey().get())); +} + +bool nsHttpConnectionMgr::GetConnectionData(nsTArray<HttpRetParams>* aArg) { + for (const RefPtr<ConnectionEntry>& ent : mCT.Values()) { + if (ent->mConnInfo->GetPrivate()) { + continue; + } + aArg->AppendElement(ent->GetConnectionData()); + } + + return true; +} + +void nsHttpConnectionMgr::ResetIPFamilyPreference(nsHttpConnectionInfo* ci) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + ConnectionEntry* ent = mCT.GetWeak(ci->HashKey()); + if (ent) { + ent->ResetIPFamilyPreference(); + } +} + +void nsHttpConnectionMgr::ExcludeHttp2(const nsHttpConnectionInfo* ci) { + LOG(("nsHttpConnectionMgr::ExcludeHttp2 excluding ci %s", + ci->HashKey().BeginReading())); + ConnectionEntry* ent = mCT.GetWeak(ci->HashKey()); + if (!ent) { + LOG(("nsHttpConnectionMgr::ExcludeHttp2 no entry found?!")); + return; + } + + ent->DisallowHttp2(); +} + +void nsHttpConnectionMgr::ExcludeHttp3(const nsHttpConnectionInfo* ci) { + LOG(("nsHttpConnectionMgr::ExcludeHttp3 exclude ci %s", + ci->HashKey().BeginReading())); + ConnectionEntry* ent = mCT.GetWeak(ci->HashKey()); + if (!ent) { + LOG(("nsHttpConnectionMgr::ExcludeHttp3 no entry found?!")); + return; + } + + ent->DontReuseHttp3Conn(); +} + +void nsHttpConnectionMgr::MoveToWildCardConnEntry( + nsHttpConnectionInfo* specificCI, nsHttpConnectionInfo* wildCardCI, + HttpConnectionBase* proxyConn) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + MOZ_ASSERT(specificCI->UsingHttpsProxy()); + + LOG( + ("nsHttpConnectionMgr::MakeConnEntryWildCard conn %p has requested to " + "change CI from %s to %s\n", + proxyConn, specificCI->HashKey().get(), wildCardCI->HashKey().get())); + + ConnectionEntry* ent = mCT.GetWeak(specificCI->HashKey()); + LOG( + ("nsHttpConnectionMgr::MakeConnEntryWildCard conn %p using ent %p (spdy " + "%d)\n", + proxyConn, ent, ent ? ent->mUsingSpdy : 0)); + + if (!ent || !ent->mUsingSpdy) { + return; + } + + bool isWildcard = false; + ConnectionEntry* wcEnt = + GetOrCreateConnectionEntry(wildCardCI, true, false, false, &isWildcard); + if (wcEnt == ent) { + // nothing to do! + return; + } + wcEnt->mUsingSpdy = true; + + LOG( + ("nsHttpConnectionMgr::MakeConnEntryWildCard ent %p " + "idle=%zu active=%zu half=%zu pending=%zu\n", + ent, ent->IdleConnectionsLength(), ent->ActiveConnsLength(), + ent->DnsAndConnectSocketsLength(), ent->PendingQueueLength())); + + LOG( + ("nsHttpConnectionMgr::MakeConnEntryWildCard wc-ent %p " + "idle=%zu active=%zu half=%zu pending=%zu\n", + wcEnt, wcEnt->IdleConnectionsLength(), wcEnt->ActiveConnsLength(), + wcEnt->DnsAndConnectSocketsLength(), wcEnt->PendingQueueLength())); + + ent->MoveConnection(proxyConn, wcEnt); +} + +bool nsHttpConnectionMgr::RemoveTransFromConnEntry(nsHttpTransaction* aTrans, + const nsACString& aHashKey) { + MOZ_ASSERT(OnSocketThread(), "not on socket thread"); + + LOG(("nsHttpConnectionMgr::RemoveTransFromConnEntry: trans=%p ci=%s", aTrans, + PromiseFlatCString(aHashKey).get())); + + if (aHashKey.IsEmpty()) { + return false; + } + + // Step 1: Get the transaction's connection entry. + ConnectionEntry* entry = mCT.GetWeak(aHashKey); + if (!entry) { + return false; + } + + // Step 2: Try to find the undispatched transaction. + return entry->RemoveTransFromPendingQ(aTrans); +} + +void nsHttpConnectionMgr::IncreaseNumDnsAndConnectSockets() { + mNumDnsAndConnectSockets++; +} + +void nsHttpConnectionMgr::DecreaseNumDnsAndConnectSockets() { + MOZ_ASSERT(mNumDnsAndConnectSockets); + if (mNumDnsAndConnectSockets) { // just in case + mNumDnsAndConnectSockets--; + } +} + +already_AddRefed<PendingTransactionInfo> +nsHttpConnectionMgr::FindTransactionHelper(bool removeWhenFound, + ConnectionEntry* aEnt, + nsAHttpTransaction* aTrans) { + nsTArray<RefPtr<PendingTransactionInfo>>* pendingQ = + aEnt->GetTransactionPendingQHelper(aTrans); + + int32_t index = + pendingQ ? pendingQ->IndexOf(aTrans, 0, PendingComparator()) : -1; + + RefPtr<PendingTransactionInfo> info; + if (index != -1) { + info = (*pendingQ)[index]; + if (removeWhenFound) { + pendingQ->RemoveElementAt(index); + } + } + return info.forget(); +} + +already_AddRefed<ConnectionEntry> nsHttpConnectionMgr::FindConnectionEntry( + const nsHttpConnectionInfo* ci) { + return mCT.Get(ci->HashKey()); +} + +nsHttpConnectionMgr* nsHttpConnectionMgr::AsHttpConnectionMgr() { return this; } + +HttpConnectionMgrParent* nsHttpConnectionMgr::AsHttpConnectionMgrParent() { + return nullptr; +} + +void nsHttpConnectionMgr::NewIdleConnectionAdded(uint32_t timeToLive) { + mNumIdleConns++; + + // If the added connection was first idle connection or has shortest + // time to live among the watched connections, pruning dead + // connections needs to be done when it can't be reused anymore. + if (!mTimer || NowInSeconds() + timeToLive < mTimeOfNextWakeUp) { + PruneDeadConnectionsAfter(timeToLive); + } +} + +void nsHttpConnectionMgr::DecrementNumIdleConns() { + MOZ_ASSERT(mNumIdleConns); + mNumIdleConns--; + ConditionallyStopPruneDeadConnectionsTimer(); +} + +void nsHttpConnectionMgr::CheckTransInPendingQueue(nsHttpTransaction* aTrans) { +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + // We only do this check on socket thread. When this function is called on + // main thread, the transaction is newly created, so we can skip this check. + if (!OnSocketThread()) { + return; + } + + nsAutoCString hashKey; + aTrans->GetHashKeyOfConnectionEntry(hashKey); + if (hashKey.IsEmpty()) { + return; + } + + bool foundInPendingQ = RemoveTransFromConnEntry(aTrans, hashKey); + MOZ_DIAGNOSTIC_ASSERT(!foundInPendingQ); +#endif +} + +bool nsHttpConnectionMgr::AllowToRetryDifferentIPFamilyForHttp3( + nsHttpConnectionInfo* ci, nsresult aError) { + ConnectionEntry* ent = mCT.GetWeak(ci->HashKey()); + if (!ent) { + return false; + } + + return ent->AllowToRetryDifferentIPFamilyForHttp3(aError); +} + +void nsHttpConnectionMgr::SetRetryDifferentIPFamilyForHttp3( + nsHttpConnectionInfo* ci, uint16_t aIPFamily) { + ConnectionEntry* ent = mCT.GetWeak(ci->HashKey()); + if (!ent) { + return; + } + + ent->SetRetryDifferentIPFamilyForHttp3(aIPFamily); +} + +} // namespace mozilla::net |