Revision control
Copy as Markdown
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:critical reason:network-protocol
#include "qhttp2connection_p.h"
#include "qhttp2configuration_p.h"
#include <private/bitstreams_p.h>
#include <QtCore/private/qnumeric_p.h>
#include <QtCore/private/qiodevice_p.h>
#include <QtCore/private/qnoncontiguousbytedevice_p.h>
#include <QtCore/qcoreapplication.h>
#include <QtCore/QRandomGenerator>
#include <QtCore/qloggingcategory.h>
#include <algorithm>
#include <memory>
#include <chrono>
QT_BEGIN_NAMESPACE
Q_STATIC_LOGGING_CATEGORY(qHttp2ConnectionLog, "qt.network.http2.connection", QtCriticalMsg)
using namespace Qt::StringLiterals;
using namespace Http2;
/*!
\class QHttp2Stream
\inmodule QtNetwork
\internal
The QHttp2Stream class represents a single HTTP/2 stream.
Must be created by QHttp2Connection.
\sa QHttp2Connection
*/
/*!
\struct QHttp2Stream::Configuration
\inmodule QtNetwork
\internal
\brief Configuration options for a QHttp2Stream.
The Configuration struct holds options that control stream behavior.
\sa QHttp2Connection::createStream()
*/
/*!
\variable QHttp2Stream::Configuration::useDownloadBuffer
Controls whether incoming DATA frames, from QHttp2Stream::dataReceived(),
are buffered. The default is \c true.
You may disable buffering for client-initiated streams when the
application processes DATA immediately.
Buffering must remain enabled for pushed streams. A pushed stream can
receive DATA before the application becomes aware of them and the buffered
DATA is required to deliver the pushed response.
\sa QHttp2Stream::downloadBuffer(), QHttp2Stream::takeDownloadBuffer(),
QHttp2Configuration::serverPushEnabled(), QHttp2Stream::dataReceived()
*/
/*!
\variable QHttp2Stream::Configuration::useHeaderBuffer
Controls whether received headers, from QHttp2Stream::headersReceived(),
are accumulated for later retrieval via QHttp2Stream::receivedHeaders().
The default is \c true.
You may disable accumulation for client-initiated streams when the
application consumes the headersReceived() signal directly. When disabled,
the \l{headersUpdated()} signal is not emitted.
Buffering must remain enabled for pushed streams. A pushed stream can
receive headers before the application becomes aware of it and the buffered
headers are required to deliver the pushed response.
\sa QHttp2Stream::receivedHeaders(), QHttp2Stream::headersReceived(),
QHttp2Configuration::serverPushEnabled()
*/
QHttp2Stream::QHttp2Stream(QHttp2Connection *connection, quint32 streamID,
Configuration configuration) noexcept
: QObject(connection), m_streamID(streamID), m_configuration(configuration)
{
Q_ASSERT(connection);
Q_ASSERT(streamID); // stream id 0 is reserved for connection control messages
qCDebug(qHttp2ConnectionLog, "[%p] new stream %u", connection, streamID);
}
QHttp2Stream::~QHttp2Stream() noexcept {
if (auto *connection = getConnection()) {
if (m_state != State::Idle && m_state != State::Closed) {
qCDebug(qHttp2ConnectionLog, "[%p] stream %u, destroyed while still open", connection,
m_streamID);
// Check if we can still send data, then send RST_STREAM:
if (connection->getSocket()) {
if (isUploadingDATA())
sendRST_STREAM(CANCEL);
else
sendRST_STREAM(HTTP2_NO_ERROR);
}
}
connection->m_streams.remove(streamID());
}
}
/*!
\fn quint32 QHttp2Stream::streamID() const noexcept
Returns the stream ID of this stream.
*/
/*!
\fn void QHttp2Stream::headersReceived(const HPack::HttpHeader &headers, bool endStream)
This signal is emitted when the remote peer has sent a HEADERS frame, and
potentially some CONTINUATION frames, ending with the END_HEADERS flag
to this stream.
The headers are internally combined and decompressed, and are accessible
through the \a headers parameter. If the END_STREAM flag was set, the
\a endStream parameter will be \c true, indicating that the peer does not
intend to send any more frames on this stream.
\sa receivedHeaders()
*/
/*!
\fn void QHttp2Stream::headersUpdated()
This signal may be emitted if a new HEADERS frame was received after
already processing a previous HEADERS frame.
\sa headersReceived(), receivedHeaders()
*/
/*!
\fn void QHttp2Stream::errorOccurred(Http2::Http2Error errorCode, const QString &errorString)
This signal is emitted when the stream has encountered an error. The
\a errorCode parameter is the HTTP/2 error code, and the \a errorString
parameter is a human-readable description of the error.
*/
/*!
\fn void QHttp2Stream::stateChanged(State newState)
This signal is emitted when the state of the stream changes. The \a newState
parameter is the new state of the stream.
Examples of this is sending or receiving a frame with the END_STREAM flag.
This will transition the stream to the HalfClosedLocal or HalfClosedRemote
state, respectively.
\sa state()
*/
/*!
\fn void QHttp2Stream::promisedStreamReceived(quint32 newStreamID)
This signal is emitted when the remote peer has promised a new stream with
the given \a newStreamID.
\sa QHttp2Connection::promisedStream()
*/
/*!
\fn void QHttp2Stream::uploadBlocked()
This signal is emitted when the stream is unable to send more data because
the remote peer's receive window is full.
This is mostly intended for diagnostics as there is no expectation that the
user can do anything to react to this.
*/
/*!
\fn void QHttp2Stream::dataReceived(const QByteArray &data, bool endStream)
This signal is emitted when the stream has received a DATA frame from the
remote peer. The \a data parameter contains the payload of the frame, and
the \a endStream parameter is \c true if the END_STREAM flag was set.
\sa downloadBuffer()
*/
/*!
\fn void QHttp2Stream::bytesWritten(qint64 bytesWritten)
This signal is emitted when the stream has written \a bytesWritten bytes to
the network.
*/
/*!
\fn void QHttp2Stream::uploadDeviceError(const QString &errorString)
This signal is emitted if the upload device encounters an error while
sending data. The \a errorString parameter is a human-readable description
of the error.
*/
/*!
\fn void QHttp2Stream::uploadFinished()
This signal is emitted when the stream has finished sending all the data
from the upload device.
If the END_STREAM flag was set for sendDATA() then the stream will be
closed for further writes before this signal is emitted.
*/
/*!
\fn bool QHttp2Stream::isUploadingDATA() const noexcept
Returns \c true if the stream is currently sending DATA frames.
*/
/*!
\fn State QHttp2Stream::state() const noexcept
Returns the current state of the stream.
\sa stateChanged()
*/
/*!
\fn bool QHttp2Stream::isActive() const noexcept
Returns \c true if the stream has been opened and is not yet closed.
*/
/*!
\fn bool QHttp2Stream::isPromisedStream() const noexcept
Returns \c true if the stream was promised by the remote peer.
*/
/*!
\fn bool QHttp2Stream::wasReset() const noexcept
Returns \c true if the stream was reset by the remote peer.
*/
/*!
\fn quint32 QHttp2Stream::RST_STREAM_code() const noexcept
Returns the HTTP/2 error code if the stream was reset by the remote peer.
If the stream was not reset, this function returns 0.
*/
/*!
\fn HPack::HttpHeader QHttp2Stream::receivedHeaders() const noexcept
Returns the headers received from the remote peer, if any.
*/
/*!
\fn QByteDataBuffer QHttp2Stream::downloadBuffer() const noexcept
Returns the buffer containing the data received from the remote peer.
*/
/*!
\fn QHttp2Stream::Configuration QHttp2Stream::configuration() const
Returns the configuration of this stream.
*/
void QHttp2Stream::finishWithError(Http2::Http2Error errorCode, const QString &message)
{
qCDebug(qHttp2ConnectionLog, "[%p] stream %u finished with error: %ls (error code: %u)",
getConnection(), m_streamID, qUtf16Printable(message), errorCode);
transitionState(StateTransition::RST);
emit errorOccurred(errorCode, message);
}
void QHttp2Stream::finishWithError(Http2::Http2Error errorCode)
{
QNetworkReply::NetworkError ignored = QNetworkReply::NoError;
QString message;
qt_error(errorCode, ignored, message);
finishWithError(errorCode, message);
}
void QHttp2Stream::streamError(Http2::Http2Error errorCode, const QString &message)
{
qCDebug(qHttp2ConnectionLog, "[%p] stream %u finished with error: %ls (error code: %u)",
getConnection(), m_streamID, qUtf16Printable(message), errorCode);
sendRST_STREAM(errorCode);
emit errorOccurred(errorCode, message);
}
/*!
Sends a RST_STREAM frame with the given \a errorCode.
This closes the stream for both sides, any further frames will be dropped.
Returns \c false if the stream is closed or idle, also if it fails to send
the RST_STREAM frame. Otherwise, returns \c true.
*/
bool QHttp2Stream::sendRST_STREAM(Http2::Http2Error errorCode)
{
if (m_state == State::Closed || m_state == State::Idle) {
qCDebug(qHttp2ConnectionLog, "[%p] could not send RST_STREAM on %s stream %u",
getConnection(), QDebug::toBytes(m_state).constData(), m_streamID);
return false;
}
// Never respond to a RST_STREAM with a RST_STREAM or looping might occur.
if (m_RST_STREAM_received.has_value())
return false;
getConnection()->registerStreamAsResetLocally(streamID());
m_RST_STREAM_sent = errorCode;
qCDebug(qHttp2ConnectionLog, "[%p] sending RST_STREAM on stream %u, code: %u", getConnection(),
m_streamID, errorCode);
transitionState(StateTransition::RST);
QHttp2Connection *connection = getConnection();
FrameWriter &frameWriter = connection->frameWriter;
frameWriter.start(FrameType::RST_STREAM, FrameFlag::EMPTY, m_streamID);
frameWriter.append(quint32(errorCode));
return frameWriter.write(*connection->getSocket());
}
/*!
Sends a DATA frame with the bytes obtained from \a payload.
This function will send as many DATA frames as needed to send all the data
from \a payload. If \a endStream is \c true, the END_STREAM flag will be
set.
Returns \c{true} if we were able to \e{start} writing to the socket,
false otherwise.
Note that even though we started writing, the socket may error out before
this function returns. Call state() for the new status.
*/
bool QHttp2Stream::sendDATA(const QByteArray &payload, bool endStream)
{
Q_ASSERT(!m_uploadByteDevice);
if (m_state != State::Open && m_state != State::HalfClosedRemote)
return false;
auto *byteDevice = QNonContiguousByteDeviceFactory::create(payload);
m_owningByteDevice = true;
byteDevice->setParent(this);
return sendDATA(byteDevice, endStream);
}
/*!
Sends a DATA frame with the bytes obtained from \a device.
This function will send as many DATA frames as needed to send all the data
from \a device. If \a endStream is \c true, the END_STREAM flag will be set.
\a device must stay alive for the duration of the upload.
A way of doing this is to heap-allocate the \a device and parent it to the
QHttp2Stream.
Returns \c{true} if we were able to \e{start} writing to the socket,
false otherwise.
Note that even though we started writing, the socket may error out before
this function returns. Call state() for the new status.
*/
bool QHttp2Stream::sendDATA(QIODevice *device, bool endStream)
{
Q_ASSERT(!m_uploadDevice);
Q_ASSERT(!m_uploadByteDevice);
Q_ASSERT(device);
if (m_state != State::Open && m_state != State::HalfClosedRemote) {
qCWarning(qHttp2ConnectionLog, "[%p] attempt to sendDATA on closed stream %u, "
"of device: %p.",
getConnection(), m_streamID, device);
return false;
}
qCDebug(qHttp2ConnectionLog, "[%p] starting sendDATA on stream %u, of device: %p",
getConnection(), m_streamID, device);
auto *byteDevice = QNonContiguousByteDeviceFactory::create(device);
m_owningByteDevice = true;
byteDevice->setParent(this);
m_uploadDevice = device;
return sendDATA(byteDevice, endStream);
}
/*!
Sends a DATA frame with the bytes obtained from \a device.
This function will send as many DATA frames as needed to send all the data
from \a device. If \a endStream is \c true, the END_STREAM flag will be set.
\a device must stay alive for the duration of the upload.
A way of doing this is to heap-allocate the \a device and parent it to the
QHttp2Stream.
Returns \c{true} if we were able to \e{start} writing to the socket,
false otherwise.
Note that even though we started writing, the socket may error out before
this function returns. Call state() for the new status.
*/
bool QHttp2Stream::sendDATA(QNonContiguousByteDevice *device, bool endStream)
{
Q_ASSERT(!m_uploadByteDevice);
Q_ASSERT(device);
if (m_state != State::Open && m_state != State::HalfClosedRemote) {
qCWarning(qHttp2ConnectionLog, "[%p] attempt to sendDATA on closed stream %u, "
"of device: %p.",
getConnection(), m_streamID, device);
return false;
}
qCDebug(qHttp2ConnectionLog, "[%p] starting sendDATA on stream %u, of device: %p",
getConnection(), m_streamID, device);
m_uploadByteDevice = device;
m_endStreamAfterDATA = endStream;
connect(m_uploadByteDevice, &QNonContiguousByteDevice::readyRead, this,
&QHttp2Stream::maybeResumeUpload);
connect(m_uploadByteDevice, &QObject::destroyed, this, &QHttp2Stream::uploadDeviceDestroyed);
internalSendDATA();
// There is no early-out in internalSendDATA so if we reach this spot we
// have at least started to send something, even if it errors out.
return true;
}
void QHttp2Stream::internalSendDATA()
{
Q_ASSERT(m_uploadByteDevice);
QHttp2Connection *connection = getConnection();
Q_ASSERT(connection->maxFrameSize > frameHeaderSize);
QIODevice *socket = connection->getSocket();
qCDebug(qHttp2ConnectionLog,
"[%p] stream %u, about to write to socket, current session window size: %d, stream "
"window size: %d, bytes available: %lld",
connection, m_streamID, connection->sessionSendWindowSize, m_sendWindow,
m_uploadByteDevice->size() - m_uploadByteDevice->pos());
qint32 remainingWindowSize = std::min<qint32>(connection->sessionSendWindowSize, m_sendWindow);
FrameWriter &frameWriter = connection->frameWriter;
qint64 totalBytesWritten = 0;
const auto deviceCanRead = [this, connection] {
// We take advantage of knowing the internals of one of the devices used.
// It will request X bytes to move over to the http thread if there's
// not enough left, so we give it a large size. It will anyway return
// the size it can actually provide.
const qint64 requestSize = connection->maxFrameSize * 10ll;
qint64 tmp = 0;
return m_uploadByteDevice->readPointer(requestSize, tmp) != nullptr && tmp > 0;
};
bool sentEND_STREAM = false;
while (remainingWindowSize && deviceCanRead()) {
quint32 bytesWritten = 0;
qint32 remainingBytesInFrame = qint32(connection->maxFrameSize);
frameWriter.start(FrameType::DATA, FrameFlag::EMPTY, streamID());
while (remainingWindowSize && deviceCanRead() && remainingBytesInFrame) {
const qint32 maxToWrite = std::min(remainingWindowSize, remainingBytesInFrame);
qint64 outBytesAvail = 0;
const char *readPointer = m_uploadByteDevice->readPointer(maxToWrite, outBytesAvail);
if (!readPointer || outBytesAvail <= 0) {
qCDebug(qHttp2ConnectionLog,
"[%p] stream %u, cannot write data, device (%p) has %lld bytes available",
connection, m_streamID, m_uploadByteDevice, outBytesAvail);
break;
}
const qint32 bytesToWrite = qint32(std::min<qint64>(maxToWrite, outBytesAvail));
frameWriter.append(QByteArrayView(readPointer, bytesToWrite));
m_uploadByteDevice->advanceReadPointer(bytesToWrite);
bytesWritten += bytesToWrite;
m_sendWindow -= bytesToWrite;
Q_ASSERT(m_sendWindow >= 0);
connection->sessionSendWindowSize -= bytesToWrite;
Q_ASSERT(connection->sessionSendWindowSize >= 0);
remainingBytesInFrame -= bytesToWrite;
Q_ASSERT(remainingBytesInFrame >= 0);
remainingWindowSize -= bytesToWrite;
Q_ASSERT(remainingWindowSize >= 0);
}
qCDebug(qHttp2ConnectionLog, "[%p] stream %u, writing %u bytes to socket", connection,
m_streamID, bytesWritten);
if (!deviceCanRead() && m_uploadByteDevice->atEnd() && m_endStreamAfterDATA) {
sentEND_STREAM = true;
frameWriter.addFlag(FrameFlag::END_STREAM);
}
if (!frameWriter.write(*socket)) {
qCDebug(qHttp2ConnectionLog, "[%p] stream %u, failed to write to socket", connection,
m_streamID);
return finishWithError(INTERNAL_ERROR, u"failed to write to socket"_s);
}
totalBytesWritten += bytesWritten;
}
qCDebug(qHttp2ConnectionLog,
"[%p] stream %u, wrote %lld bytes total, if the device is not exhausted, we'll write "
"more later. Remaining window size: %d",
connection, m_streamID, totalBytesWritten, remainingWindowSize);
emit bytesWritten(totalBytesWritten);
if (sentEND_STREAM || (!deviceCanRead() && m_uploadByteDevice->atEnd())) {
qCDebug(qHttp2ConnectionLog,
"[%p] stream %u, exhausted device %p, sent END_STREAM? %d, %ssending end stream "
"after DATA",
connection, m_streamID, m_uploadByteDevice, sentEND_STREAM,
!sentEND_STREAM && m_endStreamAfterDATA ? "" : "not ");
if (!sentEND_STREAM && m_endStreamAfterDATA) {
// We need to send an empty DATA frame with END_STREAM since we
// have exhausted the device, but we haven't sent END_STREAM yet.
// This can happen if we got a final readyRead to signify no more
// data available, but we hadn't sent the END_STREAM flag yet.
frameWriter.start(FrameType::DATA, FrameFlag::END_STREAM, streamID());
frameWriter.write(*socket);
}
finishSendDATA();
} else if (isUploadBlocked()) {
qCDebug(qHttp2ConnectionLog, "[%p] stream %u, upload blocked", connection, m_streamID);
emit uploadBlocked();
}
}
void QHttp2Stream::finishSendDATA()
{
if (m_endStreamAfterDATA)
transitionState(StateTransition::CloseLocal);
disconnect(m_uploadByteDevice, nullptr, this, nullptr);
m_uploadDevice = nullptr;
if (m_owningByteDevice) {
m_owningByteDevice = false;
delete m_uploadByteDevice;
}
m_uploadByteDevice = nullptr;
emit uploadFinished();
}
void QHttp2Stream::maybeResumeUpload()
{
qCDebug(qHttp2ConnectionLog,
"[%p] stream %u, maybeResumeUpload. Upload device: %p, bytes available: %lld, blocked? "
"%d",
getConnection(), m_streamID, m_uploadByteDevice,
!m_uploadByteDevice ? 0 : m_uploadByteDevice->size() - m_uploadByteDevice->pos(),
isUploadBlocked());
if (isUploadingDATA() && !isUploadBlocked())
internalSendDATA();
else
getConnection()->m_blockedStreams.insert(streamID());
}
/*!
Returns \c true if the stream is currently unable to send more data because
the remote peer's receive window is full.
*/
bool QHttp2Stream::isUploadBlocked() const noexcept
{
constexpr auto MinFrameSize = Http2::frameHeaderSize + 1; // 1 byte payload
return isUploadingDATA()
&& (m_sendWindow <= MinFrameSize
|| getConnection()->sessionSendWindowSize <= MinFrameSize);
}
void QHttp2Stream::uploadDeviceReadChannelFinished()
{
maybeResumeUpload();
}
/*!
Sends a HEADERS frame with the given \a headers and \a priority.
If \a endStream is \c true, the END_STREAM flag will be set, and the stream
will be closed for future writes.
If the headers are too large, or the stream is not in the correct state,
this function will return \c false. Otherwise, it will return \c true.
*/
bool QHttp2Stream::sendHEADERS(const HPack::HttpHeader &headers, bool endStream, quint8 priority)
{
using namespace HPack;
if (auto hs = header_size(headers);
!hs.first || hs.second > getConnection()->maxHeaderListSize()) {
return false;
}
transitionState(StateTransition::Open);
Q_ASSERT(m_state == State::Open || m_state == State::HalfClosedRemote);
QHttp2Connection *connection = getConnection();
qCDebug(qHttp2ConnectionLog, "[%p] stream %u, sending HEADERS frame with %u entries",
connection, streamID(), uint(headers.size()));
QIODevice *socket = connection->getSocket();
FrameWriter &frameWriter = connection->frameWriter;
frameWriter.start(FrameType::HEADERS, FrameFlag::PRIORITY | FrameFlag::END_HEADERS, streamID());
if (endStream)
frameWriter.addFlag(FrameFlag::END_STREAM);
frameWriter.append(quint32()); // No stream dependency in Qt.
frameWriter.append(priority);
// Compress in-place:
BitOStream outputStream(frameWriter.outboundFrame().buffer);
// Possibly perform and notify of dynamic table size update:
for (auto &maybePendingTableSizeUpdate : connection->pendingTableSizeUpdates) {
if (!maybePendingTableSizeUpdate)
break; // They are ordered, so if the first one is null, the other one is too.
qCDebug(qHttp2ConnectionLog, "[%p] stream %u, sending dynamic table size update of size %u",
connection, streamID(), *maybePendingTableSizeUpdate);
connection->encoder.setMaxDynamicTableSize(*maybePendingTableSizeUpdate);
connection->encoder.encodeSizeUpdate(outputStream, *maybePendingTableSizeUpdate);
maybePendingTableSizeUpdate.reset();
}
if (connection->m_connectionType == QHttp2Connection::Type::Client) {
if (!connection->encoder.encodeRequest(outputStream, headers))
return false;
} else {
if (!connection->encoder.encodeResponse(outputStream, headers))
return false;
}
bool result = frameWriter.writeHEADERS(*socket, connection->maxFrameSize);
if (endStream)
transitionState(StateTransition::CloseLocal);
return result;
}
/*!
Sends a WINDOW_UPDATE frame with the given \a delta.
This increases our receive window size for this stream, allowing the remote
peer to send more data.
*/
void QHttp2Stream::sendWINDOW_UPDATE(quint32 delta)
{
QHttp2Connection *connection = getConnection();
m_recvWindow += qint32(delta);
connection->sendWINDOW_UPDATE(streamID(), delta);
}
void QHttp2Stream::uploadDeviceDestroyed()
{
if (isUploadingDATA()) {
// We're in the middle of sending DATA frames, we need to abort
// the stream.
const QString message = u"Upload device destroyed while uploading"_s;
streamError(CANCEL, message);
emit uploadDeviceError(message);
}
m_uploadDevice = nullptr;
m_owningByteDevice = false;
m_uploadByteDevice = nullptr;
}
void QHttp2Stream::setState(State newState)
{
if (m_state == newState)
return;
qCDebug(qHttp2ConnectionLog, "[%p] stream %u, state changed from %d to %d", getConnection(),
streamID(), int(m_state), int(newState));
m_state = newState;
emit stateChanged(newState);
if (m_state == State::Closed)
getConnection()->maybeCloseOnGoingAway();
}
// Changes the state as appropriate given the current state and the transition.
// Always call this before emitting any signals since the recipient might rely
// on the new state!
void QHttp2Stream::transitionState(StateTransition transition)
{
switch (m_state) {
case State::Idle:
if (transition == StateTransition::Open)
setState(State::Open);
else
Q_UNREACHABLE(); // We should transition to Open before ever getting here
break;
case State::Open:
switch (transition) {
case StateTransition::CloseLocal:
setState(State::HalfClosedLocal);
break;
case StateTransition::CloseRemote:
setState(State::HalfClosedRemote);
break;
case StateTransition::RST:
setState(State::Closed);
break;
case StateTransition::Open: // no-op
break;
}
break;
case State::HalfClosedLocal:
if (transition == StateTransition::CloseRemote || transition == StateTransition::RST)
setState(State::Closed);
break;
case State::HalfClosedRemote:
if (transition == StateTransition::CloseLocal || transition == StateTransition::RST)
setState(State::Closed);
break;
case State::ReservedRemote:
if (transition == StateTransition::RST) {
setState(State::Closed);
} else if (transition == StateTransition::CloseLocal) { // Receiving HEADER closes local
setState(State::HalfClosedLocal);
}
break;
case State::Closed:
break;
}
}
void QHttp2Stream::handleDATA(const Frame &inboundFrame)
{
QHttp2Connection *connection = getConnection();
qCDebug(qHttp2ConnectionLog,
"[%p] stream %u, received DATA frame with payload of %u bytes, closing stream? %s",
connection, m_streamID, inboundFrame.payloadSize(),
inboundFrame.flags().testFlag(Http2::FrameFlag::END_STREAM) ? "yes" : "no");
// RFC 9113, 6.1: If a DATA frame is received whose stream is not in the "open" or "half-closed
// (local)" state, the recipient MUST respond with a stream error (Section 5.4.2) of type
// STREAM_CLOSED;
// checked in QHttp2Connection
Q_ASSERT(state() != State::HalfClosedRemote && state() != State::Closed);
if (qint32(inboundFrame.payloadSize()) > m_recvWindow) {
qCDebug(qHttp2ConnectionLog,
"[%p] stream %u, received DATA frame with payload size %u, "
"but recvWindow is %d, sending FLOW_CONTROL_ERROR",
connection, m_streamID, inboundFrame.payloadSize(), m_recvWindow);
return streamError(FLOW_CONTROL_ERROR, u"data bigger than window size"_s);
}
// RFC 9113, 6.1: The total number of padding octets is determined by the value of the Pad
// Length field. If the length of the padding is the length of the frame payload or greater,
// the recipient MUST treat this as a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
// checked in Framereader
Q_ASSERT(inboundFrame.buffer.size() >= frameHeaderSize);
Q_ASSERT(inboundFrame.payloadSize() + frameHeaderSize == inboundFrame.buffer.size());
m_recvWindow -= qint32(inboundFrame.payloadSize());
const bool endStream = inboundFrame.flags().testFlag(FrameFlag::END_STREAM);
const bool ignoreData = connection->streamIsIgnored(m_streamID);
// Uncompress data if needed and append it ...
if ((inboundFrame.dataSize() > 0 || endStream) && !ignoreData) {
QByteArray fragment(reinterpret_cast<const char *>(inboundFrame.dataBegin()),
inboundFrame.dataSize());
if (endStream)
transitionState(StateTransition::CloseRemote);
const auto shouldBuffer = m_configuration.useDownloadBuffer && !fragment.isEmpty();
QPointer<QHttp2Stream> self(this);
if (shouldBuffer) {
// Only non-empty fragments get appended!
m_downloadBuffer.append(std::move(fragment));
emit dataReceived(m_downloadBuffer.last(), endStream);
} else {
emit dataReceived(fragment, endStream);
}
if (!self)
return;
}
if (!endStream && m_recvWindow < connection->streamInitialReceiveWindowSize / 2) {
// @future[consider]: emit signal instead
sendWINDOW_UPDATE(quint32(connection->streamInitialReceiveWindowSize - m_recvWindow));
}
}
void QHttp2Stream::handleHEADERS(Http2::FrameFlags frameFlags, const HPack::HttpHeader &headers)
{
if (m_state == State::Idle)
transitionState(StateTransition::Open);
const bool endStream = frameFlags.testFlag(FrameFlag::END_STREAM);
if (endStream)
transitionState(StateTransition::CloseRemote);
if (!headers.empty() && m_configuration.useHeaderBuffer) {
m_headers.insert(m_headers.end(), headers.begin(), headers.end());
emit headersUpdated();
}
emit headersReceived(headers, endStream);
}
void QHttp2Stream::handleRST_STREAM(const Frame &inboundFrame)
{
if (m_state == State::Closed) // The stream is already closed, we're not sending anything anyway
return;
transitionState(StateTransition::RST);
m_RST_STREAM_received = qFromBigEndian<quint32>(inboundFrame.dataBegin());
if (isUploadingDATA()) {
disconnect(m_uploadByteDevice, nullptr, this, nullptr);
m_uploadDevice = nullptr;
m_uploadByteDevice = nullptr;
}
finishWithError(Http2Error(*m_RST_STREAM_received));
}
void QHttp2Stream::handleWINDOW_UPDATE(const Frame &inboundFrame)
{
const quint32 delta = qFromBigEndian<quint32>(inboundFrame.dataBegin());
const bool valid = delta && delta <= quint32(std::numeric_limits<qint32>::max());
if (!valid) {
// RFC 9113, 6.9.1: a flow-control window increment of 0 is a stream error of
// type PROTOCOL_ERROR.
qCDebug(qHttp2ConnectionLog,
"[%p] stream %u, received WINDOW_UPDATE frame with invalid delta %u, sending "
"PROTOCOL_ERROR",
getConnection(), m_streamID, delta);
return streamError(PROTOCOL_ERROR, u"invalid WINDOW_UPDATE delta"_s);
}
qint32 sum = 0;
if (qAddOverflow(m_sendWindow, qint32(delta), &sum)) {
// RFC 9113, 6.9.1: a WINDOW_UPDATE that pushes the window past 2^31-1 is a stream
// error of type FLOW_CONTROL_ERROR (the sender sends RST_STREAM).
qCDebug(qHttp2ConnectionLog,
"[%p] stream %u, WINDOW_UPDATE delta %u overflows the flow-control window, "
"sending FLOW_CONTROL_ERROR",
getConnection(), m_streamID, delta);
return streamError(FLOW_CONTROL_ERROR, u"WINDOW_UPDATE exceeds maximum window"_s);
}
m_sendWindow = sum;
// Stream may have been unblocked, so maybe try to write again
if (isUploadingDATA())
maybeResumeUpload();
}
/*!
\class QHttp2Connection
\inmodule QtNetwork
\internal
The QHttp2Connection class represents a HTTP/2 connection.
It can only be created through the static functions
createDirectConnection(), createUpgradedConnection(),
and createDirectServerConnection().
createDirectServerConnection() is used for server-side connections, and has
certain limitations that a client does not.
As a client you can create a QHttp2Stream with createStream().
\sa QHttp2Stream
*/
/*!
\fn void QHttp2Connection::newIncomingStream(QHttp2Stream *stream)
This signal is emitted when a new \a stream is received from the remote
peer.
*/
/*!
\fn void QHttp2Connection::newPromisedStream(QHttp2Stream *stream)
This signal is emitted when the remote peer has promised a new \a stream.
*/
/*!
\fn void QHttp2Connection::errorReceived()
This signal is emitted when the connection has received an error.
*/
/*!
\fn void QHttp2Connection::connectionClosed()
This signal is emitted when the connection has been closed.
*/
/*!
\fn void QHttp2Connection::settingsFrameReceived()
This signal is emitted when the connection has received a SETTINGS frame.
*/
/*!
\fn void QHttp2Connection::errorOccurred(Http2::Http2Error errorCode, const QString &errorString)
This signal is emitted when the connection has encountered an error. The
\a errorCode parameter is the HTTP/2 error code, and the \a errorString
parameter is a human-readable description of the error.
*/
/*!
\fn void QHttp2Connection::receivedGOAWAY(Http2::Http2Error errorCode, quint32 lastStreamID)
This signal is emitted when the connection has received a GOAWAY frame. The
\a errorCode parameter is the HTTP/2 error code, and the \a lastStreamID
parameter is the last stream ID that the remote peer will process.
Any streams of a higher stream ID created by us will be ignored or reset.
*/
/*!
Create a new HTTP2 connection given a \a config and a \a socket.
This function assumes that the Upgrade headers etc. in http/1 have already
been sent and that the connection is already upgraded to http/2.
The object returned will be a child to the \a socket, or null on failure.
*/
QHttp2Connection *QHttp2Connection::createUpgradedConnection(QIODevice *socket,
const QHttp2Configuration &config)
{
Q_ASSERT(socket);
auto connection = std::unique_ptr<QHttp2Connection>(new QHttp2Connection(socket));
connection->setH2Configuration(config);
connection->m_connectionType = QHttp2Connection::Type::Client;
connection->m_upgradedConnection = true;
// HTTP2 connection is already established and request was sent, so stream 1
// is already 'active' and is closed for any further outgoing data.
QHttp2Stream *stream = connection->createLocalStreamInternal().unwrap();
Q_ASSERT(stream->streamID() == 1);
stream->setState(QHttp2Stream::State::HalfClosedLocal);
if (!connection->m_prefaceSent) // Preface is sent as part of initial stream-creation.
return nullptr;
return connection.release();
}
/*!
Create a new HTTP2 connection given a \a config and a \a socket.
This function will immediately send the client preface.
The object returned will be a child to the \a socket, or null on failure.
*/
QHttp2Connection *QHttp2Connection::createDirectConnection(QIODevice *socket,
const QHttp2Configuration &config)
{
auto connection = std::unique_ptr<QHttp2Connection>(new QHttp2Connection(socket));
connection->setH2Configuration(config);
connection->m_connectionType = QHttp2Connection::Type::Client;
return connection.release();
}
/*!
Create a new HTTP2 connection given a \a config and a \a socket.
The object returned will be a child to the \a socket, or null on failure.
*/
QHttp2Connection *QHttp2Connection::createDirectServerConnection(QIODevice *socket,
const QHttp2Configuration &config)
{
auto connection = std::unique_ptr<QHttp2Connection>(new QHttp2Connection(socket));
connection->setH2Configuration(config);
connection->m_connectionType = QHttp2Connection::Type::Server;
connection->m_nextStreamID = 2; // server-initiated streams must be even
connection->m_waitingForClientPreface = true;
return connection.release();
}
/*!
\fn QH2Expected<QHttp2Stream *, QHttp2Connection::CreateStreamError> QHttp2Connection::createStream()
Creates a stream on this connection, using the default QHttp2Stream::Configuration.
//! [createStream]
Automatically picks the next available stream ID and returns a pointer to
the new stream, if possible. Otherwise returns an error.
\sa QHttp2Connection::CreateStreamError, QHttp2Stream
//! [createStream]
\sa createStream(QHttp2Stream::Configuration)
*/
/*!
Creates a stream with \a configuration on this connection.
\include qhttp2connection.cpp createStream
*/
QH2Expected<QHttp2Stream *, QHttp2Connection::CreateStreamError>
QHttp2Connection::createStream(QHttp2Stream::Configuration configuration)
{
Q_ASSERT(m_connectionType == Type::Client); // This overload is just for clients
if (m_nextStreamID > lastValidStreamID)
return { QHttp2Connection::CreateStreamError::StreamIdsExhausted };
return createLocalStreamInternal(configuration);
}
QH2Expected<QHttp2Stream *, QHttp2Connection::CreateStreamError>
QHttp2Connection::createLocalStreamInternal(QHttp2Stream::Configuration conf)
{
if (m_goingAway)
return { QHttp2Connection::CreateStreamError::ReceivedGOAWAY };
const quint32 streamID = m_nextStreamID;
if (size_t(m_peerMaxConcurrentStreams) <= size_t(numActiveLocalStreams()))
return { QHttp2Connection::CreateStreamError::MaxConcurrentStreamsReached };
if (QHttp2Stream *ptr = createStreamInternal_impl(streamID, conf)) {
m_nextStreamID += 2;
return {ptr};
}
// Connection could be broken, we could've ran out of memory, we don't know
return { QHttp2Connection::CreateStreamError::UnknownError };
}
QHttp2Stream *QHttp2Connection::createStreamInternal_impl(quint32 streamID,
QHttp2Stream::Configuration conf)
{
Q_ASSERT(streamID > m_lastIncomingStreamID || streamID >= m_nextStreamID);
if (m_connectionType == Type::Client && !m_prefaceSent && !sendClientPreface()) {
qCWarning(qHttp2ConnectionLog, "[%p] Failed to send client preface", this);
return nullptr;
}
auto result = m_streams.tryEmplace(streamID, nullptr);
if (!result.inserted)
return nullptr;
QPointer<QHttp2Stream> &stream = result.iterator.value();
stream = new QHttp2Stream(this, streamID, conf);
stream->m_recvWindow = streamInitialReceiveWindowSize;
stream->m_sendWindow = streamInitialSendWindowSize;
connect(stream, &QHttp2Stream::uploadBlocked, this, [this, stream] {
m_blockedStreams.insert(stream->streamID());
});
*result.iterator = stream;
return *result.iterator;
}
qsizetype QHttp2Connection::numActiveStreamsImpl(quint32 mask) const noexcept
{
const auto shouldCount = [mask](const QPointer<QHttp2Stream> &stream) -> bool {
return stream && (stream->streamID() & 1) == mask && stream->isActive();
};
return std::count_if(m_streams.cbegin(), m_streams.cend(), shouldCount);
}
/*!
\internal
The number of streams the remote peer has started that are still active.
*/
qsizetype QHttp2Connection::numActiveRemoteStreams() const noexcept
{
const quint32 RemoteMask = m_connectionType == Type::Client ? 0 : 1;
return numActiveStreamsImpl(RemoteMask);
}
/*!
\internal
The number of streams we have started that are still active.
*/
qsizetype QHttp2Connection::numActiveLocalStreams() const noexcept
{
const quint32 LocalMask = m_connectionType == Type::Client ? 1 : 0;
return numActiveStreamsImpl(LocalMask);
}
/*!
Return a pointer to a stream with the given \a streamID, or null if no such
stream exists or it was deleted.
*/
QHttp2Stream *QHttp2Connection::getStream(quint32 streamID) const
{
return m_streams.value(streamID, nullptr).get();
}
/*!
Initiates connection shutdown. When \a errorCode is \c{NO_ERROR}, graceful
shutdown is initiated, allowing existing streams to complete. Otherwise the
connection is closed immediately with an error.
*/
void QHttp2Connection::close(Http2::Http2Error errorCode)
{
if (m_connectionAborted)
return;
if (errorCode == Http2::HTTP2_NO_ERROR) {
if (m_connectionType == Type::Server)
sendInitialServerGracefulShutdownGoaway();
else
sendClientGracefulShutdownGoaway();
} else {
// RFC 9113, 5.4.1: After sending the GOAWAY frame for an error
// condition, the endpoint MUST close the TCP connection
connectionError(errorCode, u"Connection closed with error"_s, false);
}
}
/*!
\fn QHttp2Stream *QHttp2Connection::promisedStream(const QUrl &streamKey) const
Returns a pointer to the stream that was promised with the given
\a streamKey, if any. Otherwise, returns null.
*/
/*!
\fn bool QHttp2Connection::isGoingAway() const noexcept
Returns \c true if the connection is in the process of being closed, or
\c false otherwise.
*/
/*!
\fn quint32 QHttp2Connection::maxConcurrentStreams() const noexcept
Returns the maximum number of concurrent streams we are allowed to have
active at any given time. This is a directional setting, and the remote
peer may have a different value.
*/
/*!
\fn quint32 QHttp2Connection::maxHeaderListSize() const noexcept
Returns the maximum size of the header which the peer is willing to accept.
*/
/*!
\fn bool QHttp2Connection::isUpgradedConnection() const noexcept
Returns \c true if this connection was created as a result of an HTTP/1
upgrade to HTTP/2, or \c false otherwise.
*/
QHttp2Connection::QHttp2Connection(QIODevice *socket) : QObject(socket)
{
Q_ASSERT(socket);
Q_ASSERT(socket->isOpen());
Q_ASSERT(socket->openMode() & QIODevice::ReadWrite);
// We don't make any connections directly because this is used in
// in the http2 protocol handler, which is used by
// QHttpNetworkConnectionChannel. Which in turn owns and deals with all the
// socket connections.
}
QHttp2Connection::~QHttp2Connection()
{
// delete streams now so that any calls it might make back to this
// Connection will operate on a valid object.
for (QPointer<QHttp2Stream> &stream : std::exchange(m_streams, {}))
delete stream.get();
}
bool QHttp2Connection::serverCheckClientPreface()
{
if (!m_waitingForClientPreface)
return true;
auto *socket = getSocket();
if (socket->bytesAvailable() < Http2::clientPrefaceLength)
return false;
if (!readClientPreface()) {
socket->close();
emit errorOccurred(Http2Error::PROTOCOL_ERROR, u"invalid client preface"_s);
qCDebug(qHttp2ConnectionLog, "[%p] Invalid client preface", this);
return false;
}
qCDebug(qHttp2ConnectionLog, "[%p] Peer sent valid client preface", this);
m_waitingForClientPreface = false;
if (!sendServerPreface()) {
connectionError(INTERNAL_ERROR, u"Failed to send server preface"_s);
return false;
}
return true;
}
bool QHttp2Connection::sendPing()
{
std::array<char, 8> data;
QRandomGenerator gen;
gen.generate(data.begin(), data.end());
return sendPing(data);
}
bool QHttp2Connection::sendPing(QByteArrayView data)
{
frameWriter.start(FrameType::PING, FrameFlag::EMPTY, connectionStreamID);
Q_ASSERT(data.length() == 8);
if (!m_lastPingSignature) {
m_lastPingSignature = data.toByteArray();
} else {
qCWarning(qHttp2ConnectionLog, "[%p] No PING is sent while waiting for the previous PING.", this);
return false;
}
frameWriter.append((uchar*)data.data(), (uchar*)data.end());
frameWriter.write(*getSocket());
return true;
}
/*!
This function must be called when you have received a readyRead signal
(or equivalent) from the QIODevice. It will read and process any incoming
HTTP/2 frames and emit signals as appropriate.
*/
void QHttp2Connection::handleReadyRead()
{
/* event loop */
if (m_connectionType == Type::Server && !serverCheckClientPreface())
return;
QIODevice *socket = getSocket();
if (!socket->isOpen())
return;
qCDebug(qHttp2ConnectionLog, "[%p] Receiving data, %lld bytes available", this,
socket->bytesAvailable());
using namespace Http2;
if (!m_prefaceSent)
return;
while (!m_connectionAborted && socket->isOpen()) {
const auto result = frameReader.read(*socket);
if (result != FrameStatus::goodFrame)
qCDebug(qHttp2ConnectionLog, "[%p] Tried to read frame, got %d", this, int(result));
switch (result) {
case FrameStatus::incompleteFrame:
return; // No more complete frames available
case FrameStatus::protocolError:
return connectionError(PROTOCOL_ERROR, u"invalid frame"_s);
case FrameStatus::sizeError: {
const auto streamID = frameReader.inboundFrame().streamID();
const auto frameType = frameReader.inboundFrame().type();
auto stream = getStream(streamID);
// RFC 9113, 4.2: A frame size error in a frame that could alter the state of the
// entire connection MUST be treated as a connection error (Section 5.4.1); this
// includes any frame carrying a field block (Section 4.3) (that is, HEADERS,
// PUSH_PROMISE, and CONTINUATION), a SETTINGS frame, and any frame with a stream
// identifier of 0.
if (frameType == FrameType::HEADERS ||
frameType == FrameType::SETTINGS ||
frameType == FrameType::PUSH_PROMISE ||
frameType == FrameType::CONTINUATION ||
// never reply RST_STREAM with RST_STREAM
frameType == FrameType::RST_STREAM ||
streamID == connectionStreamID)
return connectionError(FRAME_SIZE_ERROR, u"invalid frame size"_s);
// DATA; PRIORITY; WINDOW_UPDATE
if (stream)
return stream->streamError(Http2Error::FRAME_SIZE_ERROR, u"invalid frame size"_s);
else
return; // most likely a closed and deleted stream. Can be ignored.
}
default:
break;
}
Q_ASSERT(result == FrameStatus::goodFrame);
inboundFrame = std::move(frameReader.inboundFrame());
const auto frameType = inboundFrame.type();
qCDebug(qHttp2ConnectionLog, "[%p] Successfully read a frame, with type: %d", this,
int(frameType));
// RFC 9113, 6.2/6.6: A HEADERS/PUSH_PROMISE frame without the END_HEADERS flag set MUST be
// followed by a CONTINUATION frame for the same stream. A receiver MUST treat the
// receipt of any other type of frame or a frame on a different stream as a
// connection error
if (continuationExpected && frameType != FrameType::CONTINUATION)
return connectionError(PROTOCOL_ERROR, u"CONTINUATION expected"_s);
switch (frameType) {
case FrameType::DATA:
handleDATA();
break;
case FrameType::HEADERS:
handleHEADERS();
break;
case FrameType::PRIORITY:
handlePRIORITY();
break;
case FrameType::RST_STREAM:
handleRST_STREAM();
break;
case FrameType::SETTINGS:
handleSETTINGS();
break;
case FrameType::PUSH_PROMISE:
handlePUSH_PROMISE();
break;
case FrameType::PING:
handlePING();
break;
case FrameType::GOAWAY:
handleGOAWAY();
break;
case FrameType::WINDOW_UPDATE:
handleWINDOW_UPDATE();
break;
case FrameType::CONTINUATION:
handleCONTINUATION();
break;
case FrameType::LAST_FRAME_TYPE:
// 5.1 - ignore unknown frames.
break;
}
}
}
bool QHttp2Connection::readClientPreface()
{
auto *socket = getSocket();
Q_ASSERT(socket->bytesAvailable() >= Http2::clientPrefaceLength);
char buffer[Http2::clientPrefaceLength];
const qint64 read = socket->read(buffer, Http2::clientPrefaceLength);
if (read != Http2::clientPrefaceLength)
return false;
return memcmp(buffer, Http2::Http2clientPreface, Http2::clientPrefaceLength) == 0;
}
/*!
This function must be called when the socket has been disconnected, and will
end all remaining streams with an error.
*/
void QHttp2Connection::handleConnectionClosure()
{
const auto errorString = QCoreApplication::translate("QHttp", "Connection closed");
const QList<quint32> streamIDs = m_streams.keys();
for (quint32 streamID : streamIDs) {
QHttp2Stream *stream = m_streams.value(streamID, nullptr);
if (stream && stream->isActive())
stream->finishWithError(PROTOCOL_ERROR, errorString);
}
}
void QHttp2Connection::setH2Configuration(QHttp2Configuration config)
{
m_config = std::move(config);
// These values comes from our own API so trust it to be sane.
maxSessionReceiveWindowSize = qint32(m_config.sessionReceiveWindowSize());
pushPromiseEnabled = m_config.serverPushEnabled();
streamInitialReceiveWindowSize = qint32(m_config.streamReceiveWindowSize());
m_maxConcurrentStreams = m_config.maxConcurrentStreams();
encoder.setCompressStrings(m_config.huffmanCompressionEnabled());
decoder.setMaxHeaderListSize(
QHttp2ConfigurationPrivate::get(std::as_const(m_config))->maxHeaderListSize);
}
void QHttp2Connection::connectionError(Http2Error errorCode, const QString &message,
bool logAsError)
{
if (m_connectionAborted)
return;
m_connectionAborted = true;
if (logAsError) {
qCCritical(qHttp2ConnectionLog, "[%p] Connection error: %ls (%d)", this,
qUtf16Printable(message), int(errorCode));
} else {
qCDebug(qHttp2ConnectionLog, "[%p] Closing connection: %ls (%d)", this,
qUtf16Printable(message), int(errorCode));
}
// Mark going away so other code paths will stop creating new streams
m_goingAway = true;
// RFC 9113 5.4.1: An endpoint that encounters a connection error SHOULD
// first send a GOAWAY frame with the last incoming stream ID.
m_lastStreamToProcess = std::min(m_lastIncomingStreamID, m_lastStreamToProcess);
sendGOAWAYFrame(errorCode, m_lastStreamToProcess);
const QList<quint32> streamIDs = m_streams.keys();
for (quint32 streamID : streamIDs) {
QHttp2Stream *stream = m_streams.value(streamID, nullptr);
if (stream && stream->isActive())
stream->finishWithError(errorCode, message);
}
emit errorOccurred(errorCode, message);
// RFC 9113 5.4.1: After sending the GOAWAY frame for an error condition,
// the endpoint MUST close the TCP connection
closeSession();
}
void QHttp2Connection::closeSession()
{
emit connectionClosed();
}
bool QHttp2Connection::streamWasResetLocally(quint32 streamID) noexcept
{
return m_resetStreamIDs.contains(streamID);
}
void QHttp2Connection::registerStreamAsResetLocally(quint32 streamID)
{
// RFC 9113, 6.4: However, after sending the RST_STREAM, the sending endpoint MUST be prepared
// to receive and process additional frames sent on the stream that might have been sent by the
// peer prior to the arrival of the RST_STREAM.
// Store the last 100 stream ids that were reset locally. Frames received on these streams
// are still considered valid for some time (Until 100 other streams are reset locally).
m_resetStreamIDs.append(streamID);
while (m_resetStreamIDs.size() > 100)
m_resetStreamIDs.takeFirst();
}
bool QHttp2Connection::isInvalidStream(quint32 streamID) noexcept
{
auto stream = m_streams.value(streamID, nullptr);
return (!stream || stream->wasResetbyPeer()) && !streamWasResetLocally(streamID);
}
/*!
When we send a GOAWAY we also send the ID of the last stream we know about
at the time. Any stream that starts after this one is ignored, but we still
have to process HEADERS due to compression state, and DATA due to stream and
connection window size changes.
Other than that - any \a streamID for which this returns true should be
ignored, and deleted at the earliest convenience.
*/
bool QHttp2Connection::streamIsIgnored(quint32 streamID) const noexcept
{
const bool streamIsRemote = (streamID & 1) == (m_connectionType == Type::Client ? 0 : 1);
return Q_UNLIKELY(streamIsRemote && m_lastStreamToProcess < streamID);
}
bool QHttp2Connection::sendClientPreface()
{
QIODevice *socket = getSocket();
// 3.5 HTTP/2 Connection Preface
const qint64 written = socket->write(Http2clientPreface, clientPrefaceLength);
if (written != clientPrefaceLength)
return false;
if (!sendSETTINGS()) {
qCWarning(qHttp2ConnectionLog, "[%p] Failed to send SETTINGS", this);
return false;
}
m_prefaceSent = true;
if (socket->bytesAvailable()) // We ignore incoming data until preface is sent, so handle it now
QMetaObject::invokeMethod(this, &QHttp2Connection::handleReadyRead, Qt::QueuedConnection);
return true;
}
bool QHttp2Connection::sendServerPreface()
{
// We send our SETTINGS frame and ACK the client's SETTINGS frame when it
// arrives.
if (!sendSETTINGS()) {
qCWarning(qHttp2ConnectionLog, "[%p] Failed to send SETTINGS", this);
return false;
}
m_prefaceSent = true;
return true;
}
bool QHttp2Connection::sendSETTINGS()
{
QIODevice *socket = getSocket();
// 6.5 SETTINGS
frameWriter.setOutboundFrame(configurationToSettingsFrame(m_config));
qCDebug(qHttp2ConnectionLog, "[%p] Sending SETTINGS frame, %d bytes", this,
frameWriter.outboundFrame().payloadSize());
Q_ASSERT(frameWriter.outboundFrame().payloadSize());
if (!frameWriter.write(*socket))
return false;
sessionReceiveWindowSize = maxSessionReceiveWindowSize;
// We only send WINDOW_UPDATE for the connection if the size differs from the
// default 64 KB:
const auto delta = maxSessionReceiveWindowSize - defaultSessionWindowSize;
if (delta && !sendWINDOW_UPDATE(connectionStreamID, delta))
return false;
waitingForSettingsACK = true;
return true;
}
bool QHttp2Connection::sendWINDOW_UPDATE(quint32 streamID, quint32 delta)
{
qCDebug(qHttp2ConnectionLog, "[%p] Sending WINDOW_UPDATE frame, stream %d, delta %u", this,
streamID, delta);
frameWriter.start(FrameType::WINDOW_UPDATE, FrameFlag::EMPTY, streamID);
frameWriter.append(delta);
return frameWriter.write(*getSocket());
}
/*!
\fn quint64 QHttp2Connection::totalBytesReceivedDATA() const
Returns the running total of flow-controlled DATA payload octets received on
this connection. This is the sampling signal for bandwidth-delay-product
based receive-window auto-tuning.
*/
/*!
Sets the connection-level (session) receive window to \a size on a live
connection. \a size must be between 1 and 2^31-1 inclusive; returns \c false
otherwise.
Growing the window sends a WINDOW_UPDATE on the connection stream. Shrinking
only lowers the ceiling and lets the window drain, since HTTP/2 has no
negative WINDOW_UPDATE.
*/
bool QHttp2Connection::setSessionReceiveWindowSize(qint32 size)
{
if (size <= 0) {
qCWarning(qHttp2ConnectionLog, "[%p] Invalid session receive window size: %d", this, size);
return false;
}
if (size <= maxSessionReceiveWindowSize) {
// No negative WINDOW_UPDATE exists, so just lower the ceiling and let the window drain.
maxSessionReceiveWindowSize = size;
return true;
}
const qint32 delta = size - maxSessionReceiveWindowSize;
maxSessionReceiveWindowSize = size;
sessionReceiveWindowSize += delta;
return sendWINDOW_UPDATE(connectionStreamID, quint32(delta));
}
void QHttp2Connection::sendClientGracefulShutdownGoaway()
{
// Clients send a single GOAWAY. No race condition since they control stream creation
Q_ASSERT(m_connectionType == Type::Client);
if (m_connectionAborted || m_goingAway) {
qCWarning(qHttp2ConnectionLog, "[%p] Client graceful shutdown already in progress", this);
return;
}
m_goingAway = true;
m_gracefulShutdownState = GracefulShutdownState::FinalGOAWAYSent;
m_lastStreamToProcess = m_lastIncomingStreamID;
sendGOAWAYFrame(Http2::HTTP2_NO_ERROR, m_lastStreamToProcess);
maybeCloseOnGoingAway();
}
void QHttp2Connection::sendInitialServerGracefulShutdownGoaway()
{
Q_ASSERT(m_connectionType == Type::Server);
// RFC 9113, 6.8: A server that is attempting to gracefully shut down a
// connection SHOULD send an initial GOAWAY frame with the last stream
// identifier set to 2^31-1 and a NO_ERROR code.
if (m_connectionAborted || m_goingAway) {
qCWarning(qHttp2ConnectionLog, "[%p] Server graceful shutdown already in progress", this);
return;
}
m_goingAway = true;
m_goawayGraceTimer.setRemainingTime(GoawayGracePeriod);
sendGOAWAYFrame(Http2::HTTP2_NO_ERROR, Http2::lastValidStreamID);
// Send PING to measure RTT; handlePING() continues the shutdown on ACK.
// RFC 9113 6.8: After allowing time for any in-flight stream creation
// (at least one round-trip time)
if (sendPing())
m_gracefulShutdownState = GracefulShutdownState::AwaitingShutdownPing;
else
m_gracefulShutdownState = GracefulShutdownState::AwaitingPriorPing;
}
void QHttp2Connection::sendFinalServerGracefulShutdownGoaway()
{
if (m_connectionAborted || !m_goingAway) {
qCWarning(qHttp2ConnectionLog, "[%p] Server graceful shutdown not in progress", this);
return;
}
m_gracefulShutdownState = GracefulShutdownState::FinalGOAWAYSent;
m_lastStreamToProcess = m_lastIncomingStreamID;
sendGOAWAYFrame(Http2::HTTP2_NO_ERROR, m_lastStreamToProcess);
maybeCloseOnGoingAway();
}
bool QHttp2Connection::sendGOAWAYFrame(Http2::Http2Error errorCode, quint32 lastStreamID)
{
QIODevice *socket = getSocket();
if (!socket || !socket->isOpen())
return false;
qCDebug(qHttp2ConnectionLog, "[%p] Sending GOAWAY frame, error code %u, last stream %u", this,
errorCode, lastStreamID);
frameWriter.start(FrameType::GOAWAY, FrameFlag::EMPTY,
Http2PredefinedParameters::connectionStreamID);
frameWriter.append(lastStreamID);
frameWriter.append(quint32(errorCode));
return frameWriter.write(*socket);
}
void QHttp2Connection::maybeCloseOnGoingAway()
{
// Only close if we've reached the final phase of graceful shutdown
// For the sender: after FinalGOAWAYSent
// For the receiver: after receiving GOAWAY and all our streams are done
if (m_connectionAborted || !m_goingAway) {
qCDebug(qHttp2ConnectionLog, "[%p] Connection close deferred, graceful shutdown not active",
this);
return;
}
// For graceful shutdown initiator, only close after final GOAWAY is sent
if (m_gracefulShutdownState == GracefulShutdownState::AwaitingShutdownPing)
return; // Still waiting for RTT measurement before final GOAWAY
const auto streamIsActive = [](const QPointer<QHttp2Stream> &stream) {
return stream && stream->isActive();
};
if (std::none_of(m_streams.cbegin(), m_streams.cend(), streamIsActive)) {
qCDebug(qHttp2ConnectionLog, "[%p] All streams closed, closing connection", this);
closeSession();
}
}
bool QHttp2Connection::sendSETTINGS_ACK()
{
frameWriter.start(FrameType::SETTINGS, FrameFlag::ACK, Http2::connectionStreamID);
return frameWriter.write(*getSocket());
}
void QHttp2Connection::handleDATA()
{
Q_ASSERT(inboundFrame.type() == FrameType::DATA);
const auto streamID = inboundFrame.streamID();
// RFC9113, 6.1: An endpoint that receives an unexpected stream identifier MUST respond
// with a connection error.
if (streamID == connectionStreamID)
return connectionError(PROTOCOL_ERROR, u"DATA on the connection stream"_s);
if (isInvalidStream(streamID))
return connectionError(ENHANCE_YOUR_CALM, u"DATA on invalid stream"_s);
QHttp2Stream *stream = nullptr;
if (!streamWasResetLocally(streamID)) {
stream = getStream(streamID);
// RFC9113, 6.1: If a DATA frame is received whose stream is not in the "open" or
// "half-closed (local)" state, the recipient MUST respond with a stream error.
if (stream->state() == QHttp2Stream::State::HalfClosedRemote
|| stream->state() == QHttp2Stream::State::Closed) {
return stream->streamError(Http2Error::STREAM_CLOSED, u"Data on closed stream"_s);
}
}
if (inboundFrame.payloadSize() > m_config.maxFrameSize()) {
qCDebug(qHttp2ConnectionLog,
"[%p] Received DATA frame with payload size %u, "
"but SETTINGS_MAX_FRAME_SIZE is %u, sending FRAME_SIZE_ERROR",
this, inboundFrame.payloadSize(), m_config.maxFrameSize());
if (stream)
return stream->streamError(Http2Error::FRAME_SIZE_ERROR,
u"DATA payload size exceeds SETTINGS_MAX_FRAME_SIZE"_s);
return connectionError(FRAME_SIZE_ERROR, u"DATA payload size exceeds SETTINGS_MAX_FRAME_SIZE"_s);
}
if (qint32(inboundFrame.payloadSize()) > sessionReceiveWindowSize) {
qCDebug(qHttp2ConnectionLog,
"[%p] Received DATA frame with payload size %u, "
"but recvWindow is %d, sending FLOW_CONTROL_ERROR",
this, inboundFrame.payloadSize(), sessionReceiveWindowSize);
return connectionError(FLOW_CONTROL_ERROR, u"Flow control error"_s);
}
sessionReceiveWindowSize -= inboundFrame.payloadSize();
m_totalBytesReceivedDATA += inboundFrame.payloadSize();
if (stream)
stream->handleDATA(inboundFrame);
if (inboundFrame.flags().testFlag(FrameFlag::END_STREAM)) {
const bool ignoreData = stream && streamIsIgnored(stream->streamID());
if (!ignoreData) {
emit receivedEND_STREAM(streamID);
} else {
// Stream opened after our GOAWAY cut-off. We would just drop the
// data, but needed to handle it enough to track sizes of streams and
// connection windows. Since we've now taken care of that, we can
// at last close and delete it.
stream->setState(QHttp2Stream::State::Closed);
delete stream;
}
}
if (sessionReceiveWindowSize < maxSessionReceiveWindowSize / 2) {
// @future[consider]: emit signal instead
QMetaObject::invokeMethod(this, &QHttp2Connection::sendWINDOW_UPDATE, Qt::QueuedConnection,
quint32(connectionStreamID),
quint32(maxSessionReceiveWindowSize - sessionReceiveWindowSize));
sessionReceiveWindowSize = maxSessionReceiveWindowSize;
}
}
void QHttp2Connection::handleHEADERS()
{
Q_ASSERT(inboundFrame.type() == FrameType::HEADERS);
const auto streamID = inboundFrame.streamID();
qCDebug(qHttp2ConnectionLog, "[%p] Received HEADERS frame on stream %d, end stream? %s", this,
streamID, inboundFrame.flags().testFlag(Http2::FrameFlag::END_STREAM) ? "yes" : "no");
// RFC 9113, 6.2: If a HEADERS frame is received whose Stream Identifier field is 0x00, the
// recipient MUST respond with a connection error.
if (streamID == connectionStreamID)
return connectionError(PROTOCOL_ERROR, u"HEADERS on 0x0 stream"_s);
if (inboundFrame.payloadSize() > m_config.maxFrameSize()) {
qCDebug(qHttp2ConnectionLog,
"[%p] Received HEADERS frame with payload size %u, "
"but SETTINGS_MAX_FRAME_SIZE is %u, sending FRAME_SIZE_ERROR",
this, inboundFrame.payloadSize(), m_config.maxFrameSize());
return connectionError(Http2Error::FRAME_SIZE_ERROR,
u"HEADERS payload size exceeds SETTINGS_MAX_FRAME_SIZE"_s);
}
const bool isClient = m_connectionType == Type::Client;
const bool isClientInitiatedStream = !!(streamID & 1);
const bool isRemotelyInitiatedStream = isClient ^ isClientInitiatedStream;
if (isRemotelyInitiatedStream && streamID > m_lastIncomingStreamID) {
bool streamCountIsOk = size_t(m_maxConcurrentStreams) > size_t(numActiveRemoteStreams());
QHttp2Stream *newStream = createStreamInternal_impl(streamID);
Q_ASSERT(newStream);
m_lastIncomingStreamID = streamID;
if (!streamCountIsOk) {
newStream->setState(QHttp2Stream::State::Open);
newStream->streamError(PROTOCOL_ERROR, u"Max concurrent streams reached"_s);
emit incomingStreamErrorOccured(CreateStreamError::MaxConcurrentStreamsReached);
return;
}
qCDebug(qHttp2ConnectionLog, "[%p] New incoming stream %d", this, streamID);
if (!streamIsIgnored(newStream->streamID())) {
emit newIncomingStream(newStream);
} else if (m_goawayGraceTimer.hasExpired()) {
// We gave the peer some time to handle the GOAWAY message, but they have started a new
// stream, so we error out.
connectionError(Http2Error::PROTOCOL_ERROR, u"Peer refused to GOAWAY."_s);
return;
}
} else if (streamWasResetLocally(streamID)) {
qCDebug(qHttp2ConnectionLog,
"[%p] Received HEADERS on previously locally reset stream %d (must process but ignore)",
this, streamID);
// nop
} else if (auto it = m_streams.constFind(streamID); it == m_streams.cend()) {
// RFC 9113, 6.2: HEADERS frames MUST be associated with a stream.
// A connection error is not required but it seems to be the right thing to do.
qCDebug(qHttp2ConnectionLog, "[%p] Received HEADERS on non-existent stream %d", this,
streamID);
return connectionError(PROTOCOL_ERROR, u"HEADERS on invalid stream"_s);
} else if (isInvalidStream(streamID)) {
// RFC 9113 6.4: After receiving a RST_STREAM on a stream, the receiver MUST NOT send
// additional frames for that stream
qCDebug(qHttp2ConnectionLog, "[%p] Received HEADERS on reset stream %d", this, streamID);
return connectionError(ENHANCE_YOUR_CALM, u"HEADERS on invalid stream"_s);
}
const auto flags = inboundFrame.flags();
if (flags.testFlag(FrameFlag::PRIORITY)) {
qCDebug(qHttp2ConnectionLog, "[%p] HEADERS frame on stream %d has PRIORITY flag", this,
streamID);
handlePRIORITY();
}
const bool endHeaders = flags.testFlag(FrameFlag::END_HEADERS);
continuedFrames.clear();
m_headerBlockSize = 0;
if (!validateHeaderListSize(inboundFrame))
return;
continuedFrames.push_back(std::move(inboundFrame));
if (!endHeaders) {
continuationExpected = true;
return;
}
handleContinuedHEADERS();
}
void QHttp2Connection::handlePRIORITY()
{
Q_ASSERT(inboundFrame.type() == FrameType::PRIORITY
|| inboundFrame.type() == FrameType::HEADERS);
const auto streamID = inboundFrame.streamID();
if (streamIsIgnored(streamID))
return;
// RFC 9913, 6.3: If a PRIORITY frame is received with a stream identifier of 0x00, the
// recipient MUST respond with a connection error
if (streamID == connectionStreamID)
return connectionError(PROTOCOL_ERROR, u"PRIORITY on 0x0 stream"_s);
// RFC 9113 6.4: After receiving a RST_STREAM on a stream, the receiver MUST NOT send
// additional frames for that stream
if (isInvalidStream(streamID))
return connectionError(ENHANCE_YOUR_CALM, u"PRIORITY on invalid stream"_s);
// RFC 9913, 6.3: A PRIORITY frame with a length other than 5 octets MUST be treated as a
// stream error (Section 5.4.2) of type FRAME_SIZE_ERROR.
// checked in Frame::validateHeader()
Q_ASSERT(inboundFrame.type() != FrameType::PRIORITY || inboundFrame.payloadSize() == 5);
quint32 streamDependency = 0;
uchar weight = 0;
const bool noErr = inboundFrame.priority(&streamDependency, &weight);
Q_UNUSED(noErr);
Q_ASSERT(noErr);
const bool exclusive = streamDependency & 0x80000000;
streamDependency &= ~0x80000000;
// Ignore this for now ...
// Can be used for streams (re)prioritization - 5.3
Q_UNUSED(exclusive);
Q_UNUSED(weight);
}
void QHttp2Connection::handleRST_STREAM()
{
Q_ASSERT(inboundFrame.type() == FrameType::RST_STREAM);
const auto streamID = inboundFrame.streamID();
if (streamIsIgnored(streamID))
return;
// RFC 9113, 6.4: RST_STREAM frames MUST be associated with a stream.
// If a RST_STREAM frame is received with a stream identifier of 0x0,
// the recipient MUST treat this as a connection error (Section 5.4.1)
// of type PROTOCOL_ERROR.
if (streamID == connectionStreamID)
return connectionError(PROTOCOL_ERROR, u"RST_STREAM on 0x0"_s);
// RFC 9113, 6.4: A RST_STREAM frame with a length other than 4 octets MUST be treated as a
// connection error (Section 5.4.1) of type FRAME_SIZE_ERROR.
// checked in Frame::validateHeader()
Q_ASSERT(inboundFrame.payloadSize() == 4);
const auto error = qFromBigEndian<quint32>(inboundFrame.dataBegin());
if (QPointer<QHttp2Stream> stream = m_streams.value(streamID))
emit stream->rstFrameReceived(error);
// Verify that whatever stream is being RST'd is not in the idle state:
const quint32 lastRelevantStreamID = [this, streamID]() {
quint32 peerMask = m_connectionType == Type::Client ? 0 : 1;
return ((streamID & 1) == peerMask) ? m_lastIncomingStreamID : m_nextStreamID - 2;
}();
if (streamID > lastRelevantStreamID) {
// "RST_STREAM frames MUST NOT be sent for a stream
// in the "idle" state. .. the recipient MUST treat this
// as a connection error (Section 5.4.1) of type PROTOCOL_ERROR."
return connectionError(PROTOCOL_ERROR, u"RST_STREAM on idle stream"_s);
}
Q_ASSERT(inboundFrame.dataSize() == 4);
if (QPointer<QHttp2Stream> stream = m_streams.value(streamID))
stream->handleRST_STREAM(inboundFrame);
}
void QHttp2Connection::handleSETTINGS()
{
// 6.5 SETTINGS.
Q_ASSERT(inboundFrame.type() == FrameType::SETTINGS);
// RFC 9113, 6.5: If an endpoint receives a SETTINGS frame whose Stream Identifier field is
// anything other than 0x00, the endpoint MUST respond with a connection error
if (inboundFrame.streamID() != connectionStreamID)
return connectionError(PROTOCOL_ERROR, u"SETTINGS on invalid stream"_s);
if (inboundFrame.flags().testFlag(FrameFlag::ACK)) {
// RFC 9113, 6.5: Receipt of a SETTINGS frame with the ACK flag set and a length field
// value other than 0 MUST be treated as a connection error
if (inboundFrame.payloadSize())
return connectionError(FRAME_SIZE_ERROR, u"SETTINGS ACK with data"_s);
if (!waitingForSettingsACK)
return connectionError(PROTOCOL_ERROR, u"unexpected SETTINGS ACK"_s);
qCDebug(qHttp2ConnectionLog, "[%p] Received SETTINGS ACK", this);
waitingForSettingsACK = false;
return;
}
qCDebug(qHttp2ConnectionLog, "[%p] Received SETTINGS frame", this);
if (inboundFrame.dataSize()) {
// RFC 9113, 6.5: A SETTINGS frame with a length other than a multiple of 6 octets MUST be
// treated as a connection error (Section 5.4.1) of type FRAME_SIZE_ERROR.
// checked in Frame::validateHeader()
Q_ASSERT(inboundFrame.payloadSize() % 6 == 0);
auto src = inboundFrame.dataBegin();
for (const uchar *end = src + inboundFrame.dataSize(); src != end; src += 6) {
const Settings identifier = Settings(qFromBigEndian<quint16>(src));
const quint32 intVal = qFromBigEndian<quint32>(src + 2);
if (!acceptSetting(identifier, intVal)) {
// If not accepted - we finish with connectionError.
qCDebug(qHttp2ConnectionLog, "[%p] Received an unacceptable setting, %u, %u", this,
quint32(identifier), intVal);
return; // connectionError already called in acceptSetting.
}
}
}
qCDebug(qHttp2ConnectionLog, "[%p] Sending SETTINGS ACK", this);
sendSETTINGS_ACK();
emit settingsFrameReceived();
}
void QHttp2Connection::handlePUSH_PROMISE()
{
// 6.6 PUSH_PROMISE.
Q_ASSERT(inboundFrame.type() == FrameType::PUSH_PROMISE);
// RFC 9113, 6.6: PUSH_PROMISE MUST NOT be sent if the SETTINGS_ENABLE_PUSH setting of the peer
// endpoint is set to 0. An endpoint that has set this setting and has received acknowledgment
// MUST treat the receipt of a PUSH_PROMISE frame as a connection error
if (!pushPromiseEnabled && !waitingForSettingsACK) {
// This means, server ACKed our 'NO PUSH',
// but sent us PUSH_PROMISE anyway.
return connectionError(PROTOCOL_ERROR, u"unexpected PUSH_PROMISE frame"_s);
}
// RFC 9113, 6.6: If the Stream Identifier field specifies the value 0x00, a recipient MUST
// respond with a connection error.
const auto streamID = inboundFrame.streamID();
if (streamID == connectionStreamID)
return connectionError(PROTOCOL_ERROR, u"PUSH_PROMISE with invalid associated stream (0x0)"_s);
auto it = m_streams.constFind(streamID);
#if 0 // Needs to be done after some timeout in case the stream has only just been reset
if (it != m_streams.constEnd()) {
QHttp2Stream *associatedStream = it->get();
if (associatedStream->state() != QHttp2Stream::State::Open
&& associatedStream->state() != QHttp2Stream::State::HalfClosedLocal) {
// Cause us to error out below:
it = m_streams.constEnd();
}
}
#endif
// RFC 9113, 6.6: PUSH_PROMISE frames MUST only be sent on a peer-initiated stream that
// is in either the "open" or "half-closed (remote)" state.
// I.e. If you are the server then the client must have initiated the stream you are sending
// the promise on. And since this is about _sending_ we have to invert "Remote" to "Local"
// because we are receiving.
if (it == m_streams.constEnd() || it->isNull())
return connectionError(ENHANCE_YOUR_CALM, u"PUSH_PROMISE with invalid associated stream"_s);
if ((m_connectionType == Type::Client && (streamID & 1) == 0) ||
(m_connectionType == Type::Server && (streamID & 1) == 1)) {
return connectionError(ENHANCE_YOUR_CALM, u"PUSH_PROMISE with invalid associated stream"_s);
}
if ((*it)->state() != QHttp2Stream::State::Open &&
(*it)->state() != QHttp2Stream::State::HalfClosedLocal) {
return connectionError(ENHANCE_YOUR_CALM, u"PUSH_PROMISE with invalid associated stream"_s);
}
// RFC 9113, 6.6: The promised stream identifier MUST be a valid choice for the
// next stream sent by the sender
const auto reservedID = qFromBigEndian<quint32>(inboundFrame.dataBegin());
if ((reservedID & 1) || reservedID <= m_lastIncomingStreamID || reservedID > lastValidStreamID)
return connectionError(PROTOCOL_ERROR, u"PUSH_PROMISE with invalid promised stream ID"_s);
bool streamCountIsOk = size_t(m_maxConcurrentStreams) > size_t(numActiveRemoteStreams());
// RFC 9113, 6.6: A receiver MUST treat the receipt of a PUSH_PROMISE that promises an
// illegal stream identifier (Section 5.1.1) as a connection error
auto *stream = createStreamInternal_impl(reservedID);
if (!stream)
return connectionError(PROTOCOL_ERROR, u"PUSH_PROMISE with already active stream ID"_s);
m_lastIncomingStreamID = reservedID;
stream->setState(QHttp2Stream::State::ReservedRemote);
if (!streamCountIsOk) {
stream->streamError(PROTOCOL_ERROR, u"Max concurrent streams reached"_s);
emit incomingStreamErrorOccured(CreateStreamError::MaxConcurrentStreamsReached);
return;
}
// "ignoring a PUSH_PROMISE frame causes the stream state to become
// indeterminate" - let's send RST_STREAM frame with REFUSE_STREAM code.
if (!pushPromiseEnabled)
return stream->streamError(REFUSE_STREAM, u"PUSH_PROMISE not enabled but ignored"_s);
// RFC 9113, 6.6: The total number of padding octets is determined by the value of the Pad
// Length field. If the length of the padding is the length of the frame payload or greater,
// the recipient MUST treat this as a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
// checked in Frame::validateHeader()
Q_ASSERT(inboundFrame.dataSize() > inboundFrame.padding());
const bool endHeaders = inboundFrame.flags().testFlag(FrameFlag::END_HEADERS);
continuedFrames.clear();
m_headerBlockSize = 0;
if (!validateHeaderListSize(inboundFrame))
return;
continuedFrames.push_back(std::move(inboundFrame));
if (!endHeaders) {
continuationExpected = true;
return;
}
handleContinuedHEADERS();
}
void QHttp2Connection::handlePING()
{
Q_ASSERT(inboundFrame.type() == FrameType::PING);
// RFC 9113, 6.7: PING frames are not associated with any individual stream. If a PING frame is
// received with a Stream Identifier field value other than 0x00, the recipient MUST respond
// with a connection error
if (inboundFrame.streamID() != connectionStreamID)
return connectionError(PROTOCOL_ERROR, u"PING on invalid stream"_s);
// Receipt of a PING frame with a length field value other than 8 MUST be treated
// as a connection error (Section 5.4.1) of type FRAME_SIZE_ERROR.
// checked in Frame::validateHeader()
Q_ASSERT(inboundFrame.payloadSize() == 8);
if (inboundFrame.flags() & FrameFlag::ACK) {
QByteArrayView pingSignature(reinterpret_cast<const char *>(inboundFrame.dataBegin()), 8);
if (!m_lastPingSignature.has_value()) {
emit pingFrameReceived(PingState::PongNoPingSent);
qCWarning(qHttp2ConnectionLog, "[%p] PING with ACK received but no PING was sent.", this);
} else if (pingSignature != m_lastPingSignature) {
emit pingFrameReceived(PingState::PongSignatureChanged);
qCWarning(qHttp2ConnectionLog, "[%p] PING signature does not match the last PING.", this);
} else {
emit pingFrameReceived(PingState::PongSignatureIdentical);
}
m_lastPingSignature.reset();
// Handle sendInitialServerGracefulShutdownGoaway()
if (m_gracefulShutdownState == GracefulShutdownState::AwaitingShutdownPing) {
sendFinalServerGracefulShutdownGoaway();
} else if (m_gracefulShutdownState == GracefulShutdownState::AwaitingPriorPing) {
// Prior PING completed, now send our RTT measurement PING. This shouldn't fail!
m_gracefulShutdownState = GracefulShutdownState::AwaitingShutdownPing;
[[maybe_unused]] const bool ok = sendPing();
Q_ASSERT(ok);
}
return;
} else {
emit pingFrameReceived(PingState::Ping);
}
frameWriter.start(FrameType::PING, FrameFlag::ACK, connectionStreamID);
frameWriter.append(inboundFrame.dataBegin(), inboundFrame.dataBegin() + 8);
frameWriter.write(*getSocket());
}
void QHttp2Connection::handleGOAWAY()
{
// 6.8 GOAWAY
Q_ASSERT(inboundFrame.type() == FrameType::GOAWAY);
// RFC 9113, 6.8: An endpoint MUST treat a GOAWAY frame with a stream identifier
// other than 0x0 as a connection error (Section 5.4.1) of type PROTOCOL_ERROR.
if (inboundFrame.streamID() != connectionStreamID)
return connectionError(PROTOCOL_ERROR, u"GOAWAY on invalid stream"_s);
// RFC 9113, 6.8:
// Reserved (1) + Last-Stream-ID (31) + Error Code (32) + Additional Debug Data (..)
// checked in Frame::validateHeader()
Q_ASSERT(inboundFrame.payloadSize() >= 8);
const uchar *const src = inboundFrame.dataBegin();
// RFC 9113, 4.1: 31-bit Stream ID; lastValidStreamID(0x7FFFFFFF) masks out the reserved MSB
const quint32 lastStreamID = qFromBigEndian<quint32>(src) & lastValidStreamID;
const Http2Error errorCode = Http2Error(qFromBigEndian<quint32>(src + 4));
// 6.8 "the GOAWAY contains the stream identifier of the last peer-initiated stream that was
// or might be processed on the sending endpoint in this connection."
// Alternatively, they can specify 0 as the last stream ID, meaning they are not intending to
// process any remaining stream(s).
const quint32 LocalMask = m_connectionType == Type::Client ? 1 : 0;
// The stream must match the LocalMask, meaning we initiated it, for the last stream ID to make
// sense - they are not processing their own streams.
if (lastStreamID != 0 && (lastStreamID & 0x1) != LocalMask)
return connectionError(PROTOCOL_ERROR, u"GOAWAY with invalid last stream ID"_s);
// 6.8 - An endpoint MAY send multiple GOAWAY frames if circumstances
// change. Endpoints MUST NOT increase the value they send in the last
// stream identifier
if (m_lastGoAwayLastStreamID && lastStreamID > *m_lastGoAwayLastStreamID)
return connectionError(PROTOCOL_ERROR, u"Repeated GOAWAY with invalid last stream ID"_s);
m_lastGoAwayLastStreamID = lastStreamID;
qCDebug(qHttp2ConnectionLog, "[%p] Received GOAWAY frame, error code %u, last stream %u",
this, errorCode, lastStreamID);
m_goingAway = true;
emit receivedGOAWAY(errorCode, lastStreamID);
if (errorCode == HTTP2_NO_ERROR) {
// Graceful GOAWAY (NO_ERROR): Only cancel streams the peer explicitly won't process
// (those with IDs > lastStreamID). Streams with ID <= lastStreamID can still complete.
// '0' can be used in the special case that no streams at all were or will be processed.
const quint32 firstPossibleStream = m_connectionType == Type::Client ? 1 : 2;
const quint32 firstCancelledStream = lastStreamID ? lastStreamID + 2 : firstPossibleStream;
Q_ASSERT((firstCancelledStream & 0x1) == LocalMask);
for (quint32 id = firstCancelledStream; id < m_nextStreamID; id += 2) {
QHttp2Stream *stream = m_streams.value(id, nullptr);
if (stream && stream->isActive())
stream->finishWithError(errorCode, u"Received GOAWAY"_s);
}
maybeCloseOnGoingAway(); // check if we can close now
} else {
// RFC 9113, 5.4.1: After sending the GOAWAY frame for an error
// condition, the endpoint MUST close the TCP connection.
// As the peer is closing the connection immediately, they won't
// process any more data, so we close the connection here already.
m_connectionAborted = true;
const QList<quint32> streamIDs = m_streams.keys();
for (quint32 streamID : streamIDs) {
QHttp2Stream *stream = m_streams.value(streamID, nullptr);
if (stream && stream->isActive())
stream->finishWithError(errorCode, u"Received GOAWAY"_s);
}
closeSession();
}
}
void QHttp2Connection::handleWINDOW_UPDATE()
{
Q_ASSERT(inboundFrame.type() == FrameType::WINDOW_UPDATE);
const quint32 delta = qFromBigEndian<quint32>(inboundFrame.dataBegin());
// RFC 9113, 6.9: A receiver MUST treat the receipt of a WINDOW_UPDATE frame with a
// flow-control window increment of 0 as a stream error (Section 5.4.2) of type PROTOCOL_ERROR;
// errors on the connection flow-control window MUST be treated as a connection error
const bool valid = delta && delta <= quint32(std::numeric_limits<qint32>::max());
const auto streamID = inboundFrame.streamID();
if (streamIsIgnored(streamID))
return;
// RFC 9113, 6.9: A WINDOW_UPDATE frame with a length other than 4 octets MUST be treated
// as a connection error (Section 5.4.1) of type FRAME_SIZE_ERROR.
// checked in Frame::validateHeader()
Q_ASSERT(inboundFrame.payloadSize() == 4);
qCDebug(qHttp2ConnectionLog(), "[%p] Received WINDOW_UPDATE, stream %d, delta %d", this,
streamID, delta);
if (streamID == connectionStreamID) {
if (!valid)
return connectionError(PROTOCOL_ERROR, u"WINDOW_UPDATE invalid delta"_s);
qint32 sum = 0;
// RFC 9113, 6.9.1: a WINDOW_UPDATE that pushes the connection window past 2^31-1 is a
// connection error of type FLOW_CONTROL_ERROR (the sender sends GOAWAY).
if (qAddOverflow(sessionSendWindowSize, qint32(delta), &sum))
return connectionError(FLOW_CONTROL_ERROR, u"WINDOW_UPDATE exceeds maximum window"_s);
sessionSendWindowSize = sum;
// Stream may have been unblocked, so maybe try to write again:
const auto blockedStreams = std::exchange(m_blockedStreams, {});
for (quint32 blockedStreamID : blockedStreams) {
const QPointer<QHttp2Stream> stream = m_streams.value(blockedStreamID);
if (!stream || !stream->isActive() || !stream->isUploadingDATA())
continue;
if (stream->isUploadBlocked()) {
m_blockedStreams.insert(blockedStreamID);
} else {
QMetaObject::invokeMethod(stream, &QHttp2Stream::maybeResumeUpload,
Qt::QueuedConnection);
}
}
} else {
QHttp2Stream *stream = m_streams.value(streamID);
if (!stream || !stream->isActive()) {
// WINDOW_UPDATE on closed streams can be ignored.
qCDebug(qHttp2ConnectionLog, "[%p] Received WINDOW_UPDATE on closed stream %d", this,
streamID);
return;
}
if (!valid)
return stream->streamError(PROTOCOL_ERROR, u"WINDOW_UPDATE invalid delta"_s);
stream->handleWINDOW_UPDATE(inboundFrame);
}
}
void QHttp2Connection::handleCONTINUATION()
{
Q_ASSERT(inboundFrame.type() == FrameType::CONTINUATION);
if (inboundFrame.payloadSize() > m_config.maxFrameSize()) {
qCDebug(qHttp2ConnectionLog,
"[%p] Received CONTINUATION frame with payload size %u, "
"but SETTINGS_MAX_FRAME_SIZE is %u, sending FRAME_SIZE_ERROR",
this, inboundFrame.payloadSize(), m_config.maxFrameSize());
return connectionError(Http2Error::FRAME_SIZE_ERROR,
u"CONTINUATION payload size exceeds SETTINGS_MAX_FRAME_SIZE"_s);
}
auto streamID = inboundFrame.streamID();
qCDebug(qHttp2ConnectionLog,
"[%p] Received CONTINUATION frame on stream %d, end stream? %s", this, streamID,
inboundFrame.flags().testFlag(Http2::FrameFlag::END_STREAM) ? "yes" : "no");
if (continuedFrames.empty())
return connectionError(PROTOCOL_ERROR,
u"CONTINUATION without a preceding HEADERS or PUSH_PROMISE"_s);
if (!continuationExpected)
return connectionError(PROTOCOL_ERROR,
u"CONTINUATION after a frame with the END_HEADERS flag set"_s);
if (inboundFrame.streamID() != continuedFrames.front().streamID())
return connectionError(PROTOCOL_ERROR, u"CONTINUATION on invalid stream"_s);
const bool endHeaders = inboundFrame.flags().testFlag(FrameFlag::END_HEADERS);
// No reset here: this frame continues the block begun by HEADERS/PUSH_PROMISE,
// so it must add to the running total rather than start a new one.
if (!validateHeaderListSize(inboundFrame))
return;
continuedFrames.push_back(std::move(inboundFrame));
if (!endHeaders)
return;
continuationExpected = false;
handleContinuedHEADERS();
}
bool QHttp2Connection::validateHeaderListSize(const Frame &frame)
{
const quint32 limit =
QHttp2ConfigurationPrivate::get(std::as_const(m_config))->maxHeaderListSize;
if (limit == std::numeric_limits<quint32>::max())
return true;
// Conservative pre-decode resource check. The compressed header block cannot
// exceed the decoded header list size, so decoding would inevitably exceed
// SETTINGS_MAX_HEADER_LIST_SIZE. Reject the request early with
// ENHANCE_YOUR_CALM rather than attempting HPACK decoding.
m_headerBlockSize += frame.hpackBlockSize();
if (m_headerBlockSize > limit) {
connectionError(ENHANCE_YOUR_CALM, u"Header list size limit exceeded"_s);
return false;
}
return true;
}
void QHttp2Connection::handleContinuedHEADERS()
{
// 'Continued' HEADERS can be: the initial HEADERS/PUSH_PROMISE frame
// with/without END_HEADERS flag set plus, if no END_HEADERS flag,
// a sequence of one or more CONTINUATION frames.
Q_ASSERT(!continuedFrames.empty());
const auto firstFrameType = continuedFrames[0].type();
Q_ASSERT(firstFrameType == FrameType::HEADERS || firstFrameType == FrameType::PUSH_PROMISE);
const auto streamID = continuedFrames[0].streamID();
const auto streamIt = m_streams.constFind(streamID);
if (firstFrameType == FrameType::HEADERS) {
if (streamIt != m_streams.cend() && !streamWasResetLocally(streamID)) {
QHttp2Stream *stream = streamIt.value();
if (stream->state() != QHttp2Stream::State::HalfClosedLocal
&& stream->state() != QHttp2Stream::State::ReservedRemote
&& stream->state() != QHttp2Stream::State::Idle
&& stream->state() != QHttp2Stream::State::Open) {
// We can receive HEADERS on streams initiated by our requests
// (these streams are in halfClosedLocal or open state) or
// remote-reserved streams from a server's PUSH_PROMISE.
return stream->streamError(PROTOCOL_ERROR, u"HEADERS on invalid stream"_s);
}
}
// Else: we cannot just ignore our peer's HEADERS frames - they change
// HPACK context - even though the stream was reset; apparently the peer
// has yet to see the reset.
}
std::vector<uchar> hpackBlock(assemble_hpack_block(continuedFrames));
const bool hasHeaderFields = !hpackBlock.empty();
if (hasHeaderFields) {
HPack::BitIStream inputStream{ hpackBlock.data(), hpackBlock.data() + hpackBlock.size() };
if (!decoder.decodeHeaderFields(inputStream))
return connectionError(COMPRESSION_ERROR, u"HPACK decompression failed"_s);
} else {
if (firstFrameType == FrameType::PUSH_PROMISE) {
// It could be a PRIORITY sent in HEADERS - already handled by this
// point in handleHEADERS. If it was PUSH_PROMISE (HTTP/2 8.2.1):
// "The header fields in PUSH_PROMISE and any subsequent CONTINUATION
// frames MUST be a valid and complete set of request header fields
// (Section 8.1.2.3) ... If a client receives a PUSH_PROMISE that does
// not include a complete and valid set of header fields or the :method
// pseudo-header field identifies a method that is not safe, it MUST
// respond with a stream error (Section 5.4.2) of type PROTOCOL_ERROR."
if (streamIt != m_streams.cend())
(*streamIt)->streamError(PROTOCOL_ERROR, u"PUSH_PROMISE with incomplete headers"_s);
return;
}
// We got back an empty hpack block. Now let's figure out if there was an error.
constexpr auto hpackBlockHasContent = [](const auto &c) { return c.hpackBlockSize() > 0; };
const bool anyHpackBlock = std::any_of(continuedFrames.cbegin(), continuedFrames.cend(),
hpackBlockHasContent);
if (anyHpackBlock) // There was hpack block data, but returned empty => it overflowed.
return connectionError(FRAME_SIZE_ERROR, u"HEADERS frame too large"_s);
}
if (streamWasResetLocally(streamID) || streamIt == m_streams.cend())
return; // No more processing without a stream from here on.
if (streamIsIgnored(streamID)) {
// Stream was established after GOAWAY cut-off, we ignore it, but we
// have to process things that alter state. That already happened, so we
// stop here.
if (continuedFrames[0].flags().testFlag(Http2::FrameFlag::END_STREAM)) {
if (QHttp2Stream *stream = streamIt.value()) {
stream->setState(QHttp2Stream::State::Closed);
delete stream;
}
}
return;
}
switch (firstFrameType) {
case FrameType::HEADERS:
streamIt.value()->handleHEADERS(continuedFrames[0].flags(), decoder.decodedHeader());
break;
case FrameType::PUSH_PROMISE: {
std::optional<QUrl> promiseKey = HPack::makePromiseKeyUrl(decoder.decodedHeader());
if (!promiseKey)
return; // invalid URL/key !
if (m_promisedStreams.contains(*promiseKey))
return; // already promised!
const auto promiseID = qFromBigEndian<quint32>(continuedFrames[0].dataBegin());
QHttp2Stream *stream = m_streams.value(promiseID);
stream->transitionState(QHttp2Stream::StateTransition::CloseLocal);
stream->handleHEADERS(continuedFrames[0].flags(), decoder.decodedHeader());
emit newPromisedStream(stream); // @future[consider] add promise key as argument?
m_promisedStreams.emplace(*promiseKey, promiseID);
break;
}
default:
break;
}
}
bool QHttp2Connection::acceptSetting(Http2::Settings identifier, quint32 newValue)
{
switch (identifier) {
case Settings::HEADER_TABLE_SIZE_ID: {
qCDebug(qHttp2ConnectionLog, "[%p] Received SETTINGS HEADER_TABLE_SIZE %d", this, newValue);
if (newValue > maxAcceptableTableSize) {
connectionError(PROTOCOL_ERROR, u"SETTINGS invalid table size"_s);
return false;
}
if (!pendingTableSizeUpdates[0] && encoder.dynamicTableCapacity() == newValue) {
qCDebug(qHttp2ConnectionLog,
"[%p] Ignoring SETTINGS HEADER_TABLE_SIZE %d (same as current value)", this,
newValue);
break;
}
if (pendingTableSizeUpdates[0].value_or(std::numeric_limits<quint32>::max()) >= newValue) {
pendingTableSizeUpdates[0] = newValue;
pendingTableSizeUpdates[1].reset(); // 0 is the latest _and_ smallest, so we don't need 1
qCDebug(qHttp2ConnectionLog, "[%p] Pending table size update to %u", this, newValue);
} else {
pendingTableSizeUpdates[1] = newValue; // newValue was larger than 0, so it goes to 1
qCDebug(qHttp2ConnectionLog, "[%p] Pending 2nd table size update to %u, smallest is %u",
this, newValue, *pendingTableSizeUpdates[0]);
}
break;
}
case Settings::INITIAL_WINDOW_SIZE_ID: {
qCDebug(qHttp2ConnectionLog, "[%p] Received SETTINGS INITIAL_WINDOW_SIZE %d", this,
newValue);
// For every active stream - adjust its window
// (and handle possible overflows as errors).
if (newValue > quint32(std::numeric_limits<qint32>::max())) {
connectionError(FLOW_CONTROL_ERROR, u"SETTINGS invalid initial window size"_s);
return false;
}
const qint32 delta = qint32(newValue) - streamInitialSendWindowSize;
streamInitialSendWindowSize = qint32(newValue);
qCDebug(qHttp2ConnectionLog, "[%p] Adjusting initial window size for %zu streams by %d",
this, size_t(m_streams.size()), delta);
for (const QPointer<QHttp2Stream> &stream : std::as_const(m_streams)) {
if (!stream)
continue;
qint32 sum = 0;
// RFC 9113, 6.9.2: a SETTINGS_INITIAL_WINDOW_SIZE change that pushes any
// flow-control window past 2^31-1 is a connection error of type FLOW_CONTROL_ERROR.
if (qAddOverflow(stream->m_sendWindow, delta, &sum)) {
connectionError(FLOW_CONTROL_ERROR,
u"SETTINGS_INITIAL_WINDOW_SIZE overflowed a flow-control window"_s);
return false;
}
stream->m_sendWindow = sum;
if (delta > 0 && stream->isUploadingDATA() && !stream->isUploadBlocked()) {
QMetaObject::invokeMethod(stream, &QHttp2Stream::maybeResumeUpload,
Qt::QueuedConnection);
}
}
break;
}
case Settings::MAX_CONCURRENT_STREAMS_ID: {
qCDebug(qHttp2ConnectionLog, "[%p] Received SETTINGS MAX_CONCURRENT_STREAMS %d", this,
newValue);
m_peerMaxConcurrentStreams = newValue;
break;
}
case Settings::MAX_FRAME_SIZE_ID: {
qCDebug(qHttp2ConnectionLog, "[%p] Received SETTINGS MAX_FRAME_SIZE %d", this, newValue);
if (newValue < Http2::minPayloadLimit || newValue > Http2::maxPayloadSize) {
connectionError(PROTOCOL_ERROR, u"SETTINGS max frame size is out of range"_s);
return false;
}
maxFrameSize = newValue;
break;
}
case Settings::MAX_HEADER_LIST_SIZE_ID: {
qCDebug(qHttp2ConnectionLog, "[%p] Received SETTINGS MAX_HEADER_LIST_SIZE %d", this,
newValue);
// We just remember this value, it can later
// prevent us from sending any request (and this
// will end up in request/reply error).
m_maxHeaderListSize = newValue;
break;
}
case Http2::Settings::ENABLE_PUSH_ID:
qCDebug(qHttp2ConnectionLog, "[%p] Received SETTINGS ENABLE_PUSH %d", this, newValue);
if (newValue != 0 && newValue != 1) {
connectionError(PROTOCOL_ERROR, u"SETTINGS peer sent illegal value for ENABLE_PUSH"_s);
return false;
}
if (m_connectionType == Type::Client) {
if (newValue == 1) {
connectionError(PROTOCOL_ERROR, u"SETTINGS server sent ENABLE_PUSH=1"_s);
return false;
}
} else { // server-side
pushPromiseEnabled = newValue;
break;
}
}
return true;
}
QT_END_NAMESPACE
#include "moc_qhttp2connection_p.cpp"