qt6-bb10/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp

852 lines
34 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 testPING();
void testRSTServerSide();
void testRSTClientSide();
void testRSTReplyOnDATAEND();
void testBadFrameSize_data();
void testBadFrameSize();
void testDataFrameAfterRSTIncoming();
void testDataFrameAfterRSTOutgoing();
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;
});
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);
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::testPING()
{
auto [client, server] = makeFakeConnectedSockets();
auto connection = makeHttp2Connection(client.get(), {}, Client);
auto serverConnection = makeHttp2Connection(server.get(), {}, Server);
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy serverPingSpy{ serverConnection, &QHttp2Connection::pingFrameRecived };
QSignalSpy clientPingSpy{ connection, &QHttp2Connection::pingFrameRecived };
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);
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
QHttp2Stream *clientStream = connection->createStream().unwrap();
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 rstClientSpy{ clientStream, &QHttp2Stream::rstFrameRecived };
QSignalSpy rstServerSpy{ serverStream, &QHttp2Stream::rstFrameRecived };
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);
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
QHttp2Stream *clientStream = connection->createStream().unwrap();
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 rstClientSpy{ clientStream, &QHttp2Stream::rstFrameRecived };
QSignalSpy rstServerSpy{ serverStream, &QHttp2Stream::rstFrameRecived };
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);
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
QHttp2Stream *clientStream = connection->createStream().unwrap();
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 rstClientSpy{ clientStream, &QHttp2Stream::rstFrameRecived };
QSignalSpy rstServerSpy{ serverStream, &QHttp2Stream::rstFrameRecived };
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::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);
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
QHttp2Stream *clientStream = connection->createStream().unwrap();
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 rstClientSpy{ clientStream, &QHttp2Stream::rstFrameRecived };
QSignalSpy rstServerSpy{ serverStream, &QHttp2Stream::rstFrameRecived };
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, 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);
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
QHttp2Stream *clientStream = connection->createStream().unwrap();
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::rstFrameRecived };
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);
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
QHttp2Stream *clientStream = connection->createStream().unwrap();
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::rstFrameRecived };
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::connectToServer()
{
auto [client, server] = makeFakeConnectedSockets();
auto connection = makeHttp2Connection(client.get(), {}, Client);
auto serverConnection = makeHttp2Connection(server.get(), {}, Server);
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
QSignalSpy clientIncomingStreamSpy{ connection, &QHttp2Connection::newIncomingStream };
QHttp2Stream *clientStream = connection->createStream().unwrap();
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 *>();
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);
QVERIFY(waitForSettingsExchange(connection, serverConnection));
QSignalSpy newIncomingStreamSpy{ serverConnection, &QHttp2Connection::newIncomingStream };
QHttp2Stream *clientStream = connection->createStream().unwrap();
QSignalSpy clientHeaderReceivedSpy{ clientStream, &QHttp2Stream::headersReceived };
QSignalSpy clientDataReceivedSpy{ clientStream, &QHttp2Stream::dataReceived };
QVERIFY(clientStream);
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);
QBuffer *buffer = new QBuffer(clientStream);
QByteArray uploadedData = "Hello World"_ba.repeated(1000);
buffer->setData(uploadedData);
buffer->open(QIODevice::ReadWrite);
clientStream->sendDATA(buffer, 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);
QBuffer *serverBuffer = new QBuffer(serverStream);
serverBuffer->setData(uploadedData);
serverBuffer->open(QIODevice::ReadWrite);
serverStream->sendDATA(serverBuffer, 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); \
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 }; \
\
QHttp2Stream *clientStream = clientConnection->createStream().unwrap(); \
QVERIFY(clientStream);
// 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"