qtbase/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp
Mårten Nordheim 3af5e42bdd Http2ProtocolHandler: fix logic error with potential use-after-free
We previously asserted that the reply was not nullptr, except in some
special circumstance. But then we proceeded to dereference it anyway.
This was then recently changed to be an if-check, but that just
highlighted the logic-flaw (and made static analyzers warn about it...)

What we want to assert is that the stream object is valid and
conditionally return early if the reply is nullptr, which it is for
promised streams, since no request has been made yet so no reply is
created.

At the same time, update the logic in the QHttp2Stream to not store or
emit header-related signals for a stream that has been reset.

Pick-to: 6.10 6.9
Change-Id: I55d69bbedc027893f6ad125c29468a34e7fb406f
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
2025-06-12 18:58:33 +02:00

1057 lines
43 KiB
C++

// 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/hpack_p.h>
#include <QtNetwork/private/bitstreams_p.h>
#include <limits>
using namespace Qt::StringLiterals;
class tst_QHttp2Connection : public QObject
{
Q_OBJECT
private slots:
void construct();
void constructStream();
void testSETTINGSFrame();
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();
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::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, ??? },
};
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::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::newRow("priority_correct") << uchar(Http2::FrameType::PRIORITY) << 5 << false << 0;
QTest::newRow("priority_bad") << uchar(Http2::FrameType::PRIORITY) << 6 << true << 0;
QTest::newRow("ping_correct") << uchar(Http2::FrameType::PING) << 8 << false << 0;
QTest::newRow("ping_bad") << uchar(Http2::FrameType::PING) << 13 << false << 1;
}
void tst_QHttp2Connection::testBadFrameSize()
{
QFETCH(uchar, frametype);
QFETCH(int, loadsize);
QFETCH(bool, rst_received);
QFETCH(int, goaway_received);
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);
{
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(type == uchar(Http2::FrameType::PING) ? 0 : streamID, &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()));
}
QCOMPARE(rstClientSpy.wait(), rst_received);
QCOMPARE(rstServerSpy.count(), 0);
QCOMPARE(goawayClientSpy.count(), goaway_received);
}
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());
}
}
QTEST_MAIN(tst_QHttp2Connection)
#include "tst_qhttp2connection.moc"