/* 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" #include "ConnectionHandle.h" #include "HalfOpenSocket.h" #include "TCPFastOpenLayer.h" #include "nsHttpConnection.h" #include "nsIDNSRecord.h" #include "nsIDNSService.h" #include "nsQueryObject.h" // Log on level :5, instead of default :4. #undef LOG #define LOG(args) LOG5(args) #undef LOG_ENABLED #define LOG_ENABLED() LOG5_ENABLED() namespace mozilla { namespace net { //////////////////////// HalfOpenSocket NS_IMPL_ADDREF(HalfOpenSocket) NS_IMPL_RELEASE(HalfOpenSocket) NS_INTERFACE_MAP_BEGIN(HalfOpenSocket) NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) NS_INTERFACE_MAP_ENTRY(nsIOutputStreamCallback) NS_INTERFACE_MAP_ENTRY(nsITransportEventSink) NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor) NS_INTERFACE_MAP_ENTRY(nsITimerCallback) NS_INTERFACE_MAP_ENTRY(nsINamed) NS_INTERFACE_MAP_ENTRY_CONCRETE(HalfOpenSocket) NS_INTERFACE_MAP_END HalfOpenSocket::HalfOpenSocket(ConnectionEntry* ent, nsAHttpTransaction* trans, uint32_t caps, bool speculative, bool isFromPredictor, bool urgentStart) : mTransaction(trans), mDispatchedMTransaction(false), mCaps(caps), mSpeculative(speculative), mUrgentStart(urgentStart), mIsFromPredictor(isFromPredictor), mAllow1918(true), mHasConnected(false), mPrimaryConnectedOK(false), mBackupConnectedOK(false), mBackupConnStatsSet(false), mFreeToUse(true), mPrimaryStreamStatus(NS_OK), mFastOpenInProgress(false), mEnt(ent) { MOZ_ASSERT(ent && trans, "constructor with null arguments"); LOG(("Creating HalfOpenSocket [this=%p trans=%p ent=%s key=%s]\n", this, trans, ent->mConnInfo->Origin(), ent->mConnInfo->HashKey().get())); mIsHttp3 = mEnt->mConnInfo->IsHttp3(); if (speculative) { Telemetry::AutoCounter totalSpeculativeConn; ++totalSpeculativeConn; if (isFromPredictor) { Telemetry::AutoCounter totalPreconnectsCreated; ++totalPreconnectsCreated; } } if (mEnt->mConnInfo->FirstHopSSL()) { mFastOpenStatus = TFO_UNKNOWN; } else { mFastOpenStatus = TFO_HTTP; } MOZ_ASSERT(mEnt); } HalfOpenSocket::~HalfOpenSocket() { MOZ_ASSERT(!mStreamOut); MOZ_ASSERT(!mBackupStreamOut); LOG(("Destroying HalfOpenSocket [this=%p]\n", this)); if (mEnt) { bool inqueue = mEnt->RemoveHalfOpen(this); LOG(("Destroying HalfOpenSocket was in the HalfOpenList=%d [this=%p]\n", inqueue, this)); } } nsresult HalfOpenSocket::SetupStreams(nsISocketTransport** transport, nsIAsyncInputStream** instream, nsIAsyncOutputStream** outstream, bool isBackup) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); MOZ_ASSERT(mEnt); nsresult rv; nsTArray socketTypes; const nsHttpConnectionInfo* ci = mEnt->mConnInfo; if (mIsHttp3) { socketTypes.AppendElement("quic"_ns); } else { if (ci->FirstHopSSL()) { socketTypes.AppendElement("ssl"_ns); } else { const nsCString& defaultType = gHttpHandler->DefaultSocketType(); if (!defaultType.IsVoid()) { socketTypes.AppendElement(defaultType); } } } nsCOMPtr socketTransport; nsCOMPtr sts; sts = services::GetSocketTransportService(); if (!sts) { return NS_ERROR_NOT_AVAILABLE; } LOG( ("HalfOpenSocket::SetupStreams [this=%p ent=%s] " "setup routed transport to origin %s:%d via %s:%d\n", this, ci->HashKey().get(), ci->Origin(), ci->OriginPort(), ci->RoutedHost(), ci->RoutedPort())); nsCOMPtr routedSTS(do_QueryInterface(sts)); if (routedSTS) { rv = routedSTS->CreateRoutedTransport( socketTypes, ci->GetOrigin(), ci->OriginPort(), ci->GetRoutedHost(), ci->RoutedPort(), ci->ProxyInfo(), getter_AddRefs(socketTransport)); } else { if (!ci->GetRoutedHost().IsEmpty()) { // There is a route requested, but the legacy nsISocketTransportService // can't handle it. // Origin should be reachable on origin host name, so this should // not be a problem - but log it. LOG( ("HalfOpenSocket this=%p using legacy nsISocketTransportService " "means explicit route %s:%d will be ignored.\n", this, ci->RoutedHost(), ci->RoutedPort())); } rv = sts->CreateTransport(socketTypes, ci->GetOrigin(), ci->OriginPort(), ci->ProxyInfo(), getter_AddRefs(socketTransport)); } NS_ENSURE_SUCCESS(rv, rv); uint32_t tmpFlags = 0; if (mCaps & NS_HTTP_REFRESH_DNS) { tmpFlags = nsISocketTransport::BYPASS_CACHE; } tmpFlags |= nsISocketTransport::GetFlagsFromTRRMode( NS_HTTP_TRR_MODE_FROM_FLAGS(mCaps)); if (mCaps & NS_HTTP_LOAD_ANONYMOUS) { tmpFlags |= nsISocketTransport::ANONYMOUS_CONNECT; } if (ci->GetPrivate() || ci->GetIsolated()) { tmpFlags |= nsISocketTransport::NO_PERMANENT_STORAGE; } Unused << socketTransport->SetIsPrivate(ci->GetPrivate()); if (ci->GetLessThanTls13()) { tmpFlags |= nsISocketTransport::DONT_TRY_ECH; } if (((mCaps & NS_HTTP_BE_CONSERVATIVE) || ci->GetBeConservative()) && gHttpHandler->ConnMgr()->BeConservativeIfProxied(ci->ProxyInfo())) { LOG(("Setting Socket to BE_CONSERVATIVE")); tmpFlags |= nsISocketTransport::BE_CONSERVATIVE; } if (ci->HasIPHintAddress()) { nsCOMPtr dns = do_GetService("@mozilla.org/network/dns-service;1", &rv); if (NS_FAILED(rv)) { return rv; } // The spec says: "If A and AAAA records for TargetName are locally // available, the client SHOULD ignore these hints.", so we check if the DNS // record is in cache before setting USE_IP_HINT_ADDRESS. nsCOMPtr record; rv = dns->ResolveNative(ci->GetRoutedHost(), nsIDNSService::RESOLVE_OFFLINE, mEnt->mConnInfo->GetOriginAttributes(), getter_AddRefs(record)); if (NS_FAILED(rv) || !record) { LOG(("Setting Socket to use IP hint address")); tmpFlags |= nsISocketTransport::USE_IP_HINT_ADDRESS; } } if (mCaps & NS_HTTP_DISABLE_IPV4) { tmpFlags |= nsISocketTransport::DISABLE_IPV4; } else if (mCaps & NS_HTTP_DISABLE_IPV6) { tmpFlags |= nsISocketTransport::DISABLE_IPV6; } else if (mEnt->PreferenceKnown()) { if (mEnt->mPreferIPv6) { tmpFlags |= nsISocketTransport::DISABLE_IPV4; } else if (mEnt->mPreferIPv4) { tmpFlags |= nsISocketTransport::DISABLE_IPV6; } // In case the host is no longer accessible via the preferred IP family, // try the opposite one and potentially restate the preference. tmpFlags |= nsISocketTransport::RETRY_WITH_DIFFERENT_IP_FAMILY; // From the same reason, let the backup socket fail faster to try the other // family. uint16_t fallbackTimeout = isBackup ? gHttpHandler->GetFallbackSynTimeout() : 0; if (fallbackTimeout) { socketTransport->SetTimeout(nsISocketTransport::TIMEOUT_CONNECT, fallbackTimeout); } } else if (isBackup && gHttpHandler->FastFallbackToIPv4()) { // For backup connections, we disable IPv6. That's because some users have // broken IPv6 connectivity (leading to very long timeouts), and disabling // IPv6 on the backup connection gives them a much better user experience // with dual-stack hosts, though they still pay the 250ms delay for each new // connection. This strategy is also known as "happy eyeballs". tmpFlags |= nsISocketTransport::DISABLE_IPV6; } if (!Allow1918()) { tmpFlags |= nsISocketTransport::DISABLE_RFC1918; } if ((mFastOpenStatus != TFO_HTTP) && !isBackup) { if (mEnt->mUseFastOpen) { socketTransport->SetFastOpenCallback(this); } else { mFastOpenStatus = TFO_DISABLED; } } MOZ_ASSERT(!(tmpFlags & nsISocketTransport::DISABLE_IPV4) || !(tmpFlags & nsISocketTransport::DISABLE_IPV6), "Both types should not be disabled at the same time."); socketTransport->SetConnectionFlags(tmpFlags); socketTransport->SetTlsFlags(ci->GetTlsFlags()); const OriginAttributes& originAttributes = mEnt->mConnInfo->GetOriginAttributes(); if (originAttributes != OriginAttributes()) { socketTransport->SetOriginAttributes(originAttributes); } socketTransport->SetQoSBits(gHttpHandler->GetQoSBits()); rv = socketTransport->SetEventSink(this, nullptr); NS_ENSURE_SUCCESS(rv, rv); rv = socketTransport->SetSecurityCallbacks(this); NS_ENSURE_SUCCESS(rv, rv); if (gHttpHandler->EchConfigEnabled()) { rv = socketTransport->SetEchConfig(ci->GetEchConfig()); NS_ENSURE_SUCCESS(rv, rv); } Telemetry::Accumulate(Telemetry::HTTP_CONNECTION_ENTRY_CACHE_HIT_1, mEnt->mUsedForConnection); mEnt->mUsedForConnection = true; nsCOMPtr sout; rv = socketTransport->OpenOutputStream(nsITransport::OPEN_UNBUFFERED, 0, 0, getter_AddRefs(sout)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr sin; rv = socketTransport->OpenInputStream(nsITransport::OPEN_UNBUFFERED, 0, 0, getter_AddRefs(sin)); NS_ENSURE_SUCCESS(rv, rv); socketTransport.forget(transport); CallQueryInterface(sin, instream); CallQueryInterface(sout, outstream); rv = (*outstream)->AsyncWait(this, 0, 0, nullptr); if (NS_SUCCEEDED(rv)) { gHttpHandler->ConnMgr()->StartedConnect(); } return rv; } nsresult HalfOpenSocket::SetupPrimaryStreams() { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); nsresult rv; mPrimarySynStarted = TimeStamp::Now(); rv = SetupStreams(getter_AddRefs(mSocketTransport), getter_AddRefs(mStreamIn), getter_AddRefs(mStreamOut), false); LOG(("HalfOpenSocket::SetupPrimaryStream [this=%p ent=%s rv=%" PRIx32 "]", this, mEnt->mConnInfo->Origin(), static_cast(rv))); if (NS_FAILED(rv)) { if (mStreamOut) { mStreamOut->AsyncWait(nullptr, 0, 0, nullptr); } if (mSocketTransport) { mSocketTransport->SetFastOpenCallback(nullptr); } mStreamOut = nullptr; mStreamIn = nullptr; mSocketTransport = nullptr; } return rv; } nsresult HalfOpenSocket::SetupBackupStreams() { MOZ_ASSERT(mTransaction); mBackupSynStarted = TimeStamp::Now(); nsresult rv = SetupStreams(getter_AddRefs(mBackupTransport), getter_AddRefs(mBackupStreamIn), getter_AddRefs(mBackupStreamOut), true); LOG(("HalfOpenSocket::SetupBackupStream [this=%p ent=%s rv=%" PRIx32 "]", this, mEnt->mConnInfo->Origin(), static_cast(rv))); if (NS_FAILED(rv)) { if (mBackupStreamOut) { mBackupStreamOut->AsyncWait(nullptr, 0, 0, nullptr); } mBackupStreamOut = nullptr; mBackupStreamIn = nullptr; mBackupTransport = nullptr; } return rv; } void HalfOpenSocket::SetupBackupTimer() { MOZ_ASSERT(mEnt); uint16_t timeout = gHttpHandler->GetIdleSynTimeout(); MOZ_ASSERT(!mSynTimer, "timer already initd"); if (!timeout && mFastOpenInProgress) { timeout = 250; } // When using Fast Open the correct transport will be setup for sure (it is // guaranteed), but it can be that it will happened a bit later. if (mFastOpenInProgress || (timeout && !mSpeculative)) { // Setup the timer that will establish a backup socket // if we do not get a writable event on the main one. // We do this because a lost SYN takes a very long time // to repair at the TCP level. // // Failure to setup the timer is something we can live with, // so don't return an error in that case. NS_NewTimerWithCallback(getter_AddRefs(mSynTimer), this, timeout, nsITimer::TYPE_ONE_SHOT); LOG(("HalfOpenSocket::SetupBackupTimer() [this=%p]", this)); } else if (timeout) { LOG(("HalfOpenSocket::SetupBackupTimer() [this=%p], did not arm\n", this)); } } void HalfOpenSocket::CancelBackupTimer() { // If the syntimer is still armed, we can cancel it because no backup // socket should be formed at this point if (!mSynTimer) { return; } LOG(("HalfOpenSocket::CancelBackupTimer()")); mSynTimer->Cancel(); // Keeping the reference to the timer to remember we have already // performed the backup connection. } void HalfOpenSocket::Abandon() { LOG(("HalfOpenSocket::Abandon [this=%p ent=%s] %p %p %p %p", this, mEnt->mConnInfo->Origin(), mSocketTransport.get(), mBackupTransport.get(), mStreamOut.get(), mBackupStreamOut.get())); MOZ_ASSERT(OnSocketThread(), "not on socket thread"); RefPtr deleteProtector(this); // Tell socket (and backup socket) to forget the half open socket. if (mSocketTransport) { mSocketTransport->SetEventSink(nullptr, nullptr); mSocketTransport->SetSecurityCallbacks(nullptr); mSocketTransport->SetFastOpenCallback(nullptr); mSocketTransport = nullptr; } if (mBackupTransport) { mBackupTransport->SetEventSink(nullptr, nullptr); mBackupTransport->SetSecurityCallbacks(nullptr); mBackupTransport = nullptr; } // Tell output stream (and backup) to forget the half open socket. if (mStreamOut) { if (!mFastOpenInProgress) { // If mFastOpenInProgress is true HalfOpen are not in mHalfOpen // list and are not counted so we do not need to decrease counter. gHttpHandler->ConnMgr()->RecvdConnect(); } mStreamOut->AsyncWait(nullptr, 0, 0, nullptr); mStreamOut = nullptr; } if (mBackupStreamOut) { gHttpHandler->ConnMgr()->RecvdConnect(); mBackupStreamOut->AsyncWait(nullptr, 0, 0, nullptr); mBackupStreamOut = nullptr; } // Lose references to input stream (and backup). if (mStreamIn) { mStreamIn->AsyncWait(nullptr, 0, 0, nullptr); mStreamIn = nullptr; } if (mBackupStreamIn) { mBackupStreamIn->AsyncWait(nullptr, 0, 0, nullptr); mBackupStreamIn = nullptr; } // Stop the timer - we don't want any new backups. CancelBackupTimer(); // Remove the half open from the connection entry. if (mEnt) { mEnt->mDoNotDestroy = false; mEnt->RemoveHalfOpen(this); } mEnt = nullptr; } double HalfOpenSocket::Duration(TimeStamp epoch) { if (mPrimarySynStarted.IsNull()) { return 0; } return (epoch - mPrimarySynStarted).ToMilliseconds(); } NS_IMETHODIMP // method for nsITimerCallback HalfOpenSocket::Notify(nsITimer* timer) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); MOZ_ASSERT(timer == mSynTimer, "wrong timer"); MOZ_ASSERT(!mBackupTransport); MOZ_ASSERT(mSynTimer); MOZ_ASSERT(mEnt); DebugOnly rv = SetupBackupStreams(); MOZ_ASSERT(NS_SUCCEEDED(rv)); // Keeping the reference to the timer to remember we have already // performed the backup connection. return NS_OK; } NS_IMETHODIMP // method for nsINamed HalfOpenSocket::GetName(nsACString& aName) { aName.AssignLiteral("HalfOpenSocket"); return NS_OK; } // method for nsIAsyncOutputStreamCallback NS_IMETHODIMP HalfOpenSocket::OnOutputStreamReady(nsIAsyncOutputStream* out) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); MOZ_ASSERT(mStreamOut || mBackupStreamOut); MOZ_ASSERT(out == mStreamOut || out == mBackupStreamOut, "stream mismatch"); MOZ_ASSERT(mEnt); LOG(("HalfOpenSocket::OnOutputStreamReady [this=%p ent=%s %s]\n", this, mEnt->mConnInfo->Origin(), out == mStreamOut ? "primary" : "backup")); mEnt->mDoNotDestroy = true; gHttpHandler->ConnMgr()->RecvdConnect(); CancelBackupTimer(); if (mFastOpenInProgress) { LOG( ("HalfOpenSocket::OnOutputStreamReady backup stream is ready, " "close the fast open socket %p [this=%p ent=%s]\n", mSocketTransport.get(), this, mEnt->mConnInfo->Origin())); // If fast open is used, right after a socket for the primary stream is // created a HttpConnectionBase is created for that socket. The connection // listens for OnOutputStreamReady not HalfOpenSocket. So this stream // cannot be mStreamOut. MOZ_ASSERT((out == mBackupStreamOut) && mConnectionNegotiatingFastOpen); // Here the backup, non-TFO connection has connected successfully, // before the TFO connection. // // The primary, TFO connection will be cancelled and the transaction // will be rewind. CloseConnectionFastOpenTakesTooLongOrError will // return the rewind transaction. The transaction will be put back to // the pending queue and as well connected to this halfOpenSocket. // SetupConn should set up a new HttpConnectionBase with the backup // socketTransport and the rewind transaction. mSocketTransport->SetFastOpenCallback(nullptr); mConnectionNegotiatingFastOpen->SetFastOpen(false); mEnt->RemoveHalfOpenFastOpenBackups(this); RefPtr trans = mConnectionNegotiatingFastOpen ->CloseConnectionFastOpenTakesTooLongOrError(true); mSocketTransport = nullptr; mStreamOut = nullptr; mStreamIn = nullptr; if (trans && trans->QueryHttpTransaction()) { RefPtr pendingTransInfo = new PendingTransactionInfo(trans->QueryHttpTransaction()); pendingTransInfo->AddHalfOpen(this); mEnt->InsertTransaction(pendingTransInfo, true); } if (mEnt->mUseFastOpen) { gHttpHandler->IncrementFastOpenConsecutiveFailureCounter(); mEnt->mUseFastOpen = false; } mFastOpenInProgress = false; mConnectionNegotiatingFastOpen = nullptr; if (mFastOpenStatus == TFO_NOT_TRIED) { mFastOpenStatus = TFO_FAILED_BACKUP_CONNECTION_TFO_NOT_TRIED; } else if (mFastOpenStatus == TFO_TRIED) { mFastOpenStatus = TFO_FAILED_BACKUP_CONNECTION_TFO_TRIED; } else if (mFastOpenStatus == TFO_DATA_SENT) { mFastOpenStatus = TFO_FAILED_BACKUP_CONNECTION_TFO_DATA_SENT; } else { // This is TFO_DATA_COOKIE_NOT_ACCEPTED (I think this cannot // happened, because the primary connection will be already // connected or in recovery and mFastOpenInProgress==false). mFastOpenStatus = TFO_FAILED_BACKUP_CONNECTION_TFO_DATA_COOKIE_NOT_ACCEPTED; } } if (((mFastOpenStatus == TFO_DISABLED) || (mFastOpenStatus == TFO_HTTP)) && !mBackupConnStatsSet) { // Collect telemetry for backup connection being faster than primary // connection. We want to collect this telemetry only for cases where // TFO is not used. mBackupConnStatsSet = true; Telemetry::Accumulate(Telemetry::NETWORK_HTTP_BACKUP_CONN_WON_1, (out == mBackupStreamOut)); } if (mFastOpenStatus == TFO_UNKNOWN) { MOZ_ASSERT(out == mStreamOut); if (mPrimaryStreamStatus == NS_NET_STATUS_RESOLVING_HOST) { mFastOpenStatus = TFO_UNKNOWN_RESOLVING; } else if (mPrimaryStreamStatus == NS_NET_STATUS_RESOLVED_HOST) { mFastOpenStatus = TFO_UNKNOWN_RESOLVED; } else if (mPrimaryStreamStatus == NS_NET_STATUS_CONNECTING_TO) { mFastOpenStatus = TFO_UNKNOWN_CONNECTING; } else if (mPrimaryStreamStatus == NS_NET_STATUS_CONNECTED_TO) { mFastOpenStatus = TFO_UNKNOWN_CONNECTED; } } nsresult rv = SetupConn(out, false); if (mEnt) { mEnt->mDoNotDestroy = false; } return rv; } bool HalfOpenSocket::FastOpenEnabled() { LOG(("HalfOpenSocket::FastOpenEnabled [this=%p]\n", this)); MOZ_ASSERT(mEnt); if (!mEnt) { return false; } MOZ_ASSERT(mEnt->mConnInfo->FirstHopSSL()); // If mEnt is present this HalfOpen must be in the mHalfOpens, // but we want to be sure!!! if (!mEnt->IsInHalfOpens(this)) { return false; } if (!gHttpHandler->UseFastOpen()) { // fast open was turned off. LOG(("HalfOpenSocket::FastEnabled - fast open was turned off.\n")); mEnt->mUseFastOpen = false; mFastOpenStatus = TFO_DISABLED; return false; } // We can use FastOpen if we have a transaction or if it is ssl // connection. For ssl we will use a null transaction to drive the SSL // handshake to completion if there is not a pending transaction. Afterwards // the connection will be 100% ready for the next transaction to use it. // Make an exception for SSL tunneled HTTP proxy as the NullHttpTransaction // does not know how to drive Connect. if (mEnt->mConnInfo->UsingConnect()) { LOG(("HalfOpenSocket::FastOpenEnabled - It is using Connect.")); mFastOpenStatus = TFO_DISABLED_CONNECT; return false; } return true; } nsresult HalfOpenSocket::StartFastOpen() { MOZ_ASSERT(mStreamOut); MOZ_ASSERT(!mBackupTransport); MOZ_ASSERT(mEnt); MOZ_ASSERT(mFastOpenStatus == TFO_UNKNOWN); LOG(("HalfOpenSocket::StartFastOpen [this=%p]\n", this)); RefPtr deleteProtector(this); mFastOpenInProgress = true; mEnt->mDoNotDestroy = true; // Remove this HalfOpen from mEnt->mHalfOpens. // The new connection will take care of closing this HalfOpen from now on! if (!mEnt->RemoveHalfOpen(this)) { MOZ_ASSERT(false, "HalfOpen is not in mHalfOpens!"); mSocketTransport->SetFastOpenCallback(nullptr); CancelBackupTimer(); mFastOpenInProgress = false; Abandon(); mFastOpenStatus = TFO_INIT_FAILED; return NS_ERROR_ABORT; } gHttpHandler->ConnMgr()->DecreaseNumHalfOpenConns(); // Count this socketTransport as connected. gHttpHandler->ConnMgr()->RecvdConnect(); // Remove HalfOpen from callbacks, the new connection will take them. mSocketTransport->SetEventSink(nullptr, nullptr); mSocketTransport->SetSecurityCallbacks(nullptr); mStreamOut->AsyncWait(nullptr, 0, 0, nullptr); nsresult rv = SetupConn(mStreamOut, true); if (!mConnectionNegotiatingFastOpen) { LOG( ("HalfOpenSocket::StartFastOpen SetupConn failed " "[this=%p rv=%x]\n", this, static_cast(rv))); if (NS_SUCCEEDED(rv)) { rv = NS_ERROR_ABORT; } // If SetupConn failed this will CloseTransaction and socketTransport // with an error, therefore we can close this HalfOpen. socketTransport // will remove reference to this HalfOpen as well. mSocketTransport->SetFastOpenCallback(nullptr); CancelBackupTimer(); mFastOpenInProgress = false; // The connection is responsible to take care of the halfOpen so we // need to clean it up. Abandon(); mFastOpenStatus = TFO_INIT_FAILED; } else { LOG(("HalfOpenSocket::StartFastOpen [this=%p conn=%p]\n", this, mConnectionNegotiatingFastOpen.get())); mEnt->InsertIntoHalfOpenFastOpenBackups(this); // SetupBackupTimer should setup timer which will hold a ref to this // halfOpen. It will failed only if it cannot create timer. Anyway just // to be sure I will add this deleteProtector!!! if (!mSynTimer) { // For Fast Open we will setup backup timer also for // NullTransaction. // So maybe it is not set and we need to set it here. SetupBackupTimer(); } } if (mEnt) { mEnt->mDoNotDestroy = false; } return rv; } void HalfOpenSocket::SetFastOpenConnected(nsresult aError, bool aWillRetry) { MOZ_ASSERT(mFastOpenInProgress); MOZ_ASSERT(mEnt); LOG(("HalfOpenSocket::SetFastOpenConnected [this=%p conn=%p error=%x]\n", this, mConnectionNegotiatingFastOpen.get(), static_cast(aError))); // mConnectionNegotiatingFastOpen is set after a StartFastOpen creates // and activates a HttpConnectionBase successfully (SetupConn calls // DispatchTransaction and DispatchAbstractTransaction which calls // conn->Activate). // HttpConnectionBase::Activate can fail which will close socketTransport // and socketTransport will call this function. The FastOpen clean up // in case HttpConnectionBase::Activate fails will be done in StartFastOpen. // Also OnMsgReclaimConnection can decided that we do not need this // transaction and cancel it as well. // In all other cases mConnectionNegotiatingFastOpen must not be nullptr. if (!mConnectionNegotiatingFastOpen) { return; } MOZ_ASSERT((mFastOpenStatus == TFO_NOT_TRIED) || (mFastOpenStatus == TFO_DATA_SENT) || (mFastOpenStatus == TFO_TRIED) || (mFastOpenStatus == TFO_DATA_COOKIE_NOT_ACCEPTED) || (mFastOpenStatus == TFO_DISABLED)); RefPtr deleteProtector(this); mEnt->mDoNotDestroy = true; // Delete 2 points of entry to FastOpen function so that we do not reenter. mEnt->RemoveHalfOpenFastOpenBackups(this); mSocketTransport->SetFastOpenCallback(nullptr); mConnectionNegotiatingFastOpen->SetFastOpen(false); // Check if we want to restart connection! if (aWillRetry && ((aError == NS_ERROR_CONNECTION_REFUSED) || #if defined(_WIN64) && defined(WIN95) // On Windows PR_ContinueConnect can return // NS_ERROR_FAILURE. This will be fixed in bug 1386719 and // this is just a temporary work around. (aError == NS_ERROR_FAILURE) || #endif (aError == NS_ERROR_PROXY_CONNECTION_REFUSED) || (aError == NS_ERROR_NET_TIMEOUT))) { if (mEnt->mUseFastOpen) { gHttpHandler->IncrementFastOpenConsecutiveFailureCounter(); mEnt->mUseFastOpen = false; } // This is called from nsSocketTransport::RecoverFromError. The // socket will try connect and we need to rewind nsHttpTransaction. RefPtr trans = mConnectionNegotiatingFastOpen ->CloseConnectionFastOpenTakesTooLongOrError(false); if (trans && trans->QueryHttpTransaction()) { RefPtr pendingTransInfo = new PendingTransactionInfo(trans->QueryHttpTransaction()); pendingTransInfo->AddHalfOpen(this); mEnt->InsertTransaction(pendingTransInfo, true); } // We are doing a restart without fast open, so the easiest way is to // return mSocketTransport to the halfOpenSock and destroy connection. // This makes http2 implemenntation easier. // mConnectionNegotiatingFastOpen is going away and halfOpen is taking // this mSocketTransport so add halfOpen to mEnt and update // mNumActiveConns. mEnt->InsertIntoHalfOpens(this); gHttpHandler->ConnMgr()->StartedConnect(); // Restore callbacks. mStreamOut->AsyncWait(this, 0, 0, nullptr); mSocketTransport->SetEventSink(this, nullptr); mSocketTransport->SetSecurityCallbacks(this); mStreamIn->AsyncWait(nullptr, 0, 0, nullptr); if ((aError == NS_ERROR_CONNECTION_REFUSED) || (aError == NS_ERROR_PROXY_CONNECTION_REFUSED)) { mFastOpenStatus = TFO_FAILED_CONNECTION_REFUSED; } else { mFastOpenStatus = TFO_FAILED_NET_TIMEOUT; } } else { // On success or other error we proceed with connection, we just need // to close backup timer and halfOpenSock. CancelBackupTimer(); if (NS_SUCCEEDED(aError)) { NetAddr peeraddr; if (NS_SUCCEEDED(mSocketTransport->GetPeerAddr(&peeraddr))) { mEnt->RecordIPFamilyPreference(peeraddr.raw.family); } gHttpHandler->ResetFastOpenConsecutiveFailureCounter(); } mSocketTransport = nullptr; mStreamOut = nullptr; mStreamIn = nullptr; // If backup transport has already started put this HalfOpen back to // mEnt list. if (mBackupTransport) { mFastOpenStatus = TFO_BACKUP_CONN; mEnt->InsertIntoHalfOpens(this); } } mFastOpenInProgress = false; mConnectionNegotiatingFastOpen = nullptr; if (mEnt) { mEnt->mDoNotDestroy = false; } else { MOZ_ASSERT(!mBackupTransport); MOZ_ASSERT(!mBackupStreamOut); } } void HalfOpenSocket::SetFastOpenStatus(uint8_t tfoStatus) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); MOZ_ASSERT(mFastOpenInProgress); mFastOpenStatus = tfoStatus; mConnectionNegotiatingFastOpen->SetFastOpenStatus(tfoStatus); if (mConnectionNegotiatingFastOpen->Transaction()) { // The transaction could already be canceled in the meantime, hence // nullified. mConnectionNegotiatingFastOpen->Transaction()->SetFastOpenStatus(tfoStatus); } } void HalfOpenSocket::CancelFastOpenConnection() { MOZ_ASSERT(mFastOpenInProgress); LOG(("HalfOpenSocket::CancelFastOpenConnection [this=%p conn=%p]\n", this, mConnectionNegotiatingFastOpen.get())); RefPtr deleteProtector(this); mEnt->RemoveHalfOpenFastOpenBackups(this); mSocketTransport->SetFastOpenCallback(nullptr); mConnectionNegotiatingFastOpen->SetFastOpen(false); RefPtr trans = mConnectionNegotiatingFastOpen ->CloseConnectionFastOpenTakesTooLongOrError(true); mSocketTransport = nullptr; mStreamOut = nullptr; mStreamIn = nullptr; if (trans && trans->QueryHttpTransaction()) { RefPtr pendingTransInfo = new PendingTransactionInfo(trans->QueryHttpTransaction()); mEnt->InsertTransaction(pendingTransInfo, true); } mFastOpenInProgress = false; mConnectionNegotiatingFastOpen = nullptr; Abandon(); MOZ_ASSERT(!mBackupTransport); MOZ_ASSERT(!mBackupStreamOut); } void HalfOpenSocket::FastOpenNotSupported() { MOZ_ASSERT(mFastOpenInProgress); gHttpHandler->SetFastOpenNotSupported(); } nsresult HalfOpenSocket::SetupConn(nsIAsyncOutputStream* out, bool aFastOpen) { MOZ_ASSERT(!aFastOpen || (out == mStreamOut)); // We cannot ask for a connection for TFO and Http3 ata the same time. MOZ_ASSERT(!(mIsHttp3 && aFastOpen)); // assign the new socket to the http connection RefPtr conn; if (!mIsHttp3) { conn = new nsHttpConnection(); } else { conn = new HttpConnectionUDP(); } LOG( ("HalfOpenSocket::SetupConn " "Created new nshttpconnection %p %s\n", conn.get(), mIsHttp3 ? "using http3" : "")); NullHttpTransaction* nullTrans = mTransaction->QueryNullTransaction(); if (nullTrans) { conn->BootstrapTimings(nullTrans->Timings()); } // Some capabilities are needed before a transaciton actually gets // scheduled (e.g. how to negotiate false start) conn->SetTransactionCaps(mTransaction->Caps()); NetAddr peeraddr; nsCOMPtr callbacks; mTransaction->GetSecurityCallbacks(getter_AddRefs(callbacks)); nsresult rv; if (out == mStreamOut) { TimeDuration rtt = TimeStamp::Now() - mPrimarySynStarted; rv = conn->Init( mEnt->mConnInfo, gHttpHandler->ConnMgr()->mMaxRequestDelay, mSocketTransport, mStreamIn, mStreamOut, mPrimaryConnectedOK || aFastOpen, callbacks, PR_MillisecondsToInterval(static_cast(rtt.ToMilliseconds()))); bool resetPreference = false; mSocketTransport->GetResetIPFamilyPreference(&resetPreference); if (resetPreference) { mEnt->ResetIPFamilyPreference(); } if (!aFastOpen && NS_SUCCEEDED(mSocketTransport->GetPeerAddr(&peeraddr))) { mEnt->RecordIPFamilyPreference(peeraddr.raw.family); } // The nsHttpConnection object now owns these streams and sockets if (!aFastOpen) { mStreamOut = nullptr; mStreamIn = nullptr; mSocketTransport = nullptr; } else { RefPtr connTCP = do_QueryObject(conn); MOZ_ASSERT(connTCP); if (connTCP) { connTCP->SetFastOpen(true); } } } else if (out == mBackupStreamOut) { TimeDuration rtt = TimeStamp::Now() - mBackupSynStarted; rv = conn->Init( mEnt->mConnInfo, gHttpHandler->ConnMgr()->mMaxRequestDelay, mBackupTransport, mBackupStreamIn, mBackupStreamOut, mBackupConnectedOK, callbacks, PR_MillisecondsToInterval(static_cast(rtt.ToMilliseconds()))); bool resetPreference = false; mBackupTransport->GetResetIPFamilyPreference(&resetPreference); if (resetPreference) { mEnt->ResetIPFamilyPreference(); } if (NS_SUCCEEDED(mBackupTransport->GetPeerAddr(&peeraddr))) { mEnt->RecordIPFamilyPreference(peeraddr.raw.family); } // The nsHttpConnection object now owns these streams and sockets mBackupStreamOut = nullptr; mBackupStreamIn = nullptr; mBackupTransport = nullptr; } else { MOZ_ASSERT(false, "unexpected stream"); rv = NS_ERROR_UNEXPECTED; } if (NS_FAILED(rv)) { LOG( ("HalfOpenSocket::SetupConn " "conn->init (%p) failed %" PRIx32 "\n", conn.get(), static_cast(rv))); RefPtr connTCP = do_QueryObject(conn); if (connTCP) { // Set TFO status. if ((mFastOpenStatus == TFO_HTTP) || (mFastOpenStatus == TFO_DISABLED) || (mFastOpenStatus == TFO_DISABLED_CONNECT)) { connTCP->SetFastOpenStatus(mFastOpenStatus); } else { connTCP->SetFastOpenStatus(TFO_INIT_FAILED); } } if (nsHttpTransaction* trans = mTransaction->QueryHttpTransaction()) { if (mIsHttp3) { trans->DisableHttp3(); gHttpHandler->ExcludeHttp3(mEnt->mConnInfo); } // The transaction's connection info is changed after DisableHttp3(), so // this is the only point we can remove this transaction from its conn // entry. mEnt->RemoveTransFromPendingQ(trans); } mTransaction->Close(rv); return rv; } // This half-open socket has created a connection. This flag excludes it // from counter of actual connections used for checking limits. if (!aFastOpen) { mHasConnected = true; } // if this is still in the pending list, remove it and dispatch it RefPtr pendingTransInfo = gHttpHandler->ConnMgr()->FindTransactionHelper(true, mEnt, mTransaction); if (pendingTransInfo) { MOZ_ASSERT(!mSpeculative, "Speculative Half Open found mTransaction"); mEnt->InsertIntoActiveConns(conn); if (mIsHttp3) { // Each connection must have a ConnectionHandle wrapper. // In case of Http < 2 the a ConnectionHandle is created for each // transaction in DispatchAbstractTransaction. // In case of Http2/ and Http3, ConnectionHandle is created only once. // Http2 connection always starts as http1 connection and the first // transaction use DispatchAbstractTransaction to be dispatched and // a ConnectionHandle is created. All consecutive transactions for // Http2 use a short-cut in DispatchTransaction and call // HttpConnectionBase::Activate (DispatchAbstractTransaction is never // called). // In case of Http3 the short-cut HttpConnectionBase::Activate is always // used also for the first transaction, therefore we need to create // ConnectionHandle here. RefPtr handle = new ConnectionHandle(conn); pendingTransInfo->Transaction()->SetConnection(handle); } rv = gHttpHandler->ConnMgr()->DispatchTransaction( mEnt, pendingTransInfo->Transaction(), conn); } else { // this transaction was dispatched off the pending q before all the // sockets established themselves. // After about 1 second allow for the possibility of restarting a // transaction due to server close. Keep at sub 1 second as that is the // minimum granularity we can expect a server to be timing out with. RefPtr connTCP = do_QueryObject(conn); if (connTCP) { connTCP->SetIsReusedAfter(950); } // if we are using ssl and no other transactions are waiting right now, // then form a null transaction to drive the SSL handshake to // completion. Afterwards the connection will be 100% ready for the next // transaction to use it. Make an exception for SSL tunneled HTTP proxy as // the NullHttpTransaction does not know how to drive Connect // Http3 cannot be dispatched using OnMsgReclaimConnection (see below), // therefore we need to use a Nulltransaction. if (!connTCP || (mEnt->mConnInfo->FirstHopSSL() && !mEnt->UrgentStartQueueLength() && !mEnt->PendingQueueLength() && !mEnt->mConnInfo->UsingConnect())) { LOG( ("HalfOpenSocket::SetupConn null transaction will " "be used to finish SSL handshake on conn %p\n", conn.get())); RefPtr trans; if (mTransaction->IsNullTransaction() && !mDispatchedMTransaction) { // null transactions cannot be put in the entry queue, so that // explains why it is not present. mDispatchedMTransaction = true; trans = mTransaction; } else { trans = new NullHttpTransaction(mEnt->mConnInfo, callbacks, mCaps); } mEnt->InsertIntoActiveConns(conn); rv = gHttpHandler->ConnMgr()->DispatchAbstractTransaction(mEnt, trans, mCaps, conn, 0); } else { // otherwise just put this in the persistent connection pool LOG( ("HalfOpenSocket::SetupConn no transaction match " "returning conn %p to pool\n", conn.get())); gHttpHandler->ConnMgr()->OnMsgReclaimConnection(conn); // We expect that there is at least one tranasction in the pending // queue that can take this connection, but it can happened that // all transactions are blocked or they have took other idle // connections. In that case the connection has been added to the // idle queue. // If the connection is in the idle queue but it is using ssl, make // a nulltransaction for it to finish ssl handshake! // !!! It can be that mEnt is null after OnMsgReclaimConnection.!!! if (mEnt && mEnt->mConnInfo->FirstHopSSL() && !mEnt->mConnInfo->UsingConnect()) { RefPtr connTCP = do_QueryObject(conn); // If RemoveIdleConnection succeeds that means that conn is in the // idle queue. if (connTCP && NS_SUCCEEDED(mEnt->RemoveIdleConnection(connTCP))) { RefPtr trans; if (mTransaction->IsNullTransaction() && !mDispatchedMTransaction) { mDispatchedMTransaction = true; trans = mTransaction; } else { trans = new NullHttpTransaction(mEnt->mConnInfo, callbacks, mCaps); } mEnt->InsertIntoActiveConns(conn); rv = gHttpHandler->ConnMgr()->DispatchAbstractTransaction( mEnt, trans, mCaps, conn, 0); } } } } // If this connection has a transaction get reference to its // ConnectionHandler. RefPtr connTCP = do_QueryObject(conn); if (connTCP) { if (aFastOpen) { MOZ_ASSERT(mEnt); MOZ_ASSERT(!mEnt->IsInIdleConnections(connTCP)); if (NS_SUCCEEDED(rv) && mEnt->IsInActiveConns(conn)) { mConnectionNegotiatingFastOpen = connTCP; } else { connTCP->SetFastOpen(false); connTCP->SetFastOpenStatus(TFO_INIT_FAILED); } } else { connTCP->SetFastOpenStatus(mFastOpenStatus); if ((mFastOpenStatus != TFO_HTTP) && (mFastOpenStatus != TFO_DISABLED) && (mFastOpenStatus != TFO_DISABLED_CONNECT)) { mFastOpenStatus = TFO_BACKUP_CONN; // Set this to TFO_BACKUP_CONN // so that if a backup // connection is established we // do not report values twice. } } } // If this halfOpenConn was speculative, but at the end the conn got a // non-null transaction than this halfOpen is not speculative anymore! if (conn->Transaction() && !conn->Transaction()->IsNullTransaction()) { Claim(); } return rv; } // method for nsITransportEventSink NS_IMETHODIMP HalfOpenSocket::OnTransportStatus(nsITransport* trans, nsresult status, int64_t progress, int64_t progressMax) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); MOZ_ASSERT((trans == mSocketTransport) || (trans == mBackupTransport)); MOZ_ASSERT(mEnt); if (mTransaction) { if ((trans == mSocketTransport) || ((trans == mBackupTransport) && (status == NS_NET_STATUS_CONNECTED_TO) && mSocketTransport)) { // Send this status event only if the transaction is still pending, // i.e. it has not found a free already connected socket. // Sockets in halfOpen state can only get following events: // NS_NET_STATUS_RESOLVING_HOST, NS_NET_STATUS_RESOLVED_HOST, // NS_NET_STATUS_CONNECTING_TO and NS_NET_STATUS_CONNECTED_TO. // mBackupTransport is only started after // NS_NET_STATUS_CONNECTING_TO of mSocketTransport, so ignore all // mBackupTransport events until NS_NET_STATUS_CONNECTED_TO. // mBackupTransport must be connected before mSocketTransport. mTransaction->OnTransportStatus(trans, status, progress); } } MOZ_ASSERT(trans == mSocketTransport || trans == mBackupTransport); if (status == NS_NET_STATUS_CONNECTED_TO) { if (trans == mSocketTransport) { mPrimaryConnectedOK = true; } else { mBackupConnectedOK = true; } } // The rest of this method only applies to the primary transport if (trans != mSocketTransport) { return NS_OK; } mPrimaryStreamStatus = status; // if we are doing spdy coalescing and haven't recorded the ip address // for this entry before then make the hash key if our dns lookup // just completed. We can't do coalescing if using a proxy because the // ip addresses are not available to the client. if (status == NS_NET_STATUS_CONNECTING_TO && gHttpHandler->IsSpdyEnabled() && gHttpHandler->CoalesceSpdy() && mEnt && mEnt->mConnInfo && mEnt->mConnInfo->EndToEndSSL() && mEnt->AllowHttp2() && !mEnt->mConnInfo->UsingProxy() && mEnt->mCoalescingKeys.IsEmpty()) { nsCOMPtr dnsRecord(do_GetInterface(mSocketTransport)); nsTArray addressSet; nsresult rv = NS_ERROR_NOT_AVAILABLE; if (dnsRecord) { rv = dnsRecord->GetAddresses(addressSet); } if (NS_SUCCEEDED(rv) && !addressSet.IsEmpty()) { for (uint32_t i = 0; i < addressSet.Length(); ++i) { if ((addressSet[i].raw.family == AF_INET && addressSet[i].inet.ip == 0) || (addressSet[i].raw.family == AF_INET6 && addressSet[i].inet6.ip.u64[0] == 0 && addressSet[i].inet6.ip.u64[1] == 0)) { // Bug 1680249 - Don't create the coalescing key if the ip address is // `0.0.0.0` or `::`. LOG(("HalfOpenSocket: skip creating Coalescing Key for host [%s]", mEnt->mConnInfo->Origin())); continue; } nsCString* newKey = mEnt->mCoalescingKeys.AppendElement(nsCString()); newKey->SetLength(kIPv6CStrBufSize + 26); addressSet[i].ToStringBuffer(newKey->BeginWriting(), kIPv6CStrBufSize); newKey->SetLength(strlen(newKey->BeginReading())); if (mEnt->mConnInfo->GetAnonymous()) { newKey->AppendLiteral("~A:"); } else { newKey->AppendLiteral("~.:"); } newKey->AppendInt(mEnt->mConnInfo->OriginPort()); newKey->AppendLiteral("/["); nsAutoCString suffix; mEnt->mConnInfo->GetOriginAttributes().CreateSuffix(suffix); newKey->Append(suffix); newKey->AppendLiteral("]viaDNS"); LOG(( "HalfOpenSocket::OnTransportStatus " "STATUS_CONNECTING_TO Established New Coalescing Key # %d for host " "%s [%s]", i, mEnt->mConnInfo->Origin(), newKey->get())); } gHttpHandler->ConnMgr()->ProcessSpdyPendingQ(mEnt); } } switch (status) { case NS_NET_STATUS_CONNECTING_TO: // Passed DNS resolution, now trying to connect, start the backup timer // only prevent creating another backup transport. // We also check for mEnt presence to not instantiate the timer after // this half open socket has already been abandoned. It may happen // when we get this notification right between main-thread calls to // nsHttpConnectionMgr::Shutdown and nsSocketTransportService::Shutdown // where the first abandons all half open socket instances and only // after that the second stops the socket thread. // Http3 has its own syn-retransmission, therefore it does not need a // backup connection. if (mEnt && !mBackupTransport && !mSynTimer && !mIsHttp3) { SetupBackupTimer(); } break; case NS_NET_STATUS_CONNECTED_TO: // TCP connection's up, now transfer or SSL negotiantion starts, // no need for backup socket CancelBackupTimer(); break; default: break; } return NS_OK; } // method for nsIInterfaceRequestor NS_IMETHODIMP HalfOpenSocket::GetInterface(const nsIID& iid, void** result) { if (mTransaction) { nsCOMPtr callbacks; mTransaction->GetSecurityCallbacks(getter_AddRefs(callbacks)); if (callbacks) { return callbacks->GetInterface(iid, result); } } return NS_ERROR_NO_INTERFACE; } bool HalfOpenSocket::AcceptsTransaction(nsHttpTransaction* trans) { // When marked as urgent start, only accept urgent start marked transactions. // Otherwise, accept any kind of transaction. return !mUrgentStart || (trans->Caps() & nsIClassOfService::UrgentStart); } bool HalfOpenSocket::Claim() { if (mSpeculative) { mSpeculative = false; uint32_t flags; if (mSocketTransport && NS_SUCCEEDED(mSocketTransport->GetConnectionFlags(&flags))) { flags &= ~nsISocketTransport::DISABLE_RFC1918; mSocketTransport->SetConnectionFlags(flags); } Telemetry::AutoCounter usedSpeculativeConn; ++usedSpeculativeConn; if (mIsFromPredictor) { Telemetry::AutoCounter totalPreconnectsUsed; ++totalPreconnectsUsed; } // Http3 has its own syn-retransmission, therefore it does not need a // backup connection. if ((mPrimaryStreamStatus == NS_NET_STATUS_CONNECTING_TO) && mEnt && !mBackupTransport && !mSynTimer && !mIsHttp3) { SetupBackupTimer(); } } if (mFreeToUse) { mFreeToUse = false; return true; } return false; } void HalfOpenSocket::Unclaim() { MOZ_ASSERT(!mSpeculative && !mFreeToUse); // We will keep the backup-timer running. Most probably this halfOpen will // be used by a transaction from which this transaction took the halfOpen. // (this is happening because of the transaction priority.) mFreeToUse = true; } } // namespace net } // namespace mozilla