1
0
mirror of https://github.com/roytam1/UXP.git synced 2026-05-26 23:18:26 +00:00
Files
UXP/netwerk/protocol/http/Http2Session.cpp
T
Moonchild 63a7c7f90e [http] Use a copy of nsHttpConnectionInfo.
The root cause in this bug is that the connection info used by
'SpdyConnectTransaction' is the same instance as the connection info in
'nsHttpTransaction', so we should clone it and let 'SpdyConnectTransaction' use
the cloned one.
2020-11-20 09:31:27 +08:00

4025 lines
132 KiB
C++

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "Http2Session.h"
#include "Http2Stream.h"
#include "Http2Push.h"
#include "mozilla/EndianUtils.h"
#include "mozilla/Telemetry.h"
#include "mozilla/Preferences.h"
#include "nsHttp.h"
#include "nsHttpHandler.h"
#include "nsHttpConnection.h"
#include "nsIRequestContext.h"
#include "nsISSLSocketControl.h"
#include "nsISSLStatus.h"
#include "nsISSLStatusProvider.h"
#include "nsISupportsPriority.h"
#include "nsStandardURL.h"
#include "nsURLHelper.h"
#include "prnetdb.h"
#include "sslt.h"
#include "mozilla/Sprintf.h"
#include "nsSocketTransportService2.h"
#include "nsNetUtil.h"
namespace mozilla {
namespace net {
// Http2Session has multiple inheritance of things that implement
// nsISupports, so this magic is taken from nsHttpPipeline that
// implements some of the same abstract classes.
NS_IMPL_ADDREF(Http2Session)
NS_IMPL_RELEASE(Http2Session)
NS_INTERFACE_MAP_BEGIN(Http2Session)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsAHttpConnection)
NS_INTERFACE_MAP_END
// "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
};
#define RETURN_SESSION_ERROR(o,x) \
do { \
(o)->mGoAwayReason = (x); \
return NS_ERROR_ILLEGAL_VALUE; \
} while (0)
Http2Session::Http2Session(nsISocketTransport *aSocketTransport, uint32_t 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)
, 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)
, mWaitingForSettingsAck(false)
, mGoAwayOnPush(false)
, mUseH2Deps(false)
, mAttemptingEarlyData(attemptingEarlyData)
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
static uint64_t sSerial;
mSerial = ++sSerial;
LOG3(("Http2Session::Http2Session %p serial=0x%X\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();
SendHello();
mLastDataReadEpoch = mLastReadEpoch;
mPingThreshold = gHttpHandler->SpdyPingThreshold();
mPreviousPingThreshold = mPingThreshold;
}
void
Http2Session::Shutdown()
{
for (auto iter = mStreamTransactionHash.Iter(); !iter.Done(); iter.Next()) {
nsAutoPtr<Http2Stream> &stream = iter.Data();
// 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 &&
(stream->StreamID() > mGoAwayID || !stream->HasRegisteredID())) {
CloseStream(stream, NS_ERROR_NET_RESET); // can be restarted
} else if (stream->RecvdData()) {
CloseStream(stream, NS_ERROR_NET_PARTIAL_TRANSFER);
} else if (mGoAwayReason == INADEQUATE_SECURITY) {
CloseStream(stream, NS_ERROR_NET_INADEQUATE_SECURITY);
} else {
CloseStream(stream, NS_ERROR_ABORT);
}
}
}
Http2Session::~Http2Session()
{
LOG3(("Http2Session::~Http2Session %p mDownstreamState=%X",
this, mDownstreamState));
Shutdown();
}
void
Http2Session::LogIO(Http2Session *self, Http2Stream *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));
}
}
typedef nsresult (*Http2ControlFx) (Http2Session *self);
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
};
bool
Http2Session::RoomForMoreConcurrent()
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
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(PR_GetCurrentThread() == gSocketThread);
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) {
LOG3(("Http2Session::ReadTimeoutTick %p handle outstanding ping\n"));
if ((now - mPingSentEpoch) >= gHttpHandler->SpdyPingTimeout()) {
LOG3(("Http2Session::ReadTimeoutTick %p Ping Timer Exhaustion\n", this));
mPingSentEpoch = 0;
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);
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(Http2Stream *stream, uint32_t aNewID)
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
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;
}
LOG3(("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.Get(aNewID)) {
LOG3((" New ID already present\n"));
MOZ_ASSERT(false, "New ID already present in mStreamIDHash");
mShouldGoAway = true;
return kDeadStreamID;
}
mStreamIDHash.Put(aNewID, stream);
return aNewID;
}
bool
Http2Session::AddStream(nsAHttpTransaction *aHttpTransaction,
int32_t aPriority,
bool aUseTunnel,
nsIInterfaceRequestor *aCallbacks)
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
// integrity check
if (mStreamTransactionHash.Get(aHttpTransaction)) {
LOG3((" New transaction already present\n"));
MOZ_ASSERT(false, "AddStream duplicate transaction pointer");
return false;
}
if (!mConnection) {
mConnection = aHttpTransaction->Connection();
}
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);
if (aUseTunnel) {
LOG3(("Http2Session::AddStream session=%p trans=%p OnTunnel",
this, aHttpTransaction));
DispatchOnTunnel(aHttpTransaction, aCallbacks);
return true;
}
Http2Stream *stream = new Http2Stream(aHttpTransaction, this, aPriority);
LOG3(("Http2Session::AddStream session=%p stream=%p serial=%u "
"NextID=0x%X (tentative)", this, stream, mSerial, mNextStreamID));
mStreamTransactionHash.Put(aHttpTransaction, stream);
mReadyForWrite.Push(stream);
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;
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();
}
return true;
}
void
Http2Session::QueueStream(Http2Stream *stream)
{
// will be removed via processpending or a shutdown path
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
MOZ_ASSERT(!stream->CountAsActive());
MOZ_ASSERT(!stream->Queued());
LOG3(("Http2Session::QueueStream %p stream %p queued.", this, stream));
#ifdef DEBUG
int32_t qsize = mQueuedStreams.GetSize();
for (int32_t i = 0; i < qsize; i++) {
Http2Stream *qStream = static_cast<Http2Stream *>(mQueuedStreams.ObjectAt(i));
MOZ_ASSERT(qStream != stream);
MOZ_ASSERT(qStream->Queued());
}
#endif
stream->SetQueued(true);
mQueuedStreams.Push(stream);
}
void
Http2Session::ProcessPending()
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
Http2Stream*stream;
while (RoomForMoreConcurrent() &&
(stream = static_cast<Http2Stream *>(mQueuedStreams.PopFront()))) {
LOG3(("Http2Session::ProcessPending %p stream %p woken from queue.",
this, stream));
MOZ_ASSERT(!stream->CountAsActive());
MOZ_ASSERT(stream->Queued());
stream->SetQueued(false);
mReadyForWrite.Push(stream);
SetWriteCallbacks();
}
}
nsresult
Http2Session::NetworkRead(nsAHttpSegmentWriter *writer, char *buf,
uint32_t count, uint32_t *countWritten)
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
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))
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=%x actual=%d",
this, avail, 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));
mShouldGoAway = true;
if (!mStreamTransactionHash.Count())
Close(NS_OK);
}
uint32_t
Http2Session::SpdyVersion()
{
return HTTP_VERSION_2;
}
uint32_t
Http2Session::GetWriteQueueSize()
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
return mReadyForWrite.GetSize();
}
void
Http2Session::ChangeDownstreamState(enum internalStateType newState)
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
LOG3(("Http2Session::ChangeDownstreamState() %p from %X to %X",
this, mDownstreamState, newState));
mDownstreamState = newState;
}
void
Http2Session::ResetDownstreamState()
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
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(Http2Stream *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 %d\n", this, aStream));
QueueStream(aStream);
return false;
}
LOG3(("Http2Session::TryToActivate %p stream=%p\n", this, aStream));
IncrementConcurrent(aStream);
return true;
}
void
Http2Session::IncrementConcurrent(Http2Stream *stream)
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
MOZ_ASSERT(!stream->StreamID() || (stream->StreamID() & 1),
"Do not activate pushed streams");
nsAHttpTransaction *trans = stream->Transaction();
if (!trans || !trans->IsNullTransaction() || trans->QuerySpdyConnectTransaction()) {
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(Http2Stream *aStream)
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
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(PR_GetCurrentThread() == gSocketThread);
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(PR_GetCurrentThread() == gSocketThread);
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(PR_GetCurrentThread() == gSocketThread);
LOG3(("Http2Session::GeneratePriority %p %X %X\n",
this, aID, aPriorityWeight));
uint32_t frameSize = kFrameHeaderBytes + 5;
char *packet = EnsureOutputBuffer(frameSize);
mOutputQueueUsed += frameSize;
CreateFrameHeader(packet, 5, FRAME_TYPE_PRIORITY, 0, aID);
NetworkEndian::writeUint32(packet + kFrameHeaderBytes, 0);
memcpy(packet + frameSize - 1, &aPriorityWeight, 1);
LogIO(this, nullptr, "Generate Priority", packet, frameSize);
FlushOutputQueue();
}
void
Http2Session::GenerateRstStream(uint32_t aStatusCode, uint32_t aID)
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
// make sure we don't do this twice for the same stream (at least if we
// have a stream entry for it)
Http2Stream *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(PR_GetCurrentThread() == gSocketThread);
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] 5 priority frames for streams which will never be opened with headers
// these streams (3, 5, 7, 9, b) build a dependency tree that all other
// streams will be direct leaves of.
void
Http2Session::SendHello()
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
LOG3(("Http2Session::SendHello %p\n", this));
// sized for magic + 5 settings and a session window update and 5 priority frames
// 24 magic, 33 for settings (9 header + 4 settings @6), 13 for window update,
// 5 priority frames at 14 (9 + 5) each
static const uint32_t maxSettings = 5;
static const uint32_t prioritySize = 5 * (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 (!gHttpHandler->AllowPush()) {
// 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 (gHttpHandler->UseH2Deps() && 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;
}
FlushOutputQueue();
}
void
Http2Session::CreatePriorityNode(uint32_t streamID, uint32_t dependsOn, uint8_t weight,
const char *label)
{
char *packet = mOutputQueueBuffer.get() + mOutputQueueUsed;
CreateFrameHeader(packet, 5, FRAME_TYPE_PRIORITY, 0, streamID);
mOutputQueueUsed += kFrameHeaderBytes + 5;
NetworkEndian::writeUint32(packet + kFrameHeaderBytes, dependsOn); // depends on
packet[kFrameHeaderBytes + 4] = weight; // 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(Http2Stream *aStream, uint32_t aOptionalID = 0)
{
// This is annoying, but at least it is O(1)
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
#ifndef DEBUG
// Only do the real verification in debug builds
return true;
#endif
if (!aStream)
return true;
uint32_t test = 0;
do {
if (aStream->StreamID() == kDeadStreamID)
break;
nsAHttpTransaction *trans = aStream->Transaction();
test++;
if (!trans)
break;
test++;
if (mStreamTransactionHash.Get(trans) != aStream)
break;
if (aStream->StreamID()) {
Http2Stream *idStream = mStreamIDHash.Get(aStream->StreamID());
test++;
if (idStream != aStream)
break;
if (aOptionalID) {
test++;
if (idStream->StreamID() != aOptionalID)
break;
}
}
// tests passed
return true;
} while (0);
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;
}
void
Http2Session::CleanupStream(Http2Stream *aStream, nsresult aResult,
errorType aResetCode)
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
LOG3(("Http2Session::CleanupStream %p %p 0x%X %X\n",
this, aStream, aStream ? aStream->StreamID() : 0, aResult));
if (!aStream) {
return;
}
Http2PushedStream *pushSource = aStream->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));
aStream->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;
pushStream->GetHashKey(hashKey);
nsIRequestContext *requestContext = aStream->RequestContext();
if (requestContext) {
SpdyPushCache *cache = nullptr;
requestContext->GetSpdyPushCache(&cache);
if (cache) {
Http2PushedStream *trash = cache->RemovePushedStreamHttp2(hashKey);
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 Http2Stream and drop the reference to
// its transaction
mStreamTransactionHash.Remove(aStream->Transaction());
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(PR_GetCurrentThread() == gSocketThread);
Http2Stream *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);
}
static void RemoveStreamFromQueue(Http2Stream *aStream, nsDeque &queue)
{
size_t size = queue.GetSize();
for (size_t count = 0; count < size; ++count) {
Http2Stream *stream = static_cast<Http2Stream *>(queue.PopFront());
if (stream != aStream)
queue.Push(stream);
}
}
void
Http2Session::RemoveStreamFromQueues(Http2Stream *aStream)
{
RemoveStreamFromQueue(aStream, mReadyForWrite);
RemoveStreamFromQueue(aStream, mQueuedStreams);
RemoveStreamFromQueue(aStream, mPushesReadyForRead);
RemoveStreamFromQueue(aStream, mSlowConsumersReadyForRead);
}
void
Http2Session::CloseStream(Http2Stream *aStream, nsresult aResult)
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
LOG3(("Http2Session::CloseStream %p %p 0x%x %X\n",
this, aStream, aStream->StreamID(), 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;
}
RemoveStreamFromQueues(aStream);
if (aStream->IsTunnel()) {
UnRegisterTunnel(aStream);
}
// Send the stream the close() indication
aStream->Close(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_SESSION_ERROR(this, 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;
}
self->SetInputFrameDataStream(self->mInputFrameID);
// 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;
nsresult rv;
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_SESSION_ERROR(self, 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_SESSION_ERROR(self, 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 (!endHeadersFlag) { // more are coming - don't process yet
self->ResetDownstreamState();
return NS_OK;
}
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));
// only interpret headers once, afterwards ignore as trailers
if (mInputFrameDataStream->AllHeadersReceived()) {
LOG3(("Http2Session::ResponseHeadersComplete extra headers"));
MOZ_ASSERT(mInputFrameFlags & kFlag_END_STREAM);
nsresult rv = UncompressAndDiscard(false);
if (NS_FAILED(rv)) {
LOG3(("Http2Session::ResponseHeadersComplete extra uncompress 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
// Http2Stream::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->Transaction()->DisableSpdy();
CleanupStream(mInputFrameDataStream, NS_ERROR_NET_RESET, CANCEL_ERROR);
ResetDownstreamState();
return NS_OK;
} else 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_SESSION_ERROR(self, PROTOCOL_ERROR);
}
if (!self->mInputFrameID) {
LOG3(("Http2Session::RecvPriority %p stream ID of 0.\n", self));
RETURN_SESSION_ERROR(self, 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);
if (self->mInputFrameDataStream) {
self->mInputFrameDataStream->SetPriorityDependency(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_SESSION_ERROR(self, PROTOCOL_ERROR);
}
if (!self->mInputFrameID) {
LOG3(("Http2Session::RecvRstStream %p stream ID of 0.\n", self));
RETURN_SESSION_ERROR(self, 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));
self->SetInputFrameDataStream(self->mInputFrameID);
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_SESSION_ERROR(self, 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_SESSION_ERROR(self, PROTOCOL_ERROR);
}
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"));
RETURN_SESSION_ERROR(self, 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;
self->ProcessPending();
break;
case SETTINGS_TYPE_INITIAL_WINDOW:
{
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 (auto iter = self->mStreamTransactionHash.Iter();
!iter.Done();
iter.Next()) {
iter.Data()->UpdateServerReceiveWindow(delta);
}
}
break;
case SETTINGS_TYPE_MAX_FRAME_SIZE:
{
if ((value < kMaxFrameData) || (value >= 0x01000000)) {
LOG3(("Received invalid max frame size 0x%X", value));
RETURN_SESSION_ERROR(self, PROTOCOL_ERROR);
}
// We stick to the default for simplicity's sake, so nothing to change
}
break;
default:
break;
}
}
self->ResetDownstreamState();
if (!(self->mInputFrameFlags & kFlag_ACK)) {
self->GenerateSettingsAck();
} else if (self->mWaitingForSettingsAck) {
self->mGoAwayOnPush = true;
}
return NS_OK;
}
nsresult
Http2Session::RecvPushPromise(Http2Session *self)
{
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_SESSION_ERROR(self, 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_SESSION_ERROR(self, 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_SESSION_ERROR(self, PROTOCOL_ERROR);
}
// confirm associated-to
nsresult rv = self->SetInputFrameDataStream(associatedID);
if (NS_FAILED(rv))
return rv;
Http2Stream *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 (!gHttpHandler->AllowPush()) {
// 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_SESSION_ERROR(self, 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 {
nsIRequestContext *requestContext = associatedStream->RequestContext();
if (requestContext) {
requestContext->GetSpdyPushCache(&cache);
if (!cache) {
cache = new SpdyPushCache();
if (!cache || NS_FAILED(requestContext->SetSpdyPushCache(cache))) {
delete cache;
cache = nullptr;
}
}
}
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->mInputFrameFlags & kFlag_END_PUSH_PROMISE)) {
LOG3(("Http2Session::RecvPushPromise not finishing processing for multi-frame push\n"));
self->ResetDownstreamState();
return NS_OK;
}
// Create the buffering transaction and push stream
RefPtr<Http2PushTransactionBuffer> transactionBuffer =
new Http2PushTransactionBuffer();
transactionBuffer->SetConnection(self);
Http2PushedStream *pushedStream =
new Http2PushedStream(transactionBuffer, self, associatedStream, promisedID);
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);
delete pushedStream;
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);
delete pushedStream;
self->ResetDownstreamState();
return NS_OK;
} else 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.
self->mStreamTransactionHash.Put(transactionBuffer, pushedStream);
self->mPushedStreams.AppendElement(pushedStream);
if (self->RegisterStreamID(pushedStream, 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;
pushedStream->ReadSegments(nullptr, 1, &notUsed);
nsAutoCString key;
if (!pushedStream->GetHashKey(key)) {
LOG3(("Http2Session::RecvPushPromise one of :authority :scheme :path missing from push\n"));
self->CleanupStream(pushedStream, NS_ERROR_FAILURE, PROTOCOL_ERROR);
self->ResetDownstreamState();
return NS_OK;
}
RefPtr<nsStandardURL> associatedURL, pushedURL;
rv = Http2Stream::MakeOriginURL(associatedStream->Origin(), associatedURL);
if (NS_SUCCEEDED(rv)) {
rv = Http2Stream::MakeOriginURL(pushedStream->Origin(), pushedURL);
}
LOG3(("Http2Session::RecvPushPromise %p checking %s == %s", self,
associatedStream->Origin().get(), pushedStream->Origin().get()));
bool match = false;
if (NS_SUCCEEDED(rv)) {
rv = associatedURL->Equals(pushedURL, &match);
}
if (NS_FAILED(rv)) {
// Fallback to string equality of origins. This won't be guaranteed to be as
// liberal as we want it to be, but it will at least be safe
match = associatedStream->Origin().Equals(pushedStream->Origin());
}
if (!match) {
LOG3(("Http2Session::RecvPushPromise %p pushed stream mismatched origin "
"associated origin %s .. pushed origin %s\n", self,
associatedStream->Origin().get(), pushedStream->Origin().get()));
self->CleanupStream(pushedStream, NS_ERROR_FAILURE, REFUSED_STREAM_ERROR);
self->ResetDownstreamState();
return NS_OK;
}
if (pushedStream->TryOnPush()) {
LOG3(("Http2Session::RecvPushPromise %p channel implements nsIHttpPushListener "
"stream %p will not be placed into session cache.\n", self, pushedStream));
} else {
LOG3(("Http2Session::RecvPushPromise %p place stream into session cache\n", self));
if (!cache->RegisterPushedStreamHttp2(key, pushedStream)) {
LOG3(("Http2Session::RecvPushPromise registerPushedStream Failed\n"));
self->CleanupStream(pushedStream, NS_ERROR_FAILURE, INTERNAL_ERROR);
self->ResetDownstreamState();
return NS_OK;
}
}
pushedStream->SetHTTPState(Http2Stream::RESERVED_BY_REMOTE);
static_assert(Http2Stream::kWorstPriority >= 0,
"kWorstPriority out of range");
uint8_t priorityWeight = (nsISupportsPriority::PRIORITY_LOWEST + 1) -
(Http2Stream::kWorstPriority - Http2Stream::kNormalPriority);
pushedStream->SetPriority(Http2Stream::kWorstPriority);
self->GeneratePriority(promisedID, 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_SESSION_ERROR(self, FRAME_SIZE_ERROR);
}
if (self->mInputFrameID) {
LOG3(("Http2Session::RecvPing %p PING needs stream ID of 0. 0x%X\n",
self, self->mInputFrameID));
RETURN_SESSION_ERROR(self, PROTOCOL_ERROR);
}
if (self->mInputFrameFlags & kFlag_ACK) {
// presumably a reply to our timeout ping.. don't reply to it
self->mPingSentEpoch = 0;
} else {
// reply with a ack'd ping
self->GeneratePing(true);
}
self->ResetDownstreamState();
return NS_OK;
}
nsresult
Http2Session::RecvGoAway(Http2Session *self)
{
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_SESSION_ERROR(self, PROTOCOL_ERROR);
}
if (self->mInputFrameID) {
LOG3(("Http2Session::RecvGoAway %p GOAWAY had non zero stream ID 0x%X\n",
self, self->mInputFrameID));
RETURN_SESSION_ERROR(self, PROTOCOL_ERROR);
}
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 (auto iter = self->mStreamTransactionHash.Iter();
!iter.Done();
iter.Next()) {
// 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
nsAutoPtr<Http2Stream>& stream = iter.Data();
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) {
Http2Stream *stream =
static_cast<Http2Stream *>(self->mGoAwayStreamsToRestart.PopFront());
if (self->mPeerGoAwayReason == HTTP_1_1_REQUIRED) {
stream->Transaction()->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.
size = self->mQueuedStreams.GetSize();
for (size_t count = 0; count < size; ++count) {
Http2Stream *stream =
static_cast<Http2Stream *>(self->mQueuedStreams.PopFront());
MOZ_ASSERT(stream->Queued());
stream->SetQueued(false);
if (self->mPeerGoAwayReason == HTTP_1_1_REQUIRED) {
stream->Transaction()->DisableSpdy();
}
self->CloseStream(stream, NS_ERROR_NET_RESET);
self->mStreamTransactionHash.Remove(stream->Transaction());
}
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(self->mInputFrameType == FRAME_TYPE_WINDOW_UPDATE);
if (self->mInputFrameDataSize != 4) {
LOG3(("Http2Session::RecvWindowUpdate %p Window Update wrong length %d\n",
self, self->mInputFrameDataSize));
RETURN_SESSION_ERROR(self, 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 resest 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 "
"%d increased by %d now %d.\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_SESSION_ERROR(self, 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_SESSION_ERROR(self, FLOW_CONTROL_ERROR);
}
if ((oldRemoteWindow <= 0) && (self->mServerSessionWindow > 0)) {
LOG3(("Http2Session::RecvWindowUpdate %p restart session window\n",
self));
for (auto iter = self->mStreamTransactionHash.Iter();
!iter.Done();
iter.Next()) {
MOZ_ASSERT(self->mServerSessionWindow > 0);
nsAutoPtr<Http2Stream>& stream = iter.Data();
if (!stream->BlockedOnRwin() || stream->ServerReceiveWindow() <= 0) {
continue;
}
self->mReadyForWrite.Push(stream);
self->SetWriteCallbacks();
}
}
LOG3(("Http2Session::RecvWindowUpdate %p session window "
"%d increased by %d now %d.\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));
self->SetInputFrameDataStream(self->mInputFrameID);
if (!self->mInputFrameDataStream) {
LOG3(("Http2Session::RecvContination stream ID 0x%X not found.",
self->mInputFrameID));
RETURN_SESSION_ERROR(self, 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,
nsIInterfaceRequestor *callbacks)
: mHeader(header)
, mOrigin(aOrigin)
, mCI(aCI)
, mCallbacks(callbacks)
{
}
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);
AltSvcMapping::ProcessHeader(mHeader, originScheme, originHost, originPort,
mCI->GetUsername(), mCI->GetPrivate(), mCallbacks,
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<nsISupports> securityInfo;
self->mConnection->GetSecurityInfo(getter_AddRefs(securityInfo));
nsCOMPtr<nsISSLSocketControl> ssl = do_QueryInterface(securityInfo);
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 (origin.EqualsIgnoreCase("https://", 8)) {
specifiedOriginHost.Assign(origin.get() + 8, origin.Length() - 8);
} else if (origin.EqualsIgnoreCase("http://", 7)) {
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;
}
}
nsCOMPtr<nsISupports> callbacks;
self->mConnection->GetSecurityInfo(getter_AddRefs(callbacks));
nsCOMPtr<nsIInterfaceRequestor> irCallbacks = do_QueryInterface(callbacks);
RefPtr<UpdateAltSvcEvent> event =
new UpdateAltSvcEvent(altSvcFieldValue, origin, ci, irCallbacks);
NS_DispatchToMainThread(event);
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(PR_GetCurrentThread() == gSocketThread);
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:
{
Http2Stream *target = mStreamIDHash.Get(1);
if (!target) {
// any transaction will do if we can't find the low numbered one
// generally this happens when the initial transaction hasn't been
// assigned a stream id yet.
auto iter = mStreamTransactionHash.Iter();
if (!iter.Done()) {
target = iter.Data();
}
}
nsAHttpTransaction *transaction = target ? target->Transaction() : nullptr;
if (transaction)
transaction->OnTransportStatus(aTransport, aStatus, aProgress);
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 Http2Stream::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 Http2Stream 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(PR_GetCurrentThread() == gSocketThread);
MOZ_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 %x",
this, NS_ERROR_NET_INADEQUATE_SECURITY));
rv = NS_ERROR_NET_INADEQUATE_SECURITY;
}
return rv;
}
if (reader)
mSegmentReader = reader;
*countRead = 0;
LOG3(("Http2Session::ReadSegments %p", this));
Http2Stream *stream = static_cast<Http2Stream *>(mReadyForWrite.PopFront());
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;
}
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 Http2Stream %p 0x%X",
this, stream, stream->StreamID()));
FlushOutputQueue();
SetWriteCallbacks();
// We can still send our preamble
*countRead = mOutputQueueUsed - mOutputQueueSent;
return *countRead ? NS_OK : NS_BASE_STREAM_WOULD_BLOCK;
}
if (!m0RTTStreams.Contains(stream->StreamID())) {
m0RTTStreams.AppendElement(stream->StreamID());
}
// 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 Http2Stream %p 0x%X "
"block-input=%d block-output=%d\n", this, stream, 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;
}
// 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
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 %X",
this, 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, *countRead));
mReadyForWrite.Push(stream);
SetWriteCallbacks();
return rv;
}
if (stream->BlockedOnRwin()) {
LOG3(("Http2Session %p will stream %p 0x%X suspended for flow control\n",
this, stream, stream->StreamID()));
return NS_BASE_STREAM_WOULD_BLOCK;
}
LOG3(("Http2Session::ReadSegments %p stream=%p stream send complete",
this, stream));
// 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);
mLastDataReadEpoch = mLastReadEpoch;
if (!mInputFrameID) {
LOG3(("Http2Session::ReadyToProcessDataFrame %p data frame stream 0\n",
this));
RETURN_SESSION_ERROR(this, 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);
}
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(PR_GetCurrentThread() == gSocketThread);
LOG3(("Http2Session::WriteSegments %p InternalState %X\n",
this, mDownstreamState));
*countWritten = 0;
if (mClosed)
return NS_ERROR_FAILURE;
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.
Http2Stream *pushConnectedStream =
static_cast<Http2Stream *>(mPushesReadyForRead.PopFront());
if (pushConnectedStream) {
return ProcessConnectedPush(pushConnectedStream, writer, count, countWritten);
}
// feed gecko channels that previously stopped consuming data
// only take data from stored buffers
Http2Stream *slowConsumer =
static_cast<Http2Stream *>(mSlowConsumersReadyForRead.PopFront());
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 %x\n",
this, 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_SESSION_ERROR(this, 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 = 0;
}
mPaddingLength = 0;
LOG3(("Http2Session::WriteSegments[%p::%x] 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_SESSION_ERROR(this, 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_SESSION_ERROR(this, PROTOCOL_ERROR);
}
if (mDownstreamState == BUFFERING_OPENING_SETTINGS &&
mInputFrameType != FRAME_TYPE_SETTINGS) {
LOG3(("First Frame Type Must Be Settings\n"));
RETURN_SESSION_ERROR(this, 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 %x\n",
this, 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_SESSION_ERROR(this, PROTOCOL_ERROR);
} else 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->Transaction()->ReuseConnectionOnRestartOK(true);
} else if (mDownstreamRstReason == HTTP_1_1_REQUIRED) {
streamCleanupCode = NS_ERROR_NET_RESET;
mInputFrameDataStream->Transaction()->ReuseConnectionOnRestartOK(true);
mInputFrameDataStream->Transaction()->DisableSpdy();
} else {
streamCleanupCode = mInputFrameDataStream->RecvdData() ?
NS_ERROR_NET_PARTIAL_TRANSFER :
NS_ERROR_NET_INTERRUPT;
}
if (mDownstreamRstReason == COMPRESSION_ERROR) {
mShouldGoAway = true;
}
// mInputFrameDataStream is reset by ChangeDownstreamState
Http2Stream *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 */
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 %x\n",
this, streamID, mNeedsCleanup, rv));
MOZ_ASSERT(!mNeedsCleanup || mNeedsCleanup->StreamID() == streamID);
CleanupStream(streamID, NS_OK, CANCEL_ERROR);
mNeedsCleanup = nullptr;
*again = false;
ResumeRecv();
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 %x\n", this, 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 data",
this, discardCount));
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();
ResumeRecv();
return NS_BASE_STREAM_WOULD_BLOCK;
}
rv = NetworkRead(writer, trash, discardCount, countWritten);
if (NS_FAILED(rv)) {
LOG3(("Http2Session %p discard frame read failure %x\n", this, 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) {
Http2Stream *streamToCleanup = nullptr;
if (mInputFrameFinal) {
streamToCleanup = mInputFrameDataStream;
}
ResetDownstreamState();
if (streamToCleanup) {
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 %x\n",
this, 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) {
// Instead of passing (aRestart, aAlpnChanged) here, we use aAlpnChanged for
// both arguments because as long as the alpn token stayed the same, we can
// just reuse what we have in our buffer to send instead of having to have
// the transaction rewind and read it all over again. We only need to rewind
// the transaction if we're switching to a new protocol, because our buffer
// won't get used in that case.
Http2Stream *stream = mStreamIDHash.Get(m0RTTStreams[i]);
if (stream) {
stream->Finish0RTT(aAlpnChanged, 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;
}
} else {
// 0RTT succeeded
// Make sure we look for any incoming data in repsonse to our early data.
ResumeRecv();
}
mAttemptingEarlyData = false;
m0RTTStreams.Clear();
RealignOutputQueue();
return NS_OK;
}
nsresult
Http2Session::ProcessConnectedPush(Http2Stream *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.
if (NS_SUCCEEDED(rv) && !*countWritten &&
pushConnectedStream->PushSource() &&
pushConnectedStream->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;
ResumeRecv();
}
return rv;
}
nsresult
Http2Session::ProcessSlowConsumer(Http2Stream *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 %X %d\n",
this, slowConsumer->StreamID(), 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(Http2Stream *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=%llu localWindow=%lld\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=%lld\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(Http2Stream *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(PR_GetCurrentThread() == gSocketThread);
if (mClosed)
return;
LOG3(("Http2Session::Close %p %X", this, aReason));
mClosed = true;
Shutdown();
mStreamIDHash.Clear();
mStreamTransactionHash.Clear();
uint32_t goAwayReason;
if (mGoAwayReason != NO_HTTP_ERROR) {
goAwayReason = mGoAwayReason;
} else if (NS_SUCCEEDED(aReason)) {
goAwayReason = NO_HTTP_ERROR;
} else if (aReason == NS_ERROR_ILLEGAL_VALUE) {
goAwayReason = PROTOCOL_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(PR_GetCurrentThread() == gSocketThread);
LOG3(("Http2Session::CloseTransaction %p %p %x", this, aTransaction, aResult));
// Generally this arrives as a cancel event from the connection manager.
// need to find the stream and call CleanupStream() on it.
Http2Stream *stream = mStreamTransactionHash.Get(aTransaction);
if (!stream) {
LOG3(("Http2Session::CloseTransaction %p %p %x - not found.",
this, aTransaction, aResult));
return;
}
LOG3(("Http2Session::CloseTransaction probably a cancel. "
"this=%p, trans=%p, result=%x, streamID=0x%X stream=%p",
this, aTransaction, aResult, stream->StreamID(), stream));
CleanupStream(stream, aResult, CANCEL_ERROR);
ResumeRecv();
}
//-----------------------------------------------------------------------------
// nsAHttpSegmentReader
//-----------------------------------------------------------------------------
nsresult
Http2Session::OnReadSegment(const char *buf,
uint32_t count, uint32_t *countRead)
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
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 && !mAttemptingEarlyData)
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)
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(PR_GetCurrentThread() == gSocketThread);
nsresult rv;
if (!mSegmentWriter) {
// the only way this could happen would be if Close() were called on the
// stack with WriteSegments()
return NS_ERROR_FAILURE;
}
if (mDownstreamState == NOT_USING_NETWORK ||
mDownstreamState == BUFFERING_FRAME_HEADER ||
mDownstreamState == DISCARDING_DATA_FRAME_PADDING) {
return NS_BASE_STREAM_WOULD_BLOCK;
}
if (mDownstreamState == PROCESSING_DATA_FRAME) {
if (mInputFrameFinal &&
mInputFrameDataRead == mInputFrameDataSize) {
*countWritten = 0;
SetNeedsCleanup();
return NS_BASE_STREAM_CLOSED;
}
count = std::min(count, mInputFrameDataSize - mInputFrameDataRead);
rv = NetworkRead(mSegmentWriter, buf, count, countWritten);
if (NS_FAILED(rv))
return rv;
LogIO(this, mInputFrameDataStream, "Reading Data Frame",
buf, *countWritten);
mInputFrameDataRead += *countWritten;
if (mPaddingLength && (mInputFrameDataSize - mInputFrameDataRead <= mPaddingLength)) {
// We are crossing from real HTTP data into the realm of padding. If
// we've actually crossed the line, we need to munge countWritten for the
// sake of goodness and sanity. No matter what, any future calls to
// WriteSegments need to just discard data until we reach the end of this
// frame.
if (mInputFrameDataSize != mInputFrameDataRead) {
// Only change state if we still have padding to read. If we don't do
// this, we can end up hanging on frames that combine real data,
// padding, and END_STREAM (see bug 1019921)
ChangeDownstreamState(DISCARDING_DATA_FRAME_PADDING);
}
uint32_t paddingRead = mPaddingLength - (mInputFrameDataSize - mInputFrameDataRead);
LOG3(("Http2Session::OnWriteSegment %p stream 0x%X len=%d read=%d "
"crossed from HTTP data into padding (%d of %d) countWritten=%d",
this, mInputFrameID, mInputFrameDataSize, mInputFrameDataRead,
paddingRead, mPaddingLength, *countWritten));
*countWritten -= paddingRead;
LOG3(("Http2Session::OnWriteSegment %p stream 0x%X new countWritten=%d",
this, mInputFrameID, *countWritten));
}
mInputFrameDataStream->UpdateTransportReadEvents(*countWritten);
if ((mInputFrameDataRead == mInputFrameDataSize) && !mInputFrameFinal)
ResetDownstreamState();
return rv;
}
if (mDownstreamState == PROCESSING_COMPLETE_HEADERS) {
if (mFlatHTTPResponseHeaders.Length() == mFlatHTTPResponseHeadersOut &&
mInputFrameFinal) {
*countWritten = 0;
SetNeedsCleanup();
return NS_BASE_STREAM_CLOSED;
}
count = std::min(count,
mFlatHTTPResponseHeaders.Length() -
mFlatHTTPResponseHeadersOut);
memcpy(buf,
mFlatHTTPResponseHeaders.get() + mFlatHTTPResponseHeadersOut,
count);
mFlatHTTPResponseHeadersOut += count;
*countWritten = count;
if (mFlatHTTPResponseHeaders.Length() == mFlatHTTPResponseHeadersOut) {
if (!mInputFrameFinal) {
// 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(Http2Stream *stream)
{
mPushesReadyForRead.Push(stream);
ForceRecv();
}
void
Http2Session::ConnectSlowConsumer(Http2Stream *stream)
{
LOG3(("Http2Session::ConnectSlowConsumer %p 0x%X\n",
this, stream->StreamID()));
mSlowConsumersReadyForRead.Push(stream);
ForceRecv();
}
uint32_t
Http2Session::FindTunnelCount(nsHttpConnectionInfo *aConnInfo)
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
uint32_t rv = 0;
mTunnelHash.Get(aConnInfo->HashKey(), &rv);
return rv;
}
void
Http2Session::RegisterTunnel(Http2Stream *aTunnel)
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
nsHttpConnectionInfo *ci = aTunnel->Transaction()->ConnectionInfo();
uint32_t newcount = FindTunnelCount(ci) + 1;
mTunnelHash.Remove(ci->HashKey());
mTunnelHash.Put(ci->HashKey(), newcount);
LOG3(("Http2Stream::RegisterTunnel %p stream=%p tunnels=%d [%s]",
this, aTunnel, newcount, ci->HashKey().get()));
}
void
Http2Session::UnRegisterTunnel(Http2Stream *aTunnel)
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
nsHttpConnectionInfo *ci = aTunnel->Transaction()->ConnectionInfo();
MOZ_ASSERT(FindTunnelCount(ci));
uint32_t newcount = FindTunnelCount(ci) - 1;
mTunnelHash.Remove(ci->HashKey());
if (newcount) {
mTunnelHash.Put(ci->HashKey(), newcount);
}
LOG3(("Http2Session::UnRegisterTunnel %p stream=%p tunnels=%d [%s]",
this, aTunnel, newcount, ci->HashKey().get()));
}
void
Http2Session::CreateTunnel(nsHttpTransaction* trans,
nsHttpConnectionInfo* ci,
nsIInterfaceRequestor* aCallbacks)
{
LOG(("Http2Session::CreateTunnel %p %p make new tunnel\n", this, trans));
// The connect transaction will hold onto the underlying http
// transaction so that an auth created by the connect can be mappped
// to the correct security callbacks
RefPtr<nsHttpConnectionInfo> clone(ci->Clone());
RefPtr<SpdyConnectTransaction> connectTrans =
new SpdyConnectTransaction(clone, aCallbacks, trans->Caps(), trans, this);
AddStream(connectTrans, nsISupportsPriority::PRIORITY_NORMAL, false, nullptr);
Http2Stream *tunnel = mStreamTransactionHash.Get(connectTrans);
MOZ_ASSERT(tunnel);
RegisterTunnel(tunnel);
}
void
Http2Session::DispatchOnTunnel(nsAHttpTransaction *aHttpTransaction,
nsIInterfaceRequestor *aCallbacks)
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
nsHttpTransaction *trans = aHttpTransaction->QueryHttpTransaction();
nsHttpConnectionInfo *ci = aHttpTransaction->ConnectionInfo();
MOZ_ASSERT(trans);
LOG3(("Http2Session::DispatchOnTunnel %p trans=%p", this, trans));
aHttpTransaction->SetConnection(nullptr);
// this transaction has done its work of setting up a tunnel, let
// the connection manager queue it if necessary
trans->SetTunnelProvider(this);
trans->EnableKeepAlive();
if (FindTunnelCount(ci) < gHttpHandler->MaxConnectionsPerOrigin()) {
LOG3(("Http2Session::DispatchOnTunnel %p create on new tunnel %s",
this, ci->HashKey().get()));
CreateTunnel(trans, ci, aCallbacks);
} else {
// requeue it. The connection manager is responsible for actually putting
// this on the tunnel connection with the specific ci. If that can't
// happen the cmgr checks with us via MaybeReTunnel() to see if it should
// make a new tunnel or just wait longer.
LOG3(("Http2Session::DispatchOnTunnel %p trans=%p queue in connection manager",
this, trans));
gHttpHandler->InitiateTransaction(trans, trans->Priority());
}
}
// From ASpdySession
bool
Http2Session::MaybeReTunnel(nsAHttpTransaction *aHttpTransaction)
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
nsHttpTransaction *trans = aHttpTransaction->QueryHttpTransaction();
LOG(("Http2Session::MaybeReTunnel %p trans=%p\n", this, trans));
if (!trans || trans->TunnelProvider() != this) {
// this isn't really one of our transactions.
return false;
}
if (mClosed || mShouldGoAway) {
LOG(("Http2Session::MaybeReTunnel %p %p session closed - requeue\n", this, trans));
trans->SetTunnelProvider(nullptr);
gHttpHandler->InitiateTransaction(trans, trans->Priority());
return true;
}
nsHttpConnectionInfo *ci = aHttpTransaction->ConnectionInfo();
LOG(("Http2Session:MaybeReTunnel %p %p count=%d limit %d\n",
this, trans, FindTunnelCount(ci), gHttpHandler->MaxConnectionsPerOrigin()));
if (FindTunnelCount(ci) >= gHttpHandler->MaxConnectionsPerOrigin()) {
// patience - a tunnel will open up.
return false;
}
LOG(("Http2Session::MaybeReTunnel %p %p make new tunnel\n", this, trans));
CreateTunnel(trans, ci, trans->SecurityCallbacks());
return true;
}
nsresult
Http2Session::BufferOutput(const char *buf,
uint32_t count,
uint32_t *countRead)
{
nsAHttpSegmentReader *old = mSegmentReader;
mSegmentReader = nullptr;
nsresult rv = OnReadSegment(buf, count, countRead);
mSegmentReader = old;
return rv;
}
bool // static
Http2Session::ALPNCallback(nsISupports *securityInfo)
{
if (!gHttpHandler->IsH2MandatorySuiteEnabled()) {
LOG3(("Http2Session::ALPNCallback Mandatory Cipher Suite Unavailable\n"));
return false;
}
nsCOMPtr<nsISSLSocketControl> ssl = do_QueryInterface(securityInfo);
LOG3(("Http2Session::ALPNCallback sslsocketcontrol=%p\n", ssl.get()));
if (ssl) {
int16_t version = ssl->GetSSLVersionOffered();
LOG3(("Http2Session::ALPNCallback version=%x\n", version));
if (version >= nsISSLSocketControl::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 (!gHttpHandler->EnforceHttp2TlsProfile()) {
LOG3(("Http2Session::ConfirmTLSProfile %p passed due to configuration bypass\n", this));
mTLSProfileConfirmed = true;
return NS_OK;
}
if (!mConnection)
return NS_ERROR_FAILURE;
nsCOMPtr<nsISupports> securityInfo;
mConnection->GetSecurityInfo(getter_AddRefs(securityInfo));
nsCOMPtr<nsISSLSocketControl> ssl = do_QueryInterface(securityInfo);
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 < nsISSLSocketControl::TLS_VERSION_1_2) {
LOG3(("Http2Session::ConfirmTLSProfile %p FAILED due to lack of TLS1.2\n", this));
RETURN_SESSION_ERROR(this, INADEQUATE_SECURITY);
}
uint16_t kea = ssl->GetKEAUsed();
if (kea != ssl_kea_dh && kea != ssl_kea_ecdh) {
LOG3(("Http2Session::ConfirmTLSProfile %p FAILED due to invalid KEA %d\n",
this, kea));
RETURN_SESSION_ERROR(this, 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_SESSION_ERROR(this, INADEQUATE_SECURITY);
} else 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_SESSION_ERROR(this, INADEQUATE_SECURITY);
}
int16_t macAlgorithm = ssl->GetMACAlgorithmUsed();
LOG3(("Http2Session::ConfirmTLSProfile %p MAC Algortihm (aead==6) %d\n",
this, macAlgorithm));
if (macAlgorithm != nsISSLSocketControl::SSL_MAC_AEAD) {
LOG3(("Http2Session::ConfirmTLSProfile %p FAILED due to lack of AEAD\n", this));
RETURN_SESSION_ERROR(this, 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(PR_GetCurrentThread() == gSocketThread);
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.
Http2Stream *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) {
mReadyForWrite.Push(stream);
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.
ForceSend();
}
void
Http2Session::TransactionHasDataToRecv(nsAHttpTransaction *caller)
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
LOG3(("Http2Session::TransactionHasDataToRecv %p trans=%p", this, caller));
// a signal from the http transaction to the connection that it will consume more
Http2Stream *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()));
ConnectSlowConsumer(stream);
}
void
Http2Session::TransactionHasDataToWrite(Http2Stream *stream)
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
LOG3(("Http2Session::TransactionHasDataToWrite %p stream=%p ID=0x%x",
this, stream, stream->StreamID()));
mReadyForWrite.Push(stream);
SetWriteCallbacks();
ForceSend();
}
bool
Http2Session::IsPersistent()
{
return true;
}
nsresult
Http2Session::TakeTransport(nsISocketTransport **,
nsIAsyncInputStream **, nsIAsyncOutputStream **)
{
MOZ_ASSERT(false, "TakeTransport of Http2Session");
return NS_ERROR_UNEXPECTED;
}
already_AddRefed<nsHttpConnection>
Http2Session::TakeHttpConnection()
{
MOZ_ASSERT(false, "TakeHttpConnection of Http2Session");
return nullptr;
}
uint32_t
Http2Session::CancelPipeline(nsresult reason)
{
// we don't pipeline inside http/2, so this isn't an issue
return 0;
}
nsAHttpTransaction::Classifier
Http2Session::Classification()
{
if (!mConnection)
return nsAHttpTransaction::CLASS_GENERAL;
return mConnection->Classification();
}
void
Http2Session::GetSecurityCallbacks(nsIInterfaceRequestor **aOut)
{
*aOut = nullptr;
}
//-----------------------------------------------------------------------------
// 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::SetConnection(nsAHttpConnection *)
{
// This is unexpected
MOZ_ASSERT(false, "Http2Session::SetConnection()");
}
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;
}
void
Http2Session::SetDNSWasRefreshed()
{
MOZ_ASSERT(false, "Http2Session::SetDNSWasRefreshed()");
}
uint64_t
Http2Session::Available()
{
MOZ_ASSERT(false, "Http2Session::Available()");
return 0;
}
nsHttpRequestHead *
Http2Session::RequestHead()
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
MOZ_ASSERT(false,
"Http2Session::RequestHead() "
"should not be called after http/2 is setup");
return NULL;
}
uint32_t
Http2Session::Http1xTransactionCount()
{
return 0;
}
nsresult
Http2Session::TakeSubTransactions(
nsTArray<RefPtr<nsAHttpTransaction> > &outTransactions)
{
// 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;
}
nsresult
Http2Session::AddTransaction(nsAHttpTransaction *)
{
// This API is meant for pipelining, Http2Session's should be
// extended with AddStream()
MOZ_ASSERT(false,
"Http2Session::AddTransaction() should not be called");
return NS_ERROR_NOT_IMPLEMENTED;
}
uint32_t
Http2Session::PipelineDepth()
{
return IsDone() ? 0 : 1;
}
nsresult
Http2Session::SetPipelinePosition(int32_t position)
{
// This API is meant for pipelining, Http2Session's should be
// extended with AddStream()
MOZ_ASSERT(false,
"Http2Session::SetPipelinePosition() should not be called");
return NS_ERROR_NOT_IMPLEMENTED;
}
int32_t
Http2Session::PipelinePosition()
{
return 0;
}
//-----------------------------------------------------------------------------
// Pass through methods of nsAHttpConnection
//-----------------------------------------------------------------------------
nsAHttpConnection *
Http2Session::Connection()
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
return mConnection;
}
nsresult
Http2Session::OnHeadersAvailable(nsAHttpTransaction *transaction,
nsHttpRequestHead *requestHead,
nsHttpResponseHead *responseHead, bool *reset)
{
return mConnection->OnHeadersAvailable(transaction,
requestHead,
responseHead,
reset);
}
bool
Http2Session::IsReused()
{
return mConnection->IsReused();
}
nsresult
Http2Session::PushBack(const char *buf, uint32_t len)
{
return mConnection->PushBack(buf, len);
}
void
Http2Session::SendPing()
{
MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
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();
}
GeneratePing(false);
ResumeRecv();
}
} // namespace net
} // namespace mozilla