// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include #include #include #include #include #include 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 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 _in, std::shared_ptr _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 in; std::shared_ptr out; qint64 readHead = 0; }; auto tst_QHttp2Connection::makeFakeConnectedSockets() { auto clientIn = std::make_shared(); auto serverIn = std::make_shared(); clientIn->open(QIODevice::ReadWrite); serverIn->open(QIODevice::ReadWrite); auto client = std::make_unique(this, clientIn, serverIn); auto server = std::make_unique(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::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(it); const quint32 intVal = qFromBigEndian(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); 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(); 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(); 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(); 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("frametype"); QTest::addColumn("loadsize"); QTest::addColumn("rst_received"); QTest::addColumn("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(); 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 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(&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(); 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(); 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(); QVERIFY(serverStream); const HPack::HttpHeader ExpectedResponseHeaders{ { ":status", "200" } }; serverStream->sendHEADERS(ExpectedResponseHeaders, true); QVERIFY(clientHeaderReceivedSpy.wait()); const HPack::HttpHeader headersReceived = clientHeaderReceivedSpy.front().front().value(); 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(); 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(); streamEnd = latestEmission.back().value(); } 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(); 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(), 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(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(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(frameWriter.outboundFrame().buffer.size() / 5); frameWriter.writeHEADERS(*client, sizeLimit); QVERIFY(serverIncomingStreamSpy.wait()); auto *serverStream = serverIncomingStreamSpy.front().front().value(); 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"