/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set sw=2 ts=8 et tw=80 : */ /* 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 "AltServiceChild.h" #include "CacheControlParser.h" #include "CachePushChecker.h" #include "Http2Push.h" #include "Http2Session.h" #include "Http2Stream.h" #include "Http2StreamBase.h" #include "Http2StreamTunnel.h" #include "LoadContextInfo.h" #include "mozilla/EndianUtils.h" #include "mozilla/Preferences.h" #include "mozilla/Sprintf.h" #include "mozilla/StaticPrefs_network.h" #include "mozilla/Telemetry.h" #include "nsHttp.h" #include "nsHttpConnection.h" #include "nsHttpHandler.h" #include "nsIRequestContext.h" #include "nsISupportsPriority.h" #include "nsITLSSocketControl.h" #include "nsNetUtil.h" #include "nsQueryObject.h" #include "nsSocketTransportService2.h" #include "nsStandardURL.h" #include "nsURLHelper.h" #include "prnetdb.h" #include "sslerr.h" #include "sslt.h" namespace mozilla { namespace net { // Http2Session has multiple inheritance of things that implement nsISupports NS_IMPL_ADDREF(Http2Session) NS_IMPL_RELEASE(Http2Session) NS_INTERFACE_MAP_BEGIN(Http2Session) NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) NS_INTERFACE_MAP_ENTRY_CONCRETE(Http2Session) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsAHttpConnection) NS_INTERFACE_MAP_END static void RemoveStreamFromQueue(Http2StreamBase* aStream, nsTArray>& queue) { for (const auto& stream : Reversed(queue)) { if (stream == aStream) { queue.RemoveElement(stream); } } } static void AddStreamToQueue(Http2StreamBase* aStream, nsTArray>& queue) { if (!queue.Contains(aStream)) { queue.AppendElement(aStream); } } static already_AddRefed GetNextStreamFromQueue( nsTArray>& queue) { while (!queue.IsEmpty() && !queue[0]) { MOZ_ASSERT(false); queue.RemoveElementAt(0); } if (queue.IsEmpty()) { return nullptr; } RefPtr stream = queue[0].get(); queue.RemoveElementAt(0); return stream.forget(); } // "magic" refers to the string that preceeds HTTP/2 on the wire // to help find any intermediaries speaking an older version of HTTP const uint8_t Http2Session::kMagicHello[] = { 0x50, 0x52, 0x49, 0x20, 0x2a, 0x20, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x32, 0x2e, 0x30, 0x0d, 0x0a, 0x0d, 0x0a, 0x53, 0x4d, 0x0d, 0x0a, 0x0d, 0x0a}; Http2Session* Http2Session::CreateSession(nsISocketTransport* aSocketTransport, enum SpdyVersion version, bool attemptingEarlyData) { if (!gHttpHandler) { RefPtr handler = nsHttpHandler::GetInstance(); Unused << handler.get(); } Http2Session* session = new Http2Session(aSocketTransport, version, attemptingEarlyData); session->SendHello(); return session; } Http2Session::Http2Session(nsISocketTransport* aSocketTransport, enum SpdyVersion version, bool attemptingEarlyData) : mSocketTransport(aSocketTransport), mSegmentReader(nullptr), mSegmentWriter(nullptr), mNextStreamID(3) // 1 is reserved for Updgrade handshakes , mLastPushedID(0), mConcurrentHighWater(0), mDownstreamState(BUFFERING_OPENING_SETTINGS), mInputFrameBufferSize(kDefaultBufferSize), mInputFrameBufferUsed(0), mInputFrameDataSize(0), mInputFrameDataRead(0), mInputFrameFinal(false), mInputFrameType(0), mInputFrameFlags(0), mInputFrameID(0), mPaddingLength(0), mInputFrameDataStream(nullptr), mNeedsCleanup(nullptr), mDownstreamRstReason(NO_HTTP_ERROR), mExpectedHeaderID(0), mExpectedPushPromiseID(0), mContinuedPromiseStream(0), mFlatHTTPResponseHeadersOut(0), mShouldGoAway(false), mClosed(false), mCleanShutdown(false), mReceivedSettings(false), mTLSProfileConfirmed(false), mGoAwayReason(NO_HTTP_ERROR), mClientGoAwayReason(UNASSIGNED), mPeerGoAwayReason(UNASSIGNED), mGoAwayID(0), mOutgoingGoAwayID(0), mConcurrent(0), mServerPushedResources(0), mServerInitialStreamWindow(kDefaultRwin), mLocalSessionWindow(kDefaultRwin), mServerSessionWindow(kDefaultRwin), mInitialRwin(ASpdySession::kInitialRwin), mOutputQueueSize(kDefaultQueueSize), mOutputQueueUsed(0), mOutputQueueSent(0), mLastReadEpoch(PR_IntervalNow()), mPingSentEpoch(0), mPreviousUsed(false), mAggregatedHeaderSize(0), mWaitingForSettingsAck(false), mGoAwayOnPush(false), mUseH2Deps(false), mAttemptingEarlyData(attemptingEarlyData), mOriginFrameActivated(false), mCntActivated(0), mTlsHandshakeFinished(false), mPeerFailedHandshake(false), mTrrStreams(0), mEnableWebsockets(false), mPeerAllowsWebsockets(false), mProcessedWaitingWebsockets(false) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); static uint64_t sSerial; mSerial = ++sSerial; LOG3(("Http2Session::Http2Session %p serial=0x%" PRIX64 "\n", this, mSerial)); mInputFrameBuffer = MakeUnique(mInputFrameBufferSize); mOutputQueueBuffer = MakeUnique(mOutputQueueSize); mDecompressBuffer.SetCapacity(kDefaultBufferSize); mPushAllowance = gHttpHandler->SpdyPushAllowance(); mInitialRwin = std::max(gHttpHandler->SpdyPullAllowance(), mPushAllowance); mMaxConcurrent = gHttpHandler->DefaultSpdyConcurrent(); mSendingChunkSize = gHttpHandler->SpdySendingChunkSize(); mLastDataReadEpoch = mLastReadEpoch; mPingThreshold = gHttpHandler->SpdyPingThreshold(); mPreviousPingThreshold = mPingThreshold; mCurrentBrowserId = gHttpHandler->ConnMgr()->CurrentBrowserId(); mEnableWebsockets = StaticPrefs::network_http_http2_websockets(); bool dumpHpackTables = StaticPrefs::network_http_http2_enable_hpack_dump(); mCompressor.SetDumpTables(dumpHpackTables); mDecompressor.SetDumpTables(dumpHpackTables); } void Http2Session::Shutdown(nsresult aReason) { for (const auto& stream : mStreamTransactionHash.Values()) { ShutdownStream(stream, aReason); } for (auto& stream : mTunnelStreams) { ShutdownStream(stream, aReason); } } void Http2Session::ShutdownStream(Http2StreamBase* aStream, nsresult aReason) { // On a clean server hangup the server sets the GoAwayID to be the ID of // the last transaction it processed. If the ID of stream in the // local stream is greater than that it can safely be restarted because the // server guarantees it was not partially processed. Streams that have not // registered an ID haven't actually been sent yet so they can always be // restarted. if (mCleanShutdown && (aStream->StreamID() > mGoAwayID || !aStream->HasRegisteredID())) { CloseStream(aStream, NS_ERROR_NET_RESET); // can be restarted } else if (aStream->RecvdData()) { CloseStream(aStream, NS_ERROR_NET_PARTIAL_TRANSFER); } else if (mGoAwayReason == INADEQUATE_SECURITY) { CloseStream(aStream, NS_ERROR_NET_INADEQUATE_SECURITY); } else if (!mCleanShutdown && (mGoAwayReason != NO_HTTP_ERROR)) { CloseStream(aStream, NS_ERROR_NET_HTTP2_SENT_GOAWAY); } else if (!mCleanShutdown && PossibleZeroRTTRetryError(aReason)) { CloseStream(aStream, aReason); } else { CloseStream(aStream, NS_ERROR_ABORT); } } Http2Session::~Http2Session() { MOZ_DIAGNOSTIC_ASSERT(OnSocketThread()); LOG3(("Http2Session::~Http2Session %p mDownstreamState=%X", this, mDownstreamState)); Shutdown(NS_OK); if (mTrrStreams) { Telemetry::Accumulate(Telemetry::DNS_TRR_REQUEST_PER_CONN, mTrrStreams); } Telemetry::Accumulate(Telemetry::SPDY_PARALLEL_STREAMS, mConcurrentHighWater); Telemetry::Accumulate(Telemetry::SPDY_REQUEST_PER_CONN_3, mCntActivated); Telemetry::Accumulate(Telemetry::SPDY_SERVER_INITIATED_STREAMS, mServerPushedResources); Telemetry::Accumulate(Telemetry::SPDY_GOAWAY_LOCAL, mClientGoAwayReason); Telemetry::Accumulate(Telemetry::SPDY_GOAWAY_PEER, mPeerGoAwayReason); Telemetry::Accumulate(Telemetry::HTTP2_FAIL_BEFORE_SETTINGS, mPeerFailedHandshake); } inline nsresult Http2Session::SessionError(enum errorType reason) { LOG3(("Http2Session::SessionError %p reason=0x%x mPeerGoAwayReason=0x%x", this, reason, mPeerGoAwayReason)); mGoAwayReason = reason; if (reason == INADEQUATE_SECURITY) { // This one is special, as we have an error page just for this return NS_ERROR_NET_INADEQUATE_SECURITY; } // We're the one sending a generic GOAWAY return NS_ERROR_NET_HTTP2_SENT_GOAWAY; } void Http2Session::LogIO(Http2Session* self, Http2StreamBase* stream, const char* label, const char* data, uint32_t datalen) { if (!LOG5_ENABLED()) return; LOG5(("Http2Session::LogIO %p stream=%p id=0x%X [%s]", self, stream, stream ? stream->StreamID() : 0, label)); // Max line is (16 * 3) + 10(prefix) + newline + null char linebuf[128]; uint32_t index; char* line = linebuf; linebuf[127] = 0; for (index = 0; index < datalen; ++index) { if (!(index % 16)) { if (index) { *line = 0; LOG5(("%s", linebuf)); } line = linebuf; snprintf(line, 128, "%08X: ", index); line += 10; } snprintf(line, 128 - (line - linebuf), "%02X ", (reinterpret_cast(data))[index]); line += 3; } if (index) { *line = 0; LOG5(("%s", linebuf)); } } using Http2ControlFx = nsresult (*)(Http2Session*); static Http2ControlFx sControlFunctions[] = { nullptr, // type 0 data is not a control function Http2Session::RecvHeaders, Http2Session::RecvPriority, Http2Session::RecvRstStream, Http2Session::RecvSettings, Http2Session::RecvPushPromise, Http2Session::RecvPing, Http2Session::RecvGoAway, Http2Session::RecvWindowUpdate, Http2Session::RecvContinuation, Http2Session::RecvAltSvc, // extension for type 0x0A Http2Session::RecvUnused, // 0x0B was BLOCKED still radioactive Http2Session::RecvOrigin // extension for type 0x0C }; bool Http2Session::RoomForMoreConcurrent() { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); return (mConcurrent < mMaxConcurrent); } bool Http2Session::RoomForMoreStreams() { if (mNextStreamID + mStreamTransactionHash.Count() * 2 > kMaxStreamID) { return false; } return !mShouldGoAway; } PRIntervalTime Http2Session::IdleTime() { return PR_IntervalNow() - mLastDataReadEpoch; } uint32_t Http2Session::ReadTimeoutTick(PRIntervalTime now) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); LOG3(("Http2Session::ReadTimeoutTick %p delta since last read %ds\n", this, PR_IntervalToSeconds(now - mLastReadEpoch))); if (!mPingThreshold) { return UINT32_MAX; } if ((now - mLastReadEpoch) < mPingThreshold) { // recent activity means ping is not an issue if (mPingSentEpoch) { mPingSentEpoch = 0; if (mPreviousUsed) { // restore the former value mPingThreshold = mPreviousPingThreshold; mPreviousUsed = false; } } return PR_IntervalToSeconds(mPingThreshold) - PR_IntervalToSeconds(now - mLastReadEpoch); } if (mPingSentEpoch) { bool isTrr = (mTrrStreams > 0); uint32_t pingTimeout = isTrr ? StaticPrefs::network_trr_ping_timeout() : gHttpHandler->SpdyPingTimeout(); LOG3( ("Http2Session::ReadTimeoutTick %p handle outstanding ping, " "timeout=%d\n", this, pingTimeout)); if ((now - mPingSentEpoch) >= pingTimeout) { LOG3(("Http2Session::ReadTimeoutTick %p Ping Timer Exhaustion\n", this)); if (mConnection) { mConnection->SetCloseReason(ConnectionCloseReason::IDLE_TIMEOUT); } mPingSentEpoch = 0; if (isTrr) { // These must be set this way to ensure we gracefully restart all // streams mGoAwayID = 0; mCleanShutdown = true; // If TRR is mode 2, this Http2Session will be closed due to TRR request // timeout, so we won't reach this code. If we are in mode 3, the // request timeout is usually larger than the ping timeout. We close the // stream with NS_ERROR_NET_RESET, so the transactions can be restarted. Close(NS_ERROR_NET_RESET); } else { Close(NS_ERROR_NET_TIMEOUT); } return UINT32_MAX; } return 1; // run the tick aggressively while ping is outstanding } LOG3(("Http2Session::ReadTimeoutTick %p generating ping\n", this)); mPingSentEpoch = PR_IntervalNow(); if (!mPingSentEpoch) { mPingSentEpoch = 1; // avoid the 0 sentinel value } GeneratePing(false); Unused << ResumeRecv(); // read the ping reply // Check for orphaned push streams. This looks expensive, but generally the // list is empty. Http2PushedStream* deleteMe; TimeStamp timestampNow; do { deleteMe = nullptr; for (uint32_t index = mPushedStreams.Length(); index > 0; --index) { Http2PushedStream* pushedStream = mPushedStreams[index - 1]; if (timestampNow.IsNull()) { timestampNow = TimeStamp::Now(); // lazy initializer } // if stream finished, but is not connected, and its been like that for // long then cleanup the stream. if (pushedStream->IsOrphaned(timestampNow)) { LOG3(("Http2Session Timeout Pushed Stream %p 0x%X\n", this, pushedStream->StreamID())); deleteMe = pushedStream; break; // don't CleanupStream() while iterating this vector } } if (deleteMe) CleanupStream(deleteMe, NS_ERROR_ABORT, CANCEL_ERROR); } while (deleteMe); return 1; // run the tick aggressively while ping is outstanding } uint32_t Http2Session::RegisterStreamID(Http2StreamBase* stream, uint32_t aNewID) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); MOZ_ASSERT(mNextStreamID < 0xfffffff0, "should have stopped admitting streams"); MOZ_ASSERT(!(aNewID & 1), "0 for autoassign pull, otherwise explicit even push assignment"); if (!aNewID) { // auto generate a new pull stream ID aNewID = mNextStreamID; MOZ_ASSERT(aNewID & 1, "pull ID must be odd."); mNextStreamID += 2; } LOG1( ("Http2Session::RegisterStreamID session=%p stream=%p id=0x%X " "concurrent=%d", this, stream, aNewID, mConcurrent)); // We've used up plenty of ID's on this session. Start // moving to a new one before there is a crunch involving // server push streams or concurrent non-registered submits if (aNewID >= kMaxStreamID) mShouldGoAway = true; // integrity check if (mStreamIDHash.Contains(aNewID)) { LOG3((" New ID already present\n")); MOZ_ASSERT(false, "New ID already present in mStreamIDHash"); mShouldGoAway = true; return kDeadStreamID; } mStreamIDHash.InsertOrUpdate(aNewID, stream); if (aNewID & 1) { // don't count push streams here RefPtr ci(stream->ConnectionInfo()); if (ci && ci->GetIsTrrServiceChannel()) { IncrementTrrCounter(); } } return aNewID; } bool Http2Session::AddStream(nsAHttpTransaction* aHttpTransaction, int32_t aPriority, nsIInterfaceRequestor* aCallbacks) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); // integrity check if (mStreamTransactionHash.Contains(aHttpTransaction)) { LOG3((" New transaction already present\n")); MOZ_ASSERT(false, "AddStream duplicate transaction pointer"); return false; } if (!mConnection) { mConnection = aHttpTransaction->Connection(); } if (!mFirstHttpTransaction && !mTlsHandshakeFinished) { mFirstHttpTransaction = aHttpTransaction->QueryHttpTransaction(); LOG3(("Http2Session::AddStream first session=%p trans=%p ", this, mFirstHttpTransaction.get())); } if (mClosed || mShouldGoAway) { nsHttpTransaction* trans = aHttpTransaction->QueryHttpTransaction(); if (trans) { RefPtr pushedStreamWrapper; pushedStreamWrapper = trans->GetPushedStream(); if (!pushedStreamWrapper || !pushedStreamWrapper->GetStream()) { LOG3( ("Http2Session::AddStream %p atrans=%p trans=%p session unusable - " "resched.\n", this, aHttpTransaction, trans)); aHttpTransaction->SetConnection(nullptr); nsresult rv = gHttpHandler->InitiateTransaction(trans, trans->Priority()); if (NS_FAILED(rv)) { LOG3( ("Http2Session::AddStream %p atrans=%p trans=%p failed to " "initiate " "transaction (%08x).\n", this, aHttpTransaction, trans, static_cast(rv))); } return true; } } } aHttpTransaction->SetConnection(this); aHttpTransaction->OnActivated(); CreateStream(aHttpTransaction, aPriority, Http2StreamBaseType::Normal); return true; } void Http2Session::CreateStream(nsAHttpTransaction* aHttpTransaction, int32_t aPriority, Http2StreamBaseType streamType) { RefPtr refStream; switch (streamType) { case Http2StreamBaseType::Normal: refStream = new Http2Stream(aHttpTransaction, this, aPriority, mCurrentBrowserId); break; case Http2StreamBaseType::WebSocket: case Http2StreamBaseType::Tunnel: case Http2StreamBaseType::ServerPush: MOZ_RELEASE_ASSERT(false); return; } LOG3(("Http2Session::AddStream session=%p stream=%p serial=%" PRIu64 " " "NextID=0x%X (tentative)", this, refStream.get(), mSerial, mNextStreamID)); RefPtr stream = refStream; mStreamTransactionHash.InsertOrUpdate(aHttpTransaction, std::move(refStream)); AddStreamToQueue(stream, mReadyForWrite); SetWriteCallbacks(); // Kick off the SYN transmit without waiting for the poll loop // This won't work for the first stream because there is no segment reader // yet. if (mSegmentReader) { uint32_t countRead; Unused << ReadSegments(nullptr, kDefaultBufferSize, &countRead); } if (!(aHttpTransaction->Caps() & NS_HTTP_ALLOW_KEEPALIVE) && !aHttpTransaction->IsNullTransaction()) { LOG3(("Http2Session::AddStream %p transaction %p forces keep-alive off.\n", this, aHttpTransaction)); DontReuse(); } } already_AddRefed Http2Session::CreateTunnelStream( nsAHttpTransaction* aHttpTransaction, nsIInterfaceRequestor* aCallbacks, PRIntervalTime aRtt, bool aIsWebSocket) { RefPtr refStream = CreateTunnelStreamFromConnInfo( this, mCurrentBrowserId, aHttpTransaction->ConnectionInfo(), aIsWebSocket); RefPtr newConn = refStream->CreateHttpConnection( aHttpTransaction, aCallbacks, aRtt, aIsWebSocket); mTunnelStreams.AppendElement(std::move(refStream)); return newConn.forget(); } void Http2Session::QueueStream(Http2StreamBase* stream) { // will be removed via processpending or a shutdown path MOZ_ASSERT(OnSocketThread(), "not on socket thread"); MOZ_ASSERT(!stream->CountAsActive()); MOZ_ASSERT(!stream->Queued()); LOG3(("Http2Session::QueueStream %p stream %p queued.", this, stream)); #ifdef DEBUG for (const auto& qStream : mQueuedStreams) { MOZ_ASSERT(qStream != stream); MOZ_ASSERT(qStream->Queued()); } #endif stream->SetQueued(true); AddStreamToQueue(stream, mQueuedStreams); } void Http2Session::ProcessPending() { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); RefPtr stream; while (RoomForMoreConcurrent() && (stream = GetNextStreamFromQueue(mQueuedStreams))) { LOG3(("Http2Session::ProcessPending %p stream %p woken from queue.", this, stream.get())); MOZ_ASSERT(!stream->CountAsActive()); MOZ_ASSERT(stream->Queued()); stream->SetQueued(false); AddStreamToQueue(stream, mReadyForWrite); SetWriteCallbacks(); } } nsresult Http2Session::NetworkRead(nsAHttpSegmentWriter* writer, char* buf, uint32_t count, uint32_t* countWritten) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); if (!count) { *countWritten = 0; return NS_OK; } nsresult rv = writer->OnWriteSegment(buf, count, countWritten); if (NS_SUCCEEDED(rv) && *countWritten > 0) { mLastReadEpoch = PR_IntervalNow(); } return rv; } void Http2Session::SetWriteCallbacks() { if (mConnection && (GetWriteQueueSize() || (mOutputQueueUsed > mOutputQueueSent))) { Unused << mConnection->ResumeSend(); } } void Http2Session::RealignOutputQueue() { if (mAttemptingEarlyData) { // We can't realign right now, because we may need what's in there if early // data fails. return; } mOutputQueueUsed -= mOutputQueueSent; memmove(mOutputQueueBuffer.get(), mOutputQueueBuffer.get() + mOutputQueueSent, mOutputQueueUsed); mOutputQueueSent = 0; } void Http2Session::FlushOutputQueue() { if (!mSegmentReader || !mOutputQueueUsed) return; nsresult rv; uint32_t countRead; uint32_t avail = mOutputQueueUsed - mOutputQueueSent; if (!avail && mAttemptingEarlyData) { // This is kind of a hack, but there are cases where we'll have already // written the data we want whlie doing early data, but we get called again // with a reader, and we need to avoid calling the reader when there's // nothing for it to read. return; } rv = mSegmentReader->OnReadSegment( mOutputQueueBuffer.get() + mOutputQueueSent, avail, &countRead); LOG3(("Http2Session::FlushOutputQueue %p sz=%d rv=%" PRIx32 " actual=%d", this, avail, static_cast(rv), countRead)); // Dont worry about errors on write, we will pick this up as a read error too if (NS_FAILED(rv)) return; mOutputQueueSent += countRead; if (mAttemptingEarlyData) { return; } if (countRead == avail) { mOutputQueueUsed = 0; mOutputQueueSent = 0; return; } // If the output queue is close to filling up and we have sent out a good // chunk of data from the beginning then realign it. if ((mOutputQueueSent >= kQueueMinimumCleanup) && ((mOutputQueueSize - mOutputQueueUsed) < kQueueTailRoom)) { RealignOutputQueue(); } } void Http2Session::DontReuse() { LOG3(("Http2Session::DontReuse %p\n", this)); if (!OnSocketThread()) { LOG3(("Http2Session %p not on socket thread\n", this)); nsCOMPtr event = NewRunnableMethod( "Http2Session::DontReuse", this, &Http2Session::DontReuse); gSocketTransportService->Dispatch(event, NS_DISPATCH_NORMAL); return; } mShouldGoAway = true; if (!mClosed && !mStreamTransactionHash.Count()) { Close(NS_OK); } } enum SpdyVersion Http2Session::SpdyVersion() { return SpdyVersion::HTTP_2; } uint32_t Http2Session::GetWriteQueueSize() { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); return mReadyForWrite.Length(); } void Http2Session::ChangeDownstreamState(enum internalStateType newState) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); LOG3(("Http2Session::ChangeDownstreamState() %p from %X to %X", this, mDownstreamState, newState)); mDownstreamState = newState; } void Http2Session::ResetDownstreamState() { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); LOG3(("Http2Session::ResetDownstreamState() %p", this)); ChangeDownstreamState(BUFFERING_FRAME_HEADER); if (mInputFrameFinal && mInputFrameDataStream) { mInputFrameFinal = false; LOG3((" SetRecvdFin id=0x%x\n", mInputFrameDataStream->StreamID())); mInputFrameDataStream->SetRecvdFin(true); MaybeDecrementConcurrent(mInputFrameDataStream); } mInputFrameFinal = false; mInputFrameBufferUsed = 0; mInputFrameDataStream = nullptr; } // return true if activated (and counted against max) // otherwise return false and queue bool Http2Session::TryToActivate(Http2StreamBase* aStream) { if (aStream->Queued()) { LOG3(("Http2Session::TryToActivate %p stream=%p already queued.\n", this, aStream)); return false; } if (!RoomForMoreConcurrent()) { LOG3( ("Http2Session::TryToActivate %p stream=%p no room for more concurrent " "streams\n", this, aStream)); QueueStream(aStream); return false; } LOG3(("Http2Session::TryToActivate %p stream=%p\n", this, aStream)); IncrementConcurrent(aStream); mCntActivated++; return true; } void Http2Session::IncrementConcurrent(Http2StreamBase* stream) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); MOZ_ASSERT(!stream->StreamID() || (stream->StreamID() & 1), "Do not activate pushed streams"); nsAHttpTransaction* trans = stream->Transaction(); if (!trans || !trans->IsNullTransaction()) { MOZ_ASSERT(!stream->CountAsActive()); stream->SetCountAsActive(true); ++mConcurrent; if (mConcurrent > mConcurrentHighWater) { mConcurrentHighWater = mConcurrent; } LOG3( ("Http2Session::IncrementCounter %p counting stream %p Currently %d " "streams in session, high water mark is %d\n", this, stream, mConcurrent, mConcurrentHighWater)); } } // call with data length (i.e. 0 for 0 data bytes - ignore 9 byte header) // dest must have 9 bytes of allocated space template void Http2Session::CreateFrameHeader(charType dest, uint16_t frameLength, uint8_t frameType, uint8_t frameFlags, uint32_t streamID) { MOZ_ASSERT(frameLength <= kMaxFrameData, "framelength too large"); MOZ_ASSERT(!(streamID & 0x80000000)); MOZ_ASSERT(!frameFlags || (frameType != FRAME_TYPE_PRIORITY && frameType != FRAME_TYPE_RST_STREAM && frameType != FRAME_TYPE_GOAWAY && frameType != FRAME_TYPE_WINDOW_UPDATE)); dest[0] = 0x00; NetworkEndian::writeUint16(dest + 1, frameLength); dest[3] = frameType; dest[4] = frameFlags; NetworkEndian::writeUint32(dest + 5, streamID); } char* Http2Session::EnsureOutputBuffer(uint32_t spaceNeeded) { // this is an infallible allocation (if an allocation is // needed, which is probably isn't) EnsureBuffer(mOutputQueueBuffer, mOutputQueueUsed + spaceNeeded, mOutputQueueUsed, mOutputQueueSize); return mOutputQueueBuffer.get() + mOutputQueueUsed; } template void Http2Session::CreateFrameHeader(char* dest, uint16_t frameLength, uint8_t frameType, uint8_t frameFlags, uint32_t streamID); template void Http2Session::CreateFrameHeader(uint8_t* dest, uint16_t frameLength, uint8_t frameType, uint8_t frameFlags, uint32_t streamID); void Http2Session::MaybeDecrementConcurrent(Http2StreamBase* aStream) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); LOG3(("MaybeDecrementConcurrent %p id=0x%X concurrent=%d active=%d\n", this, aStream->StreamID(), mConcurrent, aStream->CountAsActive())); if (!aStream->CountAsActive()) return; MOZ_ASSERT(mConcurrent); aStream->SetCountAsActive(false); --mConcurrent; ProcessPending(); } // Need to decompress some data in order to keep the compression // context correct, but we really don't care what the result is nsresult Http2Session::UncompressAndDiscard(bool isPush) { nsresult rv; nsAutoCString trash; rv = mDecompressor.DecodeHeaderBlock( reinterpret_cast(mDecompressBuffer.BeginReading()), mDecompressBuffer.Length(), trash, isPush); mDecompressBuffer.Truncate(); if (NS_FAILED(rv)) { LOG3(("Http2Session::UncompressAndDiscard %p Compression Error\n", this)); mGoAwayReason = COMPRESSION_ERROR; return rv; } return NS_OK; } void Http2Session::GeneratePing(bool isAck) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); LOG3(("Http2Session::GeneratePing %p isAck=%d\n", this, isAck)); char* packet = EnsureOutputBuffer(kFrameHeaderBytes + 8); mOutputQueueUsed += kFrameHeaderBytes + 8; if (isAck) { CreateFrameHeader(packet, 8, FRAME_TYPE_PING, kFlag_ACK, 0); memcpy(packet + kFrameHeaderBytes, mInputFrameBuffer.get() + kFrameHeaderBytes, 8); } else { CreateFrameHeader(packet, 8, FRAME_TYPE_PING, 0, 0); memset(packet + kFrameHeaderBytes, 0, 8); } LogIO(this, nullptr, "Generate Ping", packet, kFrameHeaderBytes + 8); FlushOutputQueue(); } void Http2Session::GenerateSettingsAck() { // need to generate ack of this settings frame MOZ_ASSERT(OnSocketThread(), "not on socket thread"); LOG3(("Http2Session::GenerateSettingsAck %p\n", this)); char* packet = EnsureOutputBuffer(kFrameHeaderBytes); mOutputQueueUsed += kFrameHeaderBytes; CreateFrameHeader(packet, 0, FRAME_TYPE_SETTINGS, kFlag_ACK, 0); LogIO(this, nullptr, "Generate Settings ACK", packet, kFrameHeaderBytes); FlushOutputQueue(); } void Http2Session::GeneratePriority(uint32_t aID, uint8_t aPriorityWeight) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); LOG3(("Http2Session::GeneratePriority %p %X %X\n", this, aID, aPriorityWeight)); char* packet = CreatePriorityFrame(aID, 0, aPriorityWeight); LogIO(this, nullptr, "Generate Priority", packet, kFrameHeaderBytes + 5); FlushOutputQueue(); } void Http2Session::GenerateRstStream(uint32_t aStatusCode, uint32_t aID) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); // make sure we don't do this twice for the same stream (at least if we // have a stream entry for it) Http2StreamBase* stream = mStreamIDHash.Get(aID); if (stream) { if (stream->SentReset()) return; stream->SetSentReset(true); } LOG3(("Http2Session::GenerateRst %p 0x%X %d\n", this, aID, aStatusCode)); uint32_t frameSize = kFrameHeaderBytes + 4; char* packet = EnsureOutputBuffer(frameSize); mOutputQueueUsed += frameSize; CreateFrameHeader(packet, 4, FRAME_TYPE_RST_STREAM, 0, aID); NetworkEndian::writeUint32(packet + kFrameHeaderBytes, aStatusCode); LogIO(this, nullptr, "Generate Reset", packet, frameSize); FlushOutputQueue(); } void Http2Session::GenerateGoAway(uint32_t aStatusCode) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); LOG3(("Http2Session::GenerateGoAway %p code=%X\n", this, aStatusCode)); mClientGoAwayReason = aStatusCode; uint32_t frameSize = kFrameHeaderBytes + 8; char* packet = EnsureOutputBuffer(frameSize); mOutputQueueUsed += frameSize; CreateFrameHeader(packet, 8, FRAME_TYPE_GOAWAY, 0, 0); // last-good-stream-id are bytes 9-12 reflecting pushes NetworkEndian::writeUint32(packet + kFrameHeaderBytes, mOutgoingGoAwayID); // bytes 13-16 are the status code. NetworkEndian::writeUint32(packet + frameSize - 4, aStatusCode); LogIO(this, nullptr, "Generate GoAway", packet, frameSize); FlushOutputQueue(); } // The Hello is comprised of // 1] 24 octets of magic, which are designed to // flush out silent but broken intermediaries // 2] a settings frame which sets a small flow control window for pushes // 3] a window update frame which creates a large session flow control window // 4] 6 priority frames for streams which will never be opened with headers // these streams (3, 5, 7, 9, b, d) build a dependency tree that all other // streams will be direct leaves of. void Http2Session::SendHello() { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); LOG3(("Http2Session::SendHello %p\n", this)); // sized for magic + 5 settings and a session window update and 6 priority // frames 24 magic, 33 for settings (9 header + 4 settings @6), 13 for window // update, 6 priority frames at 14 (9 + 5) each static const uint32_t maxSettings = 5; static const uint32_t prioritySize = kPriorityGroupCount * (kFrameHeaderBytes + 5); static const uint32_t maxDataLen = 24 + kFrameHeaderBytes + maxSettings * 6 + 13 + prioritySize; char* packet = EnsureOutputBuffer(maxDataLen); memcpy(packet, kMagicHello, 24); mOutputQueueUsed += 24; LogIO(this, nullptr, "Magic Connection Header", packet, 24); packet = mOutputQueueBuffer.get() + mOutputQueueUsed; memset(packet, 0, maxDataLen - 24); // frame header will be filled in after we know how long the frame is uint8_t numberOfEntries = 0; // entries need to be listed in order by ID // 1st entry is bytes 9 to 14 // 2nd entry is bytes 15 to 20 // 3rd entry is bytes 21 to 26 // 4th entry is bytes 27 to 32 // 5th entry is bytes 33 to 38 // Let the other endpoint know about our default HPACK decompress table size uint32_t maxHpackBufferSize = gHttpHandler->DefaultHpackBuffer(); mDecompressor.SetInitialMaxBufferSize(maxHpackBufferSize); NetworkEndian::writeUint16(packet + kFrameHeaderBytes + (6 * numberOfEntries), SETTINGS_TYPE_HEADER_TABLE_SIZE); NetworkEndian::writeUint32( packet + kFrameHeaderBytes + (6 * numberOfEntries) + 2, maxHpackBufferSize); numberOfEntries++; if (!StaticPrefs::network_http_http2_allow_push()) { // If we don't support push then set MAX_CONCURRENT to 0 and also // set ENABLE_PUSH to 0 NetworkEndian::writeUint16( packet + kFrameHeaderBytes + (6 * numberOfEntries), SETTINGS_TYPE_ENABLE_PUSH); // The value portion of the setting pair is already initialized to 0 numberOfEntries++; NetworkEndian::writeUint16( packet + kFrameHeaderBytes + (6 * numberOfEntries), SETTINGS_TYPE_MAX_CONCURRENT); // The value portion of the setting pair is already initialized to 0 numberOfEntries++; mWaitingForSettingsAck = true; } // Advertise the Push RWIN for the session, and on each new pull stream // send a window update NetworkEndian::writeUint16(packet + kFrameHeaderBytes + (6 * numberOfEntries), SETTINGS_TYPE_INITIAL_WINDOW); NetworkEndian::writeUint32( packet + kFrameHeaderBytes + (6 * numberOfEntries) + 2, mPushAllowance); numberOfEntries++; // Make sure the other endpoint knows that we're sticking to the default max // frame size NetworkEndian::writeUint16(packet + kFrameHeaderBytes + (6 * numberOfEntries), SETTINGS_TYPE_MAX_FRAME_SIZE); NetworkEndian::writeUint32( packet + kFrameHeaderBytes + (6 * numberOfEntries) + 2, kMaxFrameData); numberOfEntries++; MOZ_ASSERT(numberOfEntries <= maxSettings); uint32_t dataLen = 6 * numberOfEntries; CreateFrameHeader(packet, dataLen, FRAME_TYPE_SETTINGS, 0, 0); mOutputQueueUsed += kFrameHeaderBytes + dataLen; LogIO(this, nullptr, "Generate Settings", packet, kFrameHeaderBytes + dataLen); // now bump the local session window from 64KB uint32_t sessionWindowBump = mInitialRwin - kDefaultRwin; if (kDefaultRwin < mInitialRwin) { // send a window update for the session (Stream 0) for something large mLocalSessionWindow = mInitialRwin; packet = mOutputQueueBuffer.get() + mOutputQueueUsed; CreateFrameHeader(packet, 4, FRAME_TYPE_WINDOW_UPDATE, 0, 0); mOutputQueueUsed += kFrameHeaderBytes + 4; NetworkEndian::writeUint32(packet + kFrameHeaderBytes, sessionWindowBump); LOG3(("Session Window increase at start of session %p %u\n", this, sessionWindowBump)); LogIO(this, nullptr, "Session Window Bump ", packet, kFrameHeaderBytes + 4); } if (StaticPrefs::network_http_http2_enabled_deps() && gHttpHandler->CriticalRequestPrioritization()) { mUseH2Deps = true; MOZ_ASSERT(mNextStreamID == kLeaderGroupID); CreatePriorityNode(kLeaderGroupID, 0, 200, "leader"); mNextStreamID += 2; MOZ_ASSERT(mNextStreamID == kOtherGroupID); CreatePriorityNode(kOtherGroupID, 0, 100, "other"); mNextStreamID += 2; MOZ_ASSERT(mNextStreamID == kBackgroundGroupID); CreatePriorityNode(kBackgroundGroupID, 0, 0, "background"); mNextStreamID += 2; MOZ_ASSERT(mNextStreamID == kSpeculativeGroupID); CreatePriorityNode(kSpeculativeGroupID, kBackgroundGroupID, 0, "speculative"); mNextStreamID += 2; MOZ_ASSERT(mNextStreamID == kFollowerGroupID); CreatePriorityNode(kFollowerGroupID, kLeaderGroupID, 0, "follower"); mNextStreamID += 2; MOZ_ASSERT(mNextStreamID == kUrgentStartGroupID); CreatePriorityNode(kUrgentStartGroupID, 0, 240, "urgentStart"); mNextStreamID += 2; // Hey, you! YES YOU! If you add/remove any groups here, you almost // certainly need to change the lookup of the stream/ID hash in // Http2Session::OnTransportStatus. Yeah, that's right. YOU! } FlushOutputQueue(); } void Http2Session::SendPriorityFrame(uint32_t streamID, uint32_t dependsOn, uint8_t weight) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); LOG3( ("Http2Session::SendPriorityFrame %p Frame 0x%X depends on 0x%X " "weight %d\n", this, streamID, dependsOn, weight)); char* packet = CreatePriorityFrame(streamID, dependsOn, weight); LogIO(this, nullptr, "SendPriorityFrame", packet, kFrameHeaderBytes + 5); FlushOutputQueue(); } char* Http2Session::CreatePriorityFrame(uint32_t streamID, uint32_t dependsOn, uint8_t weight) { MOZ_ASSERT(streamID, "Priority on stream 0"); char* packet = EnsureOutputBuffer(kFrameHeaderBytes + 5); CreateFrameHeader(packet, 5, FRAME_TYPE_PRIORITY, 0, streamID); mOutputQueueUsed += kFrameHeaderBytes + 5; NetworkEndian::writeUint32(packet + kFrameHeaderBytes, dependsOn); // depends on packet[kFrameHeaderBytes + 4] = weight; // weight return packet; } void Http2Session::CreatePriorityNode(uint32_t streamID, uint32_t dependsOn, uint8_t weight, const char* label) { char* packet = CreatePriorityFrame(streamID, dependsOn, weight); LOG3( ("Http2Session %p generate Priority Frame 0x%X depends on 0x%X " "weight %d for %s class\n", this, streamID, dependsOn, weight, label)); LogIO(this, nullptr, "Priority dep node", packet, kFrameHeaderBytes + 5); } // perform a bunch of integrity checks on the stream. // returns true if passed, false (plus LOG and ABORT) if failed. bool Http2Session::VerifyStream(Http2StreamBase* aStream, uint32_t aOptionalID = 0) { // This is annoying, but at least it is O(1) MOZ_ASSERT(OnSocketThread(), "not on socket thread"); #ifndef DEBUG // Only do the real verification in debug builds return true; #else // DEBUG if (!aStream) return true; uint32_t test = 0; do { if (aStream->StreamID() == kDeadStreamID) break; test++; if (aStream->StreamID()) { Http2StreamBase* idStream = mStreamIDHash.Get(aStream->StreamID()); test++; if (idStream != aStream) break; if (aOptionalID) { test++; if (idStream->StreamID() != aOptionalID) break; } } if (aStream->IsTunnel()) { return true; } nsAHttpTransaction* trans = aStream->Transaction(); test++; if (!trans) break; test++; if (mStreamTransactionHash.GetWeak(trans) != aStream) break; // tests passed return true; } while (false); LOG3( ("Http2Session %p VerifyStream Failure %p stream->id=0x%X " "optionalID=0x%X trans=%p test=%d\n", this, aStream, aStream->StreamID(), aOptionalID, aStream->Transaction(), test)); MOZ_ASSERT(false, "VerifyStream"); return false; #endif // DEBUG } // static Http2StreamTunnel* Http2Session::CreateTunnelStreamFromConnInfo( Http2Session* session, uint64_t bcId, nsHttpConnectionInfo* info, bool isWebSocket) { MOZ_ASSERT(info); MOZ_ASSERT(session); if (isWebSocket) { LOG(("Http2Session creating Http2StreamWebSocket")); MOZ_ASSERT(session->GetWebSocketSupport() == WebSocketSupport::SUPPORTED); return new Http2StreamWebSocket( session, nsISupportsPriority::PRIORITY_NORMAL, bcId, info); } MOZ_ASSERT(info->UsingHttpProxy() && info->UsingConnect()); LOG(("Http2Session creating Http2StreamTunnel")); return new Http2StreamTunnel(session, nsISupportsPriority::PRIORITY_NORMAL, bcId, info); } void Http2Session::CleanupStream(Http2StreamBase* aStream, nsresult aResult, errorType aResetCode) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); LOG3(("Http2Session::CleanupStream %p %p 0x%X %" PRIX32 "\n", this, aStream, aStream ? aStream->StreamID() : 0, static_cast(aResult))); if (!aStream) { return; } Http2PushedStream* pushSource = nullptr; Http2Stream* h2Stream = aStream->GetHttp2Stream(); if (h2Stream) { pushSource = h2Stream->PushSource(); if (pushSource) { // aStream is a synthetic attached to an even push MOZ_ASSERT(pushSource->GetConsumerStream() == aStream); MOZ_ASSERT(!aStream->StreamID()); MOZ_ASSERT(!(pushSource->StreamID() & 0x1)); h2Stream->ClearPushSource(); } } if (aStream->DeferCleanup(aResult)) { LOG3(("Http2Session::CleanupStream 0x%X deferred\n", aStream->StreamID())); return; } if (!VerifyStream(aStream)) { LOG3(("Http2Session::CleanupStream failed to verify stream\n")); return; } // don't reset a stream that has recevied a fin or rst if (!aStream->RecvdFin() && !aStream->RecvdReset() && aStream->StreamID() && !(mInputFrameFinal && (aStream == mInputFrameDataStream))) { // !(recvdfin with mark pending) LOG3(("Stream 0x%X had not processed recv FIN, sending RST code %X\n", aStream->StreamID(), aResetCode)); GenerateRstStream(aResetCode, aStream->StreamID()); } CloseStream(aStream, aResult); // Remove the stream from the ID hash table and, if an even id, the pushed // table too. uint32_t id = aStream->StreamID(); if (id > 0) { mStreamIDHash.Remove(id); if (!(id & 1)) { mPushedStreams.RemoveElement(aStream); Http2PushedStream* pushStream = static_cast(aStream); nsAutoCString hashKey; DebugOnly rv = pushStream->GetHashKey(hashKey); MOZ_ASSERT(rv); nsIRequestContext* requestContext = aStream->RequestContext(); if (requestContext) { SpdyPushCache* cache = requestContext->GetSpdyPushCache(); if (cache) { // Make sure the id of the stream in the push cache is the same // as the id of the stream we're cleaning up! See bug 1368080. Http2PushedStream* trash = cache->RemovePushedStreamHttp2ByID(hashKey, aStream->StreamID()); LOG3( ("Http2Session::CleanupStream %p aStream=%p pushStream=%p " "trash=%p", this, aStream, pushStream, trash)); } } } } RemoveStreamFromQueues(aStream); // removing from the stream transaction hash will // delete the Http2StreamBase and drop the reference to // its transaction mStreamTransactionHash.Remove(aStream->Transaction()); mTunnelStreams.RemoveElement(aStream); if (mShouldGoAway && !mStreamTransactionHash.Count()) Close(NS_OK); if (pushSource) { pushSource->SetDeferCleanupOnSuccess(false); CleanupStream(pushSource, aResult, aResetCode); } } void Http2Session::CleanupStream(uint32_t aID, nsresult aResult, errorType aResetCode) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); Http2StreamBase* stream = mStreamIDHash.Get(aID); LOG3(("Http2Session::CleanupStream %p by ID 0x%X to stream %p\n", this, aID, stream)); if (!stream) { return; } CleanupStream(stream, aResult, aResetCode); } void Http2Session::RemoveStreamFromQueues(Http2StreamBase* aStream) { RemoveStreamFromQueue(aStream, mReadyForWrite); RemoveStreamFromQueue(aStream, mQueuedStreams); RemoveStreamFromQueue(aStream, mPushesReadyForRead); RemoveStreamFromQueue(aStream, mSlowConsumersReadyForRead); } void Http2Session::CloseStream(Http2StreamBase* aStream, nsresult aResult, bool aRemoveFromQueue) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); LOG3(("Http2Session::CloseStream %p %p 0x%x %" PRIX32 "\n", this, aStream, aStream->StreamID(), static_cast(aResult))); MaybeDecrementConcurrent(aStream); // Check if partial frame reader if (aStream == mInputFrameDataStream) { LOG3(("Stream had active partial read frame on close")); ChangeDownstreamState(DISCARDING_DATA_FRAME); mInputFrameDataStream = nullptr; } if (aRemoveFromQueue) { RemoveStreamFromQueues(aStream); } // Send the stream the close() indication aStream->CloseStream(aResult); } nsresult Http2Session::SetInputFrameDataStream(uint32_t streamID) { mInputFrameDataStream = mStreamIDHash.Get(streamID); if (VerifyStream(mInputFrameDataStream, streamID)) return NS_OK; LOG3(("Http2Session::SetInputFrameDataStream failed to verify 0x%X\n", streamID)); mInputFrameDataStream = nullptr; return NS_ERROR_UNEXPECTED; } nsresult Http2Session::ParsePadding(uint8_t& paddingControlBytes, uint16_t& paddingLength) { if (mInputFrameFlags & kFlag_PADDED) { paddingLength = *reinterpret_cast(&mInputFrameBuffer[kFrameHeaderBytes]); paddingControlBytes = 1; } else { paddingLength = 0; paddingControlBytes = 0; } if (static_cast(paddingLength + paddingControlBytes) > mInputFrameDataSize) { // This is fatal to the session LOG3( ("Http2Session::ParsePadding %p stream 0x%x PROTOCOL_ERROR " "paddingLength %d > frame size %d\n", this, mInputFrameID, paddingLength, mInputFrameDataSize)); return SessionError(PROTOCOL_ERROR); } return NS_OK; } nsresult Http2Session::RecvHeaders(Http2Session* self) { MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_HEADERS || self->mInputFrameType == FRAME_TYPE_CONTINUATION); bool isContinuation = self->mExpectedHeaderID != 0; // If this doesn't have END_HEADERS set on it then require the next // frame to be HEADERS of the same ID bool endHeadersFlag = self->mInputFrameFlags & kFlag_END_HEADERS; if (endHeadersFlag) { self->mExpectedHeaderID = 0; } else { self->mExpectedHeaderID = self->mInputFrameID; } uint32_t priorityLen = 0; if (self->mInputFrameFlags & kFlag_PRIORITY) { priorityLen = 5; } nsresult rv = self->SetInputFrameDataStream(self->mInputFrameID); MOZ_ASSERT(NS_SUCCEEDED(rv)); // Find out how much padding this frame has, so we can only extract the real // header data from the frame. uint16_t paddingLength = 0; uint8_t paddingControlBytes = 0; if (!isContinuation) { self->mDecompressBuffer.Truncate(); rv = self->ParsePadding(paddingControlBytes, paddingLength); if (NS_FAILED(rv)) { return rv; } } LOG3( ("Http2Session::RecvHeaders %p stream 0x%X priorityLen=%d stream=%p " "end_stream=%d end_headers=%d priority_group=%d " "paddingLength=%d padded=%d\n", self, self->mInputFrameID, priorityLen, self->mInputFrameDataStream, self->mInputFrameFlags & kFlag_END_STREAM, self->mInputFrameFlags & kFlag_END_HEADERS, self->mInputFrameFlags & kFlag_PRIORITY, paddingLength, self->mInputFrameFlags & kFlag_PADDED)); if ((paddingControlBytes + priorityLen + paddingLength) > self->mInputFrameDataSize) { // This is fatal to the session return self->SessionError(PROTOCOL_ERROR); } uint32_t frameSize = self->mInputFrameDataSize - paddingControlBytes - priorityLen - paddingLength; if (self->mAggregatedHeaderSize + frameSize > StaticPrefs::network_http_max_response_header_size()) { LOG(("Http2Session %p header exceeds the limit\n", self)); return self->SessionError(PROTOCOL_ERROR); } if (!self->mInputFrameDataStream) { // Cannot find stream. We can continue the session, but we need to // uncompress the header block to maintain the correct compression context LOG3( ("Http2Session::RecvHeaders %p lookup mInputFrameID stream " "0x%X failed. NextStreamID = 0x%X\n", self, self->mInputFrameID, self->mNextStreamID)); if (self->mInputFrameID >= self->mNextStreamID) { self->GenerateRstStream(PROTOCOL_ERROR, self->mInputFrameID); } self->mDecompressBuffer.Append( &self->mInputFrameBuffer[kFrameHeaderBytes + paddingControlBytes + priorityLen], frameSize); if (self->mInputFrameFlags & kFlag_END_HEADERS) { rv = self->UncompressAndDiscard(false); if (NS_FAILED(rv)) { LOG3(("Http2Session::RecvHeaders uncompress failed\n")); // this is fatal to the session self->mGoAwayReason = COMPRESSION_ERROR; return rv; } } self->ResetDownstreamState(); return NS_OK; } // make sure this is either the first headers or a trailer if (self->mInputFrameDataStream->AllHeadersReceived() && !(self->mInputFrameFlags & kFlag_END_STREAM)) { // Any header block after the first that does *not* end the stream is // illegal. LOG3(("Http2Session::Illegal Extra HeaderBlock %p 0x%X\n", self, self->mInputFrameID)); return self->SessionError(PROTOCOL_ERROR); } // queue up any compression bytes self->mDecompressBuffer.Append( &self->mInputFrameBuffer[kFrameHeaderBytes + paddingControlBytes + priorityLen], frameSize); self->mInputFrameDataStream->UpdateTransportReadEvents( self->mInputFrameDataSize); self->mLastDataReadEpoch = self->mLastReadEpoch; if (!isContinuation) { self->mAggregatedHeaderSize = frameSize; } else { self->mAggregatedHeaderSize += frameSize; } if (!endHeadersFlag) { // more are coming - don't process yet self->ResetDownstreamState(); return NS_OK; } if (isContinuation) { Telemetry::Accumulate(Telemetry::SPDY_CONTINUED_HEADERS, self->mAggregatedHeaderSize); } rv = self->ResponseHeadersComplete(); if (rv == NS_ERROR_ILLEGAL_VALUE) { LOG3(("Http2Session::RecvHeaders %p PROTOCOL_ERROR detected stream 0x%X\n", self, self->mInputFrameID)); self->CleanupStream(self->mInputFrameDataStream, rv, PROTOCOL_ERROR); self->ResetDownstreamState(); rv = NS_OK; } else if (NS_FAILED(rv)) { // This is fatal to the session. self->mGoAwayReason = COMPRESSION_ERROR; } return rv; } // ResponseHeadersComplete() returns NS_ERROR_ILLEGAL_VALUE when the stream // should be reset with a PROTOCOL_ERROR, NS_OK when the response headers were // fine, and any other error is fatal to the session. nsresult Http2Session::ResponseHeadersComplete() { LOG3(("Http2Session::ResponseHeadersComplete %p for 0x%X fin=%d", this, mInputFrameDataStream->StreamID(), mInputFrameFinal)); // Anything prior to AllHeadersReceived() => true is actual headers. After // that, we need to handle them as trailers instead (which are special-cased // so we don't have to use the nasty chunked parser for all h2, just in case). if (mInputFrameDataStream->AllHeadersReceived()) { LOG3(("Http2Session::ResponseHeadersComplete processing trailers")); MOZ_ASSERT(mInputFrameFlags & kFlag_END_STREAM); nsresult rv = mInputFrameDataStream->ConvertResponseTrailers( &mDecompressor, mDecompressBuffer); if (NS_FAILED(rv)) { LOG3(( "Http2Session::ResponseHeadersComplete trailer conversion failed\n")); return rv; } mFlatHTTPResponseHeadersOut = 0; mFlatHTTPResponseHeaders.Truncate(); if (mInputFrameFinal) { // need to process the fin ChangeDownstreamState(PROCESSING_COMPLETE_HEADERS); } else { ResetDownstreamState(); } return NS_OK; } // if this turns out to be a 1xx response code we have to // undo the headers received bit that we are setting here. bool didFirstSetAllRecvd = !mInputFrameDataStream->AllHeadersReceived(); mInputFrameDataStream->SetAllHeadersReceived(); // The stream needs to see flattened http headers // Uncompressed http/2 format headers currently live in // Http2StreamBase::mDecompressBuffer - convert that to HTTP format in // mFlatHTTPResponseHeaders via ConvertHeaders() nsresult rv; int32_t httpResponseCode; // out param to ConvertResponseHeaders mFlatHTTPResponseHeadersOut = 0; rv = mInputFrameDataStream->ConvertResponseHeaders( &mDecompressor, mDecompressBuffer, mFlatHTTPResponseHeaders, httpResponseCode); if (rv == NS_ERROR_NET_RESET) { LOG( ("Http2Session::ResponseHeadersComplete %p ConvertResponseHeaders " "reset\n", this)); // This means the stream found connection-oriented auth. Treat this like we // got a reset with HTTP_1_1_REQUIRED. mInputFrameDataStream->DisableSpdy(); CleanupStream(mInputFrameDataStream, NS_ERROR_NET_RESET, CANCEL_ERROR); ResetDownstreamState(); return NS_OK; } if (NS_FAILED(rv)) { return rv; } // allow more headers in the case of 1xx if (((httpResponseCode / 100) == 1) && didFirstSetAllRecvd) { mInputFrameDataStream->UnsetAllHeadersReceived(); } ChangeDownstreamState(PROCESSING_COMPLETE_HEADERS); return NS_OK; } nsresult Http2Session::RecvPriority(Http2Session* self) { MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_PRIORITY); if (self->mInputFrameDataSize != 5) { LOG3(("Http2Session::RecvPriority %p wrong length data=%d\n", self, self->mInputFrameDataSize)); return self->SessionError(PROTOCOL_ERROR); } if (!self->mInputFrameID) { LOG3(("Http2Session::RecvPriority %p stream ID of 0.\n", self)); return self->SessionError(PROTOCOL_ERROR); } nsresult rv = self->SetInputFrameDataStream(self->mInputFrameID); if (NS_FAILED(rv)) return rv; uint32_t newPriorityDependency = NetworkEndian::readUint32( self->mInputFrameBuffer.get() + kFrameHeaderBytes); bool exclusive = !!(newPriorityDependency & 0x80000000); newPriorityDependency &= 0x7fffffff; uint8_t newPriorityWeight = *(self->mInputFrameBuffer.get() + kFrameHeaderBytes + 4); // undefined what it means when the server sends a priority frame. ignore it. LOG3( ("Http2Session::RecvPriority %p 0x%X received dependency=0x%X " "weight=%u exclusive=%d", self->mInputFrameDataStream, self->mInputFrameID, newPriorityDependency, newPriorityWeight, exclusive)); self->ResetDownstreamState(); return NS_OK; } nsresult Http2Session::RecvRstStream(Http2Session* self) { MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_RST_STREAM); if (self->mInputFrameDataSize != 4) { LOG3(("Http2Session::RecvRstStream %p RST_STREAM wrong length data=%d", self, self->mInputFrameDataSize)); return self->SessionError(PROTOCOL_ERROR); } if (!self->mInputFrameID) { LOG3(("Http2Session::RecvRstStream %p stream ID of 0.\n", self)); return self->SessionError(PROTOCOL_ERROR); } self->mDownstreamRstReason = NetworkEndian::readUint32( self->mInputFrameBuffer.get() + kFrameHeaderBytes); LOG3(("Http2Session::RecvRstStream %p RST_STREAM Reason Code %u ID %x\n", self, self->mDownstreamRstReason, self->mInputFrameID)); DebugOnly rv = self->SetInputFrameDataStream(self->mInputFrameID); MOZ_ASSERT(NS_SUCCEEDED(rv)); if (!self->mInputFrameDataStream) { // if we can't find the stream just ignore it (4.2 closed) self->ResetDownstreamState(); return NS_OK; } self->mInputFrameDataStream->SetRecvdReset(true); self->MaybeDecrementConcurrent(self->mInputFrameDataStream); self->ChangeDownstreamState(PROCESSING_CONTROL_RST_STREAM); return NS_OK; } nsresult Http2Session::RecvSettings(Http2Session* self) { MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_SETTINGS); if (self->mInputFrameID) { LOG3(("Http2Session::RecvSettings %p needs stream ID of 0. 0x%X\n", self, self->mInputFrameID)); return self->SessionError(PROTOCOL_ERROR); } if (self->mInputFrameDataSize % 6) { // Number of Settings is determined by dividing by each 6 byte setting // entry. So the payload must be a multiple of 6. LOG3(("Http2Session::RecvSettings %p SETTINGS wrong length data=%d", self, self->mInputFrameDataSize)); return self->SessionError(PROTOCOL_ERROR); } self->mReceivedSettings = true; uint32_t numEntries = self->mInputFrameDataSize / 6; LOG3( ("Http2Session::RecvSettings %p SETTINGS Control Frame " "with %d entries ack=%X", self, numEntries, self->mInputFrameFlags & kFlag_ACK)); if ((self->mInputFrameFlags & kFlag_ACK) && self->mInputFrameDataSize) { LOG3(("Http2Session::RecvSettings %p ACK with non zero payload is err\n", self)); return self->SessionError(PROTOCOL_ERROR); } for (uint32_t index = 0; index < numEntries; ++index) { uint8_t* setting = reinterpret_cast(self->mInputFrameBuffer.get()) + kFrameHeaderBytes + index * 6; uint16_t id = NetworkEndian::readUint16(setting); uint32_t value = NetworkEndian::readUint32(setting + 2); LOG3(("Settings ID %u, Value %u", id, value)); switch (id) { case SETTINGS_TYPE_HEADER_TABLE_SIZE: LOG3(("Compression header table setting received: %d\n", value)); self->mCompressor.SetMaxBufferSize(value); break; case SETTINGS_TYPE_ENABLE_PUSH: LOG3(("Client received an ENABLE Push SETTING. Odd.\n")); // nop break; case SETTINGS_TYPE_MAX_CONCURRENT: self->mMaxConcurrent = value; Telemetry::Accumulate(Telemetry::SPDY_SETTINGS_MAX_STREAMS, value); self->ProcessPending(); break; case SETTINGS_TYPE_INITIAL_WINDOW: { Telemetry::Accumulate(Telemetry::SPDY_SETTINGS_IW, value >> 10); int32_t delta = value - self->mServerInitialStreamWindow; self->mServerInitialStreamWindow = value; // SETTINGS only adjusts stream windows. Leave the session window alone. // We need to add the delta to all open streams (delta can be negative) for (const auto& stream : self->mStreamTransactionHash.Values()) { stream->UpdateServerReceiveWindow(delta); } } break; case SETTINGS_TYPE_MAX_FRAME_SIZE: { if ((value < kMaxFrameData) || (value >= 0x01000000)) { LOG3(("Received invalid max frame size 0x%X", value)); return self->SessionError(PROTOCOL_ERROR); } // We stick to the default for simplicity's sake, so nothing to change } break; case SETTINGS_TYPE_ENABLE_CONNECT_PROTOCOL: { if (value == 1) { LOG3(("Enabling extended CONNECT")); self->mPeerAllowsWebsockets = true; } else if (value > 1) { LOG3(("Peer sent invalid value for ENABLE_CONNECT_PROTOCOL %d", value)); return self->SessionError(PROTOCOL_ERROR); } else if (self->mPeerAllowsWebsockets) { LOG3(("Peer tried to re-disable extended CONNECT")); return self->SessionError(PROTOCOL_ERROR); } self->mHasTransactionWaitingForWebsockets = true; } break; default: LOG3(("Received an unknown SETTING id %d. Ignoring.", id)); break; } } self->ResetDownstreamState(); if (!(self->mInputFrameFlags & kFlag_ACK)) { self->GenerateSettingsAck(); } else if (self->mWaitingForSettingsAck) { self->mGoAwayOnPush = true; } if (!self->mProcessedWaitingWebsockets) { self->mProcessedWaitingWebsockets = true; } if (self->mHasTransactionWaitingForWebsockets) { // trigger a queued websockets transaction -- enabled or not LOG3(("Http2Sesssion::RecvSettings triggering queued websocket")); RefPtr ci; self->GetConnectionInfo(getter_AddRefs(ci)); gHttpHandler->ConnMgr()->ProcessPendingQ(ci); self->mHasTransactionWaitingForWebsockets = false; } return NS_OK; } nsresult Http2Session::RecvPushPromise(Http2Session* self) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_PUSH_PROMISE || self->mInputFrameType == FRAME_TYPE_CONTINUATION); // Find out how much padding this frame has, so we can only extract the real // header data from the frame. uint16_t paddingLength = 0; uint8_t paddingControlBytes = 0; // If this doesn't have END_PUSH_PROMISE set on it then require the next // frame to be PUSH_PROMISE of the same ID uint32_t promiseLen; uint32_t promisedID; if (self->mExpectedPushPromiseID) { promiseLen = 0; // really a continuation frame promisedID = self->mContinuedPromiseStream; } else { self->mDecompressBuffer.Truncate(); nsresult rv = self->ParsePadding(paddingControlBytes, paddingLength); if (NS_FAILED(rv)) { return rv; } promiseLen = 4; promisedID = NetworkEndian::readUint32(self->mInputFrameBuffer.get() + kFrameHeaderBytes + paddingControlBytes); promisedID &= 0x7fffffff; if (promisedID <= self->mLastPushedID) { LOG3(("Http2Session::RecvPushPromise %p ID too low %u expected > %u.\n", self, promisedID, self->mLastPushedID)); return self->SessionError(PROTOCOL_ERROR); } self->mLastPushedID = promisedID; } uint32_t associatedID = self->mInputFrameID; if (self->mInputFrameFlags & kFlag_END_PUSH_PROMISE) { self->mExpectedPushPromiseID = 0; self->mContinuedPromiseStream = 0; } else { self->mExpectedPushPromiseID = self->mInputFrameID; self->mContinuedPromiseStream = promisedID; } if ((paddingControlBytes + promiseLen + paddingLength) > self->mInputFrameDataSize) { // This is fatal to the session LOG3( ("Http2Session::RecvPushPromise %p ID 0x%X assoc ID 0x%X " "PROTOCOL_ERROR extra %d > frame size %d\n", self, promisedID, associatedID, (paddingControlBytes + promiseLen + paddingLength), self->mInputFrameDataSize)); return self->SessionError(PROTOCOL_ERROR); } LOG3( ("Http2Session::RecvPushPromise %p ID 0x%X assoc ID 0x%X " "paddingLength %d padded %d\n", self, promisedID, associatedID, paddingLength, self->mInputFrameFlags & kFlag_PADDED)); if (!associatedID || !promisedID || (promisedID & 1)) { LOG3(("Http2Session::RecvPushPromise %p ID invalid.\n", self)); return self->SessionError(PROTOCOL_ERROR); } // confirm associated-to nsresult rv = self->SetInputFrameDataStream(associatedID); if (NS_FAILED(rv)) return rv; Http2StreamBase* associatedStream = self->mInputFrameDataStream; ++(self->mServerPushedResources); // Anytime we start using the high bit of stream ID (either client or server) // begin to migrate to a new session. if (promisedID >= kMaxStreamID) self->mShouldGoAway = true; bool resetStream = true; SpdyPushCache* cache = nullptr; if (self->mShouldGoAway && !Http2PushedStream::TestOnPush(associatedStream)) { LOG3( ("Http2Session::RecvPushPromise %p cache push while in GoAway " "mode refused.\n", self)); self->GenerateRstStream(REFUSED_STREAM_ERROR, promisedID); } else if (!StaticPrefs::network_http_http2_allow_push()) { // ENABLE_PUSH and MAX_CONCURRENT_STREAMS of 0 in settings disabled push LOG3(("Http2Session::RecvPushPromise Push Recevied when Disabled\n")); if (self->mGoAwayOnPush) { LOG3(("Http2Session::RecvPushPromise sending GOAWAY")); return self->SessionError(PROTOCOL_ERROR); } self->GenerateRstStream(REFUSED_STREAM_ERROR, promisedID); } else if (!(associatedID & 1)) { LOG3( ("Http2Session::RecvPushPromise %p assocated=0x%X on pushed (even) " "stream not allowed\n", self, associatedID)); self->GenerateRstStream(PROTOCOL_ERROR, promisedID); } else if (!associatedStream) { LOG3(("Http2Session::RecvPushPromise %p lookup associated ID failed.\n", self)); self->GenerateRstStream(PROTOCOL_ERROR, promisedID); } else if (Http2PushedStream::TestOnPush(associatedStream)) { LOG3(("Http2Session::RecvPushPromise %p will be handled by push listener.", self)); resetStream = false; } else { nsIRequestContext* requestContext = associatedStream->RequestContext(); if (requestContext) { cache = requestContext->GetSpdyPushCache(); if (!cache) { cache = new SpdyPushCache(); requestContext->SetSpdyPushCache(cache); } } if (!cache) { // this is unexpected, but we can handle it just by refusing the push LOG3( ("Http2Session::RecvPushPromise Push Recevied without push cache\n")); self->GenerateRstStream(REFUSED_STREAM_ERROR, promisedID); } else { resetStream = false; } } if (resetStream) { // Need to decompress the headers even though we aren't using them yet in // order to keep the compression context consistent for other frames self->mDecompressBuffer.Append( &self->mInputFrameBuffer[kFrameHeaderBytes + paddingControlBytes + promiseLen], self->mInputFrameDataSize - paddingControlBytes - promiseLen - paddingLength); if (self->mInputFrameFlags & kFlag_END_PUSH_PROMISE) { rv = self->UncompressAndDiscard(true); if (NS_FAILED(rv)) { LOG3(("Http2Session::RecvPushPromise uncompress failed\n")); self->mGoAwayReason = COMPRESSION_ERROR; return rv; } } self->ResetDownstreamState(); return NS_OK; } self->mDecompressBuffer.Append( &self->mInputFrameBuffer[kFrameHeaderBytes + paddingControlBytes + promiseLen], self->mInputFrameDataSize - paddingControlBytes - promiseLen - paddingLength); if (self->mInputFrameType != FRAME_TYPE_CONTINUATION) { self->mAggregatedHeaderSize = self->mInputFrameDataSize - paddingControlBytes - promiseLen - paddingLength; } else { self->mAggregatedHeaderSize += self->mInputFrameDataSize - paddingControlBytes - promiseLen - paddingLength; } if (!(self->mInputFrameFlags & kFlag_END_PUSH_PROMISE)) { LOG3( ("Http2Session::RecvPushPromise not finishing processing for " "multi-frame push\n")); self->ResetDownstreamState(); return NS_OK; } if (self->mInputFrameType == FRAME_TYPE_CONTINUATION) { Telemetry::Accumulate(Telemetry::SPDY_CONTINUED_HEADERS, self->mAggregatedHeaderSize); } // Create the buffering transaction and push stream RefPtr transactionBuffer = new Http2PushTransactionBuffer(); transactionBuffer->SetConnection(self); RefPtr pushedStream( new Http2PushedStream(transactionBuffer, self, associatedStream, promisedID, self->mCurrentBrowserId)); rv = pushedStream->ConvertPushHeaders(&self->mDecompressor, self->mDecompressBuffer, pushedStream->GetRequestString()); if (rv == NS_ERROR_NOT_IMPLEMENTED) { LOG3(("Http2Session::PushPromise Semantics not Implemented\n")); self->GenerateRstStream(REFUSED_STREAM_ERROR, promisedID); self->ResetDownstreamState(); return NS_OK; } if (rv == NS_ERROR_ILLEGAL_VALUE) { // This means the decompression completed ok, but there was a problem with // the decoded headers. Reset the stream and go away. self->GenerateRstStream(PROTOCOL_ERROR, promisedID); self->ResetDownstreamState(); return NS_OK; } if (NS_FAILED(rv)) { // This is fatal to the session. self->mGoAwayReason = COMPRESSION_ERROR; return rv; } // Ownership of the pushed stream is by the transaction hash, just as it // is for a client initiated stream. Errors that aren't fatal to the // whole session must call cleanupStream() after this point in order // to remove the stream from that hash. WeakPtr pushedWeak = pushedStream.get(); self->mStreamTransactionHash.InsertOrUpdate(transactionBuffer, std::move(pushedStream)); self->mPushedStreams.AppendElement( static_cast(pushedWeak.get())); if (self->RegisterStreamID(pushedWeak, promisedID) == kDeadStreamID) { LOG3(("Http2Session::RecvPushPromise registerstreamid failed\n")); self->mGoAwayReason = INTERNAL_ERROR; return NS_ERROR_FAILURE; } if (promisedID > self->mOutgoingGoAwayID) { self->mOutgoingGoAwayID = promisedID; } // Fake the request side of the pushed HTTP transaction. Sets up hash // key and origin uint32_t notUsed; Unused << pushedWeak->ReadSegments(nullptr, 1, ¬Used); nsAutoCString key; if (!static_cast(pushedWeak.get())->GetHashKey(key)) { LOG3( ("Http2Session::RecvPushPromise one of :authority :scheme :path " "missing from push\n")); self->CleanupStream(pushedWeak, NS_ERROR_FAILURE, PROTOCOL_ERROR); self->ResetDownstreamState(); return NS_OK; } // does the pushed origin belong on this connection? LOG3(("Http2Session::RecvPushPromise %p origin check %s", self, pushedWeak->Origin().get())); nsCOMPtr pushedOrigin; rv = MakeOriginURL(pushedWeak->Origin(), pushedOrigin); nsAutoCString pushedHostName; int32_t pushedPort = -1; if (NS_SUCCEEDED(rv)) { rv = pushedOrigin->GetHost(pushedHostName); } if (NS_SUCCEEDED(rv)) { rv = pushedOrigin->GetPort(&pushedPort); if (NS_SUCCEEDED(rv) && pushedPort == -1) { // Need to get the right default port, so TestJoinConnection below can // check things correctly. See bug 1397621. if (pushedOrigin->SchemeIs("http")) { pushedPort = NS_HTTP_DEFAULT_PORT; } else { pushedPort = NS_HTTPS_DEFAULT_PORT; } } } if (NS_FAILED(rv) || !self->TestJoinConnection(pushedHostName, pushedPort)) { LOG3(( "Http2Session::RecvPushPromise %p pushed stream mismatched origin %s\n", self, pushedWeak->Origin().get())); self->CleanupStream(pushedWeak, NS_ERROR_FAILURE, REFUSED_STREAM_ERROR); self->ResetDownstreamState(); return NS_OK; } if (static_cast(pushedWeak.get())->TryOnPush()) { LOG3( ("Http2Session::RecvPushPromise %p channel implements " "nsIHttpPushListener " "stream %p will not be placed into session cache.\n", self, pushedWeak.get())); } else { LOG3(("Http2Session::RecvPushPromise %p place stream into session cache\n", self)); if (!cache->RegisterPushedStreamHttp2( key, static_cast(pushedWeak.get()))) { // This only happens if they've already pushed us this item. LOG3(("Http2Session::RecvPushPromise registerPushedStream Failed\n")); self->CleanupStream(pushedWeak, NS_ERROR_FAILURE, REFUSED_STREAM_ERROR); self->ResetDownstreamState(); return NS_OK; } // Kick off a lookup into the HTTP cache so we can cancel the push if it's // unneeded (we already have it in our local regular cache). See bug // 1367551. // Build up our full URL for the cache lookup nsAutoCString spec; spec.Assign(pushedWeak->Origin()); spec.Append(pushedWeak->Path()); nsCOMPtr pushedURL; // Nifty trick: this doesn't actually do anything origin-specific, it's just // named that way. So by passing it the full spec here, we get a URL with // the full path. // Another nifty trick! Even though this is using nsIURIs (which are not // generally ok off the main thread), since we're not using the protocol // handler to create any URIs, this will work just fine here. Don't try this // at home, though, kids. I'm a trained professional. if (NS_SUCCEEDED(MakeOriginURL(spec, pushedURL))) { LOG3(("Http2Session::RecvPushPromise %p check disk cache for entry", self)); mozilla::OriginAttributes oa; pushedWeak->GetOriginAttributes(&oa); RefPtr session = self; auto cachePushCheckCallback = [session, promisedID](bool aAccepted) { MOZ_ASSERT(OnSocketThread()); if (!aAccepted) { session->CleanupStream(promisedID, NS_ERROR_FAILURE, Http2Session::REFUSED_STREAM_ERROR); } }; RefPtr checker = new CachePushChecker( pushedURL, oa, static_cast(pushedWeak.get())->GetRequestString(), std::move(cachePushCheckCallback)); if (NS_FAILED(checker->DoCheck())) { LOG3( ("Http2Session::RecvPushPromise %p failed to open cache entry for " "push check", self)); } } } if (!pushedWeak) { // We have an up to date entry in our cache, so the stream was closed // and released in the cachePushCheckCallback above. self->ResetDownstreamState(); return NS_OK; } pushedWeak->SetHTTPState(Http2StreamBase::RESERVED_BY_REMOTE); static_assert(Http2StreamBase::kWorstPriority >= 0, "kWorstPriority out of range"); uint32_t priorityDependency = pushedWeak->PriorityDependency(); uint8_t priorityWeight = pushedWeak->PriorityWeight(); self->SendPriorityFrame(promisedID, priorityDependency, priorityWeight); self->ResetDownstreamState(); return NS_OK; } nsresult Http2Session::RecvPing(Http2Session* self) { MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_PING); LOG3(("Http2Session::RecvPing %p PING Flags 0x%X.", self, self->mInputFrameFlags)); if (self->mInputFrameDataSize != 8) { LOG3(("Http2Session::RecvPing %p PING had wrong amount of data %d", self, self->mInputFrameDataSize)); return self->SessionError(FRAME_SIZE_ERROR); } if (self->mInputFrameID) { LOG3(("Http2Session::RecvPing %p PING needs stream ID of 0. 0x%X\n", self, self->mInputFrameID)); return self->SessionError(PROTOCOL_ERROR); } if (self->mInputFrameFlags & kFlag_ACK) { // presumably a reply to our timeout ping.. don't reply to it self->mPingSentEpoch = 0; // We need to reset mPreviousUsed. If we don't, the next time // Http2Session::SendPing is called, it will have no effect. self->mPreviousUsed = false; } else { // reply with a ack'd ping self->GeneratePing(true); } self->ResetDownstreamState(); return NS_OK; } nsresult Http2Session::RecvGoAway(Http2Session* self) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_GOAWAY); if (self->mInputFrameDataSize < 8) { // data > 8 is an opaque token that we can't interpret. NSPR Logs will // have the hex of all packets so there is no point in separately logging. LOG3(("Http2Session::RecvGoAway %p GOAWAY had wrong amount of data %d", self, self->mInputFrameDataSize)); return self->SessionError(PROTOCOL_ERROR); } if (self->mInputFrameID) { LOG3(("Http2Session::RecvGoAway %p GOAWAY had non zero stream ID 0x%X\n", self, self->mInputFrameID)); return self->SessionError(PROTOCOL_ERROR); } self->mConnection->SetCloseReason(ConnectionCloseReason::GO_AWAY); self->mShouldGoAway = true; self->mGoAwayID = NetworkEndian::readUint32(self->mInputFrameBuffer.get() + kFrameHeaderBytes); self->mGoAwayID &= 0x7fffffff; self->mCleanShutdown = true; self->mPeerGoAwayReason = NetworkEndian::readUint32( self->mInputFrameBuffer.get() + kFrameHeaderBytes + 4); // Find streams greater than the last-good ID and mark them for deletion // in the mGoAwayStreamsToRestart queue. The underlying transaction can be // restarted. for (const auto& stream : self->mStreamTransactionHash.Values()) { // these streams were not processed by the server and can be restarted. // Do that after the enumerator completes to avoid the risk of // a restart event re-entrantly modifying this hash. Be sure not to restart // a pushed (even numbered) stream if ((stream->StreamID() > self->mGoAwayID && (stream->StreamID() & 1)) || !stream->HasRegisteredID()) { self->mGoAwayStreamsToRestart.Push(stream); } } // Process the streams marked for deletion and restart. size_t size = self->mGoAwayStreamsToRestart.GetSize(); for (size_t count = 0; count < size; ++count) { Http2StreamBase* stream = static_cast(self->mGoAwayStreamsToRestart.PopFront()); if (self->mPeerGoAwayReason == HTTP_1_1_REQUIRED) { stream->DisableSpdy(); } self->CloseStream(stream, NS_ERROR_NET_RESET); if (stream->HasRegisteredID()) { self->mStreamIDHash.Remove(stream->StreamID()); } self->mStreamTransactionHash.Remove(stream->Transaction()); } // Queued streams can also be deleted from this session and restarted // in another one. (they were never sent on the network so they implicitly // are not covered by the last-good id. for (const auto& stream : self->mQueuedStreams) { MOZ_ASSERT(stream->Queued()); stream->SetQueued(false); if (self->mPeerGoAwayReason == HTTP_1_1_REQUIRED) { stream->DisableSpdy(); } self->CloseStream(stream, NS_ERROR_NET_RESET, false); self->mStreamTransactionHash.Remove(stream->Transaction()); } self->mQueuedStreams.Clear(); LOG3( ("Http2Session::RecvGoAway %p GOAWAY Last-Good-ID 0x%X status 0x%X " "live streams=%d\n", self, self->mGoAwayID, self->mPeerGoAwayReason, self->mStreamTransactionHash.Count())); self->ResetDownstreamState(); return NS_OK; } nsresult Http2Session::RecvWindowUpdate(Http2Session* self) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_WINDOW_UPDATE); if (self->mInputFrameDataSize != 4) { LOG3(("Http2Session::RecvWindowUpdate %p Window Update wrong length %d\n", self, self->mInputFrameDataSize)); return self->SessionError(PROTOCOL_ERROR); } uint32_t delta = NetworkEndian::readUint32(self->mInputFrameBuffer.get() + kFrameHeaderBytes); delta &= 0x7fffffff; LOG3(("Http2Session::RecvWindowUpdate %p len=%d Stream 0x%X.\n", self, delta, self->mInputFrameID)); if (self->mInputFrameID) { // stream window nsresult rv = self->SetInputFrameDataStream(self->mInputFrameID); if (NS_FAILED(rv)) return rv; if (!self->mInputFrameDataStream) { LOG3(("Http2Session::RecvWindowUpdate %p lookup streamID 0x%X failed.\n", self, self->mInputFrameID)); // only reset the session if the ID is one we haven't ever opened if (self->mInputFrameID >= self->mNextStreamID) { self->GenerateRstStream(PROTOCOL_ERROR, self->mInputFrameID); } self->ResetDownstreamState(); return NS_OK; } if (delta == 0) { LOG3(("Http2Session::RecvWindowUpdate %p received 0 stream window update", self)); self->CleanupStream(self->mInputFrameDataStream, NS_ERROR_ILLEGAL_VALUE, PROTOCOL_ERROR); self->ResetDownstreamState(); return NS_OK; } int64_t oldRemoteWindow = self->mInputFrameDataStream->ServerReceiveWindow(); self->mInputFrameDataStream->UpdateServerReceiveWindow(delta); if (self->mInputFrameDataStream->ServerReceiveWindow() >= 0x80000000) { // a window cannot reach 2^31 and be in compliance. Our calculations // are 64 bit safe though. LOG3( ("Http2Session::RecvWindowUpdate %p stream window " "exceeds 2^31 - 1\n", self)); self->CleanupStream(self->mInputFrameDataStream, NS_ERROR_ILLEGAL_VALUE, FLOW_CONTROL_ERROR); self->ResetDownstreamState(); return NS_OK; } LOG3( ("Http2Session::RecvWindowUpdate %p stream 0x%X window " "%" PRId64 " increased by %" PRIu32 " now %" PRId64 ".\n", self, self->mInputFrameID, oldRemoteWindow, delta, oldRemoteWindow + delta)); } else { // session window update if (delta == 0) { LOG3( ("Http2Session::RecvWindowUpdate %p received 0 session window update", self)); return self->SessionError(PROTOCOL_ERROR); } int64_t oldRemoteWindow = self->mServerSessionWindow; self->mServerSessionWindow += delta; if (self->mServerSessionWindow >= 0x80000000) { // a window cannot reach 2^31 and be in compliance. Our calculations // are 64 bit safe though. LOG3( ("Http2Session::RecvWindowUpdate %p session window " "exceeds 2^31 - 1\n", self)); return self->SessionError(FLOW_CONTROL_ERROR); } if ((oldRemoteWindow <= 0) && (self->mServerSessionWindow > 0)) { LOG3( ("Http2Session::RecvWindowUpdate %p restart session window\n", self)); for (const auto& stream : self->mStreamTransactionHash.Values()) { MOZ_ASSERT(self->mServerSessionWindow > 0); if (!stream->BlockedOnRwin() || stream->ServerReceiveWindow() <= 0) { continue; } AddStreamToQueue(stream, self->mReadyForWrite); self->SetWriteCallbacks(); } } LOG3( ("Http2Session::RecvWindowUpdate %p session window " "%" PRId64 " increased by %d now %" PRId64 ".\n", self, oldRemoteWindow, delta, oldRemoteWindow + delta)); } self->ResetDownstreamState(); return NS_OK; } nsresult Http2Session::RecvContinuation(Http2Session* self) { MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_CONTINUATION); MOZ_ASSERT(self->mInputFrameID); MOZ_ASSERT(self->mExpectedPushPromiseID || self->mExpectedHeaderID); MOZ_ASSERT(!(self->mExpectedPushPromiseID && self->mExpectedHeaderID)); LOG3( ("Http2Session::RecvContinuation %p Flags 0x%X id 0x%X " "promise id 0x%X header id 0x%X\n", self, self->mInputFrameFlags, self->mInputFrameID, self->mExpectedPushPromiseID, self->mExpectedHeaderID)); DebugOnly rv = self->SetInputFrameDataStream(self->mInputFrameID); MOZ_ASSERT(NS_SUCCEEDED(rv)); if (!self->mInputFrameDataStream) { LOG3(("Http2Session::RecvContination stream ID 0x%X not found.", self->mInputFrameID)); return self->SessionError(PROTOCOL_ERROR); } // continued headers if (self->mExpectedHeaderID) { self->mInputFrameFlags &= ~kFlag_PRIORITY; return RecvHeaders(self); } // continued push promise if (self->mInputFrameFlags & kFlag_END_HEADERS) { self->mInputFrameFlags &= ~kFlag_END_HEADERS; self->mInputFrameFlags |= kFlag_END_PUSH_PROMISE; } return RecvPushPromise(self); } class UpdateAltSvcEvent : public Runnable { public: UpdateAltSvcEvent(const nsCString& header, const nsCString& aOrigin, nsHttpConnectionInfo* aCI) : Runnable("net::UpdateAltSvcEvent"), mHeader(header), mOrigin(aOrigin), mCI(aCI) {} NS_IMETHOD Run() override { MOZ_ASSERT(NS_IsMainThread()); nsCString originScheme; nsCString originHost; int32_t originPort = -1; nsCOMPtr uri; if (NS_FAILED(NS_NewURI(getter_AddRefs(uri), mOrigin))) { LOG(("UpdateAltSvcEvent origin does not parse %s\n", mOrigin.get())); return NS_OK; } uri->GetScheme(originScheme); uri->GetHost(originHost); uri->GetPort(&originPort); if (XRE_IsSocketProcess()) { AltServiceChild::ProcessHeader( mHeader, originScheme, originHost, originPort, mCI->GetUsername(), mCI->GetPrivate(), nullptr, mCI->ProxyInfo(), 0, mCI->GetOriginAttributes()); return NS_OK; } AltSvcMapping::ProcessHeader(mHeader, originScheme, originHost, originPort, mCI->GetUsername(), mCI->GetPrivate(), nullptr, mCI->ProxyInfo(), 0, mCI->GetOriginAttributes()); return NS_OK; } private: nsCString mHeader; nsCString mOrigin; RefPtr mCI; nsCOMPtr mCallbacks; }; // defined as an http2 extension - alt-svc // defines receipt of frame type 0x0A.. See AlternateSevices.h at least draft // -06 sec 4 as this is an extension, never generate protocol error - just // ignore problems nsresult Http2Session::RecvAltSvc(Http2Session* self) { MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_ALTSVC); LOG3(("Http2Session::RecvAltSvc %p Flags 0x%X id 0x%X\n", self, self->mInputFrameFlags, self->mInputFrameID)); if (self->mInputFrameDataSize < 2) { LOG3(("Http2Session::RecvAltSvc %p frame too small", self)); self->ResetDownstreamState(); return NS_OK; } uint16_t originLen = NetworkEndian::readUint16(self->mInputFrameBuffer.get() + kFrameHeaderBytes); if (originLen + 2U > self->mInputFrameDataSize) { LOG3(("Http2Session::RecvAltSvc %p origin len too big for frame", self)); self->ResetDownstreamState(); return NS_OK; } if (!gHttpHandler->AllowAltSvc()) { LOG3(("Http2Session::RecvAltSvc %p frame alt service pref'd off", self)); self->ResetDownstreamState(); return NS_OK; } uint16_t altSvcFieldValueLen = static_cast(self->mInputFrameDataSize) - 2U - originLen; LOG3(( "Http2Session::RecvAltSvc %p frame originLen=%u altSvcFieldValueLen=%u\n", self, originLen, altSvcFieldValueLen)); if (self->mInputFrameDataSize > 2000) { LOG3(("Http2Session::RecvAltSvc %p frame too large to parse sensibly", self)); self->ResetDownstreamState(); return NS_OK; } nsAutoCString origin; bool impliedOrigin = true; if (originLen) { origin.Assign(self->mInputFrameBuffer.get() + kFrameHeaderBytes + 2, originLen); impliedOrigin = false; } nsAutoCString altSvcFieldValue; if (altSvcFieldValueLen) { altSvcFieldValue.Assign( self->mInputFrameBuffer.get() + kFrameHeaderBytes + 2 + originLen, altSvcFieldValueLen); } if (altSvcFieldValue.IsEmpty() || !nsHttp::IsReasonableHeaderValue(altSvcFieldValue)) { LOG( ("Http2Session %p Alt-Svc Response Header seems unreasonable - " "skipping\n", self)); self->ResetDownstreamState(); return NS_OK; } if (self->mInputFrameID & 1) { // pulled streams apply to the origin of the pulled stream. // If the origin field is filled in the frame, the frame should be ignored if (!origin.IsEmpty()) { LOG(("Http2Session %p Alt-Svc pulled stream has non empty origin\n", self)); self->ResetDownstreamState(); return NS_OK; } if (NS_FAILED(self->SetInputFrameDataStream(self->mInputFrameID)) || !self->mInputFrameDataStream || !self->mInputFrameDataStream->Transaction() || !self->mInputFrameDataStream->Transaction()->RequestHead()) { LOG3( ("Http2Session::RecvAltSvc %p got frame w/o origin on invalid stream", self)); self->ResetDownstreamState(); return NS_OK; } self->mInputFrameDataStream->Transaction()->RequestHead()->Origin(origin); } else if (!self->mInputFrameID) { // ID 0 streams must supply their own origin if (origin.IsEmpty()) { LOG(("Http2Session %p Alt-Svc Stream 0 has empty origin\n", self)); self->ResetDownstreamState(); return NS_OK; } } else { // handling of push streams is not defined. Let's ignore it LOG(("Http2Session %p Alt-Svc received on pushed stream - ignoring\n", self)); self->ResetDownstreamState(); return NS_OK; } RefPtr ci(self->ConnectionInfo()); if (!self->mConnection || !ci) { LOG3(("Http2Session::RecvAltSvc %p no connection or conninfo for %d", self, self->mInputFrameID)); self->ResetDownstreamState(); return NS_OK; } if (!impliedOrigin) { bool okToReroute = true; nsCOMPtr ssl; self->mConnection->GetTLSSocketControl(getter_AddRefs(ssl)); if (!ssl) { okToReroute = false; } // a little off main thread origin parser. This is a non critical function // because any alternate route created has to be verified anyhow nsAutoCString specifiedOriginHost; if (StringBeginsWith(origin, "https://"_ns, nsCaseInsensitiveCStringComparator)) { specifiedOriginHost.Assign(origin.get() + 8, origin.Length() - 8); } else if (StringBeginsWith(origin, "http://"_ns, nsCaseInsensitiveCStringComparator)) { specifiedOriginHost.Assign(origin.get() + 7, origin.Length() - 7); } int32_t colonOffset = specifiedOriginHost.FindCharInSet(":", 0); if (colonOffset != kNotFound) { specifiedOriginHost.Truncate(colonOffset); } if (okToReroute) { ssl->IsAcceptableForHost(specifiedOriginHost, &okToReroute); } if (!okToReroute) { LOG3( ("Http2Session::RecvAltSvc %p can't reroute non-authoritative origin " "%s", self, origin.BeginReading())); self->ResetDownstreamState(); return NS_OK; } } RefPtr event = new UpdateAltSvcEvent(altSvcFieldValue, origin, ci); NS_DispatchToMainThread(event); self->ResetDownstreamState(); return NS_OK; } void Http2Session::Received421(nsHttpConnectionInfo* ci) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); LOG3(("Http2Session::Recevied421 %p %d\n", this, mOriginFrameActivated)); if (!mOriginFrameActivated || !ci) { return; } nsAutoCString key(ci->GetOrigin()); key.Append(':'); key.AppendInt(ci->OriginPort()); mOriginFrame.Remove(key); LOG3(("Http2Session::Received421 %p key %s removed\n", this, key.get())); } nsresult Http2Session::RecvUnused(Http2Session* self) { LOG3(("Http2Session %p unknown frame type %x ignored\n", self, self->mInputFrameType)); self->ResetDownstreamState(); return NS_OK; } // defined as an http2 extension - origin // defines receipt of frame type 0x0b.. // http://httpwg.org/http-extensions/origin-frame.html as this is an extension, // never generate protocol error - just ignore problems nsresult Http2Session::RecvOrigin(Http2Session* self) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); MOZ_ASSERT(self->mInputFrameType == FRAME_TYPE_ORIGIN); LOG3(("Http2Session::RecvOrigin %p Flags 0x%X id 0x%X\n", self, self->mInputFrameFlags, self->mInputFrameID)); if (self->mInputFrameFlags & 0x0F) { LOG3(("Http2Session::RecvOrigin %p leading flags must be 0", self)); self->ResetDownstreamState(); return NS_OK; } if (self->mInputFrameID) { LOG3(("Http2Session::RecvOrigin %p not stream 0", self)); self->ResetDownstreamState(); return NS_OK; } if (self->ConnectionInfo()->UsingProxy()) { LOG3(("Http2Session::RecvOrigin %p must not use proxy", self)); self->ResetDownstreamState(); return NS_OK; } if (!gHttpHandler->AllowOriginExtension()) { LOG3(("Http2Session::RecvOrigin %p origin extension pref'd off", self)); self->ResetDownstreamState(); return NS_OK; } uint32_t offset = 0; self->mOriginFrameActivated = true; while (self->mInputFrameDataSize >= (offset + 2U)) { uint16_t originLen = NetworkEndian::readUint16( self->mInputFrameBuffer.get() + kFrameHeaderBytes + offset); LOG3(("Http2Session::RecvOrigin %p origin extension defined as %d bytes\n", self, originLen)); if (originLen + 2U + offset > self->mInputFrameDataSize) { LOG3(("Http2Session::RecvOrigin %p origin len too big for frame", self)); break; } nsAutoCString originString; nsCOMPtr originURL; originString.Assign( self->mInputFrameBuffer.get() + kFrameHeaderBytes + offset + 2, originLen); offset += originLen + 2; if (NS_FAILED(MakeOriginURL(originString, originURL))) { LOG3( ("Http2Session::RecvOrigin %p origin frame string %s failed to " "parse\n", self, originString.get())); continue; } LOG3(("Http2Session::RecvOrigin %p origin frame string %s parsed OK\n", self, originString.get())); if (!originURL->SchemeIs("https")) { LOG3(("Http2Session::RecvOrigin %p origin frame not https\n", self)); continue; } int32_t port = -1; originURL->GetPort(&port); if (port == -1) { port = 443; } // dont use ->GetHostPort because we want explicit 443 nsAutoCString host; originURL->GetHost(host); nsAutoCString key(host); key.Append(':'); key.AppendInt(port); self->mOriginFrame.WithEntryHandle(key, [&](auto&& entry) { if (!entry) { entry.Insert(true); RefPtr conn(self->HttpConnection()); MOZ_ASSERT(conn.get()); gHttpHandler->ConnMgr()->RegisterOriginCoalescingKey(conn, host, port); } else { LOG3(("Http2Session::RecvOrigin %p origin frame already in set\n", self)); } }); } self->ResetDownstreamState(); return NS_OK; } //----------------------------------------------------------------------------- // nsAHttpTransaction. It is expected that nsHttpConnection is the caller // of these methods //----------------------------------------------------------------------------- void Http2Session::OnTransportStatus(nsITransport* aTransport, nsresult aStatus, int64_t aProgress) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); switch (aStatus) { // These should appear only once, deliver to the first // transaction on the session. case NS_NET_STATUS_RESOLVING_HOST: case NS_NET_STATUS_RESOLVED_HOST: case NS_NET_STATUS_CONNECTING_TO: case NS_NET_STATUS_CONNECTED_TO: case NS_NET_STATUS_TLS_HANDSHAKE_STARTING: case NS_NET_STATUS_TLS_HANDSHAKE_ENDED: { if (!mFirstHttpTransaction) { // if we still do not have a HttpTransaction store timings info in // a HttpConnection. // If some error occur it can happen that we do not have a connection. if (mConnection) { RefPtr conn = mConnection->HttpConnection(); conn->SetEvent(aStatus); } } else { mFirstHttpTransaction->OnTransportStatus(aTransport, aStatus, aProgress); } if (aStatus == NS_NET_STATUS_TLS_HANDSHAKE_ENDED) { mFirstHttpTransaction = nullptr; mTlsHandshakeFinished = true; } break; } default: // The other transport events are ignored here because there is no good // way to map them to the right transaction in http/2. Instead, the events // are generated again from the http/2 code and passed directly to the // correct transaction. // NS_NET_STATUS_SENDING_TO: // This is generated by the socket transport when (part) of // a transaction is written out // // There is no good way to map it to the right transaction in http/2, // so it is ignored here and generated separately when the request // is sent from Http2StreamBase::TransmitFrame // NS_NET_STATUS_WAITING_FOR: // Created by nsHttpConnection when the request has been totally sent. // There is no good way to map it to the right transaction in http/2, // so it is ignored here and generated separately when the same // condition is complete in Http2StreamBase when there is no more // request body left to be transmitted. // NS_NET_STATUS_RECEIVING_FROM // Generated in session whenever we read a data frame or a HEADERS // that can be attributed to a particular stream/transaction break; } } // ReadSegments() is used to write data to the network. Generally, HTTP // request data is pulled from the approriate transaction and // converted to http/2 data. Sometimes control data like window-update are // generated instead. nsresult Http2Session::ReadSegmentsAgain(nsAHttpSegmentReader* reader, uint32_t count, uint32_t* countRead, bool* again) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); MOZ_DIAGNOSTIC_ASSERT( !mSegmentReader || !reader || (mSegmentReader == reader), "Inconsistent Write Function Callback"); nsresult rv = ConfirmTLSProfile(); if (NS_FAILED(rv)) { if (mGoAwayReason == INADEQUATE_SECURITY) { LOG3( ("Http2Session::ReadSegments %p returning INADEQUATE_SECURITY " "%" PRIx32, this, static_cast(NS_ERROR_NET_INADEQUATE_SECURITY))); rv = NS_ERROR_NET_INADEQUATE_SECURITY; } return rv; } if (reader) mSegmentReader = reader; *countRead = 0; LOG3(("Http2Session::ReadSegments %p", this)); RefPtr stream = GetNextStreamFromQueue(mReadyForWrite); if (!stream) { LOG3(("Http2Session %p could not identify a stream to write; suspending.", this)); uint32_t availBeforeFlush = mOutputQueueUsed - mOutputQueueSent; FlushOutputQueue(); uint32_t availAfterFlush = mOutputQueueUsed - mOutputQueueSent; if (availBeforeFlush != availAfterFlush) { LOG3(("Http2Session %p ResumeRecv After early flush in ReadSegments", this)); Unused << ResumeRecv(); } SetWriteCallbacks(); if (mAttemptingEarlyData) { // We can still try to send our preamble as early-data *countRead = mOutputQueueUsed - mOutputQueueSent; LOG(("Http2Session %p nothing to send because of 0RTT failed", this)); Unused << ResumeRecv(); } return *countRead ? NS_OK : NS_BASE_STREAM_WOULD_BLOCK; } uint32_t earlyDataUsed = 0; if (mAttemptingEarlyData) { if (!stream->Do0RTT()) { LOG3( ("Http2Session %p will not get early data from Http2StreamBase %p " "0x%X", this, stream.get(), stream->StreamID())); FlushOutputQueue(); SetWriteCallbacks(); if (!mCannotDo0RTTStreams.Contains(stream)) { mCannotDo0RTTStreams.AppendElement(stream); } // We can still send our preamble *countRead = mOutputQueueUsed - mOutputQueueSent; return *countRead ? NS_OK : NS_BASE_STREAM_WOULD_BLOCK; } // Need to adjust this to only take as much as we can fit in with the // preamble/settings/priority stuff count -= (mOutputQueueUsed - mOutputQueueSent); // Keep track of this to add it into countRead later, as // stream->ReadSegments will likely change the value of mOutputQueueUsed. earlyDataUsed = mOutputQueueUsed - mOutputQueueSent; } LOG3( ("Http2Session %p will write from Http2StreamBase %p 0x%X " "block-input=%d block-output=%d\n", this, stream.get(), stream->StreamID(), stream->RequestBlockedOnRead(), stream->BlockedOnRwin())); rv = stream->ReadSegments(this, count, countRead); if (earlyDataUsed) { // Do this here because countRead could get reset somewhere down the rabbit // hole of stream->ReadSegments, and we want to make sure we return the // proper value to our caller. *countRead += earlyDataUsed; } if (mAttemptingEarlyData && !m0RTTStreams.Contains(stream)) { LOG3(("Http2Session::ReadSegmentsAgain adding stream %d to m0RTTStreams\n", stream->StreamID())); m0RTTStreams.AppendElement(stream); } // Not every permutation of stream->ReadSegents produces data (and therefore // tries to flush the output queue) - SENDING_FIN_STREAM can be an example // of that. But we might still have old data buffered that would be good // to flush. FlushOutputQueue(); // Allow new server reads - that might be data or control information // (e.g. window updates or http replies) that are responses to these writes Unused << ResumeRecv(); if (stream->RequestBlockedOnRead()) { // We are blocked waiting for input - either more http headers or // any request body data. When more data from the request stream // becomes available the httptransaction will call conn->ResumeSend(). LOG3(("Http2Session::ReadSegments %p dealing with block on read", this)); // call readsegments again if there are other streams ready // to run in this session if (GetWriteQueueSize()) { rv = NS_OK; } else { rv = NS_BASE_STREAM_WOULD_BLOCK; } SetWriteCallbacks(); return rv; } if (NS_FAILED(rv)) { LOG3(("Http2Session::ReadSegments %p may return FAIL code %" PRIX32, this, static_cast(rv))); if (rv == NS_BASE_STREAM_WOULD_BLOCK) { return rv; } CleanupStream(stream, rv, CANCEL_ERROR); if (SoftStreamError(rv)) { LOG3(("Http2Session::ReadSegments %p soft error override\n", this)); *again = false; SetWriteCallbacks(); rv = NS_OK; } return rv; } if (*countRead > 0) { LOG3(("Http2Session::ReadSegments %p stream=%p countread=%d", this, stream.get(), *countRead)); AddStreamToQueue(stream, mReadyForWrite); SetWriteCallbacks(); return rv; } if (stream->BlockedOnRwin()) { LOG3(("Http2Session %p will stream %p 0x%X suspended for flow control\n", this, stream.get(), stream->StreamID())); return NS_BASE_STREAM_WOULD_BLOCK; } LOG3(("Http2Session::ReadSegments %p stream=%p stream send complete", this, stream.get())); // call readsegments again if there are other streams ready // to go in this session SetWriteCallbacks(); return rv; } nsresult Http2Session::ReadSegments(nsAHttpSegmentReader* reader, uint32_t count, uint32_t* countRead) { bool again = false; return ReadSegmentsAgain(reader, count, countRead, &again); } nsresult Http2Session::ReadyToProcessDataFrame( enum internalStateType newState) { MOZ_ASSERT(newState == PROCESSING_DATA_FRAME || newState == DISCARDING_DATA_FRAME_PADDING); ChangeDownstreamState(newState); Telemetry::Accumulate(Telemetry::SPDY_CHUNK_RECVD, mInputFrameDataSize >> 10); mLastDataReadEpoch = mLastReadEpoch; if (!mInputFrameID) { LOG3(("Http2Session::ReadyToProcessDataFrame %p data frame stream 0\n", this)); return SessionError(PROTOCOL_ERROR); } nsresult rv = SetInputFrameDataStream(mInputFrameID); if (NS_FAILED(rv)) { LOG3( ("Http2Session::ReadyToProcessDataFrame %p lookup streamID 0x%X " "failed. probably due to verification.\n", this, mInputFrameID)); return rv; } if (!mInputFrameDataStream) { LOG3( ("Http2Session::ReadyToProcessDataFrame %p lookup streamID 0x%X " "failed. Next = 0x%X", this, mInputFrameID, mNextStreamID)); if (mInputFrameID >= mNextStreamID) { GenerateRstStream(PROTOCOL_ERROR, mInputFrameID); } ChangeDownstreamState(DISCARDING_DATA_FRAME); } else if (mInputFrameDataStream->RecvdFin() || mInputFrameDataStream->RecvdReset() || mInputFrameDataStream->SentReset()) { LOG3( ("Http2Session::ReadyToProcessDataFrame %p streamID 0x%X " "Data arrived for already server closed stream.\n", this, mInputFrameID)); if (mInputFrameDataStream->RecvdFin() || mInputFrameDataStream->RecvdReset()) { GenerateRstStream(STREAM_CLOSED_ERROR, mInputFrameID); } ChangeDownstreamState(DISCARDING_DATA_FRAME); } else if (mInputFrameDataSize == 0 && !mInputFrameFinal) { // Only if non-final because the stream properly handles final frames of any // size, and we want the stream to be able to notice its own end flag. LOG3( ("Http2Session::ReadyToProcessDataFrame %p streamID 0x%X " "Ignoring 0-length non-terminal data frame.", this, mInputFrameID)); ChangeDownstreamState(DISCARDING_DATA_FRAME); } else if (newState == PROCESSING_DATA_FRAME && !mInputFrameDataStream->AllHeadersReceived()) { LOG3( ("Http2Session::ReadyToProcessDataFrame %p streamID 0x%X " "Receiving data frame without having headers.", this, mInputFrameID)); CleanupStream(mInputFrameDataStream, NS_ERROR_NET_HTTP2_SENT_GOAWAY, PROTOCOL_ERROR); return NS_OK; } LOG3( ("Start Processing Data Frame. " "Session=%p Stream ID 0x%X Stream Ptr %p Fin=%d Len=%d", this, mInputFrameID, mInputFrameDataStream, mInputFrameFinal, mInputFrameDataSize)); UpdateLocalRwin(mInputFrameDataStream, mInputFrameDataSize); if (mInputFrameDataStream) { mInputFrameDataStream->SetRecvdData(true); } return NS_OK; } // WriteSegments() is used to read data off the socket. Generally this is // just the http2 frame header and from there the appropriate *Stream // is identified from the Stream-ID. The http transaction associated with // that read then pulls in the data directly, which it will feed to // OnWriteSegment(). That function will gateway it into http and feed // it to the appropriate transaction. // we call writer->OnWriteSegment via NetworkRead() to get a http2 header.. // and decide if it is data or control.. if it is control, just deal with it. // if it is data, identify the stream // call stream->WriteSegments which can call this::OnWriteSegment to get the // data. It always gets full frames if they are part of the stream nsresult Http2Session::WriteSegmentsAgain(nsAHttpSegmentWriter* writer, uint32_t count, uint32_t* countWritten, bool* again) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); LOG3(("Http2Session::WriteSegments %p InternalState %X\n", this, mDownstreamState)); *countWritten = 0; if (mClosed) { LOG(("Http2Session::WriteSegments %p already closed", this)); // We return NS_ERROR_ABORT (a "soft" error) here, so when this error is // propagated to another Http2Session, the Http2Session will not be closed // due to this error code. return NS_ERROR_ABORT; } nsresult rv = ConfirmTLSProfile(); if (NS_FAILED(rv)) return rv; SetWriteCallbacks(); // If there are http transactions attached to a push stream with filled // buffers trigger that data pump here. This only reads from buffers (not the // network) so mDownstreamState doesn't matter. RefPtr pushConnectedStream = GetNextStreamFromQueue(mPushesReadyForRead); if (pushConnectedStream) { return ProcessConnectedPush(pushConnectedStream, writer, count, countWritten); } // feed gecko channels that previously stopped consuming data // only take data from stored buffers RefPtr slowConsumer = GetNextStreamFromQueue(mSlowConsumersReadyForRead); if (slowConsumer) { internalStateType savedState = mDownstreamState; mDownstreamState = NOT_USING_NETWORK; rv = ProcessSlowConsumer(slowConsumer, writer, count, countWritten); mDownstreamState = savedState; return rv; } // The BUFFERING_OPENING_SETTINGS state is just like any // BUFFERING_FRAME_HEADER except the only frame type it will allow is SETTINGS // The session layer buffers the leading 8 byte header of every frame. // Non-Data frames are then buffered for their full length, but data // frames (type 0) are passed through to the http stack unprocessed if (mDownstreamState == BUFFERING_OPENING_SETTINGS || mDownstreamState == BUFFERING_FRAME_HEADER) { // The first 9 bytes of every frame is header information that // we are going to want to strip before passing to http. That is // true of both control and data packets. MOZ_ASSERT(mInputFrameBufferUsed < kFrameHeaderBytes, "Frame Buffer Used Too Large for State"); rv = NetworkRead(writer, &mInputFrameBuffer[mInputFrameBufferUsed], kFrameHeaderBytes - mInputFrameBufferUsed, countWritten); if (NS_FAILED(rv)) { LOG3(("Http2Session %p buffering frame header read failure %" PRIx32 "\n", this, static_cast(rv))); // maybe just blocked reading from network if (rv == NS_BASE_STREAM_WOULD_BLOCK) rv = NS_OK; return rv; } LogIO(this, nullptr, "Reading Frame Header", &mInputFrameBuffer[mInputFrameBufferUsed], *countWritten); mInputFrameBufferUsed += *countWritten; if (mInputFrameBufferUsed < kFrameHeaderBytes) { LOG3( ("Http2Session::WriteSegments %p " "BUFFERING FRAME HEADER incomplete size=%d", this, mInputFrameBufferUsed)); return rv; } // 3 bytes of length, 1 type byte, 1 flag byte, 1 unused bit, 31 bits of ID uint8_t totallyWastedByte = mInputFrameBuffer.get()[0]; mInputFrameDataSize = NetworkEndian::readUint16(mInputFrameBuffer.get() + 1); if (totallyWastedByte || (mInputFrameDataSize > kMaxFrameData)) { LOG3(("Got frame too large 0x%02X%04X", totallyWastedByte, mInputFrameDataSize)); return SessionError(PROTOCOL_ERROR); } mInputFrameType = *reinterpret_cast(mInputFrameBuffer.get() + kFrameLengthBytes); mInputFrameFlags = *reinterpret_cast( mInputFrameBuffer.get() + kFrameLengthBytes + kFrameTypeBytes); mInputFrameID = NetworkEndian::readUint32(mInputFrameBuffer.get() + kFrameLengthBytes + kFrameTypeBytes + kFrameFlagBytes); mInputFrameID &= 0x7fffffff; mInputFrameDataRead = 0; if (mInputFrameType == FRAME_TYPE_DATA || mInputFrameType == FRAME_TYPE_HEADERS) { mInputFrameFinal = mInputFrameFlags & kFlag_END_STREAM; } else { mInputFrameFinal = false; } mPaddingLength = 0; LOG3(("Http2Session::WriteSegments[%p::%" PRIu64 "] Frame Header Read " "type %X data len %u flags %x id 0x%X", this, mSerial, mInputFrameType, mInputFrameDataSize, mInputFrameFlags, mInputFrameID)); // if mExpectedHeaderID is non 0, it means this frame must be a CONTINUATION // of a HEADERS frame with a matching ID (section 6.2) if (mExpectedHeaderID && ((mInputFrameType != FRAME_TYPE_CONTINUATION) || (mExpectedHeaderID != mInputFrameID))) { LOG3( ("Expected CONINUATION OF HEADERS for ID 0x%X\n", mExpectedHeaderID)); return SessionError(PROTOCOL_ERROR); } // if mExpectedPushPromiseID is non 0, it means this frame must be a // CONTINUATION of a PUSH_PROMISE with a matching ID (section 6.2) if (mExpectedPushPromiseID && ((mInputFrameType != FRAME_TYPE_CONTINUATION) || (mExpectedPushPromiseID != mInputFrameID))) { LOG3(("Expected CONTINUATION of PUSH PROMISE for ID 0x%X\n", mExpectedPushPromiseID)); return SessionError(PROTOCOL_ERROR); } if (mDownstreamState == BUFFERING_OPENING_SETTINGS && mInputFrameType != FRAME_TYPE_SETTINGS) { LOG3(("First Frame Type Must Be Settings\n")); mPeerFailedHandshake = true; // Don't allow any more h2 connections to this host RefPtr ci = ConnectionInfo(); if (ci) { gHttpHandler->ExcludeHttp2(ci); } // Go through and re-start all of our transactions with h2 disabled. for (const auto& stream : mStreamTransactionHash.Values()) { stream->DisableSpdy(); CloseStream(stream, NS_ERROR_NET_RESET); } mStreamTransactionHash.Clear(); return SessionError(PROTOCOL_ERROR); } if (mInputFrameType != FRAME_TYPE_DATA) { // control frame EnsureBuffer(mInputFrameBuffer, mInputFrameDataSize + kFrameHeaderBytes, kFrameHeaderBytes, mInputFrameBufferSize); ChangeDownstreamState(BUFFERING_CONTROL_FRAME); } else if (mInputFrameFlags & kFlag_PADDED) { ChangeDownstreamState(PROCESSING_DATA_FRAME_PADDING_CONTROL); } else { rv = ReadyToProcessDataFrame(PROCESSING_DATA_FRAME); if (NS_FAILED(rv)) { return rv; } } } if (mDownstreamState == PROCESSING_DATA_FRAME_PADDING_CONTROL) { MOZ_ASSERT(mInputFrameFlags & kFlag_PADDED, "Processing padding control on unpadded frame"); MOZ_ASSERT(mInputFrameBufferUsed < (kFrameHeaderBytes + 1), "Frame buffer used too large for state"); rv = NetworkRead(writer, &mInputFrameBuffer[mInputFrameBufferUsed], (kFrameHeaderBytes + 1) - mInputFrameBufferUsed, countWritten); if (NS_FAILED(rv)) { LOG3( ("Http2Session %p buffering data frame padding control read failure " "%" PRIx32 "\n", this, static_cast(rv))); // maybe just blocked reading from network if (rv == NS_BASE_STREAM_WOULD_BLOCK) rv = NS_OK; return rv; } LogIO(this, nullptr, "Reading Data Frame Padding Control", &mInputFrameBuffer[mInputFrameBufferUsed], *countWritten); mInputFrameBufferUsed += *countWritten; if (mInputFrameBufferUsed - kFrameHeaderBytes < 1) { LOG3( ("Http2Session::WriteSegments %p " "BUFFERING DATA FRAME CONTROL PADDING incomplete size=%d", this, mInputFrameBufferUsed - 8)); return rv; } ++mInputFrameDataRead; char* control = &mInputFrameBuffer[kFrameHeaderBytes]; mPaddingLength = static_cast(*control); LOG3(("Http2Session::WriteSegments %p stream 0x%X mPaddingLength=%d", this, mInputFrameID, mPaddingLength)); if (1U + mPaddingLength > mInputFrameDataSize) { LOG3( ("Http2Session::WriteSegments %p stream 0x%X padding too large for " "frame", this, mInputFrameID)); return SessionError(PROTOCOL_ERROR); } if (1U + mPaddingLength == mInputFrameDataSize) { // This frame consists entirely of padding, we can just discard it LOG3( ("Http2Session::WriteSegments %p stream 0x%X frame with only padding", this, mInputFrameID)); rv = ReadyToProcessDataFrame(DISCARDING_DATA_FRAME_PADDING); if (NS_FAILED(rv)) { return rv; } } else { LOG3( ("Http2Session::WriteSegments %p stream 0x%X ready to read HTTP data", this, mInputFrameID)); rv = ReadyToProcessDataFrame(PROCESSING_DATA_FRAME); if (NS_FAILED(rv)) { return rv; } } } if (mDownstreamState == PROCESSING_CONTROL_RST_STREAM) { nsresult streamCleanupCode; // There is no bounds checking on the error code.. we provide special // handling for a couple of cases and all others (including unknown) are // equivalent to cancel. if (mDownstreamRstReason == REFUSED_STREAM_ERROR) { streamCleanupCode = NS_ERROR_NET_RESET; // can retry this 100% safely mInputFrameDataStream->ReuseConnectionOnRestartOK(true); } else if (mDownstreamRstReason == HTTP_1_1_REQUIRED) { streamCleanupCode = NS_ERROR_NET_RESET; mInputFrameDataStream->ReuseConnectionOnRestartOK(true); mInputFrameDataStream->DisableSpdy(); // actually allow restart by unsticking mInputFrameDataStream->MakeNonSticky(); } else { streamCleanupCode = mInputFrameDataStream->RecvdData() ? NS_ERROR_NET_PARTIAL_TRANSFER : NS_ERROR_NET_INTERRUPT; } if (mDownstreamRstReason == COMPRESSION_ERROR) { mShouldGoAway = true; } // mInputFrameDataStream is reset by ChangeDownstreamState Http2StreamBase* stream = mInputFrameDataStream; ResetDownstreamState(); LOG3( ("Http2Session::WriteSegments cleanup stream on recv of rst " "session=%p stream=%p 0x%X\n", this, stream, stream ? stream->StreamID() : 0)); CleanupStream(stream, streamCleanupCode, CANCEL_ERROR); return NS_OK; } if (mDownstreamState == PROCESSING_DATA_FRAME || mDownstreamState == PROCESSING_COMPLETE_HEADERS) { // The cleanup stream should only be set while stream->WriteSegments is // on the stack and then cleaned up in this code block afterwards. MOZ_ASSERT(!mNeedsCleanup, "cleanup stream set unexpectedly"); mNeedsCleanup = nullptr; /* just in case */ if (!mInputFrameDataStream) { return NS_ERROR_UNEXPECTED; } uint32_t streamID = mInputFrameDataStream->StreamID(); mSegmentWriter = writer; rv = mInputFrameDataStream->WriteSegments(this, count, countWritten); mSegmentWriter = nullptr; mLastDataReadEpoch = mLastReadEpoch; if (SoftStreamError(rv)) { // This will happen when the transaction figures out it is EOF, generally // due to a content-length match being made. Return OK from this function // otherwise the whole session would be torn down. // if we were doing PROCESSING_COMPLETE_HEADERS need to pop the state // back to PROCESSING_DATA_FRAME where we came from mDownstreamState = PROCESSING_DATA_FRAME; if (mInputFrameDataRead == mInputFrameDataSize) ResetDownstreamState(); LOG3( ("Http2Session::WriteSegments session=%p id 0x%X " "needscleanup=%p. cleanup stream based on " "stream->writeSegments returning code %" PRIx32 "\n", this, streamID, mNeedsCleanup, static_cast(rv))); MOZ_ASSERT(!mNeedsCleanup || mNeedsCleanup->StreamID() == streamID); CleanupStream( streamID, (rv == NS_BINDING_RETARGETED) ? NS_BINDING_RETARGETED : NS_OK, CANCEL_ERROR); mNeedsCleanup = nullptr; *again = false; rv = ResumeRecv(); if (NS_FAILED(rv)) { LOG3(("ResumeRecv returned code %x", static_cast(rv))); } return NS_OK; } if (mNeedsCleanup) { LOG3( ("Http2Session::WriteSegments session=%p stream=%p 0x%X " "cleanup stream based on mNeedsCleanup.\n", this, mNeedsCleanup, mNeedsCleanup ? mNeedsCleanup->StreamID() : 0)); CleanupStream(mNeedsCleanup, NS_OK, CANCEL_ERROR); mNeedsCleanup = nullptr; } if (NS_FAILED(rv)) { LOG3(("Http2Session %p data frame read failure %" PRIx32 "\n", this, static_cast(rv))); // maybe just blocked reading from network if (rv == NS_BASE_STREAM_WOULD_BLOCK) rv = NS_OK; } return rv; } if (mDownstreamState == DISCARDING_DATA_FRAME || mDownstreamState == DISCARDING_DATA_FRAME_PADDING) { char trash[4096]; uint32_t discardCount = std::min(mInputFrameDataSize - mInputFrameDataRead, 4096U); LOG3(("Http2Session::WriteSegments %p trying to discard %d bytes of %s", this, discardCount, mDownstreamState == DISCARDING_DATA_FRAME ? "data" : "padding")); if (!discardCount && mDownstreamState == DISCARDING_DATA_FRAME) { // Only do this short-cirtuit if we're not discarding a pure padding // frame, as we need to potentially handle the stream FIN in those cases. // See bug 1381016 comment 36 for more details. ResetDownstreamState(); Unused << ResumeRecv(); return NS_BASE_STREAM_WOULD_BLOCK; } rv = NetworkRead(writer, trash, discardCount, countWritten); if (NS_FAILED(rv)) { LOG3(("Http2Session %p discard frame read failure %" PRIx32 "\n", this, static_cast(rv))); // maybe just blocked reading from network if (rv == NS_BASE_STREAM_WOULD_BLOCK) rv = NS_OK; return rv; } LogIO(this, nullptr, "Discarding Frame", trash, *countWritten); mInputFrameDataRead += *countWritten; if (mInputFrameDataRead == mInputFrameDataSize) { Http2StreamBase* streamToCleanup = nullptr; if (mInputFrameFinal) { streamToCleanup = mInputFrameDataStream; } bool discardedPadding = (mDownstreamState == DISCARDING_DATA_FRAME_PADDING); ResetDownstreamState(); if (streamToCleanup) { Http2PushedStream* pushed = streamToCleanup->GetHttp2PushedStream(); if (discardedPadding && pushed) { // Pushed streams are special on padding-only final data frames. // See bug 1409570 comments 6-8 for details. pushed->SetPushComplete(); Http2StreamBase* pushSink = pushed->GetConsumerStream(); if (pushSink) { bool enqueueSink = true; for (const auto& s : mPushesReadyForRead) { if (s == pushSink) { enqueueSink = false; break; } } if (enqueueSink) { AddStreamToQueue(pushSink, mPushesReadyForRead); // No use trying to clean up, it won't do anything, anyway streamToCleanup = nullptr; } } } CleanupStream(streamToCleanup, NS_OK, CANCEL_ERROR); } } return rv; } if (mDownstreamState != BUFFERING_CONTROL_FRAME) { MOZ_ASSERT(false); // this cannot happen return NS_ERROR_UNEXPECTED; } MOZ_ASSERT(mInputFrameBufferUsed == kFrameHeaderBytes, "Frame Buffer Header Not Present"); MOZ_ASSERT(mInputFrameDataSize + kFrameHeaderBytes <= mInputFrameBufferSize, "allocation for control frame insufficient"); rv = NetworkRead(writer, &mInputFrameBuffer[kFrameHeaderBytes + mInputFrameDataRead], mInputFrameDataSize - mInputFrameDataRead, countWritten); if (NS_FAILED(rv)) { LOG3(("Http2Session %p buffering control frame read failure %" PRIx32 "\n", this, static_cast(rv))); // maybe just blocked reading from network if (rv == NS_BASE_STREAM_WOULD_BLOCK) rv = NS_OK; return rv; } LogIO(this, nullptr, "Reading Control Frame", &mInputFrameBuffer[kFrameHeaderBytes + mInputFrameDataRead], *countWritten); mInputFrameDataRead += *countWritten; if (mInputFrameDataRead != mInputFrameDataSize) return NS_OK; MOZ_ASSERT(mInputFrameType != FRAME_TYPE_DATA); if (mInputFrameType < FRAME_TYPE_LAST) { rv = sControlFunctions[mInputFrameType](this); } else { // Section 4.1 requires this to be ignored; though protocol_error would // be better LOG3(("Http2Session %p unknown frame type %x ignored\n", this, mInputFrameType)); ResetDownstreamState(); rv = NS_OK; } MOZ_ASSERT(NS_FAILED(rv) || mDownstreamState != BUFFERING_CONTROL_FRAME, "Control Handler returned OK but did not change state"); if (mShouldGoAway && !mStreamTransactionHash.Count()) Close(NS_OK); return rv; } nsresult Http2Session::WriteSegments(nsAHttpSegmentWriter* writer, uint32_t count, uint32_t* countWritten) { bool again = false; return WriteSegmentsAgain(writer, count, countWritten, &again); } nsresult Http2Session::Finish0RTT(bool aRestart, bool aAlpnChanged) { MOZ_ASSERT(mAttemptingEarlyData); LOG3(("Http2Session::Finish0RTT %p aRestart=%d aAlpnChanged=%d", this, aRestart, aAlpnChanged)); for (size_t i = 0; i < m0RTTStreams.Length(); ++i) { if (m0RTTStreams[i]) { m0RTTStreams[i]->Finish0RTT(aRestart, aAlpnChanged); } } if (aRestart) { // 0RTT failed if (aAlpnChanged) { // This is a slightly more involved case - we need to get all our streams/ // transactions back in the queue so they can restart as http/1 // These must be set this way to ensure we gracefully restart all streams mGoAwayID = 0; mCleanShutdown = true; // Close takes care of the rest of our work for us. The reason code here // doesn't matter, as we aren't actually going to send a GOAWAY frame, but // we use NS_ERROR_NET_RESET as it's closest to the truth. Close(NS_ERROR_NET_RESET); } else { // This is the easy case - early data failed, but we're speaking h2, so // we just need to rewind to the beginning of the preamble and try again. mOutputQueueSent = 0; for (size_t i = 0; i < mCannotDo0RTTStreams.Length(); ++i) { if (mCannotDo0RTTStreams[i] && VerifyStream(mCannotDo0RTTStreams[i])) { TransactionHasDataToWrite(mCannotDo0RTTStreams[i]); } } } } else { // 0RTT succeeded for (size_t i = 0; i < mCannotDo0RTTStreams.Length(); ++i) { if (mCannotDo0RTTStreams[i] && VerifyStream(mCannotDo0RTTStreams[i])) { TransactionHasDataToWrite(mCannotDo0RTTStreams[i]); } } // Make sure we look for any incoming data in repsonse to our early data. Unused << ResumeRecv(); } mAttemptingEarlyData = false; m0RTTStreams.Clear(); mCannotDo0RTTStreams.Clear(); RealignOutputQueue(); return NS_OK; } nsresult Http2Session::ProcessConnectedPush( Http2StreamBase* pushConnectedStream, nsAHttpSegmentWriter* writer, uint32_t count, uint32_t* countWritten) { LOG3(("Http2Session::ProcessConnectedPush %p 0x%X\n", this, pushConnectedStream->StreamID())); mSegmentWriter = writer; nsresult rv = pushConnectedStream->WriteSegments(this, count, countWritten); mSegmentWriter = nullptr; // The pipe in nsHttpTransaction rewrites CLOSED error codes into OK // so we need this check to determine the truth. Http2Stream* h2Stream = pushConnectedStream->GetHttp2Stream(); MOZ_ASSERT(h2Stream); if (NS_SUCCEEDED(rv) && !*countWritten && h2Stream && h2Stream->PushSource() && h2Stream->PushSource()->GetPushComplete()) { rv = NS_BASE_STREAM_CLOSED; } if (rv == NS_BASE_STREAM_CLOSED) { CleanupStream(pushConnectedStream, NS_OK, CANCEL_ERROR); rv = NS_OK; } // if we return OK to nsHttpConnection it will use mSocketInCondition // to determine whether to schedule more reads, incorrectly // assuming that nsHttpConnection::OnSocketWrite() was called. if (NS_SUCCEEDED(rv) || rv == NS_BASE_STREAM_WOULD_BLOCK) { rv = NS_BASE_STREAM_WOULD_BLOCK; Unused << ResumeRecv(); } return rv; } nsresult Http2Session::ProcessSlowConsumer(Http2StreamBase* slowConsumer, nsAHttpSegmentWriter* writer, uint32_t count, uint32_t* countWritten) { LOG3(("Http2Session::ProcessSlowConsumer %p 0x%X\n", this, slowConsumer->StreamID())); mSegmentWriter = writer; nsresult rv = slowConsumer->WriteSegments(this, count, countWritten); mSegmentWriter = nullptr; LOG3(("Http2Session::ProcessSlowConsumer Writesegments %p 0x%X rv %" PRIX32 " %d\n", this, slowConsumer->StreamID(), static_cast(rv), *countWritten)); if (NS_SUCCEEDED(rv) && !*countWritten && slowConsumer->RecvdFin()) { rv = NS_BASE_STREAM_CLOSED; } if (NS_SUCCEEDED(rv) && (*countWritten > 0)) { // There have been buffered bytes successfully fed into the // formerly blocked consumer. Repeat until buffer empty or // consumer is blocked again. UpdateLocalRwin(slowConsumer, 0); ConnectSlowConsumer(slowConsumer); } if (rv == NS_BASE_STREAM_CLOSED) { CleanupStream(slowConsumer, NS_OK, CANCEL_ERROR); rv = NS_OK; } return rv; } void Http2Session::UpdateLocalStreamWindow(Http2StreamBase* stream, uint32_t bytes) { if (!stream) { // this is ok - it means there was a data frame for a rst // stream return; } // If this data packet was not for a valid or live stream then there // is no reason to mess with the flow control if (!stream || stream->RecvdFin() || stream->RecvdReset() || mInputFrameFinal) { return; } stream->DecrementClientReceiveWindow(bytes); // Don't necessarily ack every data packet. Only do it // after a significant amount of data. uint64_t unacked = stream->LocalUnAcked(); int64_t localWindow = stream->ClientReceiveWindow(); LOG3( ("Http2Session::UpdateLocalStreamWindow this=%p id=0x%X newbytes=%u " "unacked=%" PRIu64 " localWindow=%" PRId64 "\n", this, stream->StreamID(), bytes, unacked, localWindow)); if (!unacked) return; if ((unacked < kMinimumToAck) && (localWindow > kEmergencyWindowThreshold)) { return; } if (!stream->HasSink()) { LOG3( ("Http2Session::UpdateLocalStreamWindow %p 0x%X Pushed Stream Has No " "Sink\n", this, stream->StreamID())); return; } // Generate window updates directly out of session instead of the stream // in order to avoid queue delays in getting the 'ACK' out. uint32_t toack = (unacked <= 0x7fffffffU) ? unacked : 0x7fffffffU; LOG3( ("Http2Session::UpdateLocalStreamWindow Ack this=%p id=0x%X acksize=%d\n", this, stream->StreamID(), toack)); stream->IncrementClientReceiveWindow(toack); if (toack == 0) { // Ensure we never send an illegal 0 window update return; } // room for this packet needs to be ensured before calling this function char* packet = mOutputQueueBuffer.get() + mOutputQueueUsed; mOutputQueueUsed += kFrameHeaderBytes + 4; MOZ_ASSERT(mOutputQueueUsed <= mOutputQueueSize); CreateFrameHeader(packet, 4, FRAME_TYPE_WINDOW_UPDATE, 0, stream->StreamID()); NetworkEndian::writeUint32(packet + kFrameHeaderBytes, toack); LogIO(this, stream, "Stream Window Update", packet, kFrameHeaderBytes + 4); // dont flush here, this write can commonly be coalesced with a // session window update to immediately follow. } void Http2Session::UpdateLocalSessionWindow(uint32_t bytes) { if (!bytes) return; mLocalSessionWindow -= bytes; LOG3( ("Http2Session::UpdateLocalSessionWindow this=%p newbytes=%u " "localWindow=%" PRId64 "\n", this, bytes, mLocalSessionWindow)); // Don't necessarily ack every data packet. Only do it // after a significant amount of data. if ((mLocalSessionWindow > (mInitialRwin - kMinimumToAck)) && (mLocalSessionWindow > kEmergencyWindowThreshold)) { return; } // Only send max bits of window updates at a time. uint64_t toack64 = mInitialRwin - mLocalSessionWindow; uint32_t toack = (toack64 <= 0x7fffffffU) ? toack64 : 0x7fffffffU; LOG3(("Http2Session::UpdateLocalSessionWindow Ack this=%p acksize=%u\n", this, toack)); mLocalSessionWindow += toack; if (toack == 0) { // Ensure we never send an illegal 0 window update return; } // room for this packet needs to be ensured before calling this function char* packet = mOutputQueueBuffer.get() + mOutputQueueUsed; mOutputQueueUsed += kFrameHeaderBytes + 4; MOZ_ASSERT(mOutputQueueUsed <= mOutputQueueSize); CreateFrameHeader(packet, 4, FRAME_TYPE_WINDOW_UPDATE, 0, 0); NetworkEndian::writeUint32(packet + kFrameHeaderBytes, toack); LogIO(this, nullptr, "Session Window Update", packet, kFrameHeaderBytes + 4); // dont flush here, this write can commonly be coalesced with others } void Http2Session::UpdateLocalRwin(Http2StreamBase* stream, uint32_t bytes) { // make sure there is room for 2 window updates even though // we may not generate any. EnsureOutputBuffer(2 * (kFrameHeaderBytes + 4)); UpdateLocalStreamWindow(stream, bytes); UpdateLocalSessionWindow(bytes); FlushOutputQueue(); } void Http2Session::Close(nsresult aReason) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); if (mClosed) return; LOG3(("Http2Session::Close %p %" PRIX32, this, static_cast(aReason))); mClosed = true; Shutdown(aReason); mStreamIDHash.Clear(); mStreamTransactionHash.Clear(); mTunnelStreams.Clear(); mProcessedWaitingWebsockets = true; uint32_t goAwayReason; if (mGoAwayReason != NO_HTTP_ERROR) { goAwayReason = mGoAwayReason; } else if (NS_SUCCEEDED(aReason)) { goAwayReason = NO_HTTP_ERROR; } else if (aReason == NS_ERROR_NET_HTTP2_SENT_GOAWAY) { goAwayReason = PROTOCOL_ERROR; } else if (mCleanShutdown) { goAwayReason = NO_HTTP_ERROR; } else { goAwayReason = INTERNAL_ERROR; } if (!mAttemptingEarlyData) { GenerateGoAway(goAwayReason); } mConnection = nullptr; mSegmentReader = nullptr; mSegmentWriter = nullptr; } nsHttpConnectionInfo* Http2Session::ConnectionInfo() { RefPtr ci; GetConnectionInfo(getter_AddRefs(ci)); return ci.get(); } void Http2Session::CloseTransaction(nsAHttpTransaction* aTransaction, nsresult aResult) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); LOG3(("Http2Session::CloseTransaction %p %p %" PRIx32, this, aTransaction, static_cast(aResult))); // Generally this arrives as a cancel event from the connection manager. // need to find the stream and call CleanupStream() on it. RefPtr stream = mStreamTransactionHash.Get(aTransaction); if (!stream) { LOG3(("Http2Session::CloseTransaction %p %p %" PRIx32 " - not found.", this, aTransaction, static_cast(aResult))); return; } LOG3( ("Http2Session::CloseTransaction probably a cancel. " "this=%p, trans=%p, result=%" PRIx32 ", streamID=0x%X stream=%p", this, aTransaction, static_cast(aResult), stream->StreamID(), stream.get())); CleanupStream(stream, aResult, CANCEL_ERROR); nsresult rv = ResumeRecv(); if (NS_FAILED(rv)) { LOG3(("Http2Session::CloseTransaction %p %p %x ResumeRecv returned %x", this, aTransaction, static_cast(aResult), static_cast(rv))); } } //----------------------------------------------------------------------------- // nsAHttpSegmentReader //----------------------------------------------------------------------------- nsresult Http2Session::OnReadSegment(const char* buf, uint32_t count, uint32_t* countRead) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); nsresult rv; // If we can release old queued data then we can try and write the new // data directly to the network without using the output queue at all if (mOutputQueueUsed) FlushOutputQueue(); if (!mOutputQueueUsed && mSegmentReader) { // try and write directly without output queue rv = mSegmentReader->OnReadSegment(buf, count, countRead); if (rv == NS_BASE_STREAM_WOULD_BLOCK) { *countRead = 0; } else if (NS_FAILED(rv)) { return rv; } if (*countRead < count) { uint32_t required = count - *countRead; // assuming a commitment() happened, this ensurebuffer is a nop // but just in case the queuesize is too small for the required data // call ensurebuffer(). EnsureBuffer(mOutputQueueBuffer, required, 0, mOutputQueueSize); memcpy(mOutputQueueBuffer.get(), buf + *countRead, required); mOutputQueueUsed = required; } *countRead = count; return NS_OK; } // At this point we are going to buffer the new data in the output // queue if it fits. By coalescing multiple small submissions into one larger // buffer we can get larger writes out to the network later on. // This routine should not be allowed to fill up the output queue // all on its own - at least kQueueReserved bytes are always left // for other routines to use - but this is an all-or-nothing function, // so if it will not all fit just return WOULD_BLOCK if ((mOutputQueueUsed + count) > (mOutputQueueSize - kQueueReserved)) { return NS_BASE_STREAM_WOULD_BLOCK; } memcpy(mOutputQueueBuffer.get() + mOutputQueueUsed, buf, count); mOutputQueueUsed += count; *countRead = count; FlushOutputQueue(); return NS_OK; } nsresult Http2Session::CommitToSegmentSize(uint32_t count, bool forceCommitment) { if (mOutputQueueUsed && !mAttemptingEarlyData) FlushOutputQueue(); // would there be enough room to buffer this if needed? if ((mOutputQueueUsed + count) <= (mOutputQueueSize - kQueueReserved)) { return NS_OK; } // if we are using part of our buffers already, try again later unless // forceCommitment is set. if (mOutputQueueUsed && !forceCommitment) return NS_BASE_STREAM_WOULD_BLOCK; if (mOutputQueueUsed) { // normally we avoid the memmove of RealignOutputQueue, but we'll try // it if forceCommitment is set before growing the buffer. RealignOutputQueue(); // is there enough room now? if ((mOutputQueueUsed + count) <= (mOutputQueueSize - kQueueReserved)) { return NS_OK; } } // resize the buffers as needed EnsureOutputBuffer(count + kQueueReserved); MOZ_ASSERT((mOutputQueueUsed + count) <= (mOutputQueueSize - kQueueReserved), "buffer not as large as expected"); return NS_OK; } //----------------------------------------------------------------------------- // nsAHttpSegmentWriter //----------------------------------------------------------------------------- nsresult Http2Session::OnWriteSegment(char* buf, uint32_t count, uint32_t* countWritten) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); nsresult rv; if (!mSegmentWriter) { // the only way this could happen would be if Close() were called on the // stack with WriteSegments() return NS_ERROR_FAILURE; } if (mDownstreamState == NOT_USING_NETWORK || mDownstreamState == BUFFERING_FRAME_HEADER || mDownstreamState == DISCARDING_DATA_FRAME_PADDING) { return NS_BASE_STREAM_WOULD_BLOCK; } if (mDownstreamState == PROCESSING_DATA_FRAME) { if (mInputFrameFinal && mInputFrameDataRead == mInputFrameDataSize) { *countWritten = 0; SetNeedsCleanup(); return NS_BASE_STREAM_CLOSED; } count = std::min(count, mInputFrameDataSize - mInputFrameDataRead); rv = NetworkRead(mSegmentWriter, buf, count, countWritten); if (NS_FAILED(rv)) return rv; LogIO(this, mInputFrameDataStream, "Reading Data Frame", buf, *countWritten); mInputFrameDataRead += *countWritten; if (mPaddingLength && (mInputFrameDataSize - mInputFrameDataRead <= mPaddingLength)) { // We are crossing from real HTTP data into the realm of padding. If // we've actually crossed the line, we need to munge countWritten for the // sake of goodness and sanity. No matter what, any future calls to // WriteSegments need to just discard data until we reach the end of this // frame. if (mInputFrameDataSize != mInputFrameDataRead) { // Only change state if we still have padding to read. If we don't do // this, we can end up hanging on frames that combine real data, // padding, and END_STREAM (see bug 1019921) ChangeDownstreamState(DISCARDING_DATA_FRAME_PADDING); } uint32_t paddingRead = mPaddingLength - (mInputFrameDataSize - mInputFrameDataRead); LOG3( ("Http2Session::OnWriteSegment %p stream 0x%X len=%d read=%d " "crossed from HTTP data into padding (%d of %d) countWritten=%d", this, mInputFrameID, mInputFrameDataSize, mInputFrameDataRead, paddingRead, mPaddingLength, *countWritten)); *countWritten -= paddingRead; LOG3(("Http2Session::OnWriteSegment %p stream 0x%X new countWritten=%d", this, mInputFrameID, *countWritten)); } mInputFrameDataStream->UpdateTransportReadEvents(*countWritten); if ((mInputFrameDataRead == mInputFrameDataSize) && !mInputFrameFinal) { ResetDownstreamState(); } return rv; } if (mDownstreamState == PROCESSING_COMPLETE_HEADERS) { if (mFlatHTTPResponseHeaders.Length() == mFlatHTTPResponseHeadersOut && mInputFrameFinal) { *countWritten = 0; SetNeedsCleanup(); return NS_BASE_STREAM_CLOSED; } count = std::min( count, mFlatHTTPResponseHeaders.Length() - mFlatHTTPResponseHeadersOut); memcpy(buf, mFlatHTTPResponseHeaders.get() + mFlatHTTPResponseHeadersOut, count); mFlatHTTPResponseHeadersOut += count; *countWritten = count; if (mFlatHTTPResponseHeaders.Length() == mFlatHTTPResponseHeadersOut) { // Since mInputFrameFinal can be reset, we need to also check RecvdFin to // see if a stream doesn’t expect more frames. if (!mInputFrameFinal && !mInputFrameDataStream->RecvdFin()) { // If more frames are expected in this stream, then reset the state so // they can be handled. Otherwise (e.g. a 0 length response with the fin // on the incoming headers) stay in PROCESSING_COMPLETE_HEADERS state so // the SetNeedsCleanup() code above can cleanup the stream. ResetDownstreamState(); } } return NS_OK; } MOZ_ASSERT(false); return NS_ERROR_UNEXPECTED; } void Http2Session::SetNeedsCleanup() { LOG3( ("Http2Session::SetNeedsCleanup %p - recorded downstream fin of " "stream %p 0x%X", this, mInputFrameDataStream, mInputFrameDataStream->StreamID())); // This will result in Close() being called MOZ_ASSERT(!mNeedsCleanup, "mNeedsCleanup unexpectedly set"); mInputFrameDataStream->SetResponseIsComplete(); mNeedsCleanup = mInputFrameDataStream; ResetDownstreamState(); } void Http2Session::ConnectPushedStream(Http2StreamBase* stream) { AddStreamToQueue(stream, mPushesReadyForRead); Unused << ForceRecv(); } void Http2Session::ConnectSlowConsumer(Http2StreamBase* stream) { LOG3(("Http2Session::ConnectSlowConsumer %p 0x%X\n", this, stream->StreamID())); AddStreamToQueue(stream, mSlowConsumersReadyForRead); Unused << ForceRecv(); } nsresult Http2Session::BufferOutput(const char* buf, uint32_t count, uint32_t* countRead) { RefPtr old; mSegmentReader.swap(old); // Make mSegmentReader null nsresult rv = OnReadSegment(buf, count, countRead); mSegmentReader.swap(old); // Restore the old mSegmentReader return rv; } bool // static Http2Session::ALPNCallback(nsITLSSocketControl* tlsSocketControl) { LOG3(("Http2Session::ALPNCallback sslsocketcontrol=%p\n", tlsSocketControl)); if (tlsSocketControl) { int16_t version = tlsSocketControl->GetSSLVersionOffered(); LOG3(("Http2Session::ALPNCallback version=%x\n", version)); if (version == nsITLSSocketControl::TLS_VERSION_1_2 && !gHttpHandler->IsH2MandatorySuiteEnabled()) { LOG3(("Http2Session::ALPNCallback Mandatory Cipher Suite Unavailable\n")); return false; } if (version >= nsITLSSocketControl::TLS_VERSION_1_2) { return true; } } return false; } nsresult Http2Session::ConfirmTLSProfile() { if (mTLSProfileConfirmed) { return NS_OK; } LOG3(("Http2Session::ConfirmTLSProfile %p mConnection=%p\n", this, mConnection.get())); if (mAttemptingEarlyData) { LOG3( ("Http2Session::ConfirmTLSProfile %p temporarily passing due to early " "data\n", this)); return NS_OK; } if (!StaticPrefs::network_http_http2_enforce_tls_profile()) { LOG3( ("Http2Session::ConfirmTLSProfile %p passed due to configuration " "bypass\n", this)); mTLSProfileConfirmed = true; return NS_OK; } if (!mConnection) return NS_ERROR_FAILURE; nsCOMPtr ssl; mConnection->GetTLSSocketControl(getter_AddRefs(ssl)); LOG3(("Http2Session::ConfirmTLSProfile %p sslsocketcontrol=%p\n", this, ssl.get())); if (!ssl) return NS_ERROR_FAILURE; int16_t version = ssl->GetSSLVersionUsed(); LOG3(("Http2Session::ConfirmTLSProfile %p version=%x\n", this, version)); if (version < nsITLSSocketControl::TLS_VERSION_1_2) { LOG3(("Http2Session::ConfirmTLSProfile %p FAILED due to lack of TLS1.2\n", this)); return SessionError(INADEQUATE_SECURITY); } uint16_t kea = ssl->GetKEAUsed(); if (kea == ssl_kea_ecdh_hybrid && !StaticPrefs::security_tls_enable_kyber()) { LOG3(("Http2Session::ConfirmTLSProfile %p FAILED due to disabled KEA %d\n", this, kea)); return SessionError(INADEQUATE_SECURITY); } if (kea != ssl_kea_dh && kea != ssl_kea_ecdh && kea != ssl_kea_ecdh_hybrid) { LOG3(("Http2Session::ConfirmTLSProfile %p FAILED due to invalid KEA %d\n", this, kea)); return SessionError(INADEQUATE_SECURITY); } uint32_t keybits = ssl->GetKEAKeyBits(); if (kea == ssl_kea_dh && keybits < 2048) { LOG3(("Http2Session::ConfirmTLSProfile %p FAILED due to DH %d < 2048\n", this, keybits)); return SessionError(INADEQUATE_SECURITY); } if (kea == ssl_kea_ecdh && keybits < 224) { // see rfc7540 9.2.1. LOG3(("Http2Session::ConfirmTLSProfile %p FAILED due to ECDH %d < 224\n", this, keybits)); return SessionError(INADEQUATE_SECURITY); } int16_t macAlgorithm = ssl->GetMACAlgorithmUsed(); LOG3(("Http2Session::ConfirmTLSProfile %p MAC Algortihm (aead==6) %d\n", this, macAlgorithm)); if (macAlgorithm != nsITLSSocketControl::SSL_MAC_AEAD) { LOG3(("Http2Session::ConfirmTLSProfile %p FAILED due to lack of AEAD\n", this)); return SessionError(INADEQUATE_SECURITY); } /* We are required to send SNI. We do that already, so no check is done * here to make sure we did. */ /* We really should check to ensure TLS compression isn't enabled on * this connection. However, we never enable TLS compression on our end, * anyway, so it'll never be on. All the same, see https://bugzil.la/965881 * for the possibility for an interface to ensure it never gets turned on. */ mTLSProfileConfirmed = true; return NS_OK; } //----------------------------------------------------------------------------- // Modified methods of nsAHttpConnection //----------------------------------------------------------------------------- void Http2Session::TransactionHasDataToWrite(nsAHttpTransaction* caller) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); LOG3(("Http2Session::TransactionHasDataToWrite %p trans=%p", this, caller)); // a trapped signal from the http transaction to the connection that // it is no longer blocked on read. RefPtr stream = mStreamTransactionHash.Get(caller); if (!stream || !VerifyStream(stream)) { LOG3(("Http2Session::TransactionHasDataToWrite %p caller %p not found", this, caller)); return; } LOG3(("Http2Session::TransactionHasDataToWrite %p ID is 0x%X\n", this, stream->StreamID())); if (!mClosed) { AddStreamToQueue(stream, mReadyForWrite); SetWriteCallbacks(); } else { LOG3( ("Http2Session::TransactionHasDataToWrite %p closed so not setting " "Ready4Write\n", this)); } // NSPR poll will not poll the network if there are non system PR_FileDesc's // that are ready - so we can get into a deadlock waiting for the system IO // to come back here if we don't force the send loop manually. Unused << ForceSend(); } void Http2Session::TransactionHasDataToRecv(nsAHttpTransaction* caller) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); LOG3(("Http2Session::TransactionHasDataToRecv %p trans=%p", this, caller)); // a signal from the http transaction to the connection that it will consume // more RefPtr stream = mStreamTransactionHash.Get(caller); if (!stream || !VerifyStream(stream)) { LOG3(("Http2Session::TransactionHasDataToRecv %p caller %p not found", this, caller)); return; } LOG3(("Http2Session::TransactionHasDataToRecv %p ID is 0x%X\n", this, stream->StreamID())); TransactionHasDataToRecv(stream); } void Http2Session::TransactionHasDataToWrite(Http2StreamBase* stream) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); LOG3(("Http2Session::TransactionHasDataToWrite %p stream=%p ID=0x%x", this, stream, stream->StreamID())); AddStreamToQueue(stream, mReadyForWrite); SetWriteCallbacks(); Unused << ForceSend(); } void Http2Session::TransactionHasDataToRecv(Http2StreamBase* caller) { ConnectSlowConsumer(caller); } bool Http2Session::IsPersistent() { return true; } nsresult Http2Session::TakeTransport(nsISocketTransport**, nsIAsyncInputStream**, nsIAsyncOutputStream**) { MOZ_ASSERT(false, "TakeTransport of Http2Session"); return NS_ERROR_UNEXPECTED; } Http3WebTransportSession* Http2Session::GetWebTransportSession( nsAHttpTransaction* aTransaction) { MOZ_ASSERT(false, "GetWebTransportSession of Http2Session"); return nullptr; } already_AddRefed Http2Session::TakeHttpConnection() { MOZ_ASSERT(false, "TakeHttpConnection of Http2Session"); return nullptr; } already_AddRefed Http2Session::HttpConnection() { if (mConnection) { return mConnection->HttpConnection(); } return nullptr; } void Http2Session::GetSecurityCallbacks(nsIInterfaceRequestor** aOut) { *aOut = nullptr; } void Http2Session::SetConnection(nsAHttpConnection* aConn) { mConnection = aConn; } //----------------------------------------------------------------------------- // unused methods of nsAHttpTransaction // We can be sure of this because Http2Session is only constructed in // nsHttpConnection and is never passed out of that object or a // TLSFilterTransaction TLS tunnel //----------------------------------------------------------------------------- void Http2Session::SetProxyConnectFailed() { MOZ_ASSERT(false, "Http2Session::SetProxyConnectFailed()"); } bool Http2Session::IsDone() { return !mStreamTransactionHash.Count(); } nsresult Http2Session::Status() { MOZ_ASSERT(false, "Http2Session::Status()"); return NS_ERROR_UNEXPECTED; } uint32_t Http2Session::Caps() { MOZ_ASSERT(false, "Http2Session::Caps()"); return 0; } nsHttpRequestHead* Http2Session::RequestHead() { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); MOZ_ASSERT(false, "Http2Session::RequestHead() " "should not be called after http/2 is setup"); return nullptr; } uint32_t Http2Session::Http1xTransactionCount() { return 0; } nsresult Http2Session::TakeSubTransactions( nsTArray>& outTransactions) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); // Generally this cannot be done with http/2 as transactions are // started right away. LOG3(("Http2Session::TakeSubTransactions %p\n", this)); if (mConcurrentHighWater > 0) return NS_ERROR_ALREADY_OPENED; LOG3((" taking %d\n", mStreamTransactionHash.Count())); for (auto iter = mStreamTransactionHash.Iter(); !iter.Done(); iter.Next()) { outTransactions.AppendElement(iter.Key()); // Removing the stream from the hash will delete the stream and drop the // transaction reference the hash held. iter.Remove(); } return NS_OK; } //----------------------------------------------------------------------------- // Pass through methods of nsAHttpConnection //----------------------------------------------------------------------------- nsAHttpConnection* Http2Session::Connection() { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); return mConnection; } nsresult Http2Session::OnHeadersAvailable(nsAHttpTransaction* transaction, nsHttpRequestHead* requestHead, nsHttpResponseHead* responseHead, bool* reset) { return NS_OK; } bool Http2Session::IsReused() { if (!mConnection) { return false; } return mConnection->IsReused(); } nsresult Http2Session::PushBack(const char* buf, uint32_t len) { return mConnection->PushBack(buf, len); } void Http2Session::SendPing() { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); LOG(("Http2Session::SendPing %p mPreviousUsed=%d", this, mPreviousUsed)); if (mPreviousUsed) { // alredy in progress, get out return; } mPingSentEpoch = PR_IntervalNow(); if (!mPingSentEpoch) { mPingSentEpoch = 1; // avoid the 0 sentinel value } if (!mPingThreshold || (mPingThreshold > gHttpHandler->NetworkChangedTimeout())) { mPreviousPingThreshold = mPingThreshold; mPreviousUsed = true; mPingThreshold = gHttpHandler->NetworkChangedTimeout(); // Reset mLastReadEpoch, so we can really check when do we got pong from the // server. mLastReadEpoch = 0; } GeneratePing(false); Unused << ResumeRecv(); } bool Http2Session::TestOriginFrame(const nsACString& hostname, int32_t port) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); MOZ_ASSERT(mOriginFrameActivated); nsAutoCString key(hostname); key.Append(':'); key.AppendInt(port); bool rv = mOriginFrame.Get(key); LOG3(("TestOriginFrame() hash.get %p %s %d\n", this, key.get(), rv)); if (!rv && ConnectionInfo()) { // the SNI is also implicitly in this list, so consult that too nsHttpConnectionInfo* ci = ConnectionInfo(); rv = nsCString(hostname).EqualsIgnoreCase(ci->Origin()) && (port == ci->OriginPort()); LOG3(("TestOriginFrame() %p sni test %d\n", this, rv)); } return rv; } bool Http2Session::TestJoinConnection(const nsACString& hostname, int32_t port) { return RealJoinConnection(hostname, port, true); } bool Http2Session::JoinConnection(const nsACString& hostname, int32_t port) { return RealJoinConnection(hostname, port, false); } bool Http2Session::RealJoinConnection(const nsACString& hostname, int32_t port, bool justKidding) { if (!mConnection || mClosed || mShouldGoAway) { return false; } nsHttpConnectionInfo* ci = ConnectionInfo(); if (nsCString(hostname).EqualsIgnoreCase(ci->Origin()) && (port == ci->OriginPort())) { return true; } if (!mReceivedSettings) { return false; } if (mOriginFrameActivated) { bool originFrameResult = TestOriginFrame(hostname, port); if (!originFrameResult) { return false; } } else { LOG3(("JoinConnection %p no origin frame check used.\n", this)); } nsAutoCString key(hostname); key.Append(':'); key.Append(justKidding ? 'k' : '.'); key.AppendInt(port); bool cachedResult; if (mJoinConnectionCache.Get(key, &cachedResult)) { LOG(("joinconnection [%p %s] %s result=%d cache\n", this, ConnectionInfo()->HashKey().get(), key.get(), cachedResult)); return cachedResult; } nsresult rv; bool isJoined = false; nsCOMPtr sslSocketControl; mConnection->GetTLSSocketControl(getter_AddRefs(sslSocketControl)); if (!sslSocketControl) { return false; } // try all the coalescable versions we support. const SpdyInformation* info = gHttpHandler->SpdyInfo(); bool joinedReturn = false; if (StaticPrefs::network_http_http2_enabled()) { if (justKidding) { rv = sslSocketControl->TestJoinConnection(info->VersionString, hostname, port, &isJoined); } else { rv = sslSocketControl->JoinConnection(info->VersionString, hostname, port, &isJoined); } if (NS_SUCCEEDED(rv) && isJoined) { joinedReturn = true; } } LOG(("joinconnection [%p %s] %s result=%d lookup\n", this, ConnectionInfo()->HashKey().get(), key.get(), joinedReturn)); mJoinConnectionCache.InsertOrUpdate(key, joinedReturn); if (!justKidding) { // cache a kidding entry too as this one is good for both nsAutoCString key2(hostname); key2.Append(':'); key2.Append('k'); key2.AppendInt(port); if (!mJoinConnectionCache.Get(key2)) { mJoinConnectionCache.InsertOrUpdate(key2, joinedReturn); } } return joinedReturn; } void Http2Session::CurrentBrowserIdChanged(uint64_t id) { MOZ_ASSERT(OnSocketThread(), "not on socket thread"); mCurrentBrowserId = id; for (const auto& stream : mStreamTransactionHash.Values()) { stream->CurrentBrowserIdChanged(id); } } void Http2Session::SetCleanShutdown(bool aCleanShutdown) { mCleanShutdown = aCleanShutdown; } WebSocketSupport Http2Session::GetWebSocketSupport() { LOG3(("Http2Session::GetWebSocketSupport %p enable=%d allow=%d processed=%d", this, mEnableWebsockets, mPeerAllowsWebsockets, mProcessedWaitingWebsockets)); if (!mEnableWebsockets) { return WebSocketSupport::NO_SUPPORT; } if (mPeerAllowsWebsockets) { return WebSocketSupport::SUPPORTED; } if (!mProcessedWaitingWebsockets) { mHasTransactionWaitingForWebsockets = true; return WebSocketSupport::UNSURE; } return WebSocketSupport::NO_SUPPORT; } PRIntervalTime Http2Session::LastWriteTime() { return mConnection->LastWriteTime(); } } // namespace net } // namespace mozilla