зеркало из https://github.com/mozilla/gecko-dev.git
1365 строки
46 KiB
C++
1365 строки
46 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set sw=2 ts=8 et tw=80 : */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
// HttpLog.h should generally be included first
|
|
#include "HttpLog.h"
|
|
|
|
// Log on level :5, instead of default :4.
|
|
#undef LOG
|
|
#define LOG(args) LOG5(args)
|
|
#undef LOG_ENABLED
|
|
#define LOG_ENABLED() LOG5_ENABLED()
|
|
|
|
#include <algorithm>
|
|
|
|
#include "Http2Compression.h"
|
|
#include "Http2Session.h"
|
|
#include "Http2StreamBase.h"
|
|
#include "Http2Stream.h"
|
|
|
|
#include "mozilla/BasePrincipal.h"
|
|
#include "mozilla/StaticPrefs_network.h"
|
|
#include "mozilla/Telemetry.h"
|
|
#include "nsAlgorithm.h"
|
|
#include "nsHttp.h"
|
|
#include "nsHttpHandler.h"
|
|
#include "nsHttpRequestHead.h"
|
|
#include "nsIClassOfService.h"
|
|
#include "prnetdb.h"
|
|
|
|
namespace mozilla::net {
|
|
|
|
Http2StreamBase::Http2StreamBase(uint64_t aTransactionBrowserId,
|
|
Http2Session* session, int32_t priority,
|
|
uint64_t currentBrowserId)
|
|
: mSession(
|
|
do_GetWeakReference(static_cast<nsISupportsWeakReference*>(session))),
|
|
mRequestHeadersDone(0),
|
|
mOpenGenerated(0),
|
|
mAllHeadersReceived(0),
|
|
mQueued(0),
|
|
mSocketTransport(session->SocketTransport()),
|
|
mCurrentBrowserId(currentBrowserId),
|
|
mTransactionBrowserId(aTransactionBrowserId),
|
|
mTxInlineFrameSize(Http2Session::kDefaultBufferSize),
|
|
mChunkSize(session->SendingChunkSize()),
|
|
mRequestBlockedOnRead(0),
|
|
mRecvdFin(0),
|
|
mReceivedData(0),
|
|
mRecvdReset(0),
|
|
mSentReset(0),
|
|
mCountAsActive(0),
|
|
mSentFin(0),
|
|
mSentWaitingFor(0),
|
|
mSetTCPSocketBuffer(0),
|
|
mBypassInputBuffer(0) {
|
|
MOZ_ASSERT(OnSocketThread(), "not on socket thread");
|
|
|
|
LOG1(("Http2StreamBase::Http2StreamBase %p", this));
|
|
|
|
mServerReceiveWindow = session->GetServerInitialStreamWindow();
|
|
mClientReceiveWindow = session->PushAllowance();
|
|
|
|
mTxInlineFrame = MakeUnique<uint8_t[]>(mTxInlineFrameSize);
|
|
|
|
static_assert(nsISupportsPriority::PRIORITY_LOWEST <= kNormalPriority,
|
|
"Lowest Priority should be less than kNormalPriority");
|
|
|
|
// values of priority closer to 0 are higher priority for the priority
|
|
// argument. This value is used as a group, which maps to a
|
|
// weight that is related to the nsISupportsPriority that we are given.
|
|
int32_t httpPriority;
|
|
if (priority >= nsISupportsPriority::PRIORITY_LOWEST) {
|
|
httpPriority = kWorstPriority;
|
|
} else if (priority <= nsISupportsPriority::PRIORITY_HIGHEST) {
|
|
httpPriority = kBestPriority;
|
|
} else {
|
|
httpPriority = kNormalPriority + priority;
|
|
}
|
|
MOZ_ASSERT(httpPriority >= 0);
|
|
SetPriority(static_cast<uint32_t>(httpPriority));
|
|
}
|
|
|
|
Http2StreamBase::~Http2StreamBase() {
|
|
MOZ_DIAGNOSTIC_ASSERT(OnSocketThread());
|
|
|
|
mStreamID = Http2Session::kDeadStreamID;
|
|
|
|
LOG3(("Http2StreamBase::~Http2StreamBase %p", this));
|
|
}
|
|
|
|
already_AddRefed<Http2Session> Http2StreamBase::Session() {
|
|
RefPtr<Http2Session> session = do_QueryReferent(mSession);
|
|
return session.forget();
|
|
}
|
|
|
|
// ReadSegments() is used to write data down the socket. Generally, HTTP
|
|
// request data is pulled from the approriate transaction and
|
|
// converted to HTTP/2 data. Sometimes control data like a window-update is
|
|
// generated instead.
|
|
|
|
nsresult Http2StreamBase::ReadSegments(nsAHttpSegmentReader* reader,
|
|
uint32_t count, uint32_t* countRead) {
|
|
LOG3(("Http2StreamBase %p ReadSegments reader=%p count=%d state=%x", this,
|
|
reader, count, mUpstreamState));
|
|
RefPtr<Http2Session> session = Session();
|
|
// Reader is nullptr when this is a push stream.
|
|
MOZ_DIAGNOSTIC_ASSERT(!reader || (reader == session) ||
|
|
(IsTunnel() && NS_FAILED(Condition())));
|
|
|
|
if (NS_FAILED(Condition())) {
|
|
return Condition();
|
|
}
|
|
|
|
MOZ_ASSERT(OnSocketThread(), "not on socket thread");
|
|
|
|
nsresult rv = NS_ERROR_UNEXPECTED;
|
|
mRequestBlockedOnRead = 0;
|
|
|
|
if (mRecvdFin || mRecvdReset) {
|
|
// Don't transmit any request frames if the peer cannot respond
|
|
LOG3(
|
|
("Http2StreamBase %p ReadSegments request stream aborted due to"
|
|
" response side closure\n",
|
|
this));
|
|
return NS_ERROR_ABORT;
|
|
}
|
|
|
|
// avoid runt chunks if possible by anticipating
|
|
// full data frames
|
|
if (count > (mChunkSize + 8)) {
|
|
uint32_t numchunks = count / (mChunkSize + 8);
|
|
count = numchunks * (mChunkSize + 8);
|
|
}
|
|
|
|
switch (mUpstreamState) {
|
|
case GENERATING_HEADERS:
|
|
case GENERATING_BODY:
|
|
case SENDING_BODY:
|
|
// Call into the HTTP Transaction to generate the HTTP request
|
|
// stream. That stream will show up in OnReadSegment().
|
|
mSegmentReader = reader;
|
|
rv = CallToReadData(count, countRead);
|
|
mSegmentReader = nullptr;
|
|
|
|
LOG3(("Http2StreamBase::ReadSegments %p trans readsegments rv %" PRIx32
|
|
" read=%d\n",
|
|
this, static_cast<uint32_t>(rv), *countRead));
|
|
|
|
// Check to see if the transaction's request could be written out now.
|
|
// If not, mark the stream for callback when writing can proceed.
|
|
if (NS_SUCCEEDED(rv) && mUpstreamState == GENERATING_HEADERS &&
|
|
!mRequestHeadersDone) {
|
|
session->TransactionHasDataToWrite(this);
|
|
}
|
|
|
|
// mTxinlineFrameUsed represents any queued un-sent frame. It might
|
|
// be 0 if there is no such frame, which is not a gurantee that we
|
|
// don't have more request body to send - just that any data that was
|
|
// sent comprised a complete HTTP/2 frame. Likewise, a non 0 value is
|
|
// a queued, but complete, http/2 frame length.
|
|
|
|
// Mark that we are blocked on read if the http transaction needs to
|
|
// provide more of the request message body and there is nothing queued
|
|
// for writing
|
|
if (rv == NS_BASE_STREAM_WOULD_BLOCK && !mTxInlineFrameUsed) {
|
|
LOG(("Http2StreamBase %p mRequestBlockedOnRead = 1", this));
|
|
mRequestBlockedOnRead = 1;
|
|
}
|
|
|
|
// A transaction that had already generated its headers before it was
|
|
// queued at the session level (due to concurrency concerns) may not call
|
|
// onReadSegment off the ReadSegments() stack above.
|
|
|
|
// When mTransaction->ReadSegments returns NS_BASE_STREAM_WOULD_BLOCK it
|
|
// means it may have already finished providing all the request data
|
|
// necessary to generate open, calling OnReadSegment will drive sending
|
|
// the request; this may happen after dequeue of the stream.
|
|
|
|
if (mUpstreamState == GENERATING_HEADERS &&
|
|
(NS_SUCCEEDED(rv) || rv == NS_BASE_STREAM_WOULD_BLOCK)) {
|
|
LOG3(("Http2StreamBase %p ReadSegments forcing OnReadSegment call\n",
|
|
this));
|
|
uint32_t wasted = 0;
|
|
mSegmentReader = reader;
|
|
nsresult rv2 = OnReadSegment("", 0, &wasted);
|
|
mSegmentReader = nullptr;
|
|
|
|
LOG3((" OnReadSegment returned 0x%08" PRIx32,
|
|
static_cast<uint32_t>(rv2)));
|
|
if (NS_SUCCEEDED(rv2)) {
|
|
mRequestBlockedOnRead = 0;
|
|
}
|
|
}
|
|
|
|
// If the sending flow control window is open (!mBlockedOnRwin) then
|
|
// continue sending the request
|
|
if (!mBlockedOnRwin && mOpenGenerated && !mTxInlineFrameUsed &&
|
|
NS_SUCCEEDED(rv) && (!*countRead) && CloseSendStreamWhenDone()) {
|
|
MOZ_ASSERT(!mQueued);
|
|
MOZ_ASSERT(mRequestHeadersDone);
|
|
LOG3(
|
|
("Http2StreamBase::ReadSegments %p 0x%X: Sending request data "
|
|
"complete, "
|
|
"mUpstreamState=%x\n",
|
|
this, mStreamID, mUpstreamState));
|
|
if (mSentFin) {
|
|
ChangeState(UPSTREAM_COMPLETE);
|
|
} else {
|
|
GenerateDataFrameHeader(0, true);
|
|
ChangeState(SENDING_FIN_STREAM);
|
|
session->TransactionHasDataToWrite(this);
|
|
rv = NS_BASE_STREAM_WOULD_BLOCK;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case SENDING_FIN_STREAM:
|
|
// We were trying to send the FIN-STREAM but were blocked from
|
|
// sending it out - try again.
|
|
if (!mSentFin) {
|
|
mSegmentReader = reader;
|
|
rv = TransmitFrame(nullptr, nullptr, false);
|
|
mSegmentReader = nullptr;
|
|
MOZ_ASSERT(NS_FAILED(rv) || !mTxInlineFrameUsed,
|
|
"Transmit Frame should be all or nothing");
|
|
if (NS_SUCCEEDED(rv)) ChangeState(UPSTREAM_COMPLETE);
|
|
} else {
|
|
rv = NS_OK;
|
|
mTxInlineFrameUsed = 0; // cancel fin data packet
|
|
ChangeState(UPSTREAM_COMPLETE);
|
|
}
|
|
|
|
*countRead = 0;
|
|
|
|
// don't change OK to WOULD BLOCK. we are really done sending if OK
|
|
break;
|
|
|
|
case UPSTREAM_COMPLETE:
|
|
*countRead = 0;
|
|
rv = NS_OK;
|
|
break;
|
|
|
|
default:
|
|
MOZ_ASSERT(false, "Http2StreamBase::ReadSegments unknown state");
|
|
break;
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
uint64_t Http2StreamBase::LocalUnAcked() {
|
|
// reduce unacked by the amount of undelivered data
|
|
// to help assert flow control
|
|
uint64_t undelivered = mSimpleBuffer.Available();
|
|
|
|
if (undelivered > mLocalUnacked) {
|
|
return 0;
|
|
}
|
|
return mLocalUnacked - undelivered;
|
|
}
|
|
|
|
nsresult Http2StreamBase::BufferInput(uint32_t count, uint32_t* countWritten) {
|
|
char buf[SimpleBufferPage::kSimpleBufferPageSize];
|
|
if (SimpleBufferPage::kSimpleBufferPageSize < count) {
|
|
count = SimpleBufferPage::kSimpleBufferPageSize;
|
|
}
|
|
|
|
mBypassInputBuffer = 1;
|
|
nsresult rv = mSegmentWriter->OnWriteSegment(buf, count, countWritten);
|
|
mBypassInputBuffer = 0;
|
|
|
|
if (NS_SUCCEEDED(rv)) {
|
|
rv = mSimpleBuffer.Write(buf, *countWritten);
|
|
if (NS_FAILED(rv)) {
|
|
MOZ_ASSERT(rv == NS_ERROR_OUT_OF_MEMORY);
|
|
return NS_ERROR_OUT_OF_MEMORY;
|
|
}
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
bool Http2StreamBase::DeferCleanup(nsresult status) {
|
|
// do not cleanup a stream that has data buffered for the transaction
|
|
return (NS_SUCCEEDED(status) && mSimpleBuffer.Available());
|
|
}
|
|
|
|
// WriteSegments() is used to read data off the socket. Generally this is
|
|
// just a call through to the associated nsHttpTransaction for this stream
|
|
// for the remaining data bytes indicated by the current DATA frame.
|
|
|
|
nsresult Http2StreamBase::WriteSegments(nsAHttpSegmentWriter* writer,
|
|
uint32_t count,
|
|
uint32_t* countWritten) {
|
|
MOZ_ASSERT(OnSocketThread(), "not on socket thread");
|
|
MOZ_ASSERT(!mSegmentWriter, "segment writer in progress");
|
|
|
|
LOG3(("Http2StreamBase::WriteSegments %p count=%d state=%x", this, count,
|
|
mUpstreamState));
|
|
|
|
mSegmentWriter = writer;
|
|
nsresult rv = CallToWriteData(count, countWritten);
|
|
|
|
if (rv == NS_BASE_STREAM_WOULD_BLOCK) {
|
|
// consuming transaction won't take data. but we need to read it into a
|
|
// buffer so that it won't block other streams. but we should not advance
|
|
// the flow control window so that we'll eventually push back on the sender.
|
|
rv = BufferInput(count, countWritten);
|
|
LOG3(("Http2StreamBase::WriteSegments %p Buffered %" PRIX32 " %d\n", this,
|
|
static_cast<uint32_t>(rv), *countWritten));
|
|
}
|
|
|
|
LOG3(("Http2StreamBase::WriteSegments %" PRIX32 "",
|
|
static_cast<uint32_t>(rv)));
|
|
mSegmentWriter = nullptr;
|
|
return rv;
|
|
}
|
|
|
|
nsresult Http2StreamBase::ParseHttpRequestHeaders(const char* buf,
|
|
uint32_t avail,
|
|
uint32_t* countUsed) {
|
|
// Returns NS_OK even if the headers are incomplete
|
|
// set mRequestHeadersDone flag if they are complete
|
|
|
|
MOZ_ASSERT(OnSocketThread(), "not on socket thread");
|
|
MOZ_ASSERT(mUpstreamState == GENERATING_HEADERS);
|
|
MOZ_ASSERT(!mRequestHeadersDone);
|
|
|
|
LOG3(("Http2StreamBase::ParseHttpRequestHeaders %p avail=%d state=%x", this,
|
|
avail, mUpstreamState));
|
|
|
|
mFlatHttpRequestHeaders.Append(buf, avail);
|
|
|
|
// We can use the simple double crlf because firefox is the
|
|
// only client we are parsing
|
|
int32_t endHeader = mFlatHttpRequestHeaders.Find("\r\n\r\n");
|
|
|
|
if (endHeader == kNotFound) {
|
|
// We don't have all the headers yet
|
|
LOG3(
|
|
("Http2StreamBase::ParseHttpRequestHeaders %p "
|
|
"Need more header bytes. Len = %zd",
|
|
this, mFlatHttpRequestHeaders.Length()));
|
|
*countUsed = avail;
|
|
return NS_OK;
|
|
}
|
|
|
|
// We have recvd all the headers, trim the local
|
|
// buffer of the final empty line, and set countUsed to reflect
|
|
// the whole header has been consumed.
|
|
uint32_t oldLen = mFlatHttpRequestHeaders.Length();
|
|
mFlatHttpRequestHeaders.SetLength(endHeader + 2);
|
|
*countUsed = avail - (oldLen - endHeader) + 4;
|
|
mRequestHeadersDone = 1;
|
|
|
|
Http2Stream* selfRegularStream = this->GetHttp2Stream();
|
|
if (selfRegularStream) {
|
|
return selfRegularStream->CheckPushCache();
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
// This is really a headers frame, but open is pretty clear from a workflow pov
|
|
nsresult Http2StreamBase::GenerateOpen() {
|
|
// It is now OK to assign a streamID that we are assured will
|
|
// be monotonically increasing amongst new streams on this
|
|
// session
|
|
RefPtr<Http2Session> session = Session();
|
|
mStreamID = session->RegisterStreamID(this);
|
|
MOZ_ASSERT(mStreamID & 1, "Http2 Stream Channel ID must be odd");
|
|
MOZ_ASSERT(!mOpenGenerated);
|
|
|
|
mOpenGenerated = 1;
|
|
|
|
LOG3(("Http2StreamBase %p Stream ID 0x%X [session=%p]\n", this, mStreamID,
|
|
session.get()));
|
|
|
|
if (mStreamID >= 0x80000000) {
|
|
// streamID must fit in 31 bits. Evading This is theoretically possible
|
|
// because stream ID assignment is asynchronous to stream creation
|
|
// because of the protocol requirement that the new stream ID
|
|
// be monotonically increasing. In reality this is really not possible
|
|
// because new streams stop being added to a session with millions of
|
|
// IDs still available and no race condition is going to bridge that gap;
|
|
// so we can be comfortable on just erroring out for correctness in that
|
|
// case.
|
|
LOG3(("Stream assigned out of range ID: 0x%X", mStreamID));
|
|
return NS_ERROR_UNEXPECTED;
|
|
}
|
|
|
|
// Now we need to convert the flat http headers into a set
|
|
// of HTTP/2 headers by writing to mTxInlineFrame{sz}
|
|
|
|
nsCString compressedData;
|
|
uint8_t firstFrameFlags = Http2Session::kFlag_PRIORITY;
|
|
|
|
nsresult rv = GenerateHeaders(compressedData, firstFrameFlags);
|
|
if (NS_FAILED(rv)) {
|
|
return rv;
|
|
}
|
|
|
|
if (firstFrameFlags & Http2Session::kFlag_END_STREAM) {
|
|
SetSentFin(true);
|
|
}
|
|
|
|
// split this one HEADERS frame up into N HEADERS + CONTINUATION frames if it
|
|
// exceeds the 2^14-1 limit for 1 frame. Do it by inserting header size gaps
|
|
// in the existing frame for the new headers and for the first one a priority
|
|
// field. There is no question this is ugly, but a 16KB HEADERS frame should
|
|
// be a long tail event, so this is really just for correctness and a nop in
|
|
// the base case.
|
|
//
|
|
|
|
MOZ_ASSERT(!mTxInlineFrameUsed);
|
|
|
|
uint32_t dataLength = compressedData.Length();
|
|
uint32_t maxFrameData =
|
|
Http2Session::kMaxFrameData - 5; // 5 bytes for priority
|
|
uint32_t numFrames = 1;
|
|
|
|
if (dataLength > maxFrameData) {
|
|
numFrames +=
|
|
((dataLength - maxFrameData) + Http2Session::kMaxFrameData - 1) /
|
|
Http2Session::kMaxFrameData;
|
|
MOZ_ASSERT(numFrames > 1);
|
|
}
|
|
|
|
// note that we could still have 1 frame for 0 bytes of data. that's ok.
|
|
|
|
uint32_t messageSize = dataLength;
|
|
messageSize += Http2Session::kFrameHeaderBytes +
|
|
5; // frame header + priority overhead in HEADERS frame
|
|
messageSize += (numFrames - 1) *
|
|
Http2Session::kFrameHeaderBytes; // frame header overhead in
|
|
// CONTINUATION frames
|
|
|
|
EnsureBuffer(mTxInlineFrame, messageSize, mTxInlineFrameUsed,
|
|
mTxInlineFrameSize);
|
|
|
|
mTxInlineFrameUsed += messageSize;
|
|
UpdatePriorityDependency();
|
|
LOG1(
|
|
("Http2StreamBase %p Generating %d bytes of HEADERS for stream 0x%X with "
|
|
"priority weight %u dep 0x%X frames %u\n",
|
|
this, mTxInlineFrameUsed, mStreamID, mPriorityWeight,
|
|
mPriorityDependency, numFrames));
|
|
|
|
uint32_t outputOffset = 0;
|
|
uint32_t compressedDataOffset = 0;
|
|
for (uint32_t idx = 0; idx < numFrames; ++idx) {
|
|
uint32_t flags, frameLen;
|
|
bool lastFrame = (idx == numFrames - 1);
|
|
|
|
flags = 0;
|
|
frameLen = maxFrameData;
|
|
if (!idx) {
|
|
flags |= firstFrameFlags;
|
|
// Only the first frame needs the 4-byte offset
|
|
maxFrameData = Http2Session::kMaxFrameData;
|
|
}
|
|
if (lastFrame) {
|
|
frameLen = dataLength;
|
|
flags |= Http2Session::kFlag_END_HEADERS;
|
|
}
|
|
dataLength -= frameLen;
|
|
|
|
session->CreateFrameHeader(mTxInlineFrame.get() + outputOffset,
|
|
frameLen + (idx ? 0 : 5),
|
|
(idx) ? Http2Session::FRAME_TYPE_CONTINUATION
|
|
: Http2Session::FRAME_TYPE_HEADERS,
|
|
flags, mStreamID);
|
|
outputOffset += Http2Session::kFrameHeaderBytes;
|
|
|
|
if (!idx) {
|
|
uint32_t wireDep = PR_htonl(mPriorityDependency);
|
|
memcpy(mTxInlineFrame.get() + outputOffset, &wireDep, 4);
|
|
memcpy(mTxInlineFrame.get() + outputOffset + 4, &mPriorityWeight, 1);
|
|
outputOffset += 5;
|
|
}
|
|
|
|
memcpy(mTxInlineFrame.get() + outputOffset,
|
|
compressedData.BeginReading() + compressedDataOffset, frameLen);
|
|
compressedDataOffset += frameLen;
|
|
outputOffset += frameLen;
|
|
}
|
|
|
|
Telemetry::Accumulate(Telemetry::SPDY_SYN_SIZE, compressedData.Length());
|
|
|
|
mFlatHttpRequestHeaders.Truncate();
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void Http2StreamBase::AdjustInitialWindow() {
|
|
// The default initial_window is sized for pushed streams. When we
|
|
// generate a client pulled stream we want to disable flow control for
|
|
// the stream with a window update. Do the same for pushed streams
|
|
// when they connect to a pull.
|
|
|
|
uint32_t wireStreamId = GetWireStreamId();
|
|
if (wireStreamId == 0) {
|
|
return;
|
|
}
|
|
|
|
// right now mClientReceiveWindow is the lower push limit
|
|
// bump it up to the pull limit set by the channel or session
|
|
// don't allow windows less than push
|
|
uint32_t bump = 0;
|
|
RefPtr<Http2Session> session = Session();
|
|
nsHttpTransaction* trans = HttpTransaction();
|
|
if (trans && trans->InitialRwin()) {
|
|
bump = (trans->InitialRwin() > mClientReceiveWindow)
|
|
? (trans->InitialRwin() - mClientReceiveWindow)
|
|
: 0;
|
|
} else {
|
|
MOZ_ASSERT(session->InitialRwin() >= mClientReceiveWindow);
|
|
bump = session->InitialRwin() - mClientReceiveWindow;
|
|
}
|
|
|
|
LOG3(("AdjustInitialwindow increased flow control window %p 0x%X %u\n", this,
|
|
wireStreamId, bump));
|
|
if (!bump) { // nothing to do
|
|
return;
|
|
}
|
|
|
|
EnsureBuffer(mTxInlineFrame,
|
|
mTxInlineFrameUsed + Http2Session::kFrameHeaderBytes + 4,
|
|
mTxInlineFrameUsed, mTxInlineFrameSize);
|
|
uint8_t* packet = mTxInlineFrame.get() + mTxInlineFrameUsed;
|
|
mTxInlineFrameUsed += Http2Session::kFrameHeaderBytes + 4;
|
|
|
|
session->CreateFrameHeader(packet, 4, Http2Session::FRAME_TYPE_WINDOW_UPDATE,
|
|
0, wireStreamId);
|
|
|
|
mClientReceiveWindow += bump;
|
|
bump = PR_htonl(bump);
|
|
memcpy(packet + Http2Session::kFrameHeaderBytes, &bump, 4);
|
|
}
|
|
|
|
void Http2StreamBase::UpdateTransportReadEvents(uint32_t count) {
|
|
mTotalRead += count;
|
|
if (!mSocketTransport) {
|
|
return;
|
|
}
|
|
|
|
if (Transaction()) {
|
|
Transaction()->OnTransportStatus(mSocketTransport,
|
|
NS_NET_STATUS_RECEIVING_FROM, mTotalRead);
|
|
}
|
|
}
|
|
|
|
void Http2StreamBase::UpdateTransportSendEvents(uint32_t count) {
|
|
mTotalSent += count;
|
|
|
|
// Setting the TCP send buffer, introduced in
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=790184, which the following
|
|
// comment refers to, is being removed once we verify no increases in error
|
|
// rate.
|
|
//
|
|
// normally on non-windows platform we use TCP autotuning for
|
|
// the socket buffers, and this works well (managing enough
|
|
// buffers for BDP while conserving memory) for HTTP even when
|
|
// it creates really deep queues. However this 'buffer bloat' is
|
|
// a problem for http/2 because it ruins the low latency properties
|
|
// necessary for PING and cancel to work meaningfully.
|
|
|
|
// If this stream represents a large upload, disable autotuning for
|
|
// the session and cap the send buffers by default at 128KB.
|
|
// (10Mbit/sec @ 100ms)
|
|
//
|
|
uint32_t bufferSize = gHttpHandler->SpdySendBufferSize();
|
|
if (StaticPrefs::network_http_http2_send_buffer_size() > 0 &&
|
|
(mTotalSent > bufferSize) && !mSetTCPSocketBuffer) {
|
|
mSetTCPSocketBuffer = 1;
|
|
mSocketTransport->SetSendBufferSize(bufferSize);
|
|
}
|
|
|
|
if ((mUpstreamState != SENDING_FIN_STREAM) && Transaction()) {
|
|
Transaction()->OnTransportStatus(mSocketTransport, NS_NET_STATUS_SENDING_TO,
|
|
mTotalSent);
|
|
}
|
|
|
|
if (!mSentWaitingFor && !mRequestBodyLenRemaining) {
|
|
mSentWaitingFor = 1;
|
|
if (Transaction()) {
|
|
Transaction()->OnTransportStatus(mSocketTransport,
|
|
NS_NET_STATUS_WAITING_FOR, 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
nsresult Http2StreamBase::TransmitFrame(const char* buf, uint32_t* countUsed,
|
|
bool forceCommitment) {
|
|
// If TransmitFrame returns SUCCESS than all the data is sent (or at least
|
|
// buffered at the session level), if it returns WOULD_BLOCK then none of
|
|
// the data is sent.
|
|
|
|
// You can call this function with no data and no out parameter in order to
|
|
// flush internal buffers that were previously blocked on writing. You can
|
|
// of course feed new data to it as well.
|
|
|
|
LOG3(("Http2StreamBase::TransmitFrame %p inline=%d stream=%d", this,
|
|
mTxInlineFrameUsed, mTxStreamFrameSize));
|
|
if (countUsed) *countUsed = 0;
|
|
|
|
if (!mTxInlineFrameUsed) {
|
|
MOZ_ASSERT(!buf);
|
|
return NS_OK;
|
|
}
|
|
|
|
MOZ_ASSERT(mTxInlineFrameUsed, "empty stream frame in transmit");
|
|
MOZ_ASSERT(mSegmentReader, "TransmitFrame with null mSegmentReader");
|
|
MOZ_ASSERT((buf && countUsed) || (!buf && !countUsed),
|
|
"TransmitFrame arguments inconsistent");
|
|
|
|
uint32_t transmittedCount;
|
|
nsresult rv;
|
|
RefPtr<Http2Session> session = Session();
|
|
|
|
// In the (relatively common) event that we have a small amount of data
|
|
// split between the inlineframe and the streamframe, then move the stream
|
|
// data into the inlineframe via copy in order to coalesce into one write.
|
|
// Given the interaction with ssl this is worth the small copy cost.
|
|
if (mTxStreamFrameSize && mTxInlineFrameUsed &&
|
|
mTxStreamFrameSize < Http2Session::kDefaultBufferSize &&
|
|
mTxInlineFrameUsed + mTxStreamFrameSize < mTxInlineFrameSize) {
|
|
LOG3(("Coalesce Transmit"));
|
|
memcpy(&mTxInlineFrame[mTxInlineFrameUsed], buf, mTxStreamFrameSize);
|
|
if (countUsed) *countUsed += mTxStreamFrameSize;
|
|
mTxInlineFrameUsed += mTxStreamFrameSize;
|
|
mTxStreamFrameSize = 0;
|
|
}
|
|
|
|
rv = mSegmentReader->CommitToSegmentSize(
|
|
mTxStreamFrameSize + mTxInlineFrameUsed, forceCommitment);
|
|
|
|
if (rv == NS_BASE_STREAM_WOULD_BLOCK) {
|
|
MOZ_ASSERT(!forceCommitment, "forceCommitment with WOULD_BLOCK");
|
|
session->TransactionHasDataToWrite(this);
|
|
}
|
|
if (NS_FAILED(rv)) { // this will include WOULD_BLOCK
|
|
return rv;
|
|
}
|
|
|
|
// This function calls mSegmentReader->OnReadSegment to report the actual
|
|
// http/2 bytes through to the session object and then the HttpConnection
|
|
// which calls the socket write function. It will accept all of the inline and
|
|
// stream data because of the above 'commitment' even if it has to buffer
|
|
|
|
rv = session->BufferOutput(reinterpret_cast<char*>(mTxInlineFrame.get()),
|
|
mTxInlineFrameUsed, &transmittedCount);
|
|
LOG3(
|
|
("Http2StreamBase::TransmitFrame for inline BufferOutput session=%p "
|
|
"stream=%p result %" PRIx32 " len=%d",
|
|
session.get(), this, static_cast<uint32_t>(rv), transmittedCount));
|
|
|
|
MOZ_ASSERT(rv != NS_BASE_STREAM_WOULD_BLOCK,
|
|
"inconsistent inline commitment result");
|
|
|
|
if (NS_FAILED(rv)) return rv;
|
|
|
|
MOZ_ASSERT(transmittedCount == mTxInlineFrameUsed,
|
|
"inconsistent inline commitment count");
|
|
|
|
Http2Session::LogIO(session, this, "Writing from Inline Buffer",
|
|
reinterpret_cast<char*>(mTxInlineFrame.get()),
|
|
transmittedCount);
|
|
|
|
if (mTxStreamFrameSize) {
|
|
if (!buf) {
|
|
// this cannot happen
|
|
MOZ_ASSERT(false,
|
|
"Stream transmit with null buf argument to "
|
|
"TransmitFrame()");
|
|
LOG3(("Stream transmit with null buf argument to TransmitFrame()\n"));
|
|
return NS_ERROR_UNEXPECTED;
|
|
}
|
|
|
|
// If there is already data buffered, just add to that to form
|
|
// a single TLS Application Data Record - otherwise skip the memcpy
|
|
if (session->AmountOfOutputBuffered()) {
|
|
rv = session->BufferOutput(buf, mTxStreamFrameSize, &transmittedCount);
|
|
} else {
|
|
rv = session->OnReadSegment(buf, mTxStreamFrameSize, &transmittedCount);
|
|
}
|
|
|
|
LOG3(
|
|
("Http2StreamBase::TransmitFrame for regular session=%p "
|
|
"stream=%p result %" PRIx32 " len=%d",
|
|
session.get(), this, static_cast<uint32_t>(rv), transmittedCount));
|
|
|
|
MOZ_ASSERT(rv != NS_BASE_STREAM_WOULD_BLOCK,
|
|
"inconsistent stream commitment result");
|
|
|
|
if (NS_FAILED(rv)) return rv;
|
|
|
|
MOZ_ASSERT(transmittedCount == mTxStreamFrameSize,
|
|
"inconsistent stream commitment count");
|
|
|
|
Http2Session::LogIO(session, this, "Writing from Transaction Buffer", buf,
|
|
transmittedCount);
|
|
|
|
*countUsed += mTxStreamFrameSize;
|
|
}
|
|
|
|
if (!mAttempting0RTT) {
|
|
session->FlushOutputQueue();
|
|
}
|
|
|
|
// calling this will trigger waiting_for if mRequestBodyLenRemaining is 0
|
|
UpdateTransportSendEvents(mTxInlineFrameUsed + mTxStreamFrameSize);
|
|
|
|
mTxInlineFrameUsed = 0;
|
|
mTxStreamFrameSize = 0;
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void Http2StreamBase::ChangeState(enum upstreamStateType newState) {
|
|
LOG3(("Http2StreamBase::ChangeState() %p from %X to %X", this, mUpstreamState,
|
|
newState));
|
|
mUpstreamState = newState;
|
|
}
|
|
|
|
void Http2StreamBase::GenerateDataFrameHeader(uint32_t dataLength,
|
|
bool lastFrame) {
|
|
LOG3(("Http2StreamBase::GenerateDataFrameHeader %p len=%d last=%d", this,
|
|
dataLength, lastFrame));
|
|
|
|
MOZ_ASSERT(OnSocketThread(), "not on socket thread");
|
|
MOZ_ASSERT(!mTxInlineFrameUsed, "inline frame not empty");
|
|
MOZ_ASSERT(!mTxStreamFrameSize, "stream frame not empty");
|
|
|
|
uint8_t frameFlags = 0;
|
|
if (lastFrame) {
|
|
frameFlags |= Http2Session::kFlag_END_STREAM;
|
|
if (dataLength) SetSentFin(true);
|
|
}
|
|
|
|
RefPtr<Http2Session> session = Session();
|
|
session->CreateFrameHeader(mTxInlineFrame.get(), dataLength,
|
|
Http2Session::FRAME_TYPE_DATA, frameFlags,
|
|
mStreamID);
|
|
|
|
mTxInlineFrameUsed = Http2Session::kFrameHeaderBytes;
|
|
mTxStreamFrameSize = dataLength;
|
|
}
|
|
|
|
// ConvertResponseHeaders is used to convert the response headers
|
|
// into HTTP/1 format and report some telemetry
|
|
nsresult Http2StreamBase::ConvertResponseHeaders(
|
|
Http2Decompressor* decompressor, nsACString& aHeadersIn,
|
|
nsACString& aHeadersOut, int32_t& httpResponseCode) {
|
|
nsresult rv = decompressor->DecodeHeaderBlock(
|
|
reinterpret_cast<const uint8_t*>(aHeadersIn.BeginReading()),
|
|
aHeadersIn.Length(), aHeadersOut, false);
|
|
if (NS_FAILED(rv)) {
|
|
LOG3(("Http2StreamBase::ConvertResponseHeaders %p decode Error\n", this));
|
|
return rv;
|
|
}
|
|
|
|
nsAutoCString statusString;
|
|
decompressor->GetStatus(statusString);
|
|
if (statusString.IsEmpty()) {
|
|
LOG3(("Http2StreamBase::ConvertResponseHeaders %p Error - no status\n",
|
|
this));
|
|
return NS_ERROR_ILLEGAL_VALUE;
|
|
}
|
|
|
|
nsresult errcode;
|
|
httpResponseCode = statusString.ToInteger(&errcode);
|
|
|
|
// Ensure the :status is just an HTTP status code
|
|
// https://tools.ietf.org/html/rfc7540#section-8.1.2.4
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1352146
|
|
nsAutoCString parsedStatusString;
|
|
parsedStatusString.AppendInt(httpResponseCode);
|
|
if (!parsedStatusString.Equals(statusString)) {
|
|
LOG3(
|
|
("Http2StreamBase::ConvertResposeHeaders %p status %s is not just a "
|
|
"code",
|
|
this, statusString.BeginReading()));
|
|
// Results in stream reset with PROTOCOL_ERROR
|
|
return NS_ERROR_ILLEGAL_VALUE;
|
|
}
|
|
|
|
LOG3(("Http2StreamBase::ConvertResponseHeaders %p response code %d\n", this,
|
|
httpResponseCode));
|
|
|
|
if (httpResponseCode == 421) {
|
|
// Origin Frame requires 421 to remove this origin from the origin set
|
|
RefPtr<Http2Session> session = Session();
|
|
session->Received421(ConnectionInfo());
|
|
}
|
|
|
|
if (aHeadersIn.Length() && aHeadersOut.Length()) {
|
|
Telemetry::Accumulate(Telemetry::SPDY_SYN_REPLY_SIZE, aHeadersIn.Length());
|
|
uint32_t ratio = aHeadersIn.Length() * 100 / aHeadersOut.Length();
|
|
Telemetry::Accumulate(Telemetry::SPDY_SYN_REPLY_RATIO, ratio);
|
|
}
|
|
|
|
// The decoding went ok. Now we can customize and clean up.
|
|
|
|
aHeadersIn.Truncate();
|
|
aHeadersOut.AppendLiteral("X-Firefox-Spdy: h2");
|
|
aHeadersOut.AppendLiteral("\r\n\r\n");
|
|
LOG(("decoded response headers are:\n%s", aHeadersOut.BeginReading()));
|
|
HandleResponseHeaders(aHeadersOut, httpResponseCode);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult Http2StreamBase::ConvertResponseTrailers(
|
|
Http2Decompressor* decompressor, nsACString& aTrailersIn) {
|
|
LOG3(("Http2StreamBase::ConvertResponseTrailers %p", this));
|
|
nsAutoCString flatTrailers;
|
|
|
|
nsresult rv = decompressor->DecodeHeaderBlock(
|
|
reinterpret_cast<const uint8_t*>(aTrailersIn.BeginReading()),
|
|
aTrailersIn.Length(), flatTrailers, false);
|
|
if (NS_FAILED(rv)) {
|
|
LOG3(("Http2StreamBase::ConvertResponseTrailers %p decode Error", this));
|
|
return rv;
|
|
}
|
|
|
|
nsHttpTransaction* trans = HttpTransaction();
|
|
if (trans) {
|
|
trans->SetHttpTrailers(flatTrailers);
|
|
} else {
|
|
LOG3(("Http2StreamBase::ConvertResponseTrailers %p no trans", this));
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void Http2StreamBase::SetResponseIsComplete() {
|
|
nsHttpTransaction* trans = HttpTransaction();
|
|
if (trans) {
|
|
trans->SetResponseIsComplete();
|
|
}
|
|
}
|
|
|
|
void Http2StreamBase::SetAllHeadersReceived() {
|
|
if (mAllHeadersReceived) {
|
|
return;
|
|
}
|
|
|
|
if (mState == RESERVED_BY_REMOTE) {
|
|
// pushed streams needs to wait until headers have
|
|
// arrived to open up their window
|
|
LOG3(
|
|
("Http2StreamBase::SetAllHeadersReceived %p state OPEN from reserved\n",
|
|
this));
|
|
mState = OPEN;
|
|
AdjustInitialWindow();
|
|
}
|
|
|
|
mAllHeadersReceived = 1;
|
|
}
|
|
|
|
bool Http2StreamBase::AllowFlowControlledWrite() {
|
|
RefPtr<Http2Session> session = Session();
|
|
return (session->ServerSessionWindow() > 0) && (mServerReceiveWindow > 0);
|
|
}
|
|
|
|
void Http2StreamBase::UpdateServerReceiveWindow(int32_t delta) {
|
|
mServerReceiveWindow += delta;
|
|
|
|
if (mBlockedOnRwin && AllowFlowControlledWrite()) {
|
|
LOG3(
|
|
("Http2StreamBase::UpdateServerReceived UnPause %p 0x%X "
|
|
"Open stream window\n",
|
|
this, mStreamID));
|
|
RefPtr<Http2Session> session = Session();
|
|
session->TransactionHasDataToWrite(this);
|
|
}
|
|
}
|
|
|
|
void Http2StreamBase::SetPriority(uint32_t newPriority) {
|
|
int32_t httpPriority = static_cast<int32_t>(newPriority);
|
|
if (httpPriority > kWorstPriority) {
|
|
httpPriority = kWorstPriority;
|
|
} else if (httpPriority < kBestPriority) {
|
|
httpPriority = kBestPriority;
|
|
}
|
|
mRFC7540Priority = static_cast<uint32_t>(httpPriority);
|
|
mPriorityWeight = (nsISupportsPriority::PRIORITY_LOWEST + 1) -
|
|
(httpPriority - kNormalPriority);
|
|
|
|
mPriorityDependency = 0; // maybe adjusted later
|
|
}
|
|
|
|
void Http2StreamBase::SetPriorityDependency(uint32_t newPriority,
|
|
uint32_t newDependency) {
|
|
SetPriority(newPriority);
|
|
mPriorityDependency = newDependency;
|
|
}
|
|
|
|
static uint32_t GetPriorityDependencyFromTransaction(nsHttpTransaction* trans) {
|
|
MOZ_ASSERT(trans);
|
|
|
|
uint32_t classFlags = trans->GetClassOfService().Flags();
|
|
|
|
if (classFlags & nsIClassOfService::UrgentStart) {
|
|
return Http2Session::kUrgentStartGroupID;
|
|
}
|
|
|
|
if (classFlags & nsIClassOfService::Leader) {
|
|
return Http2Session::kLeaderGroupID;
|
|
}
|
|
|
|
if (classFlags & nsIClassOfService::Follower) {
|
|
return Http2Session::kFollowerGroupID;
|
|
}
|
|
|
|
if (classFlags & nsIClassOfService::Speculative) {
|
|
return Http2Session::kSpeculativeGroupID;
|
|
}
|
|
|
|
if (classFlags & nsIClassOfService::Background) {
|
|
return Http2Session::kBackgroundGroupID;
|
|
}
|
|
|
|
if (classFlags & nsIClassOfService::Unblocked) {
|
|
return Http2Session::kOtherGroupID;
|
|
}
|
|
|
|
return Http2Session::kFollowerGroupID; // unmarked followers
|
|
}
|
|
|
|
void Http2StreamBase::UpdatePriorityDependency() {
|
|
RefPtr<Http2Session> session = Session();
|
|
if (!session->UseH2Deps()) {
|
|
return;
|
|
}
|
|
|
|
nsHttpTransaction* trans = HttpTransaction();
|
|
if (!trans) {
|
|
return;
|
|
}
|
|
|
|
// we create 6 fake dependency streams per session,
|
|
// these streams are never opened with HEADERS. our first opened stream is 0xd
|
|
// 3 depends 0, weight 200, leader class (kLeaderGroupID)
|
|
// 5 depends 0, weight 100, other (kOtherGroupID)
|
|
// 7 depends 0, weight 0, background (kBackgroundGroupID)
|
|
// 9 depends 7, weight 0, speculative (kSpeculativeGroupID)
|
|
// b depends 3, weight 0, follower class (kFollowerGroupID)
|
|
// d depends 0, weight 240, urgent-start class (kUrgentStartGroupID)
|
|
//
|
|
// streams for leaders (html, js, css) depend on 3
|
|
// streams for folowers (images) depend on b
|
|
// default streams (xhr, async js) depend on 5
|
|
// explicit bg streams (beacon, etc..) depend on 7
|
|
// spculative bg streams depend on 9
|
|
// urgent-start streams depend on d
|
|
|
|
mPriorityDependency = GetPriorityDependencyFromTransaction(trans);
|
|
|
|
if (gHttpHandler->ActiveTabPriority() &&
|
|
mTransactionBrowserId != mCurrentBrowserId &&
|
|
mPriorityDependency != Http2Session::kUrgentStartGroupID) {
|
|
LOG3(
|
|
("Http2StreamBase::UpdatePriorityDependency %p "
|
|
" depends on background group for trans %p\n",
|
|
this, trans));
|
|
mPriorityDependency = Http2Session::kBackgroundGroupID;
|
|
|
|
nsHttp::NotifyActiveTabLoadOptimization();
|
|
}
|
|
|
|
LOG1(
|
|
("Http2StreamBase::UpdatePriorityDependency %p "
|
|
"depends on stream 0x%X\n",
|
|
this, mPriorityDependency));
|
|
}
|
|
|
|
void Http2StreamBase::CurrentBrowserIdChanged(uint64_t id) {
|
|
if (!mStreamID) {
|
|
// For pushed streams, we ignore the direct call from the session and
|
|
// instead let it come to the internal function from the pushed stream, so
|
|
// we don't accidentally send two PRIORITY frames for the same stream.
|
|
return;
|
|
}
|
|
|
|
CurrentBrowserIdChangedInternal(id);
|
|
}
|
|
|
|
void Http2StreamBase::CurrentBrowserIdChangedInternal(uint64_t id) {
|
|
MOZ_ASSERT(gHttpHandler->ActiveTabPriority());
|
|
RefPtr<Http2Session> session = Session();
|
|
LOG3(
|
|
("Http2StreamBase::CurrentBrowserIdChangedInternal "
|
|
"%p browserId=%" PRIx64 "\n",
|
|
this, id));
|
|
|
|
mCurrentBrowserId = id;
|
|
|
|
// Urgent start takes an absolute precedence, so don't
|
|
// change mPriorityDependency here.
|
|
if (mPriorityDependency == Http2Session::kUrgentStartGroupID) {
|
|
return;
|
|
}
|
|
|
|
if (session->UseH2Deps()) {
|
|
UpdatePriorityRFC7540(session);
|
|
} else {
|
|
UpdatePriority(session);
|
|
}
|
|
}
|
|
|
|
void Http2StreamBase::UpdatePriority(Http2Session* session) {
|
|
MOZ_ASSERT(!session->UseH2Deps());
|
|
bool isInBackground = mTransactionBrowserId != mCurrentBrowserId;
|
|
|
|
if (isInBackground) {
|
|
LOG3(
|
|
("Http2StreamBase::CurrentBrowserIdChangedInternal %p "
|
|
"move into background group.\n",
|
|
this));
|
|
|
|
nsHttp::NotifyActiveTabLoadOptimization();
|
|
}
|
|
|
|
nsHttpTransaction* trans = HttpTransaction();
|
|
if (!trans) {
|
|
return;
|
|
}
|
|
|
|
uint8_t urgency =
|
|
nsHttpHandler::UrgencyFromCoSFlags(trans->GetClassOfService().Flags());
|
|
bool incremental = trans->GetClassOfService().Incremental();
|
|
uint32_t streamID = GetWireStreamId();
|
|
|
|
// If the tab is in the background
|
|
// we can probably lower the priority of this request by 1.
|
|
// (to get lower priority we increase urgency).
|
|
if (isInBackground && urgency < 6) {
|
|
urgency++;
|
|
}
|
|
|
|
if (streamID) {
|
|
session->SendPriorityUpdateFrame(streamID, urgency, incremental);
|
|
}
|
|
}
|
|
|
|
void Http2StreamBase::UpdatePriorityRFC7540(Http2Session* session) {
|
|
MOZ_ASSERT(session->UseH2Deps());
|
|
if (mTransactionBrowserId != mCurrentBrowserId) {
|
|
mPriorityDependency = Http2Session::kBackgroundGroupID;
|
|
LOG3(
|
|
("Http2StreamBase::CurrentBrowserIdChangedInternal %p "
|
|
"move into background group.\n",
|
|
this));
|
|
|
|
nsHttp::NotifyActiveTabLoadOptimization();
|
|
} else {
|
|
nsHttpTransaction* trans = HttpTransaction();
|
|
if (!trans) {
|
|
return;
|
|
}
|
|
|
|
mPriorityDependency = GetPriorityDependencyFromTransaction(trans);
|
|
LOG3(
|
|
("Http2StreamBase::CurrentBrowserIdChangedInternal %p "
|
|
"depends on stream 0x%X\n",
|
|
this, mPriorityDependency));
|
|
}
|
|
|
|
uint32_t modifyStreamID = GetWireStreamId();
|
|
|
|
if (modifyStreamID) {
|
|
session->SendPriorityFrame(modifyStreamID, mPriorityDependency,
|
|
mPriorityWeight);
|
|
}
|
|
}
|
|
|
|
void Http2StreamBase::SetRecvdFin(bool aStatus) {
|
|
mRecvdFin = aStatus ? 1 : 0;
|
|
if (!aStatus) return;
|
|
|
|
if (mState == OPEN || mState == RESERVED_BY_REMOTE) {
|
|
mState = CLOSED_BY_REMOTE;
|
|
} else if (mState == CLOSED_BY_LOCAL) {
|
|
mState = CLOSED;
|
|
}
|
|
}
|
|
|
|
void Http2StreamBase::SetSentFin(bool aStatus) {
|
|
mSentFin = aStatus ? 1 : 0;
|
|
if (!aStatus) return;
|
|
|
|
if (mState == OPEN || mState == RESERVED_BY_REMOTE) {
|
|
mState = CLOSED_BY_LOCAL;
|
|
} else if (mState == CLOSED_BY_REMOTE) {
|
|
mState = CLOSED;
|
|
}
|
|
}
|
|
|
|
void Http2StreamBase::SetRecvdReset(bool aStatus) {
|
|
mRecvdReset = aStatus ? 1 : 0;
|
|
if (!aStatus) return;
|
|
mState = CLOSED;
|
|
}
|
|
|
|
void Http2StreamBase::SetSentReset(bool aStatus) {
|
|
mSentReset = aStatus ? 1 : 0;
|
|
if (!aStatus) return;
|
|
mState = CLOSED;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// nsAHttpSegmentReader
|
|
//-----------------------------------------------------------------------------
|
|
|
|
nsresult Http2StreamBase::OnReadSegment(const char* buf, uint32_t count,
|
|
uint32_t* countRead) {
|
|
LOG3(("Http2StreamBase::OnReadSegment %p count=%d state=%x", this, count,
|
|
mUpstreamState));
|
|
|
|
MOZ_ASSERT(OnSocketThread(), "not on socket thread");
|
|
if (!mSegmentReader) {
|
|
return NS_BASE_STREAM_WOULD_BLOCK;
|
|
}
|
|
|
|
nsresult rv = NS_ERROR_UNEXPECTED;
|
|
uint32_t dataLength;
|
|
RefPtr<Http2Session> session = Session();
|
|
|
|
switch (mUpstreamState) {
|
|
case GENERATING_HEADERS:
|
|
// The buffer is the HTTP request stream, including at least part of the
|
|
// HTTP request header. This state's job is to build a HEADERS frame
|
|
// from the header information. count is the number of http bytes
|
|
// available (which may include more than the header), and in countRead we
|
|
// return the number of those bytes that we consume (i.e. the portion that
|
|
// are header bytes)
|
|
|
|
if (!mRequestHeadersDone) {
|
|
if (NS_FAILED(rv = ParseHttpRequestHeaders(buf, count, countRead))) {
|
|
return rv;
|
|
}
|
|
}
|
|
|
|
if (mRequestHeadersDone && !mOpenGenerated) {
|
|
if (!session->TryToActivate(this)) {
|
|
LOG3(
|
|
("Http2StreamBase::OnReadSegment %p cannot activate now. "
|
|
"queued.\n",
|
|
this));
|
|
return *countRead ? NS_OK : NS_BASE_STREAM_WOULD_BLOCK;
|
|
}
|
|
if (NS_FAILED(rv = GenerateOpen())) {
|
|
return rv;
|
|
}
|
|
}
|
|
|
|
LOG3(
|
|
("ParseHttpRequestHeaders %p used %d of %d. "
|
|
"requestheadersdone = %d mOpenGenerated = %d\n",
|
|
this, *countRead, count, mRequestHeadersDone, mOpenGenerated));
|
|
if (mOpenGenerated) {
|
|
SetHTTPState(OPEN);
|
|
AdjustInitialWindow();
|
|
// This version of TransmitFrame cannot block
|
|
rv = TransmitFrame(nullptr, nullptr, true);
|
|
ChangeState(GENERATING_BODY);
|
|
break;
|
|
}
|
|
MOZ_ASSERT(*countRead == count,
|
|
"Header parsing not complete but unused data");
|
|
break;
|
|
|
|
case GENERATING_BODY:
|
|
// if there is session flow control and either the stream window is active
|
|
// and exhaused or the session window is exhausted then suspend
|
|
if (!AllowFlowControlledWrite()) {
|
|
*countRead = 0;
|
|
LOG3(
|
|
("Http2StreamBase this=%p, id 0x%X request body suspended because "
|
|
"remote window is stream=%" PRId64 " session=%" PRId64 ".\n",
|
|
this, mStreamID, mServerReceiveWindow,
|
|
session->ServerSessionWindow()));
|
|
mBlockedOnRwin = true;
|
|
return NS_BASE_STREAM_WOULD_BLOCK;
|
|
}
|
|
mBlockedOnRwin = false;
|
|
|
|
// The chunk is the smallest of: availableData, configured chunkSize,
|
|
// stream window, session window, or 14 bit framing limit.
|
|
// Its amazing we send anything at all.
|
|
dataLength = std::min(count, mChunkSize);
|
|
|
|
if (dataLength > Http2Session::kMaxFrameData) {
|
|
dataLength = Http2Session::kMaxFrameData;
|
|
}
|
|
|
|
if (dataLength > session->ServerSessionWindow()) {
|
|
dataLength = static_cast<uint32_t>(session->ServerSessionWindow());
|
|
}
|
|
|
|
if (dataLength > mServerReceiveWindow) {
|
|
dataLength = static_cast<uint32_t>(mServerReceiveWindow);
|
|
}
|
|
|
|
LOG3(
|
|
("Http2StreamBase this=%p id 0x%X send calculation "
|
|
"avail=%d chunksize=%d stream window=%" PRId64
|
|
" session window=%" PRId64 " "
|
|
"max frame=%d USING=%u\n",
|
|
this, mStreamID, count, mChunkSize, mServerReceiveWindow,
|
|
session->ServerSessionWindow(), Http2Session::kMaxFrameData,
|
|
dataLength));
|
|
|
|
session->DecrementServerSessionWindow(dataLength);
|
|
mServerReceiveWindow -= dataLength;
|
|
|
|
LOG3(("Http2StreamBase %p id 0x%x request len remaining %" PRId64 ", "
|
|
"count avail %u, chunk used %u",
|
|
this, mStreamID, mRequestBodyLenRemaining, count, dataLength));
|
|
if (!dataLength && mRequestBodyLenRemaining) {
|
|
return NS_BASE_STREAM_WOULD_BLOCK;
|
|
}
|
|
if (dataLength > mRequestBodyLenRemaining) {
|
|
return NS_ERROR_UNEXPECTED;
|
|
}
|
|
mRequestBodyLenRemaining -= dataLength;
|
|
GenerateDataFrameHeader(dataLength, !mRequestBodyLenRemaining);
|
|
ChangeState(SENDING_BODY);
|
|
[[fallthrough]];
|
|
|
|
case SENDING_BODY:
|
|
MOZ_ASSERT(mTxInlineFrameUsed, "OnReadSegment Send Data Header 0b");
|
|
rv = TransmitFrame(buf, countRead, false);
|
|
MOZ_ASSERT(NS_FAILED(rv) || !mTxInlineFrameUsed,
|
|
"Transmit Frame should be all or nothing");
|
|
|
|
LOG3(("TransmitFrame() rv=%" PRIx32 " returning %d data bytes. "
|
|
"Header is %d Body is %d.",
|
|
static_cast<uint32_t>(rv), *countRead, mTxInlineFrameUsed,
|
|
mTxStreamFrameSize));
|
|
|
|
// normalize a partial write with a WOULD_BLOCK into just a partial write
|
|
// as some code will take WOULD_BLOCK to mean an error with nothing
|
|
// written (e.g. nsHttpTransaction::ReadRequestSegment()
|
|
if (rv == NS_BASE_STREAM_WOULD_BLOCK && *countRead) rv = NS_OK;
|
|
|
|
// If that frame was all sent, look for another one
|
|
if (!mTxInlineFrameUsed) ChangeState(GENERATING_BODY);
|
|
break;
|
|
|
|
case SENDING_FIN_STREAM:
|
|
MOZ_ASSERT(false, "resuming partial fin stream out of OnReadSegment");
|
|
break;
|
|
|
|
case UPSTREAM_COMPLETE: {
|
|
MOZ_ASSERT(this->GetHttp2Stream() &&
|
|
this->GetHttp2Stream()->IsReadingFromPushStream());
|
|
rv = TransmitFrame(nullptr, nullptr, true);
|
|
break;
|
|
}
|
|
default:
|
|
MOZ_ASSERT(false, "Http2StreamBase::OnReadSegment non-write state");
|
|
break;
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// nsAHttpSegmentWriter
|
|
//-----------------------------------------------------------------------------
|
|
|
|
nsresult Http2StreamBase::OnWriteSegment(char* buf, uint32_t count,
|
|
uint32_t* countWritten) {
|
|
LOG3(("Http2StreamBase::OnWriteSegment %p count=%d state=%x 0x%X\n", this,
|
|
count, mUpstreamState, mStreamID));
|
|
|
|
MOZ_ASSERT(OnSocketThread(), "not on socket thread");
|
|
if (!mSegmentWriter) {
|
|
return NS_BASE_STREAM_WOULD_BLOCK;
|
|
}
|
|
|
|
// sometimes we have read data from the network and stored it in a pipe
|
|
// so that other streams can proceed when the gecko caller is not processing
|
|
// data events fast enough and flow control hasn't caught up yet. This
|
|
// gets the stored data out of that pipe
|
|
if (!mBypassInputBuffer && mSimpleBuffer.Available()) {
|
|
*countWritten = mSimpleBuffer.Read(buf, count);
|
|
MOZ_ASSERT(*countWritten);
|
|
LOG3(
|
|
("Http2StreamBase::OnWriteSegment read from flow control buffer %p %x "
|
|
"%d\n",
|
|
this, mStreamID, *countWritten));
|
|
return NS_OK;
|
|
}
|
|
|
|
// read from the network
|
|
return mSegmentWriter->OnWriteSegment(buf, count, countWritten);
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// mirror nsAHttpTransaction
|
|
// -----------------------------------------------------------------------------
|
|
|
|
bool Http2StreamBase::Do0RTT() {
|
|
MOZ_ASSERT(Transaction());
|
|
mAttempting0RTT = false;
|
|
nsAHttpTransaction* trans = Transaction();
|
|
if (trans) {
|
|
mAttempting0RTT = trans->Do0RTT();
|
|
}
|
|
return mAttempting0RTT;
|
|
}
|
|
|
|
nsresult Http2StreamBase::Finish0RTT(bool aRestart, bool aAlpnChanged) {
|
|
MOZ_ASSERT(Transaction());
|
|
mAttempting0RTT = false;
|
|
// 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.
|
|
// ..
|
|
// however, we send in the aRestart value to indicate that early data failed
|
|
// for devtools purposes
|
|
nsresult rv = NS_OK;
|
|
nsAHttpTransaction* trans = Transaction();
|
|
if (trans) {
|
|
rv = trans->Finish0RTT(aAlpnChanged, aAlpnChanged);
|
|
if (aRestart) {
|
|
nsHttpTransaction* hTrans = trans->QueryHttpTransaction();
|
|
if (hTrans) {
|
|
hTrans->Refused0RTT();
|
|
}
|
|
}
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
nsresult Http2StreamBase::GetOriginAttributes(mozilla::OriginAttributes* oa) {
|
|
if (!mSocketTransport) {
|
|
return NS_ERROR_UNEXPECTED;
|
|
}
|
|
|
|
return mSocketTransport->GetOriginAttributes(oa);
|
|
}
|
|
|
|
nsHttpTransaction* Http2StreamBase::HttpTransaction() {
|
|
return (Transaction()) ? Transaction()->QueryHttpTransaction() : nullptr;
|
|
}
|
|
|
|
nsHttpConnectionInfo* Http2StreamBase::ConnectionInfo() {
|
|
if (Transaction()) {
|
|
return Transaction()->ConnectionInfo();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
} // namespace mozilla::net
|