Revision control
Copy as Markdown
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtTest/QTest>
#include <QtTest/QSignalSpy>
#include <QtNetwork/private/qhttp2connection_p.h>
#include <QtNetwork/private/qhttp2configuration_p.h>
#include <QtNetwork/private/hpack_p.h>
#include <QtNetwork/private/bitstreams_p.h>
#include <QtCore/qbuffer.h>
#include <QtCore/qregularexpression.h>
#include <QtCore/qthread.h>
#include <QtCore/private/qnoncontiguousbytedevice_p.h> // for uploadByteDeviceDestroyedWhileBlocked
#include <limits>
using namespace Qt::StringLiterals;
class tst_QHttp2Connection : public QObject
{
Q_OBJECT
private slots:
void construct();
void constructStream();
void streamConfiguration_data();
void streamConfiguration();
void testSETTINGSFrame();
void maxHeaderListSizeConfiguration();
void maxHeaderListSizeEnforced_data();
void maxHeaderListSizeEnforced();
void maxHeaderListSizeDecoderEnforced();
void maxHeaderTableSize();
void testPING();
void testRSTServerSide();
void testRSTClientSide();
void testRSTReplyOnDATAEND();
void resetAfterClose();
void testBadFrameSize_data();
void testBadFrameSize();
void testDataFrameAfterRSTIncoming();
void testDataFrameAfterRSTOutgoing();
void headerFrameAfterRSTOutgoing_data();
void headerFrameAfterRSTOutgoing();
void connectToServer();
void WINDOW_UPDATE();
void testCONTINUATIONFrame();
void goaway_data();
void goaway();
void serverInitiatedGoaways_data();
void serverInitiatedGoaways();
void clientInitiatedGoaway();
void runtimeSessionWindowResize();
void settingsInitialWindowSizeOverflow();
void windowUpdateOverflow_data();
void windowUpdateOverflow();
void totalBytesReceivedDATACounts();
void uploadByteDeviceDestroyedWhileBlocked();
void pushPromiseOnStreamRemovedAfterRST_data();
void pushPromiseOnStreamRemovedAfterRST();
private:
enum PeerType { Client, Server };
[[nodiscard]] auto makeFakeConnectedSockets();
[[nodiscard]] auto getRequiredHeaders();
[[nodiscard]] QHttp2Connection *makeHttp2Connection(QIODevice *socket,
QHttp2Configuration config, PeerType type);
[[nodiscard]] bool waitForSettingsExchange(QHttp2Connection *client, QHttp2Connection *server);
};
class IOBuffer : public QIODevice
{
Q_OBJECT
public:
IOBuffer(QObject *parent, std::shared_ptr<QBuffer> _in, std::shared_ptr<QBuffer> _out)
: QIODevice(parent), in(std::move(_in)), out(std::move(_out))
{
connect(in.get(), &QIODevice::readyRead, this, &IOBuffer::readyRead);
connect(out.get(), &QIODevice::bytesWritten, this, &IOBuffer::bytesWritten);
connect(out.get(), &QIODevice::aboutToClose, this, &IOBuffer::readChannelFinished);
connect(out.get(), &QIODevice::aboutToClose, this, &IOBuffer::aboutToClose);
}
bool open(OpenMode mode) override
{
QIODevice::open(mode);
Q_ASSERT(in->isOpen());
Q_ASSERT(out->isOpen());
return false;
}
bool isSequential() const override { return true; }
qint64 bytesAvailable() const override { return in->pos() - readHead; }
qint64 bytesToWrite() const override { return 0; }
qint64 readData(char *data, qint64 maxlen) override
{
qint64 temp = in->pos();
in->seek(readHead);
qint64 res = in->read(data, std::min(maxlen, temp - readHead));
readHead += res;
if (readHead == temp) {
// Reached end of buffer, reset
in->seek(0);
in->buffer().resize(0);
readHead = 0;
} else {
in->seek(temp);
}
return res;
}
qint64 writeData(const char *data, qint64 len) override
{
return out->write(data, len);
}
std::shared_ptr<QBuffer> in;
std::shared_ptr<QBuffer> out;
qint64 readHead = 0;
};
auto tst_QHttp2Connection::makeFakeConnectedSockets()
{
auto clientIn = std::make_shared<QBuffer>();
auto serverIn = std::make_shared<QBuffer>();
clientIn->open(QIODevice::ReadWrite);
serverIn->open(QIODevice::ReadWrite);
auto client = std::make_unique<IOBuffer>(this, clientIn, serverIn);
auto server = std::make_unique<IOBuffer>(this, std::move(serverIn), std::move(clientIn));
client->open(QIODevice::ReadWrite);
server->open(QIODevice::ReadWrite);
return std::pair{ std::move(client), std::move(server) };
}
auto tst_QHttp2Connection::getRequiredHeaders()
{
return HPack::HttpHeader{
{ ":authority", "example.com" },
{ ":method", "GET" },
{ ":path", "/" },
{ ":scheme", "https" },
};
}
QHttp2Connection *tst_QHttp2Connection::makeHttp2Connection(QIODevice *socket,
QHttp2Configuration config,
PeerType type)
{
QHttp2Connection *connection = nullptr;
if (type == PeerType::Server)
connection = QHttp2Connection::createDirectServerConnection(socket, config);
else
connection = QHttp2Connection::createDirectConnection(socket, config);
connect(socket, &QIODevice::readyRead, connection, &QHttp2Connection::handleReadyRead);
return connection;
}
bool tst_QHttp2Connection::waitForSettingsExchange(QHttp2Connection *client,
QHttp2Connection *server)
{
bool settingsFrameReceived = false;
bool serverSettingsFrameReceived = false;
QMetaObject::Connection c = connect(client, &QHttp2Connection::settingsFrameReceived, client,
[&settingsFrameReceived]() {
settingsFrameReceived = true;
});
QMetaObject::Connection s = connect(server, &QHttp2Connection::settingsFrameReceived, server,
[&serverSettingsFrameReceived]() {
serverSettingsFrameReceived = true;
});
client->handleReadyRead(); // handle incoming frames, send response
bool success = QTest::qWaitFor([&]() {
return settingsFrameReceived && serverSettingsFrameReceived
&& !client->waitingForSettingsACK && !server->waitingForSettingsACK;
});
disconnect(c);
disconnect(s);
return success;
}
void tst_QHttp2Connection::construct()
{
QBuffer buffer;
buffer.open(QIODevice::ReadWrite);
auto *connection = QHttp2Connection::createDirectConnection(&buffer, {});
QVERIFY(!connection->isGoingAway());
QCOMPARE(connection->maxConcurrentStreams(), 100u);
QCOMPARE(connection->maxHeaderListSize(), std::numeric_limits<quint32>::max());
QVERIFY(!connection->isUpgradedConnection());
QVERIFY(!connection->getStream(1)); // No stream has been created yet
auto *upgradedConnection = QHttp2Connection::createUpgradedConnection(&buffer, {});
QVERIFY(upgradedConnection->isUpgradedConnection());
// Stream 1 is created by default for an upgraded connection
QVERIFY(upgradedConnection->getStream(1));
}
void tst_QHttp2Connection::constructStream()
{
QBuffer buffer;
buffer.open(QIODevice::ReadWrite);
auto connection = QHttp2Connection::createDirectConnection(&buffer, {});
QHttp2Stream *stream = connection->createStream().unwrap();
QVERIFY(stream);
QCOMPARE(stream->isPromisedStream(), false);
QCOMPARE(stream->isActive(), false);
QCOMPARE(stream->RST_STREAMCodeReceived(), 0u);
QCOMPARE(stream->streamID(), 1u);
QCOMPARE(stream->receivedHeaders(), {});
QCOMPARE(stream->state(), QHttp2Stream::State::Idle);
QCOMPARE(stream->isUploadBlocked(), false);
QCOMPARE(stream->isUploadingDATA(), false);
}
void tst_QHttp2Connection::streamConfiguration_data()
{
QTest::addColumn<bool>("useDownloadBuffer");
QTest::addColumn<bool>("useHeaderBuffer");
QTest::addRow("buffers=on") << true << true;
QTest::addRow("downloadBuffer=off") << false << true;
QTest::addRow("headerBuffer=off") << true << false;
QTest::addRow("buffers=off") << false << false;
}
void tst_QHttp2Connection::streamConfiguration()
{
QFETCH(const bool, useDownloadBuffer);
QFETCH(const bool, useHeaderBuffer);
auto [client, server] = makeFakeConnectedSockets();
auto *clientConnection = makeHttp2Connection(client.get(), {}, Client);
auto *serverConnection = makeHttp2Connection(server.get(), {}, Server);
QHttp2Stream::Configuration config;
config.useDownloadBuffer = useDownloadBuffer;
config.useHeaderBuffer = useHeaderBuffer;
QHttp2Stream *clientStream = clientConnection->createStream(config).unwrap();
QVERIFY(clientStream);
QCOMPARE(clientStream->configuration().useDownloadBuffer, useDownloadBuffer);
QCOMPARE(clientStream->configuration().useHeaderBuffer, useHeaderBuffer);
QVERIFY(waitForSettingsExchange(clientConnection, serverConnection));
QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
QSignalSpy clientDataReceivedSpy{ clientStream, &QHttp2Stream::dataReceived };
QSignalSpy clientHeadersReceivedSpy{ clientStream, &QHttp2Stream::headersReceived };
HPack::HttpHeader headers = getRequiredHeaders();
clientStream->sendHEADERS(headers, false);
QVERIFY(newIncomingStreamSpy.wait());
auto *serverStream = newIncomingStreamSpy.front().front().value<QHttp2Stream *>();
QVERIFY(serverStream);
const HPack::HttpHeader responseHeaders{ { ":status", "200" } };
serverStream->sendHEADERS(responseHeaders, false);
const QByteArray testData = "Hello World"_ba.repeated(100);
serverStream->sendDATA(testData, true);
QVERIFY(clientDataReceivedSpy.wait());
QCOMPARE(clientDataReceivedSpy.count(), 1);
const QByteArray receivedData = clientDataReceivedSpy.front().front().value<QByteArray>();
QCOMPARE(receivedData, testData);
if (useDownloadBuffer) {
QCOMPARE(clientStream->downloadBuffer().byteAmount(), testData.size());
} else {
QVERIFY(clientStream->downloadBuffer().isEmpty());
}
// headersReceived() always fires; receivedHeaders() only accumulates when
// the header buffer is enabled.
QCOMPARE_GE(clientHeadersReceivedSpy.count(), 1);
if (useHeaderBuffer)
QCOMPARE(clientStream->receivedHeaders(), responseHeaders);
else
QVERIFY(clientStream->receivedHeaders().empty());
}
void tst_QHttp2Connection::testSETTINGSFrame()
{
constexpr qint32 PrefaceLength = 24;
QBuffer buffer;
buffer.open(QIODevice::ReadWrite);
QHttp2Configuration config;
constexpr quint32 MaxFrameSize = 16394;
constexpr bool ServerPushEnabled = false;
constexpr quint32 StreamReceiveWindowSize = 50000;
constexpr quint32 SessionReceiveWindowSize = 50001;
config.setMaxFrameSize(MaxFrameSize);
config.setServerPushEnabled(ServerPushEnabled);
config.setStreamReceiveWindowSize(StreamReceiveWindowSize);
config.setSessionReceiveWindowSize(SessionReceiveWindowSize);
auto connection = QHttp2Connection::createDirectConnection(&buffer, config);
Q_UNUSED(connection);
QHttp2Stream *stream = connection->createStream().unwrap();
QVERIFY(stream);
QCOMPARE_GE(buffer.size(), PrefaceLength);
// Preface
QByteArray preface = buffer.data().first(PrefaceLength);
QCOMPARE(preface, "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
// SETTINGS
buffer.seek(PrefaceLength);
const quint32 maxSize = buffer.size() - PrefaceLength;
Http2::FrameReader reader;
Http2::FrameStatus status = reader.read(buffer);
QCOMPARE(status, Http2::FrameStatus::goodFrame);
Http2::Frame f = reader.inboundFrame();
QCOMPARE(f.type(), Http2::FrameType::SETTINGS);
QCOMPARE_LT(f.payloadSize(), maxSize);
const qint32 settingsReceived = f.dataSize() / 6;
QCOMPARE_GT(settingsReceived, 0);
QCOMPARE_LE(settingsReceived, 6);
struct ExpectedSetting
{
Http2::Settings identifier;
quint32 value;
};
// Commented-out settings are not sent since they are defaults
ExpectedSetting expectedSettings[]{
// { Http2::Settings::HEADER_TABLE_SIZE_ID, HPack::FieldLookupTable::DefaultSize },
{ Http2::Settings::ENABLE_PUSH_ID, ServerPushEnabled ? 1 : 0 },
// { Http2::Settings::MAX_CONCURRENT_STREAMS_ID, Http2::maxConcurrentStreams },
{ Http2::Settings::INITIAL_WINDOW_SIZE_ID, StreamReceiveWindowSize },
{ Http2::Settings::MAX_FRAME_SIZE_ID, MaxFrameSize },
{ Http2::Settings::MAX_HEADER_LIST_SIZE_ID,
QHttp2ConfigurationPrivate::get(config)->maxHeaderListSize },
};
QCOMPARE(quint32(settingsReceived), std::size(expectedSettings));
for (qint32 i = 0; i < settingsReceived; ++i) {
const uchar *it = f.dataBegin() + i * 6;
const quint16 ident = qFromBigEndian<quint16>(it);
const quint32 intVal = qFromBigEndian<quint32>(it + 2);
ExpectedSetting expectedSetting = expectedSettings[i];
QVERIFY2(ident == quint16(expectedSetting.identifier),
qPrintable("ident: %1, expected: %2, index: %3"_L1
.arg(QString::number(ident),
QString::number(quint16(expectedSetting.identifier)),
QString::number(i))));
QVERIFY2(intVal == expectedSetting.value,
qPrintable("intVal: %1, expected: %2, index: %3"_L1
.arg(QString::number(intVal),
QString::number(expectedSetting.value),
QString::number(i))));
}
}
void tst_QHttp2Connection::maxHeaderListSizeConfiguration()
{
QHttp2Configuration config;
// Defaults to 256 KiB.
QCOMPARE(config.maxHeaderListSize(), 256u * 1024u);
constexpr quint32 CustomLimit = 1234;
config.setMaxHeaderListSize(CustomLimit);
QCOMPARE(config.maxHeaderListSize(), CustomLimit);
// The parameter participates in equality comparison.
QHttp2Configuration other;
QVERIFY(config != other);
other.setMaxHeaderListSize(CustomLimit);
QVERIFY(config == other);
}
void tst_QHttp2Connection::maxHeaderListSizeEnforced_data()
{
QTest::addColumn<bool>("serverSends");
QTest::newRow("client-to-server") << false;
QTest::newRow("server-to-client") << true;
}
void tst_QHttp2Connection::maxHeaderListSizeEnforced()
{
QFETCH(const bool, serverSends);
// Each peer advertises a different limit so we can tell which one the
// sender is actually being held to.
constexpr quint32 ClientLimit = 4096;
constexpr quint32 ServerLimit = 8192;
QHttp2Configuration clientConfig;
QHttp2ConfigurationPrivate::get(clientConfig)->maxHeaderListSize = ClientLimit;
QHttp2Configuration serverConfig;
QHttp2ConfigurationPrivate::get(serverConfig)->maxHeaderListSize = ServerLimit;
auto [client, server] = makeFakeConnectedSockets();
auto clientConn = makeHttp2Connection(client.get(), clientConfig, Client);
auto serverConn = makeHttp2Connection(server.get(), serverConfig, Server);
QHttp2Stream *clientStream = clientConn->createStream().unwrap();
QVERIFY(clientStream);
QVERIFY(waitForSettingsExchange(clientConn, serverConn));
// Each peer parsed and stored the limit its counterpart advertised.
QCOMPARE(clientConn->maxHeaderListSize(), ServerLimit);
QCOMPARE(serverConn->maxHeaderListSize(), ClientLimit);
// Resolve the sender (whose send is checked) and the receiver (which
// enforces its advertised limit on incoming header blocks).
QHttp2Stream *sender = clientStream;
QHttp2Connection *receiver = serverConn;
QHttp2Connection *flooder = clientConn;
QIODevice *receiverInput = client->out.get(); // the buffer the server reads
quint32 limit = ServerLimit;
quint32 floodStreamID = 3; // fresh, client-initiated
HPack::HttpHeader base = getRequiredHeaders();
if (serverSends) {
// A server stream only exists once the client has opened one.
QSignalSpy newIncomingStreamSpy{ serverConn, &QHttp2Connection::newIncomingStream };
QVERIFY(clientStream->sendHEADERS(base, false));
QVERIFY(newIncomingStreamSpy.wait());
sender = newIncomingStreamSpy.front().front().value<QHttp2Stream *>();
QVERIFY(sender);
receiver = clientConn;
flooder = serverConn;
receiverInput = server->out.get(); // the buffer the client reads
limit = ClientLimit;
floodStreamID = 2; // fresh, server-initiated
base = HPack::HttpHeader{ { ":status", "200" } };
}
// Send side: a header list exceeding the peer's advertised limit is
// refused; the rejection leaves the stream untouched...
HPack::HttpHeader oversized = base;
oversized.emplace_back("x-big", QByteArray(limit, 'a'));
QVERIFY(!sender->sendHEADERS(oversized, true));
// ...so a list within the limit is still accepted on the same stream.
QVERIFY(sender->sendHEADERS(base, false));
// Receive side: verify that the advertised limit is enforced across
// HEADERS/CONTINUATION frames. A block that starts below the limit and
// exceeds it only after a CONTINUATION frame must be rejected before
// HPACK decoding.
QSignalSpy goawaySpy{ flooder, &QHttp2Connection::receivedGOAWAY };
using namespace Http2;
const QByteArray filler(limit, char(0));
FrameWriter headers(FrameType::HEADERS, FrameFlag::EMPTY, floodStreamID);
headers.append(QByteArrayView(filler).first(limit / 2));
QVERIFY(headers.write(*receiverInput));
FrameWriter continuation(FrameType::CONTINUATION, FrameFlag::END_HEADERS, floodStreamID);
continuation.append(QByteArrayView(filler));
QVERIFY(continuation.write(*receiverInput));
receiver->handleReadyRead();
QVERIFY(receiver->isGoingAway());
QTRY_COMPARE(goawaySpy.count(), 1);
QCOMPARE(goawaySpy.first().first().value<Http2::Http2Error>(),
Http2::Http2Error::ENHANCE_YOUR_CALM);
}
void tst_QHttp2Connection::maxHeaderListSizeDecoderEnforced()
{
// Verify that a compressed header block within the limit is rejected
// if HPACK decoding expands it beyond the advertised limit.
constexpr quint32 Limit = 8192;
QHttp2Configuration serverConfig;
QHttp2ConfigurationPrivate::get(serverConfig)->maxHeaderListSize = Limit;
auto [client, server] = makeFakeConnectedSockets();
auto clientConn = makeHttp2Connection(client.get(), { }, Client);
auto serverConn = makeHttp2Connection(server.get(), serverConfig, Server);
QHttp2Stream *clientStream = clientConn->createStream().unwrap();
QVERIFY(clientStream);
QVERIFY(waitForSettingsExchange(clientConn, serverConn));
// HPACK "bomb": 0x00 0x00 0x00 encodes one empty-name/empty-value field.
// Each costs 3 bytes on the wire but 32 toward the header-list size (+32
// bytes overhead per entry) -> 3000 / 3 * 32 = ~32000 decoded bytes.
using namespace Http2;
const QByteArray block(3000, char(0));
FrameWriter headers(FrameType::HEADERS, FrameFlag::END_HEADERS, 3);
headers.append(QByteArrayView(block));
QVERIFY(headers.write(*client->out));
QSignalSpy goawaySpy{ clientConn, &QHttp2Connection::receivedGOAWAY };
serverConn->handleReadyRead();
QVERIFY(serverConn->isGoingAway());
QTRY_COMPARE(goawaySpy.count(), 1);
QCOMPARE(goawaySpy.first().first().value<Http2::Http2Error>(),
Http2::Http2Error::COMPRESSION_ERROR);
}
void tst_QHttp2Connection::maxHeaderTableSize()
{
auto [client, server] = makeFakeConnectedSockets();
auto connection = makeHttp2Connection(client.get(), {}, Client);
auto serverConnection = makeHttp2Connection(server.get(), {}, Server);
QSignalSpy incomingRequestSpy(serverConnection, &QHttp2Connection::newIncomingStream);
QHttp2Stream *clientStream = connection->createStream().unwrap();
QVERIFY(clientStream);
QVERIFY(waitForSettingsExchange(connection, serverConnection));
// Test defaults:
// encoder:
QCOMPARE(connection->encoder.dynamicTableSize(), 0u);
QCOMPARE(connection->encoder.maxDynamicTableCapacity(), 4096u);
QCOMPARE(connection->encoder.dynamicTableCapacity(), 4096u);
QCOMPARE(serverConnection->encoder.dynamicTableSize(), 0u);
QCOMPARE(serverConnection->encoder.maxDynamicTableCapacity(), 4096u);
QCOMPARE(serverConnection->encoder.dynamicTableCapacity(), 4096u);
// decoder:
QCOMPARE(connection->decoder.dynamicTableSize(), 0u);
QCOMPARE(connection->decoder.maxDynamicTableCapacity(), 4096u);
QCOMPARE(connection->decoder.dynamicTableCapacity(), 4096u);
QCOMPARE(serverConnection->decoder.dynamicTableSize(), 0u);
QCOMPARE(serverConnection->decoder.maxDynamicTableCapacity(), 4096u);
QCOMPARE(serverConnection->decoder.dynamicTableCapacity(), 4096u);
// Send a HEADER block with a custom header, to add something to the dynamic table:
HPack::HttpHeader headers = getRequiredHeaders();
headers.emplace_back("x-test", "test");
clientStream->sendHEADERS(headers, true);
QVERIFY(incomingRequestSpy.wait());
// Test that the size has been updated:
// encoder:
QCOMPARE_GT(connection->encoder.dynamicTableSize(), 0u); // now > 0
QCOMPARE(connection->encoder.maxDynamicTableCapacity(), 4096u);
QCOMPARE(connection->encoder.dynamicTableCapacity(), 4096u);
QCOMPARE(serverConnection->encoder.dynamicTableSize(), 0u);
QCOMPARE(serverConnection->encoder.maxDynamicTableCapacity(), 4096u);
QCOMPARE(serverConnection->encoder.dynamicTableCapacity(), 4096u);
// decoder:
QCOMPARE(connection->decoder.dynamicTableSize(), 0u);
QCOMPARE(connection->decoder.maxDynamicTableCapacity(), 4096u);
QCOMPARE(connection->decoder.dynamicTableCapacity(), 4096u);
QCOMPARE_GT(serverConnection->decoder.dynamicTableSize(), 0u); // now > 0
QCOMPARE(serverConnection->decoder.maxDynamicTableCapacity(), 4096u);
QCOMPARE(serverConnection->decoder.dynamicTableCapacity(), 4096u);
const quint32 initialTableSize = connection->encoder.dynamicTableSize();
QCOMPARE(initialTableSize, serverConnection->decoder.dynamicTableSize());
// Notify from server that we want a smaller size, reset to 0 first:
for (quint32 nextSize : {0, 2048}) {
// @note: currently we don't have an API for this so just do it by hand:
serverConnection->waitingForSettingsACK = true;
using namespace Http2;
FrameWriter builder(FrameType::SETTINGS, FrameFlag::EMPTY, connectionStreamID);
builder.append(Settings::HEADER_TABLE_SIZE_ID);
builder.append(nextSize);
builder.write(*server->out);
QVERIFY(QTest::qWaitFor([&]() { return !serverConnection->waitingForSettingsACK; }));
}
// Now we have to send another HEADER block without extra field, so we can see that the size of
// the dynamic table has decreased after we cleared it:
headers = getRequiredHeaders();
QHttp2Stream *clientStream2 = connection->createStream().unwrap();
clientStream2->sendHEADERS(headers, true);
QVERIFY(incomingRequestSpy.wait());
// Test that the size has been updated:
// encoder:
QCOMPARE_LT(connection->encoder.dynamicTableSize(), initialTableSize);
QCOMPARE(connection->encoder.maxDynamicTableCapacity(), 2048u);
QCOMPARE(connection->encoder.dynamicTableCapacity(), 2048u);
QCOMPARE(serverConnection->encoder.dynamicTableSize(), 0u);
QCOMPARE(serverConnection->encoder.maxDynamicTableCapacity(), 4096u);
QCOMPARE(serverConnection->encoder.dynamicTableCapacity(), 4096u);
// decoder:
QCOMPARE(connection->decoder.dynamicTableSize(), 0u);
QCOMPARE(connection->decoder.maxDynamicTableCapacity(), 4096u);
QCOMPARE(connection->decoder.dynamicTableCapacity(), 4096u);
QCOMPARE_LT(serverConnection->decoder.dynamicTableSize(), initialTableSize);
// If we add an API for this then this should also be updated at some stage:
QCOMPARE(serverConnection->decoder.maxDynamicTableCapacity(), 4096u);
QCOMPARE(serverConnection->decoder.dynamicTableCapacity(), 2048u);
quint32 newTableSize = connection->encoder.dynamicTableSize();
QCOMPARE(newTableSize, serverConnection->decoder.dynamicTableSize());
}
void tst_QHttp2Connection::testPING()
{
auto [client, server] = makeFakeConnectedSockets();
auto connection = makeHttp2Connection(client.get(), {}, Client);
auto serverConnection = makeHttp2Connection(server.get(), {}, Server);
QHttp2Stream *clientStream = connection->createStream().unwrap();
QVERIFY(clientStream);
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy serverPingSpy{ serverConnection, &QHttp2Connection::pingFrameReceived };
QSignalSpy clientPingSpy{ connection, &QHttp2Connection::pingFrameReceived };
QByteArray data{"pingpong"};
connection->sendPing(data);
QVERIFY(serverPingSpy.wait());
QVERIFY(clientPingSpy.wait());
QCOMPARE(serverPingSpy.last().at(0).toInt(), int(QHttp2Connection::PingState::Ping));
QCOMPARE(clientPingSpy.last().at(0).toInt(), int(QHttp2Connection::PingState::PongSignatureIdentical));
serverConnection->sendPing();
QVERIFY(clientPingSpy.wait());
QVERIFY(serverPingSpy.wait());
QCOMPARE(clientPingSpy.last().at(0).toInt(), int(QHttp2Connection::PingState::Ping));
QCOMPARE(serverPingSpy.last().at(0).toInt(), int(QHttp2Connection::PingState::PongSignatureIdentical));
}
void tst_QHttp2Connection::testRSTServerSide()
{
auto [client, server] = makeFakeConnectedSockets();
auto connection = makeHttp2Connection(client.get(), {}, Client);
auto serverConnection = makeHttp2Connection(server.get(), {}, Server);
QHttp2Stream *clientStream = connection->createStream().unwrap();
QVERIFY(clientStream);
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
QSignalSpy clientHeaderReceivedSpy{ clientStream, &QHttp2Stream::headersReceived };
HPack::HttpHeader headers = getRequiredHeaders();
clientStream->sendHEADERS(headers, false);
QVERIFY(newIncomingStreamSpy.wait());
auto *serverStream = newIncomingStreamSpy.front().front().value<QHttp2Stream *>();
QCOMPARE(clientStream->streamID(), serverStream->streamID());
QSignalSpy rstClientSpy{ clientStream, &QHttp2Stream::rstFrameReceived };
QSignalSpy rstServerSpy{ serverStream, &QHttp2Stream::rstFrameReceived };
QCOMPARE(clientStream->state(), QHttp2Stream::State::Open);
QCOMPARE(serverStream->state(), QHttp2Stream::State::Open);
serverStream->sendRST_STREAM(Http2::INTERNAL_ERROR);
QCOMPARE(serverStream->state(), QHttp2Stream::State::Closed);
QVERIFY(rstClientSpy.wait());
QCOMPARE(rstServerSpy.count(), 0);
QCOMPARE(clientStream->state(), QHttp2Stream::State::Closed);
}
void tst_QHttp2Connection::testRSTClientSide()
{
auto [client, server] = makeFakeConnectedSockets();
auto connection = makeHttp2Connection(client.get(), {}, Client);
auto serverConnection = makeHttp2Connection(server.get(), {}, Server);
QHttp2Stream *clientStream = connection->createStream().unwrap();
QVERIFY(clientStream);
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
QSignalSpy clientHeaderReceivedSpy{ clientStream, &QHttp2Stream::headersReceived };
HPack::HttpHeader headers = getRequiredHeaders();
clientStream->sendHEADERS(headers, false);
QVERIFY(newIncomingStreamSpy.wait());
auto *serverStream = newIncomingStreamSpy.front().front().value<QHttp2Stream *>();
QCOMPARE(clientStream->streamID(), serverStream->streamID());
QSignalSpy rstClientSpy{ clientStream, &QHttp2Stream::rstFrameReceived };
QSignalSpy rstServerSpy{ serverStream, &QHttp2Stream::rstFrameReceived };
QCOMPARE(clientStream->state(), QHttp2Stream::State::Open);
QCOMPARE(serverStream->state(), QHttp2Stream::State::Open);
clientStream->sendRST_STREAM(Http2::INTERNAL_ERROR);
QCOMPARE(clientStream->state(), QHttp2Stream::State::Closed);
QVERIFY(rstServerSpy.wait());
QCOMPARE(rstClientSpy.count(), 0);
QCOMPARE(serverStream->state(), QHttp2Stream::State::Closed);
}
void tst_QHttp2Connection::testRSTReplyOnDATAEND()
{
auto [client, server] = makeFakeConnectedSockets();
auto connection = makeHttp2Connection(client.get(), {}, Client);
auto serverConnection = makeHttp2Connection(server.get(), {}, Server);
QHttp2Stream *clientStream = connection->createStream().unwrap();
QVERIFY(clientStream);
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
QSignalSpy clientHeaderReceivedSpy{ clientStream, &QHttp2Stream::headersReceived };
HPack::HttpHeader headers = getRequiredHeaders();
clientStream->sendHEADERS(headers, false);
QVERIFY(newIncomingStreamSpy.wait());
auto *serverStream = newIncomingStreamSpy.front().front().value<QHttp2Stream *>();
QCOMPARE(clientStream->streamID(), serverStream->streamID());
QSignalSpy rstClientSpy{ clientStream, &QHttp2Stream::rstFrameReceived };
QSignalSpy rstServerSpy{ serverStream, &QHttp2Stream::rstFrameReceived };
QSignalSpy endServerSpy{ serverConnection, &QHttp2Connection::receivedEND_STREAM };
QSignalSpy errrorServerSpy{ serverStream, &QHttp2Stream::errorOccurred };
QCOMPARE(clientStream->state(), QHttp2Stream::State::Open);
QCOMPARE(serverStream->state(), QHttp2Stream::State::Open);
// Send data with END_STREAM = true
QBuffer *buffer = new QBuffer(clientStream);
QByteArray uploadedData = "Hello World"_ba.repeated(10);
buffer->setData(uploadedData);
buffer->open(QIODevice::ReadWrite);
// send data with endstream true
clientStream->sendDATA(buffer, true);
QVERIFY(endServerSpy.wait());
QCOMPARE(clientStream->state(), QHttp2Stream::State::HalfClosedLocal);
QCOMPARE(serverStream->state(), QHttp2Stream::State::HalfClosedRemote);
clientStream->setState(QHttp2Stream::State::Open);
buffer = new QBuffer(clientStream);
buffer->setData(uploadedData);
buffer->open(QIODevice::ReadWrite);
// send data on closed stream
clientStream->sendDATA(buffer, true);
QVERIFY(rstClientSpy.wait());
QCOMPARE(rstServerSpy.count(), 0);
QCOMPARE(errrorServerSpy.count(), 1);
}
void tst_QHttp2Connection::resetAfterClose()
{
auto [client, server] = makeFakeConnectedSockets();
auto connection = makeHttp2Connection(client.get(), {}, Client);
auto serverConnection = makeHttp2Connection(server.get(), {}, Server);
QHttp2Stream *clientStream = connection->createStream().unwrap();
QVERIFY(clientStream);
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
QSignalSpy clientHeaderReceivedSpy{ clientStream, &QHttp2Stream::headersReceived };
HPack::HttpHeader headers = getRequiredHeaders();
clientStream->sendHEADERS(headers, true);
QVERIFY(newIncomingStreamSpy.wait());
auto *serverStream = newIncomingStreamSpy.front().front().value<QHttp2Stream *>();
QCOMPARE(clientStream->streamID(), serverStream->streamID());
QSignalSpy errorSpy(clientStream, &QHttp2Stream::errorOccurred);
const HPack::HttpHeader StatusOKHeaders{ { ":status", "200" } };
serverStream->sendHEADERS(StatusOKHeaders, true);
// Write the RST_STREAM frame manually because we guard against sending RST_STREAM on closed
// streams
auto &frameWriter = serverConnection->frameWriter;
frameWriter.start(Http2::FrameType::RST_STREAM, Http2::FrameFlag::EMPTY,
serverStream->streamID());
frameWriter.append(quint32(Http2::Http2Error::STREAM_CLOSED));
QVERIFY(frameWriter.write(*serverConnection->getSocket()));
QVERIFY(clientHeaderReceivedSpy.wait());
QCOMPARE(clientStream->state(), QHttp2Stream::State::Closed);
QTest::qWait(10); // Just needs to process events basically
QCOMPARE(errorSpy.count(), 0);
}
void tst_QHttp2Connection::testBadFrameSize_data()
{
QTest::addColumn<uchar>("frametype");
QTest::addColumn<int>("loadsize");
QTest::addColumn<bool>("rst_received");
QTest::addColumn<int>("goaway_received");
QTest::addColumn<bool>("use_stream_id");
QTest::newRow("priority_correct")
<< uchar(Http2::FrameType::PRIORITY) << 5 << false << 0 << true;
QTest::newRow("priority_too_long")
<< uchar(Http2::FrameType::PRIORITY) << 6 << true << 0 << true;
QTest::newRow("priority_no_payload")
<< uchar(Http2::FrameType::PRIORITY) << 0 << true << 0 << true;
QTest::newRow("priority_bad_on_stream0")
<< uchar(Http2::FrameType::PRIORITY) << 6 << false << 1 << false;
QTest::newRow("ping_correct") << uchar(Http2::FrameType::PING) << 8 << false << 0 << false;
QTest::newRow("ping_bad") << uchar(Http2::FrameType::PING) << 13 << false << 1 << false;
QHttp2Configuration configuration;
const int longerThanMaxFrameSize = (int)configuration.maxFrameSize() + 1;
QTest::newRow("data_too_long")
<< uchar(Http2::FrameType::DATA) << longerThanMaxFrameSize << true << 0 << true;
QTest::newRow("headers_too_long")
<< uchar(Http2::FrameType::HEADERS) << longerThanMaxFrameSize << false << 1 << true;
QTest::newRow("continuation_too_long") << uchar(Http2::FrameType::CONTINUATION)
<< longerThanMaxFrameSize << false << 1 << true;
}
void tst_QHttp2Connection::testBadFrameSize()
{
using namespace std::chrono_literals;
QFETCH(uchar, frametype);
QFETCH(int, loadsize);
QFETCH(bool, rst_received);
QFETCH(int, goaway_received);
QFETCH(bool, use_stream_id);
auto [client, server] = makeFakeConnectedSockets();
auto connection = makeHttp2Connection(client.get(), {}, Client);
auto serverConnection = makeHttp2Connection(server.get(), {}, Server);
QHttp2Stream *clientStream = connection->createStream().unwrap();
QVERIFY(clientStream);
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
QSignalSpy clientHeaderReceivedSpy{ clientStream, &QHttp2Stream::headersReceived };
HPack::HttpHeader headers = getRequiredHeaders();
clientStream->sendHEADERS(headers, false);
QVERIFY(newIncomingStreamSpy.wait());
auto *serverStream = newIncomingStreamSpy.front().front().value<QHttp2Stream *>();
QCOMPARE(clientStream->streamID(), serverStream->streamID());
QSignalSpy rstClientSpy{ clientStream, &QHttp2Stream::rstFrameReceived };
QSignalSpy rstServerSpy{ serverStream, &QHttp2Stream::rstFrameReceived };
QSignalSpy goawayClientSpy{ connection, &QHttp2Connection::receivedGOAWAY };
QSignalSpy goawayServerSpy{ serverConnection, &QHttp2Connection::receivedGOAWAY };
QCOMPARE(clientStream->state(), QHttp2Stream::State::Open);
QCOMPARE(serverStream->state(), QHttp2Stream::State::Open);
if (Http2::FrameType(frametype) == Http2::FrameType::CONTINUATION) {
// Send an empty HEADERS frame to be able to send a CONTINUATION frame later
auto type = uchar(Http2::FrameType::HEADERS);
auto flags = uchar(Http2::FrameFlag::EMPTY);
quint32 streamID = clientStream->streamID();
std::vector<uchar> buffer;
buffer.resize(Http2::frameHeaderSize);
buffer[3] = type;
buffer[4] = flags;
qToBigEndian(streamID, &buffer[5]);
// RFC9113 4.1: The 9 octets of the frame header are not included in this value.
quint32 size = quint32(buffer.size() - Http2::frameHeaderSize);
buffer[0] = size >> 16;
buffer[1] = size >> 8;
buffer[2] = size;
auto writtenN = connection->getSocket()->write(reinterpret_cast<const char *>(&buffer[0]),
buffer.size());
QCOMPARE(writtenN, qint64(buffer.size()));
QCOMPARE(clientStream->state(), QHttp2Stream::State::Open);
QCOMPARE(serverStream->state(), QHttp2Stream::State::Open);
// No RST/GOAWAY expected after an empty HEADERS. Use a short wait to
// flush the QueuedConnection chain (QBuffer emits readyRead deferred,
// so the path client-write → server readyRead → potential RST → client
// readyRead takes at least two event-loop rounds).
QVERIFY(!rstClientSpy.wait(50ms));
QCOMPARE(rstServerSpy.count(), 0);
QCOMPARE(goawayClientSpy.count(), 0);
}
{
auto type = frametype;
auto flags = uchar(Http2::FrameFlag::EMPTY);
quint32 streamID = clientStream->streamID();
std::vector<uchar> buffer;
buffer.resize(Http2::frameHeaderSize);
//012 Length (24) = 0x05 (set below),
//3 Type (8) = 0x02,
//4 Unused Flags (8),
// Reserved (1),
//5 Stream Identifier (31),
buffer[3] = type;
buffer[4] = flags;
qToBigEndian(use_stream_id ? streamID : 0, &buffer[5]);
buffer.resize(buffer.size() + loadsize);
// RFC9113 4.1: The 9 octets of the frame header are not included in this value.
quint32 size = quint32(buffer.size() - Http2::frameHeaderSize);
buffer[0] = size >> 16;
buffer[1] = size >> 8;
buffer[2] = size;
auto writtenN = connection->getSocket()->write(reinterpret_cast<const char *>(&buffer[0]), buffer.size());
QCOMPARE(writtenN, qint64(buffer.size()));
}
// Wait on whichever signal *should* arrive (fast path), then verify the
// other spy is still empty. Avoid the inverse pattern (spy.wait()==false)
// which unconditionally burns the full 5-second QSignalSpy timeout.
if (rst_received) {
QVERIFY(rstClientSpy.wait());
} else if (goaway_received) {
QVERIFY(goawayClientSpy.wait());
QCOMPARE(rstClientSpy.count(), 0);
} else {
// Neither RST nor GOAWAY expected. A 50 ms wait flushes the
// QueuedConnection chain; any late signal would arrive in the same window.
QVERIFY(!rstClientSpy.wait(50ms));
}
QCOMPARE(rstServerSpy.count(), 0);
QCOMPARE(goawayClientSpy.count(), goaway_received);
if (goaway_received)
return; // Connection is closed
// Reuse the connection
// This caused an assertion in QTBUG-141207
QHttp2Stream *clientStream2 = connection->createStream().unwrap();
QVERIFY(clientStream2);
clientStream2->sendHEADERS(headers, false);
QVERIFY(newIncomingStreamSpy.wait());
auto *serverStream2 = newIncomingStreamSpy.back().front().value<QHttp2Stream *>();
QCOMPARE(clientStream2->streamID(), serverStream2->streamID());
QCOMPARE(clientStream2->state(), QHttp2Stream::State::Open);
QCOMPARE(serverStream2->state(), QHttp2Stream::State::Open);
}
void tst_QHttp2Connection::testDataFrameAfterRSTIncoming()
{
auto [client, server] = makeFakeConnectedSockets();
auto connection = makeHttp2Connection(client.get(), {}, Client);
auto serverConnection = makeHttp2Connection(server.get(), {}, Server);
QHttp2Stream *clientStream = connection->createStream().unwrap();
QVERIFY(clientStream);
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
QSignalSpy clientHeaderReceivedSpy{ clientStream, &QHttp2Stream::headersReceived };
HPack::HttpHeader headers = getRequiredHeaders();
clientStream->sendHEADERS(headers, false);
QVERIFY(newIncomingStreamSpy.wait());
auto *serverStream = newIncomingStreamSpy.front().front().value<QHttp2Stream *>();
QCOMPARE(clientStream->streamID(), serverStream->streamID());
QSignalSpy rstServerSpy{ serverStream, &QHttp2Stream::rstFrameReceived };
QCOMPARE(clientStream->state(), QHttp2Stream::State::Open);
QCOMPARE(serverStream->state(), QHttp2Stream::State::Open);
// Reset the stream and wait until it the RST_STREAM frame is received
clientStream->sendRST_STREAM(Http2::Http2Error::STREAM_CLOSED);
QVERIFY(rstServerSpy.wait());
QSignalSpy closedClientSpy{ connection, &QHttp2Connection::connectionClosed };
// Send data as if we didn't receive the RST_STREAM
// It should not trigger an error since we could have not received the RST_STREAM frame yet
serverStream->setState(QHttp2Stream::State::Open);
QBuffer *buffer = new QBuffer(clientStream);
QByteArray uploadedData = "Hello World"_ba.repeated(10);
buffer->setData(uploadedData);
buffer->open(QIODevice::ReadWrite);
serverStream->sendDATA(buffer, false);
QVERIFY(!closedClientSpy.wait(std::chrono::seconds(1)));
}
void tst_QHttp2Connection::testDataFrameAfterRSTOutgoing()
{
auto [client, server] = makeFakeConnectedSockets();
auto connection = makeHttp2Connection(client.get(), {}, Client);
auto serverConnection = makeHttp2Connection(server.get(), {}, Server);
QHttp2Stream *clientStream = connection->createStream().unwrap();
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
QSignalSpy clientHeaderReceivedSpy{ clientStream, &QHttp2Stream::headersReceived };
QVERIFY(clientStream);
HPack::HttpHeader headers = getRequiredHeaders();
clientStream->sendHEADERS(headers, false);
QVERIFY(newIncomingStreamSpy.wait());
auto *serverStream = newIncomingStreamSpy.front().front().value<QHttp2Stream *>();
QCOMPARE(clientStream->streamID(), serverStream->streamID());
QSignalSpy rstServerSpy{ serverStream, &QHttp2Stream::rstFrameReceived };
QCOMPARE(clientStream->state(), QHttp2Stream::State::Open);
QCOMPARE(serverStream->state(), QHttp2Stream::State::Open);
// Reset the stream and wait until it the RST_STREAM frame is received
clientStream->sendRST_STREAM(Http2::Http2Error::STREAM_CLOSED);
QVERIFY(rstServerSpy.wait());
QSignalSpy closedServerSpy{ serverConnection, &QHttp2Connection::connectionClosed };
// Send data as if we didn't send the RST_STREAM
// It should trigger an error since we send the RST_STREAM frame ourself
clientStream->setState(QHttp2Stream::State::Open);
QBuffer *buffer = new QBuffer(serverStream);
QByteArray uploadedData = "Hello World"_ba.repeated(10);
buffer->setData(uploadedData);
buffer->open(QIODevice::ReadWrite);
clientStream->sendDATA(buffer, false);
QVERIFY(closedServerSpy.wait());
}
void tst_QHttp2Connection::headerFrameAfterRSTOutgoing_data()
{
QTest::addColumn<bool>("deleteStream");
QTest::addRow("retain-stream") << false;
QTest::addRow("delete-stream") << true;
}
void tst_QHttp2Connection::headerFrameAfterRSTOutgoing()
{
QFETCH(const bool, deleteStream);
auto [client, server] = makeFakeConnectedSockets();
auto *connection = makeHttp2Connection(client.get(), {}, Client);
auto *serverConnection = makeHttp2Connection(server.get(), {}, Server);
QHttp2Stream *clientStream = connection->createStream().unwrap();
QVERIFY(clientStream);
QSignalSpy client1HeadersSpy{ clientStream, &QHttp2Stream::headersReceived};
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
HPack::HttpHeader headers = getRequiredHeaders();
// Send some headers to let the server know about the stream
clientStream->sendHEADERS(headers, false);
// Wait for the stream on the server side
QVERIFY(newIncomingStreamSpy.wait());
auto *serverStream = newIncomingStreamSpy.front().front().value<QHttp2Stream *>();
newIncomingStreamSpy.clear();
QSignalSpy serverRSTReceivedSpy{ serverStream, &QHttp2Stream::rstFrameReceived };
// Send an RST frame from the client, but we don't process it yet
clientStream->sendRST_STREAM(Http2::CANCEL);
if (deleteStream)
delete std::exchange(clientStream, nullptr);
// The server sends a reply, not knowing about the inbound RST frame
const HPack::HttpHeader StandardReply{ { ":status", "200" }, { "x-whatever", "some info" } };
serverStream->sendHEADERS(StandardReply, true);
// With the bug in QTBUG-135800 we would ignore the RST frame, not processing it at all.
// This caused the HPACK lookup tables to be out of sync between server and client, eventually
// causing an error on Qt's side.
QVERIFY(serverRSTReceivedSpy.wait());
// We don't emit any headers for a reset stream
QVERIFY(!client1HeadersSpy.count());
// Create a new stream then send and handle a new request!
QHttp2Stream *clientStream2 = connection->createStream().unwrap();
QSignalSpy client2HeaderReceivedSpy{ clientStream2, &QHttp2Stream::headersReceived };
QSignalSpy client2ErrorOccurredSpy{ clientStream2, &QHttp2Stream::errorOccurred };
clientStream2->sendHEADERS(headers, true);
QVERIFY(newIncomingStreamSpy.wait());
QHttp2Stream *serverStream2 = newIncomingStreamSpy.front().front().value<QHttp2Stream *>();
serverStream2->sendHEADERS(StandardReply, true);
QVERIFY(client2HeaderReceivedSpy.wait());
QCOMPARE(client2ErrorOccurredSpy.count(), 0);
}
void tst_QHttp2Connection::connectToServer()
{
auto [client, server] = makeFakeConnectedSockets();
auto connection = makeHttp2Connection(client.get(), {}, Client);
auto serverConnection = makeHttp2Connection(server.get(), {}, Server);
QHttp2Stream *clientStream = connection->createStream().unwrap();
QVERIFY(clientStream);
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
QSignalSpy clientIncomingStreamSpy{ connection, &QHttp2Connection::newIncomingStream };
QSignalSpy clientHeaderReceivedSpy{ clientStream, &QHttp2Stream::headersReceived };
HPack::HttpHeader headers = getRequiredHeaders();
clientStream->sendHEADERS(headers, false);
QVERIFY(newIncomingStreamSpy.wait());
auto *serverStream = newIncomingStreamSpy.front().front().value<QHttp2Stream *>();
QVERIFY(serverStream);
const HPack::HttpHeader ExpectedResponseHeaders{ { ":status", "200" } };
serverStream->sendHEADERS(ExpectedResponseHeaders, true);
QVERIFY(clientHeaderReceivedSpy.wait());
const HPack::HttpHeader
headersReceived = clientHeaderReceivedSpy.front().front().value<HPack::HttpHeader>();
QCOMPARE(headersReceived, ExpectedResponseHeaders);
QCOMPARE(clientIncomingStreamSpy.count(), 0);
}
void tst_QHttp2Connection::WINDOW_UPDATE()
{
auto [client, server] = makeFakeConnectedSockets();
auto connection = makeHttp2Connection(client.get(), {}, Client);
QHttp2Configuration config;
config.setStreamReceiveWindowSize(1024); // Small window on server to provoke WINDOW_UPDATE
auto serverConnection = makeHttp2Connection(server.get(), config, Server);
QHttp2Stream *clientStream = connection->createStream().unwrap();
QVERIFY(clientStream);
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
QSignalSpy clientHeaderReceivedSpy{ clientStream, &QHttp2Stream::headersReceived };
QSignalSpy clientDataReceivedSpy{ clientStream, &QHttp2Stream::dataReceived };
HPack::HttpHeader expectedRequestHeaders = HPack::HttpHeader{
{ ":authority", "example.com" },
{ ":method", "POST" },
{ ":path", "/" },
{ ":scheme", "https" },
};
clientStream->sendHEADERS(expectedRequestHeaders, false);
QVERIFY(newIncomingStreamSpy.wait());
auto *serverStream = newIncomingStreamSpy.front().front().value<QHttp2Stream *>();
QVERIFY(serverStream);
QSignalSpy serverDataReceivedSpy{ serverStream, &QHttp2Stream::dataReceived };
// Since a stream is only opened on the remote side when the header is received,
// we can check the headers now immediately
QCOMPARE(serverStream->receivedHeaders(), expectedRequestHeaders);
QByteArray uploadedData = "Hello World"_ba.repeated(1000);
clientStream->sendDATA(uploadedData, true);
bool streamEnd = false;
QByteArray serverReceivedData;
while (!streamEnd) { // The window is too small to receive all data at once, so loop
QVERIFY(serverDataReceivedSpy.wait());
auto latestEmission = serverDataReceivedSpy.back();
serverReceivedData += latestEmission.front().value<QByteArray>();
streamEnd = latestEmission.back().value<bool>();
}
QCOMPARE(serverReceivedData.size(), uploadedData.size());
QCOMPARE(serverReceivedData, uploadedData);
QCOMPARE(clientStream->state(), QHttp2Stream::State::HalfClosedLocal);
QCOMPARE(serverStream->state(), QHttp2Stream::State::HalfClosedRemote);
const HPack::HttpHeader ExpectedResponseHeaders{ { ":status", "200" } };
serverStream->sendHEADERS(ExpectedResponseHeaders, false);
serverStream->sendDATA(uploadedData, true);
QVERIFY(clientHeaderReceivedSpy.wait());
const HPack::HttpHeader
headersReceived = clientHeaderReceivedSpy.front().front().value<HPack::HttpHeader>();
QCOMPARE(headersReceived, ExpectedResponseHeaders);
QTRY_COMPARE_GT(clientDataReceivedSpy.count(), 0);
QCOMPARE(clientDataReceivedSpy.count(), 1); // Only one DATA frame since our window is larger
QCOMPARE(clientDataReceivedSpy.front().front().value<QByteArray>(), uploadedData);
QCOMPARE(clientStream->state(), QHttp2Stream::State::Closed);
QCOMPARE(serverStream->state(), QHttp2Stream::State::Closed);
}
namespace {
void sendHEADERSFrame(HPack::Encoder &encoder,
Http2::FrameWriter &frameWriter,
quint32 streamId,
const HPack::HttpHeader &headers,
Http2::FrameFlags flags,
QIODevice &socket)
{
frameWriter.start(Http2::FrameType::HEADERS,
flags,
streamId);
frameWriter.append(quint32());
frameWriter.append(QHttp2Stream::DefaultPriority);
HPack::BitOStream outputStream(frameWriter.outboundFrame().buffer);
QVERIFY(encoder.encodeRequest(outputStream, headers));
frameWriter.setPayloadSize(static_cast<quint32>(frameWriter.outboundFrame().buffer.size()
- Http2::Http2PredefinedParameters::frameHeaderSize));
frameWriter.write(socket);
}
void sendDATAFrame(Http2::FrameWriter &frameWriter,
quint32 streamId,
Http2::FrameFlags flags,
QIODevice &socket)
{
frameWriter.start(Http2::FrameType::DATA,
flags,
streamId);
frameWriter.write(socket);
}
void sendCONTINUATIONFrame(HPack::Encoder &encoder,
Http2::FrameWriter &frameWriter,
quint32 streamId,
const HPack::HttpHeader &headers,
Http2::FrameFlags flags,
QIODevice &socket)
{
frameWriter.start(Http2::FrameType::CONTINUATION,
flags,
streamId);
HPack::BitOStream outputStream(frameWriter.outboundFrame().buffer);
QVERIFY(encoder.encodeRequest(outputStream, headers));
frameWriter.setPayloadSize(static_cast<quint32>(frameWriter.outboundFrame().buffer.size()
- Http2::Http2PredefinedParameters::frameHeaderSize));
frameWriter.write(socket);
}
}
void tst_QHttp2Connection::testCONTINUATIONFrame()
{
static const HPack::HttpHeader headers = HPack::HttpHeader {
{ ":authority", "example.com" },
{ ":method", "GET" },
{ ":path", "/" },
{ ":scheme", "https" },
{ "n1", "v1" },
{ "n2", "v2" },
{ "n3", "v3" }
};
#define CREATE_CONNECTION() \
auto [client, server] = makeFakeConnectedSockets(); \
auto clientConnection = makeHttp2Connection(client.get(), {}, Client); \
auto serverConnection = makeHttp2Connection(server.get(), {}, Server); \
QHttp2Stream *clientStream = clientConnection->createStream().unwrap(); \
QVERIFY(clientStream); \
QVERIFY(waitForSettingsExchange(clientConnection, serverConnection)); \
\
HPack::Encoder encoder = HPack::Encoder(HPack::FieldLookupTable::DefaultSize, true); \
Http2::FrameWriter frameWriter; \
\
QSignalSpy serverIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream }; \
QSignalSpy receivedGOAWAYSpy{ clientConnection, &QHttp2Connection::receivedGOAWAY }; \
\
// Send multiple CONTINUATION frames
{
CREATE_CONNECTION();
frameWriter.start(Http2::FrameType::HEADERS,
Http2::FrameFlag::PRIORITY,
clientStream->streamID());
frameWriter.append(quint32());
frameWriter.append(QHttp2Stream::DefaultPriority);
HPack::BitOStream outputStream(frameWriter.outboundFrame().buffer);
QVERIFY(encoder.encodeRequest(outputStream, headers));
// split headers into multiple CONTINUATION frames
const auto sizeLimit = static_cast<qint32>(frameWriter.outboundFrame().buffer.size() / 5);
frameWriter.writeHEADERS(*client, sizeLimit);
QVERIFY(serverIncomingStreamSpy.wait());
auto *serverStream = serverIncomingStreamSpy.front().front().value<QHttp2Stream *>();
QVERIFY(serverStream);
// correct behavior accepted and handled sensibly
QCOMPARE(serverStream->receivedHeaders(), headers);
}
// A DATA frame between a HEADERS frame and a CONTINUATION frame.
{
CREATE_CONNECTION();
sendHEADERSFrame(encoder, frameWriter, clientStream->streamID(),
headers, Http2::FrameFlag::PRIORITY, *client);
QVERIFY(serverIncomingStreamSpy.wait());
sendDATAFrame(frameWriter, clientStream->streamID(), Http2::FrameFlag::EMPTY, *client);
// the client correctly rejected our malformed stream contents by telling us to GO AWAY
QVERIFY(receivedGOAWAYSpy.wait());
}
// A CONTINUATION frame after a frame with the END_HEADERS set.
{
CREATE_CONNECTION();
sendHEADERSFrame(encoder, frameWriter, clientStream->streamID(), headers,
Http2::FrameFlag::PRIORITY | Http2::FrameFlag::END_HEADERS, *client);
QVERIFY(serverIncomingStreamSpy.wait());
sendCONTINUATIONFrame(encoder, frameWriter, clientStream->streamID(),
headers, Http2::FrameFlag::EMPTY, *client);
// the client correctly rejected our malformed stream contents by telling us to GO AWAY
QVERIFY(receivedGOAWAYSpy.wait());
}
// A CONTINUATION frame with the stream id 0x00.
{
CREATE_CONNECTION();
sendHEADERSFrame(encoder, frameWriter, clientStream->streamID(),
headers, Http2::FrameFlag::PRIORITY, *client);
QVERIFY(serverIncomingStreamSpy.wait());
sendCONTINUATIONFrame(encoder, frameWriter, 0,
headers, Http2::FrameFlag::EMPTY, *client);
// the client correctly rejected our malformed stream contents by telling us to GO AWAY
QVERIFY(receivedGOAWAYSpy.wait());
}
// A CONTINUATION frame with a different stream id then the previous frame.
{
CREATE_CONNECTION();
sendHEADERSFrame(encoder, frameWriter, clientStream->streamID(),
headers, Http2::FrameFlag::PRIORITY, *client);
QVERIFY(serverIncomingStreamSpy.wait());
QHttp2Stream *newClientStream = clientConnection->createStream().unwrap();
QVERIFY(newClientStream);
sendCONTINUATIONFrame(encoder, frameWriter, newClientStream->streamID(),
headers, Http2::FrameFlag::EMPTY, *client);
// the client correctly rejected our malformed stream contents by telling us to GO AWAY
QVERIFY(receivedGOAWAYSpy.wait());
}
}
void tst_QHttp2Connection::goaway_data()
{
QTest::addColumn<bool>("endStreamOnHEADERS");
QTest::addColumn<bool>("createNewStreamAfterDelay");
QTest::addRow("end-on-headers") << true << false;
QTest::addRow("end-after-data") << false << false;
QTest::addRow("end-after-new-late-stream") << false << true;
}
void tst_QHttp2Connection::goaway()
{
QFETCH(const bool, endStreamOnHEADERS);
QFETCH(const bool, createNewStreamAfterDelay);
auto [client, server] = makeFakeConnectedSockets();
auto connection = makeHttp2Connection(client.get(), {}, Client);
auto serverConnection = makeHttp2Connection(server.get(), {}, Server);
QHttp2Stream *clientStream = connection->createStream().unwrap();
QVERIFY(clientStream);
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
QSignalSpy clientGoawaySpy{ connection, &QHttp2Connection::receivedGOAWAY };
QSignalSpy clientClosedSpy{ connection, &QHttp2Connection::connectionClosed };
QSignalSpy serverGoawaySpy{ serverConnection, &QHttp2Connection::receivedGOAWAY };
QSignalSpy serverClosedSpy{ serverConnection, &QHttp2Connection::connectionClosed };
const HPack::HttpHeader headers = getRequiredHeaders();
clientStream->sendHEADERS(headers, false);
QVERIFY(newIncomingStreamSpy.wait());
auto *serverStream = newIncomingStreamSpy.front().front().value<QHttp2Stream *>();
QVERIFY(serverStream);
serverConnection->close(); // NO_ERROR -> graceful shutdown
// New stream creation is illegal now
auto createStreamResult = serverConnection->createLocalStreamInternal();
QVERIFY(createStreamResult.has_error());
QCOMPARE(createStreamResult.error(), QHttp2Connection::CreateStreamError::ReceivedGOAWAY);
// Client received initial GOAWAY
QVERIFY(clientGoawaySpy.wait());
QCOMPARE(clientGoawaySpy.first().first().value<Http2::Http2Error>(), Http2::HTTP2_NO_ERROR);
QCOMPARE(clientGoawaySpy.first().last().value<quint32>(), Http2::lastValidStreamID);
clientGoawaySpy.clear();
// New client-stream creation is illegal now
auto invalidStream = connection->createStream();
QVERIFY(!invalidStream.ok());
QVERIFY(invalidStream.has_error());
QCOMPARE(invalidStream.error(), QHttp2Connection::CreateStreamError::ReceivedGOAWAY);
// Client receives final GOAWAY with actual lastStreamID (after PING RTT)
QVERIFY(clientGoawaySpy.wait());
QCOMPARE(clientGoawaySpy.first().at(0).value<Http2::Http2Error>(), Http2::HTTP2_NO_ERROR);
QCOMPARE(clientGoawaySpy.first().at(1).value<quint32>(), clientStream->streamID());
clientGoawaySpy.clear();
// Directly create a stream to avoid the GOAWAY check:
quint32 nextStreamId = clientStream->streamID() + 2;
QHttp2Stream *ignoredClientStream = connection->createStreamInternal_impl(nextStreamId);
QSignalSpy streamResetSpy{ ignoredClientStream, &QHttp2Stream::rstFrameReceived };
ignoredClientStream->sendHEADERS(headers, endStreamOnHEADERS);
// The stream should be ignored:
using namespace std::chrono_literals;
QVERIFY(!streamResetSpy.wait(100ms)); // We don't get reset because we are ignored
if (endStreamOnHEADERS)
return;
ignoredClientStream->sendDATA("my data", createNewStreamAfterDelay);
// We cheat and try to send data after the END_STREAM flag has been sent
if (!createNewStreamAfterDelay) {
// Manually send a frame with END_STREAM so the QHttp2Stream thinks it's fine to send more
// DATA
connection->frameWriter.start(Http2::FrameType::DATA, Http2::FrameFlag::END_STREAM,
ignoredClientStream->streamID());
connection->frameWriter.write(*connection->getSocket());
QVERIFY(!streamResetSpy.wait(100ms)); // We don't get reset because we are ignored
const auto tstStream = [](const auto &spy, Http2::Http2Error code,
QLatin1StringView errMsg) {
QCOMPARE(spy.first().at(0).template value<Http2::Http2Error>(), code);
QCOMPARE(spy.first().at(1).template value<QString>(), errMsg);
};
QLatin1StringView serverErrorMsg("DATA on invalid stream");
QTest::ignoreMessage(QtCriticalMsg, QRegularExpression(".*" + serverErrorMsg + ".*"));
QSignalSpy clientStreamErrorSpy(clientStream, &QHttp2Stream::errorOccurred);
QSignalSpy secondclientStreamErrorSpy(ignoredClientStream, &QHttp2Stream::errorOccurred);
QSignalSpy serverStreamErrorSpy(serverStream, &QHttp2Stream::errorOccurred);
// Triggers a connectionError of 'ENHANCE_YOUR_CALM' on the server
// (more activity after END_STREAM)
ignoredClientStream->sendDATA("my data", true);
QTRY_COMPARE(serverClosedSpy.count(), 1);
tstStream(serverStreamErrorSpy, Http2::ENHANCE_YOUR_CALM, serverErrorMsg);
QTRY_COMPARE(clientGoawaySpy.count(), 1);
QCOMPARE(clientGoawaySpy.first().at(0).value<Http2::Http2Error>(),
Http2::ENHANCE_YOUR_CALM);
QCOMPARE(clientGoawaySpy.first().at(1).value<quint32>(), clientStream->streamID());
QTRY_COMPARE(clientClosedSpy.count(), 1);
QLatin1StringView clientErrorMsg("Received GOAWAY");
tstStream(clientStreamErrorSpy, Http2::ENHANCE_YOUR_CALM, clientErrorMsg);
tstStream(secondclientStreamErrorSpy, Http2::ENHANCE_YOUR_CALM, clientErrorMsg);
return;
}
// Override the deadline timer so we don't have to wait too long
serverConnection->m_goawayGraceTimer.setRemainingTime(50ms);
// We can create the stream whenever, it is not noticed by the server until we send something.
nextStreamId += 2;
QHttp2Stream *rejectedStream = connection->createStreamInternal_impl(nextStreamId);
// Sleep until the grace period is over:
QTRY_VERIFY(serverConnection->m_goawayGraceTimer.hasExpired());
QVERIFY(rejectedStream->sendHEADERS(headers, true));
QTest::ignoreMessage(QtCriticalMsg,
QRegularExpression(u".*Connection error: Peer refused to GOAWAY\\..*"_s));
QVERIFY(clientGoawaySpy.wait());
QCOMPARE(clientGoawaySpy.size(), 1);
QCOMPARE(clientGoawaySpy.first().first().value<Http2::Http2Error>(), Http2::PROTOCOL_ERROR);
// The first stream is still the last processed one:
QCOMPARE(clientGoawaySpy.first().last().value<quint32>(), clientStream->streamID());
}
void tst_QHttp2Connection::serverInitiatedGoaways_data()
{
QTest::addColumn<QString>("scenario");
QTest::newRow("graceful-shutdown") << u"graceful-shutdown"_s;
QTest::newRow("graceful-then-error") << u"graceful-then-error"_s;
QTest::newRow("increasing-lastStreamId") << u"increasing-lastStreamId"_s;
}
void tst_QHttp2Connection::serverInitiatedGoaways()
{
QFETCH(QString, scenario);
auto [client, server] = makeFakeConnectedSockets();
auto clientConn = makeHttp2Connection(client.get(), {}, Client);
auto serverConn = makeHttp2Connection(server.get(), {}, Server);
// Client creates stream
auto *clientStream = clientConn->createStream().unwrap();
QVERIFY(clientStream);
QVERIFY(waitForSettingsExchange(clientConn, serverConn));
QVERIFY(clientStream->sendHEADERS(getRequiredHeaders(), false));
// Server receives stream
QSignalSpy newStreamSpy{ serverConn, &QHttp2Connection::newIncomingStream };
QVERIFY(newStreamSpy.wait());
auto *serverStream = newStreamSpy.front().front().value<QHttp2Stream *>();
QVERIFY(serverStream);
QSignalSpy clientGoawaySpy{ clientConn, &QHttp2Connection::receivedGOAWAY };
QSignalSpy clientClosedSpy{ clientConn, &QHttp2Connection::connectionClosed };
QSignalSpy serverGoawaySpy{ serverConn, &QHttp2Connection::receivedGOAWAY };
QSignalSpy serverClosedSpy{ serverConn, &QHttp2Connection::connectionClosed };
QSignalSpy clientErrorSpy{ clientStream, &QHttp2Stream::errorOccurred };
serverConn->close(); // Server initiates graceful shutdown
// Client receives initial GOAWAY with 2^31-1
QVERIFY(clientGoawaySpy.wait());
QCOMPARE(clientGoawaySpy.count(), 1);
QCOMPARE(clientGoawaySpy.at(0).at(0).value<Http2::Http2Error>(), Http2::HTTP2_NO_ERROR);
QCOMPARE(clientGoawaySpy.at(0).at(1).value<quint32>(), Http2::lastValidStreamID);
// After receiving a GOAWAY we should not be able to create new streams
auto clientStream2 = clientConn->createStream();
QCOMPARE_EQ(clientStream2.error(), QHttp2Connection::CreateStreamError::ReceivedGOAWAY);
// Client receives final GOAWAY with actual lastStreamID (after PING RTT)
QVERIFY(clientGoawaySpy.wait(500));
QCOMPARE(clientGoawaySpy.count(), 2);
QCOMPARE(clientGoawaySpy.at(1).at(0).value<Http2::Http2Error>(), Http2::HTTP2_NO_ERROR);
const quint32 finalLastStreamId = clientGoawaySpy.at(1).at(1).value<quint32>();
QCOMPARE(finalLastStreamId, clientStream->streamID());
// Stream still active - graceful shutdown allows completion
QVERIFY(clientStream->isActive());
QCOMPARE(clientClosedSpy.count(), 0);
if (scenario == "increasing-lastStreamId"_L1) {
QLatin1StringView errMsg("Repeated GOAWAY with invalid last stream ID");
QTest::ignoreMessage(QtCriticalMsg, QRegularExpression(".*" + errMsg + ".*"));
// Send GOAWAY with higher lastStreamId than the final one (protocol violation)
const quint32 invalidHigherId = finalLastStreamId + 2;
serverConn->sendGOAWAYFrame(Http2::HTTP2_NO_ERROR, invalidHigherId);
QTRY_COMPARE(clientErrorSpy.count(), 1);
QCOMPARE(clientErrorSpy.count(), 1);
QCOMPARE(clientErrorSpy.first().first().value<Http2::Http2Error>(), Http2::PROTOCOL_ERROR);
QCOMPARE(clientErrorSpy.first().last().value<QString>(), errMsg);
// Client detects violation and responds with PROTOCOL_ERROR
QVERIFY(serverGoawaySpy.wait());
QCOMPARE(serverGoawaySpy.last().at(0).value<Http2::Http2Error>(), Http2::PROTOCOL_ERROR);
QTRY_COMPARE(clientClosedSpy.count(), 1);
QTRY_COMPARE(serverClosedSpy.count(), 1);
return;
} else if (scenario == "graceful-then-error") {
// RFC 9113 6.8: An endpoint MAY send multiple GOAWAY frames if circumstances change
serverConn->close(Http2::INTERNAL_ERROR);
// Client receives error GOAWAY
QTRY_COMPARE(clientGoawaySpy.count(), 3);
QCOMPARE(clientGoawaySpy.at(2).at(0).value<Http2::Http2Error>(), Http2::INTERNAL_ERROR);
// Error GOAWAY lastStreamId must not exceed previous
QVERIFY(clientGoawaySpy.at(2).at(1).value<quint32>() <= finalLastStreamId);
// Server closes immediately after error
QTRY_COMPARE(serverClosedSpy.count(), 1);
// Client stream should receive error
QVERIFY(!clientStream->isActive());
QTRY_COMPARE(clientErrorSpy.count(), 1);
QCOMPARE(clientErrorSpy.count(), 1);
QCOMPARE(clientErrorSpy.first().first().value<Http2::Http2Error>(), Http2::INTERNAL_ERROR);
QTRY_COMPARE(clientClosedSpy.count(), 1);
// Additional close() calls should be ignored
serverConn->close();
serverConn->close(Http2::FLOW_CONTROL_ERROR);
qApp->processEvents();
QCOMPARE(clientGoawaySpy.count(), 3); // No new GOAWAY
return;
} else if ("graceful-shutdown") {
QSignalSpy serverDataSpy{ serverStream, &QHttp2Stream::dataReceived };
QVERIFY(clientStream->sendDATA("final-data", true));
QVERIFY(serverDataSpy.wait());
QSignalSpy clientHeadersSpy{ clientStream, &QHttp2Stream::headersReceived };
const HPack::HttpHeader responseHeaders{ { ":status", "200" } };
QVERIFY(serverStream->sendHEADERS(responseHeaders, true));
QVERIFY(clientHeadersSpy.wait());
QCOMPARE(clientStream->state(), QHttp2Stream::State::Closed);
// Connection closes after all streams complete
QTRY_COMPARE(serverClosedSpy.count(), 1);
QTRY_COMPARE(clientClosedSpy.count(), 1);
// No additional GOAWAYs
QCOMPARE(clientGoawaySpy.count(), 2);
}
}
void tst_QHttp2Connection::clientInitiatedGoaway()
{
// Clients don't need two-phase GOAWAY because they control their own
// stream creation (no race condition). Client sends single GOAWAY with the
// last server-initiated (even) stream ID it processed.
auto [client, server] = makeFakeConnectedSockets();
auto clientConn = makeHttp2Connection(client.get(), {}, Client);
auto serverConn = makeHttp2Connection(server.get(), {}, Server);
// Client creates stream
auto *clientStream = clientConn->createStream().unwrap();
QVERIFY(clientStream);
QVERIFY(waitForSettingsExchange(clientConn, serverConn));
QVERIFY(clientStream->sendHEADERS(getRequiredHeaders(), false));
// Server receives stream
QSignalSpy newStreamSpy{ serverConn, &QHttp2Connection::newIncomingStream };
QVERIFY(newStreamSpy.wait());
auto *serverStream = newStreamSpy.front().front().value<QHttp2Stream *>();
QVERIFY(serverStream);
QSignalSpy clientGoawaySpy{ clientConn, &QHttp2Connection::receivedGOAWAY };
QSignalSpy clientClosedSpy{ clientConn, &QHttp2Connection::connectionClosed };
QSignalSpy serverGoawaySpy{ serverConn, &QHttp2Connection::receivedGOAWAY };
QSignalSpy serverClosedSpy{ serverConn, &QHttp2Connection::connectionClosed };
// Client initiates graceful shutdown
clientConn->close();
// Client should not be able to create new streams now
auto rejectedClientStream = clientConn->createStream();
QCOMPARE_EQ(rejectedClientStream.error(), QHttp2Connection::CreateStreamError::ReceivedGOAWAY);
// Server receives GOAWAY
QVERIFY(serverGoawaySpy.wait());
QCOMPARE(serverGoawaySpy.count(), 1);
QCOMPARE(serverGoawaySpy.at(0).at(0).value<Http2::Http2Error>(), Http2::HTTP2_NO_ERROR);
const quint32 lastStreamId = serverGoawaySpy.at(0).at(1).value<quint32>();
QCOMPARE(lastStreamId, 0u);
// Existing streams can still complete
QVERIFY(clientStream->isActive());
QVERIFY(serverStream->isActive());
// Complete the stream exchange
QSignalSpy serverDataSpy{ serverStream, &QHttp2Stream::dataReceived };
QVERIFY(clientStream->sendDATA("final-data", true));
QVERIFY(serverDataSpy.wait());
QSignalSpy clientHeadersSpy{ clientStream, &QHttp2Stream::headersReceived };
const HPack::HttpHeader responseHeaders{ { ":status", "200" } };
QVERIFY(serverStream->sendHEADERS(responseHeaders, true));
QVERIFY(clientHeadersSpy.wait());
QCOMPARE(clientStream->state(), QHttp2Stream::State::Closed);
QCOMPARE(serverStream->state(), QHttp2Stream::State::Closed);
QTRY_COMPARE(clientClosedSpy.count(), 1);
QCOMPARE(serverGoawaySpy.count(), 1);
QTRY_COMPARE(serverClosedSpy.count(), 1);
}
void tst_QHttp2Connection::runtimeSessionWindowResize()
{
// Growing the connection window emits WINDOW_UPDATE(stream 0, delta); a
// zero-delta resize must be a no-op (a WINDOW_UPDATE with increment 0 is a
// PROTOCOL_ERROR). Shrinking sends nothing and only lowers the ceiling.
auto [client, server] = makeFakeConnectedSockets();
QHttp2Configuration config;
config.setSessionReceiveWindowSize(128 * 1024);
auto clientConn = makeHttp2Connection(client.get(), config, Client);
auto serverConn = makeHttp2Connection(server.get(), { }, Server);
QVERIFY(clientConn->createStream().unwrap()); // flush the client preface
QVERIFY(waitForSettingsExchange(clientConn, serverConn));
const qint32 serverSendWindowBefore = serverConn->sessionSendWindowSize;
QCOMPARE(serverSendWindowBefore, 128 * 1024);
QVERIFY(!clientConn->setSessionReceiveWindowSize(0));
QVERIFY(!clientConn->setSessionReceiveWindowSize(-1));
QVERIFY(clientConn->setSessionReceiveWindowSize(128 * 1024)); // no-op: writes nothing
QCOMPARE(serverConn->sessionSendWindowSize, serverSendWindowBefore);
const qint32 newMax = 1 * 1024 * 1024;
QVERIFY(clientConn->setSessionReceiveWindowSize(newMax));
QVERIFY(QTest::qWaitFor([&]() { return serverConn->sessionSendWindowSize == newMax; }));
// Shrinking writes nothing; the window drains against the lower ceiling.
QVERIFY(clientConn->setSessionReceiveWindowSize(64 * 1024));
QCOMPARE(clientConn->maxSessionReceiveWindowSize, 64 * 1024);
QCOMPARE(serverConn->sessionSendWindowSize, newMax);
QVERIFY(!serverConn->m_connectionAborted);
}
void tst_QHttp2Connection::settingsInitialWindowSizeOverflow()
{
// RFC 9113, 6.9.2: raising SETTINGS_INITIAL_WINDOW_SIZE so a send window
// exceeds 2^31-1 is a connection FLOW_CONTROL_ERROR. Reached by maxing a
// stream's send window, then raising it.
constexpr qint32 MaxWindow = std::numeric_limits<qint32>::max(); // 2^31 - 1
auto [client, server] = makeFakeConnectedSockets();
auto clientConn = makeHttp2Connection(client.get(), { }, Client);
auto serverConn = makeHttp2Connection(server.get(), { }, Server);
QHttp2Stream *clientStream = clientConn->createStream().unwrap();
QVERIFY(clientStream);
QVERIFY(waitForSettingsExchange(clientConn, serverConn));
QSignalSpy newIncomingStreamSpy{ serverConn, &QHttp2Connection::newIncomingStream };
clientStream->sendHEADERS(getRequiredHeaders(), false);
QVERIFY(newIncomingStreamSpy.wait());
QCOMPARE(clientStream->m_sendWindow, qint32(Http2::defaultSessionWindowSize));
// Raw peer: WINDOW_UPDATE the stream up to exactly the maximum window (QBuffer delivers
// readyRead deferred, so wait for the client to apply it).
{
Http2::FrameWriter wu(Http2::FrameType::WINDOW_UPDATE, Http2::FrameFlag::EMPTY,
clientStream->streamID());
wu.append(quint32(MaxWindow - Http2::defaultSessionWindowSize));
QVERIFY(wu.write(*server));
}
QVERIFY(QTest::qWaitFor([&]() { return clientStream->m_sendWindow == MaxWindow; }));
QSignalSpy serverGoawaySpy{ serverConn, &QHttp2Connection::receivedGOAWAY };
// Raw peer: raise SETTINGS_INITIAL_WINDOW_SIZE by 1, overflowing the maxed-out send window.
QTest::ignoreMessage(QtCriticalMsg,
QRegularExpression(u".*SETTINGS_INITIAL_WINDOW_SIZE overflowed.*"_s));
{
Http2::FrameWriter s(Http2::FrameType::SETTINGS, Http2::FrameFlag::EMPTY,
Http2::connectionStreamID);
s.append(Http2::Settings::INITIAL_WINDOW_SIZE_ID);
s.append(quint32(Http2::defaultSessionWindowSize + 1));
QVERIFY(s.write(*server));
}
QVERIFY(QTest::qWaitFor([&]() { return serverGoawaySpy.count() == 1; }));
QCOMPARE(serverGoawaySpy.first().first().value<Http2::Http2Error>(), Http2::FLOW_CONTROL_ERROR);
QVERIFY(clientConn->m_connectionAborted);
}
void tst_QHttp2Connection::windowUpdateOverflow_data()
{
QTest::addColumn<bool>("connectionLevel");
QTest::newRow("stream") << false;
QTest::newRow("connection") << true;
}
void tst_QHttp2Connection::windowUpdateOverflow()
{
// RFC 9113, 6.9.1: a WINDOW_UPDATE growing a window past 2^31-1 is a
// FLOW_CONTROL_ERROR (RST_STREAM for a stream, GOAWAY for the connection).
QFETCH(const bool, connectionLevel);
constexpr quint32 MaxDelta = std::numeric_limits<qint32>::max(); // 2^31 - 1
auto [client, server] = makeFakeConnectedSockets();
auto clientConn = makeHttp2Connection(client.get(), { }, Client);
auto serverConn = makeHttp2Connection(server.get(), { }, Server);
QHttp2Stream *clientStream = clientConn->createStream().unwrap();
QVERIFY(clientStream);
QVERIFY(waitForSettingsExchange(clientConn, serverConn));
QSignalSpy newIncomingStreamSpy{ serverConn, &QHttp2Connection::newIncomingStream };
clientStream->sendHEADERS(getRequiredHeaders(), false);
QVERIFY(newIncomingStreamSpy.wait());
// Default window is 65535; a single maximum-sized increment overflows it.
const quint32 targetStreamID =
connectionLevel ? quint32(Http2::connectionStreamID) : clientStream->streamID();
QSignalSpy streamErrorSpy{ clientStream, &QHttp2Stream::errorOccurred };
QSignalSpy serverGoawaySpy{ serverConn, &QHttp2Connection::receivedGOAWAY };
if (connectionLevel) {
QTest::ignoreMessage(QtCriticalMsg,
QRegularExpression(u".*WINDOW_UPDATE exceeds maximum window.*"_s));
}
{
Http2::FrameWriter wu(Http2::FrameType::WINDOW_UPDATE, Http2::FrameFlag::EMPTY,
targetStreamID);
wu.append(MaxDelta);
QVERIFY(wu.write(*server));
}
if (connectionLevel) {
QVERIFY(QTest::qWaitFor([&]() { return serverGoawaySpy.count() == 1; }));
QCOMPARE(serverGoawaySpy.first().first().value<Http2::Http2Error>(),
Http2::FLOW_CONTROL_ERROR);
QVERIFY(clientConn->m_connectionAborted);
} else {
QVERIFY(QTest::qWaitFor([&]() { return streamErrorSpy.count() == 1; }));
QCOMPARE(streamErrorSpy.first().first().value<Http2::Http2Error>(),
Http2::FLOW_CONTROL_ERROR);
QVERIFY(!clientConn->m_connectionAborted); // stream-level error leaves the connection up
}
}
void tst_QHttp2Connection::totalBytesReceivedDATACounts()
{
auto [client, server] = makeFakeConnectedSockets();
QHttp2Configuration config;
config.setStreamReceiveWindowSize(4 * 1024 * 1024);
auto clientConn = makeHttp2Connection(client.get(), { }, Client);
auto serverConn = makeHttp2Connection(server.get(), config, Server);
QHttp2Stream *clientStream = clientConn->createStream().unwrap();
QVERIFY(clientStream);
QVERIFY(waitForSettingsExchange(clientConn, serverConn));
QSignalSpy newIncomingStreamSpy{ serverConn, &QHttp2Connection::newIncomingStream };
clientStream->sendHEADERS(getRequiredHeaders(), false);
QVERIFY(newIncomingStreamSpy.wait());
auto *serverStream = newIncomingStreamSpy.front().front().value<QHttp2Stream *>();
QVERIFY(serverStream);
QCOMPARE(serverConn->totalBytesReceivedDATA(), 0u);
const QByteArray payload = "x"_ba.repeated(50000);
QSignalSpy serverDataReceivedSpy{ serverStream, &QHttp2Stream::dataReceived };
clientStream->sendDATA(payload, true);
bool streamEnd = false;
while (!streamEnd) {
QVERIFY(serverDataReceivedSpy.wait());
streamEnd = serverDataReceivedSpy.back().back().value<bool>();
}
QCOMPARE(serverConn->totalBytesReceivedDATA(), quint64(payload.size()));
}
void tst_QHttp2Connection::uploadByteDeviceDestroyedWhileBlocked()
{
// uploadDeviceDestroyed() used to clear m_uploadDevice only, leaving
// m_uploadByteDevice dangling. A later SETTINGS INITIAL_WINDOW_SIZE
// increase from the peer would then queue maybeResumeUpload(), which
// dereferences the freed device (heap-use-after-free).
auto [client, server] = makeFakeConnectedSockets();
auto clientConn = makeHttp2Connection(client.get(), {}, Client);
// Creating the stream flushes the client preface; handleReadyRead()
// ignores incoming frames until that has happened.
QHttp2Stream *clientStream = clientConn->createStream().unwrap();
QVERIFY(clientStream);
// Shrink the per-stream send window so a 4096-byte upload stalls almost
// immediately, well before the device is exhausted. There's no public
// API for this, so inject the SETTINGS frame directly.
{
Http2::FrameWriter s(Http2::FrameType::SETTINGS, Http2::FrameFlag::EMPTY,
Http2::connectionStreamID);
s.append(Http2::Settings::INITIAL_WINDOW_SIZE_ID);
s.append(quint32(64));
QVERIFY(s.write(*server));
}
QVERIFY(QTest::qWaitFor([&]() { return clientStream->m_sendWindow == 64; }));
QVERIFY(clientStream->sendHEADERS(getRequiredHeaders(), false));
auto *buffer = new QBuffer;
buffer->setData(QByteArray(4096, 'A'));
buffer->open(QIODevice::ReadOnly);
auto *uploadDevice = QNonContiguousByteDeviceFactory::create(buffer);
buffer->setParent(uploadDevice);
QSignalSpy uploadBlockedSpy{ clientStream, &QHttp2Stream::uploadBlocked };
// This is the overload of sendDATA() QNetworkAccessManager's HTTP/2 handler uses: the
// caller (not QHttp2Stream) owns the device's lifetime. Because we can't send everything in one
// go it will return early and try to send more later.
QVERIFY(clientStream->sendDATA(uploadDevice, true));
QCOMPARE(uploadBlockedSpy.count(), 1);
QVERIFY(clientStream->isUploadingDATA());
// The upload source is destroyed while the upload is window-stalled and
// the stream object survives.
delete uploadDevice;
QVERIFY(!clientStream->isUploadingDATA());
// Grow the window again, as if resuming a live upload.
{
Http2::FrameWriter s(Http2::FrameType::SETTINGS, Http2::FrameFlag::EMPTY,
Http2::connectionStreamID);
s.append(Http2::Settings::INITIAL_WINDOW_SIZE_ID);
s.append(quint32(1024 * 1024));
QVERIFY(s.write(*server));
}
// Processing the frame above queues a maybeResumeUpload() call that runs
// during this wait. This would crash, reading through the already-freed
// device's vtable, before this patch.
QVERIFY(QTest::qWaitFor(
[&]() { return clientConn->streamInitialSendWindowSize == 1024 * 1024; }));
QVERIFY(!clientConn->m_connectionAborted);
}
void tst_QHttp2Connection::pushPromiseOnStreamRemovedAfterRST_data()
{
QTest::addColumn<bool>("pushEnabled");
QTest::newRow("push-disabled-default") << false;
QTest::newRow("push-enabled") << true;
}
void tst_QHttp2Connection::pushPromiseOnStreamRemovedAfterRST()
{
QFETCH(const bool, pushEnabled);
// handleRST_STREAM() used QHash::operator[] to look up the stream,
// planting a null QPointer<QHttp2Stream> for an unknown/disposed stream
// ID. A later PUSH_PROMISE referencing that ID only checked constEnd()
// and dereferenced the null QPointer via (*it)->state(). Reachable with
// push enabled or disabled (see below).
auto [client, server] = makeFakeConnectedSockets();
QHttp2Configuration config;
config.setServerPushEnabled(pushEnabled);
auto clientConn = makeHttp2Connection(client.get(), config, Client);
QHttp2Stream *clientStream = clientConn->createStream().unwrap();
QVERIFY(clientStream);
const quint32 streamId = clientStream->streamID();
QVERIFY(clientStream->sendHEADERS(getRequiredHeaders(), true));
// Simulate normal disposal of a (from the client's point of view)
// finished stream, the same way QNetworkAccessManager's handler
// deleteLater()s streams. The destructor removes the m_streams entry.
delete clientStream;
QVERIFY(!clientConn->getStream(streamId));
if (!pushEnabled) {
// With push disabled, PUSH_PROMISE is otherwise rejected outright;
// the client's own initial SETTINGS being still unacknowledged is
// what lets it past the "!pushPromiseEnabled" gate
QVERIFY(clientConn->waitingForSettingsACK);
}
// The attack makes the *client* raise the connection error (and send its
// own GOAWAY out) - it never receives one, so spy on errorOccurred().
QSignalSpy clientErrorSpy{ clientConn, &QHttp2Connection::errorOccurred };
// RST_STREAM on the now-absent stream.
{
Http2::FrameWriter rst(Http2::FrameType::RST_STREAM, Http2::FrameFlag::EMPTY, streamId);
rst.append(quint32(Http2::CANCEL));
QVERIFY(rst.write(*server));
}
QIODevice *clientDevice = client.get();
QVERIFY(QTest::qWaitFor([clientDevice]() { return clientDevice->bytesAvailable() == 0; }));
// PUSH_PROMISE associated with that same now-absent stream ID.
{
Http2::FrameWriter pp(Http2::FrameType::PUSH_PROMISE, Http2::FrameFlag::END_HEADERS,
streamId);
pp.append(quint32(streamId + 1)); // promised stream ID
QVERIFY(pp.write(*server));
}
// Processing the frame above is what would crash, dereferencing the
// planted null QPointer, before this patch.
QVERIFY(QTest::qWaitFor([&]() { return clientErrorSpy.count() == 1; }));
QCOMPARE(clientErrorSpy.first().first().value<Http2::Http2Error>(), Http2::ENHANCE_YOUR_CALM);
QVERIFY(clientConn->m_connectionAborted);
}
QTEST_MAIN(tst_QHttp2Connection)
#include "tst_qhttp2connection.moc"