summaryrefslogtreecommitdiffstats
path: root/netwerk/protocol/http/Http2Session.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'netwerk/protocol/http/Http2Session.cpp')
-rw-r--r--netwerk/protocol/http/Http2Session.cpp4513
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, &notUsed);
+
+ 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