/* 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 #include #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 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 sts; nsCOMPtr ioService = components::IO::Service(); if (ioService) { nsCOMPtr 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 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 mMgr; nsConnEventHandler mHandler; int32_t mIParam; RefPtr mVParam; }; nsresult nsHttpConnectionMgr::PostEvent(nsConnEventHandler handler, int32_t iparam, ARefBase* vparam) { Unused << EnsureSocketThreadTarget(); nsCOMPtr target; { ReentrantMonitorAutoEnter mon(mReentrantMonitor); target = mSocketThreadTarget; } if (!target) { NS_WARNING("cannot post event if not initialized"); return NS_ERROR_NOT_INITIALIZED; } nsCOMPtr 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 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 mTrans; int32_t mPriority; RefPtr 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 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(classOfService.Flags()), classOfService.Incremental())); Unused << EnsureSocketThreadTarget(); nsCOMPtr target; { ReentrantMonitorAutoEnter mon(mReentrantMonitor); target = mSocketThreadTarget; } if (!target) { NS_WARNING("cannot post event if not initialized"); return; } RefPtr 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(reason))); return PostEvent(&nsHttpConnectionMgr::OnMsgCancelTransaction, static_cast(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 ci = aCI->Clone(); return PostEvent(&nsHttpConnectionMgr::OnMsgDoShiftReloadConnectionCleanup, 0, ci); } nsresult nsHttpConnectionMgr::DoSingleConnectionCleanup( nsHttpConnectionInfo* aCI) { if (!aCI) { return NS_ERROR_INVALID_ARG; } RefPtr 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 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 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 args = new SpeculativeConnectArgs(); // Wrap up the callbacks and the target to ensure they're released on the // target thread properly. nsCOMPtr 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 temp(mSocketThreadTarget); temp.forget(target); return NS_OK; } nsresult nsHttpConnectionMgr::ReclaimConnection(HttpConnectionBase* conn) { LOG(("nsHttpConnectionMgr::ReclaimConnection [conn=%p]\n", conn)); Unused << EnsureSocketThreadTarget(); nsCOMPtr target; { ReentrantMonitorAutoEnter mon(mReentrantMonitor); target = mSocketThreadTarget; } if (!target) { NS_WARNING("cannot post event if not initialized"); return NS_ERROR_NOT_INITIALIZED; } RefPtr connRef(conn); RefPtr 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 mTrans; nsCOMPtr mUpgradeListener; nsCOMPtr mSocketTransport; nsCOMPtr mSocketIn; nsCOMPtr 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 wrapper = do_QueryInterface(aUpgradeListener); bool wrapped = !!wrapper; RefPtr 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(param), nullptr); } nsresult nsHttpConnectionMgr::ProcessPendingQ(nsHttpConnectionInfo* aCI) { LOG(("nsHttpConnectionMgr::ProcessPendingQ [ci=%s]\n", aCI->HashKey().get())); RefPtr 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(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 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* listOfWeakConns = mCoalescingHash.Get(key); if (!listOfWeakConns) { return nullptr; } uint32_t listLen = listOfWeakConns->Length(); for (uint32_t j = 0; j < listLen;) { RefPtr 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); auto usableEntry = [&](HttpConnectionBase* conn) { // This is allowed by the spec, but other browsers don't coalesce // so agressively, which surprises developers. See bug 1420777. if (StaticPrefs::network_http_http2_aggressive_coalescing()) { return true; } // Make sure that the connection's IP address is one that is in // the set of IP addresses in the entry's DNS response. NetAddr addr; nsresult rv = conn->GetPeerAddr(&addr); if (NS_FAILED(rv)) { // Err on the side of not coalescing return false; } // We don't care about remote port when matching entries. addr.inet.port = 0; return ent->mAddresses.Contains(addr); }; if (conn) { LOG(("Found connection with matching hash")); if (usableEntry(conn)) { LOG(("> coalescing")); return conn; } else { LOG(("> not coalescing as remote address not present in DNS records")); } } } 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 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 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>(1); }) ->AppendElement(do_GetWeakReference( static_cast(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(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(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(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(rv))); } } //----------------------------------------------------------------------------- bool nsHttpConnectionMgr::DispatchPendingQ( nsTArray>& 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(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(mMaxPersistConnsPerProxy); } return static_cast(mMaxPersistConnsPerHost); } void nsHttpConnectionMgr::PreparePendingQForDispatching( ConnectionEntry* ent, nsTArray>& 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> 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> pendingQ; ent->AppendPendingUrgentStartQ(pendingQ); dispatchedSuccessfully = DispatchPendingQ(pendingQ, ent, considerAll); for (const auto& transactionInfo : Reversed(pendingQ)) { ent->InsertTransaction(transactionInfo); } } if (dispatchedSuccessfully && !considerAll) { return dispatchedSuccessfully; } nsTArray> 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 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& 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 unusedSpdyPersistentConnection; // step 0 // look for existing spdy connection - that's always best because it is // essentially pipelining without head of line blocking RefPtr 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 connTCP = do_QueryObject(conn); LOG(("TryingDispatchTransaction: websockets over Http2")); // No limit for number of websockets, dispatch transaction to the tunnel RefPtr 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(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 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 transaction(aTrans); RefPtr 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(rv))); DebugOnly 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 pushedStreamWrapper = trans->GetPushedStream(); if (pushedStreamWrapper) { Http2PushedStream* pushedStream = pushedStreamWrapper->GetStream(); if (pushedStream) { RefPtr 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 conn; RefPtr 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 conn = GetH2orH3ActiveConn(ent, false, true); RefPtr 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 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 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(rv))); return rv; } void nsHttpConnectionMgr::IncrementActiveConnCount() { mNumActiveConns++; ActivateTimeoutTick(); } void nsHttpConnectionMgr::DecrementActiveConnCount(HttpConnectionBase* conn) { MOZ_DIAGNOSTIC_ASSERT(mNumActiveConns > 0); if (mNumActiveConns > 0) { mNumActiveConns--; } RefPtr 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>& pendingQ, ConnectionEntry* ent, HttpConnectionBase* connH2, HttpConnectionBase* connH3) { if (pendingQ.Length() == 0) { return; } nsTArray> 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> 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> 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 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 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(param); shutdown->mBool = true; } void nsHttpConnectionMgr::OnMsgNewTransaction(int32_t priority, ARefBase* param) { nsHttpTransaction* trans = static_cast(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(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 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 trans = static_cast(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(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(reason); // caller holds a ref to param/trans on stack nsHttpTransaction* trans = static_cast(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 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(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(code); return PostEvent(&nsHttpConnectionMgr::OnMsgCancelTransactions, intReason, ci); } void nsHttpConnectionMgr::OnMsgCancelTransactions(int32_t code, ARefBase* param) { nsresult reason = static_cast(code); nsHttpConnectionInfo* ci = static_cast(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 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& 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(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(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 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 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 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(param); MOZ_ASSERT(data->mTrans && data->mTrans->Caps() & NS_HTTP_STICKY_CONNECTION); RefPtr 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(rv))); } } RefPtr upgradeData(data); nsCOMPtr socketIn; nsCOMPtr 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 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 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(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 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>* 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>* 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 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>* 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 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>>& 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>* 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>* transactions = nullptr; nsTArray> connections; auto addConnectionHelper = [&connections](nsTArray>* trans) { if (!trans) { return; } for (const auto& t : *trans) { RefPtr 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(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>* 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& 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 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 wildCardProxyCI; DebugOnly 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 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(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(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(conn))); LOG( ("nsHttpConnectionMgr::RegisterOriginCoalescingKey " "Established New Coalescing Key %s to %p %s\n", newKey.get(), conn, ci->HashKey().get())); } bool nsHttpConnectionMgr::GetConnectionData(nsTArray* aArg) { for (const RefPtr& 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 nsHttpConnectionMgr::FindTransactionHelper(bool removeWhenFound, ConnectionEntry* aEnt, nsAHttpTransaction* aTrans) { nsTArray>* pendingQ = aEnt->GetTransactionPendingQHelper(aTrans); int32_t index = pendingQ ? pendingQ->IndexOf(aTrans, 0, PendingComparator()) : -1; RefPtr info; if (index != -1) { info = (*pendingQ)[index]; if (removeWhenFound) { pendingQ->RemoveElementAt(index); } } return info.forget(); } already_AddRefed 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(); } // A structure used to marshall objects necessary for ServerCertificateHashaes class nsStoreServerCertHashesData : public ARefBase { public: nsStoreServerCertHashesData( nsHttpConnectionInfo* aConnInfo, bool aNoSpdy, bool aNoHttp3, nsTArray>&& aServerCertHashes) : mConnInfo(aConnInfo), mNoSpdy(aNoSpdy), mNoHttp3(aNoHttp3), mServerCertHashes(std::move(aServerCertHashes)) {} NS_INLINE_DECL_THREADSAFE_REFCOUNTING(nsStoreServerCertHashesData, override) RefPtr mConnInfo; bool mNoSpdy; bool mNoHttp3; nsTArray> mServerCertHashes; private: virtual ~nsStoreServerCertHashesData() = default; }; // The connection manager needs to know the hashes used for a WebTransport // connection authenticated with serverCertHashes nsresult nsHttpConnectionMgr::StoreServerCertHashes( nsHttpConnectionInfo* aConnInfo, bool aNoSpdy, bool aNoHttp3, nsTArray>&& aServerCertHashes) { RefPtr ci = aConnInfo->Clone(); RefPtr data = new nsStoreServerCertHashesData( ci, aNoSpdy, aNoHttp3, std::move(aServerCertHashes)); return PostEvent(&nsHttpConnectionMgr::OnMsgStoreServerCertHashes, 0, data); } void nsHttpConnectionMgr::OnMsgStoreServerCertHashes(int32_t, ARefBase* param) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); nsStoreServerCertHashesData* data = static_cast(param); bool isWildcard; ConnectionEntry* connEnt = GetOrCreateConnectionEntry( data->mConnInfo, true, data->mNoSpdy, data->mNoHttp3, &isWildcard); MOZ_ASSERT(!isWildcard, "No webtransport with wildcard"); connEnt->SetServerCertHashes(std::move(data->mServerCertHashes)); } const nsTArray>* nsHttpConnectionMgr::GetServerCertHashes(nsHttpConnectionInfo* aConnInfo) { ConnectionEntry* connEnt = mCT.GetWeak(aConnInfo->HashKey()); if (!connEnt) { MOZ_ASSERT(0); return nullptr; } return &connEnt->GetServerCertHashes(); } 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