diff options
Diffstat (limited to 'netwerk/protocol/http/Http2Session.cpp')
-rw-r--r-- | netwerk/protocol/http/Http2Session.cpp | 4513 |
1 files changed, 4513 insertions, 0 deletions
diff --git a/netwerk/protocol/http/Http2Session.cpp b/netwerk/protocol/http/Http2Session.cpp new file mode 100644 index 0000000000..e969d60c4d --- /dev/null +++ b/netwerk/protocol/http/Http2Session.cpp @@ -0,0 +1,4513 @@ +/* -*- 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 <algorithm> + +#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<WeakPtr<Http2StreamBase>>& queue) { + for (const auto& stream : Reversed(queue)) { + if (stream == aStream) { + queue.RemoveElement(stream); + } + } +} + +static void AddStreamToQueue(Http2StreamBase* aStream, + nsTArray<WeakPtr<Http2StreamBase>>& queue) { + if (!queue.Contains(aStream)) { + queue.AppendElement(aStream); + } +} + +static already_AddRefed<Http2StreamBase> GetNextStreamFromQueue( + nsTArray<WeakPtr<Http2StreamBase>>& queue) { + while (!queue.IsEmpty() && !queue[0]) { + MOZ_ASSERT(false); + queue.RemoveElementAt(0); + } + if (queue.IsEmpty()) { + return nullptr; + } + + RefPtr<Http2StreamBase> 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<nsHttpHandler> 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<char[]>(mInputFrameBufferSize); + mOutputQueueBuffer = MakeUnique<char[]>(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 && SecurityErrorThatMayNeedRestart(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<const uint8_t*>(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<nsHttpConnectionInfo> 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<Http2PushedStreamWrapper> 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<uint32_t>(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<Http2StreamBase> 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<Http2StreamBase> 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<nsHttpConnection> Http2Session::CreateTunnelStream( + nsAHttpTransaction* aHttpTransaction, nsIInterfaceRequestor* aCallbacks, + PRIntervalTime aRtt, bool aIsWebSocket) { + RefPtr<Http2StreamTunnel> refStream = CreateTunnelStreamFromConnInfo( + this, mCurrentBrowserId, aHttpTransaction->ConnectionInfo(), + aIsWebSocket); + + RefPtr<nsHttpConnection> 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<Http2StreamBase> 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<uint32_t>(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<nsIRunnable> 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 <typename charType> +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<const uint8_t*>(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<uint32_t>(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<Http2PushedStream*>(aStream); + nsAutoCString hashKey; + DebugOnly<bool> 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<uint32_t>(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<uint8_t*>(&mInputFrameBuffer[kFrameHeaderBytes]); + paddingControlBytes = 1; + } else { + paddingLength = 0; + paddingControlBytes = 0; + } + + if (static_cast<uint32_t>(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); + } + + 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], + self->mInputFrameDataSize - paddingControlBytes - priorityLen - + paddingLength); + + 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], + self->mInputFrameDataSize - paddingControlBytes - priorityLen - + paddingLength); + + self->mInputFrameDataStream->UpdateTransportReadEvents( + self->mInputFrameDataSize); + self->mLastDataReadEpoch = self->mLastReadEpoch; + + if (!isContinuation) { + self->mAggregatedHeaderSize = self->mInputFrameDataSize - + paddingControlBytes - priorityLen - + paddingLength; + } else { + self->mAggregatedHeaderSize += self->mInputFrameDataSize - + paddingControlBytes - priorityLen - + paddingLength; + } + + 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<nsresult> 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<uint8_t*>(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<nsHttpConnectionInfo> 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<Http2PushTransactionBuffer> transactionBuffer = + new Http2PushTransactionBuffer(); + transactionBuffer->SetConnection(self); + RefPtr<Http2PushedStream> 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<Http2StreamBase> pushedWeak = pushedStream.get(); + self->mStreamTransactionHash.InsertOrUpdate(transactionBuffer, + std::move(pushedStream)); + self->mPushedStreams.AppendElement( + static_cast<Http2PushedStream*>(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<Http2PushedStream*>(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<nsIURI> 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<Http2PushedStream*>(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<Http2PushedStream*>(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<nsIURI> 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<Http2Session> session = self; + auto cachePushCheckCallback = [session, promisedID](bool aAccepted) { + MOZ_ASSERT(OnSocketThread()); + + if (!aAccepted) { + session->CleanupStream(promisedID, NS_ERROR_FAILURE, + Http2Session::REFUSED_STREAM_ERROR); + } + }; + RefPtr<CachePushChecker> checker = new CachePushChecker( + pushedURL, oa, + static_cast<Http2PushedStream*>(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<Http2StreamBase*>(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<nsresult> 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<nsIURI> 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<nsHttpConnectionInfo> mCI; + nsCOMPtr<nsIInterfaceRequestor> 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<uint16_t>(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<nsHttpConnectionInfo> 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<nsITLSSocketControl> 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<UpdateAltSvcEvent> 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<nsIURI> 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<HttpConnectionBase> 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<HttpConnectionBase> 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<uint32_t>(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<Http2StreamBase> 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<uint32_t>(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<Http2StreamBase> 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<Http2StreamBase> 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<uint32_t>(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<uint8_t*>(mInputFrameBuffer.get() + + kFrameLengthBytes); + mInputFrameFlags = *reinterpret_cast<uint8_t*>( + 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<nsHttpConnectionInfo> 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<uint32_t>(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<uint8_t>(*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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<uint32_t>(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<nsHttpConnectionInfo> 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<uint32_t>(aResult))); + + // Generally this arrives as a cancel event from the connection manager. + + // need to find the stream and call CleanupStream() on it. + RefPtr<Http2StreamBase> stream = mStreamTransactionHash.Get(aTransaction); + if (!stream) { + LOG3(("Http2Session::CloseTransaction %p %p %" PRIx32 " - not found.", this, + aTransaction, static_cast<uint32_t>(aResult))); + return; + } + LOG3( + ("Http2Session::CloseTransaction probably a cancel. " + "this=%p, trans=%p, result=%" PRIx32 ", streamID=0x%X stream=%p", + this, aTransaction, static_cast<uint32_t>(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<uint32_t>(aResult), + static_cast<uint32_t>(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<uint32_t>( + 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<nsAHttpSegmentReader> 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<nsITLSSocketControl> 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<Http2StreamBase> 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<Http2StreamBase> 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<HttpConnectionBase> Http2Session::TakeHttpConnection() { + MOZ_ASSERT(false, "TakeHttpConnection of Http2Session"); + return nullptr; +} + +already_AddRefed<HttpConnectionBase> 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<RefPtr<nsAHttpTransaction>>& 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<nsITLSSocketControl> 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 |