Privately introduce QHttp2Connection

For use in QtGRPC.

There is some duplication between this code and the code in
QHttp2ProtocolHandler. But let's not change the implementation of
the protocol handler after the 6.7 beta release. Nor do I think we
should do it for 6.8 LTS. So let's just live with the duplication
until that has branched.

Fixes: QTBUG-105491
Change-Id: I69aa38a3c341347e702f9c07c27287aee38a16f2
Reviewed-by:  Alexey Edelev <alexey.edelev@qt.io>
(cherry picked from commit 0dba3f6b713a657eb3bf2037face72d16253eb92)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
This commit is contained in:
Mårten Nordheim 2023-07-05 16:38:32 +02:00 committed by Qt Cherry-pick Bot
parent 6972b8deea
commit c9a9469f55
8 changed files with 2171 additions and 13 deletions

View File

@ -122,6 +122,7 @@ qt_internal_extend_target(Network CONDITION QT_FEATURE_http
access/qdecompresshelper.cpp access/qdecompresshelper_p.h
access/qhttp1configuration.cpp access/qhttp1configuration.h
access/qhttp2configuration.cpp access/qhttp2configuration.h
access/qhttp2connection.cpp access/qhttp2connection_p.h
access/qhttp2protocolhandler.cpp access/qhttp2protocolhandler_p.h
access/qhttpheaders.cpp access/qhttpheaders.h
access/qhttpmultipart.cpp access/qhttpmultipart.h access/qhttpmultipart_p.h

View File

@ -258,7 +258,7 @@ const uchar *Frame::hpackBlockBegin() const
return begin;
}
FrameStatus FrameReader::read(QAbstractSocket &socket)
FrameStatus FrameReader::read(QIODevice &socket)
{
if (offset < frameHeaderSize) {
if (!readHeader(socket))
@ -286,7 +286,7 @@ FrameStatus FrameReader::read(QAbstractSocket &socket)
return frame.validatePayload();
}
bool FrameReader::readHeader(QAbstractSocket &socket)
bool FrameReader::readHeader(QIODevice &socket)
{
Q_ASSERT(offset < frameHeaderSize);
@ -302,7 +302,7 @@ bool FrameReader::readHeader(QAbstractSocket &socket)
return offset == frameHeaderSize;
}
bool FrameReader::readPayload(QAbstractSocket &socket)
bool FrameReader::readPayload(QIODevice &socket)
{
Q_ASSERT(offset < frame.buffer.size());
Q_ASSERT(frame.buffer.size() > frameHeaderSize);
@ -393,7 +393,7 @@ void FrameWriter::updatePayloadSize()
setPayloadSize(size);
}
bool FrameWriter::write(QAbstractSocket &socket) const
bool FrameWriter::write(QIODevice &socket) const
{
auto &buffer = frame.buffer;
Q_ASSERT(buffer.size() >= frameHeaderSize);
@ -407,7 +407,7 @@ bool FrameWriter::write(QAbstractSocket &socket) const
return nWritten != -1 && size_type(nWritten) == buffer.size();
}
bool FrameWriter::writeHEADERS(QAbstractSocket &socket, quint32 sizeLimit)
bool FrameWriter::writeHEADERS(QIODevice &socket, quint32 sizeLimit)
{
auto &buffer = frame.buffer;
Q_ASSERT(buffer.size() >= frameHeaderSize);
@ -457,7 +457,7 @@ bool FrameWriter::writeHEADERS(QAbstractSocket &socket, quint32 sizeLimit)
return true;
}
bool FrameWriter::writeDATA(QAbstractSocket &socket, quint32 sizeLimit,
bool FrameWriter::writeDATA(QIODevice &socket, quint32 sizeLimit,
const uchar *src, quint32 size)
{
// With DATA frame(s) we always have:

View File

@ -27,7 +27,7 @@
QT_BEGIN_NAMESPACE
class QHttp2ProtocolHandler;
class QAbstractSocket;
class QIODevice;
namespace Http2
{
@ -65,15 +65,15 @@ struct Q_AUTOTEST_EXPORT Frame
class Q_AUTOTEST_EXPORT FrameReader
{
public:
FrameStatus read(QAbstractSocket &socket);
FrameStatus read(QIODevice &socket);
Frame &inboundFrame()
{
return frame;
}
private:
bool readHeader(QAbstractSocket &socket);
bool readPayload(QAbstractSocket &socket);
bool readHeader(QIODevice &socket);
bool readPayload(QIODevice &socket);
quint32 offset = 0;
Frame frame;
@ -123,20 +123,25 @@ public:
{
append(&payload[0], &payload[0] + payload.size());
}
void append(QByteArrayView payload)
{
append(reinterpret_cast<const uchar *>(payload.begin()),
reinterpret_cast<const uchar *>(payload.end()));
}
void append(const uchar *begin, const uchar *end);
// Write as a single frame:
bool write(QAbstractSocket &socket) const;
bool write(QIODevice &socket) const;
// Two types of frames we are sending are affected by frame size limits:
// HEADERS and DATA. HEADERS' payload (hpacked HTTP headers, following a
// frame header) is always in our 'buffer', we send the initial HEADERS
// frame first and then CONTINUTATION frame(s) if needed:
bool writeHEADERS(QAbstractSocket &socket, quint32 sizeLimit);
bool writeHEADERS(QIODevice &socket, quint32 sizeLimit);
// With DATA frames the actual payload is never in our 'buffer', it's a
// 'readPointer' from QNonContiguousData. We split this payload as needed
// into DATA frames with correct payload size fitting into frame size limit:
bool writeDATA(QAbstractSocket &socket, quint32 sizeLimit,
bool writeDATA(QIODevice &socket, quint32 sizeLimit,
const uchar *src, quint32 size);
private:
void updatePayloadSize();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,362 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#ifndef HTTP2CONNECTION_P_H
#define HTTP2CONNECTION_P_H
//
// W A R N I N G
// -------------
//
// This file is not part of the Qt API. It exists for the convenience
// of the Network Access API. This header file may change from
// version to version without notice, or even be removed.
//
// We mean it.
//
#include <private/qtnetworkglobal_p.h>
#include <QtCore/qobject.h>
#include <QtCore/qhash.h>
#include <QtCore/qvarlengtharray.h>
#include <QtNetwork/qhttp2configuration.h>
#include <QtNetwork/qtcpsocket.h>
#include <private/http2protocol_p.h>
#include <private/http2streams_p.h>
#include <private/http2frames_p.h>
#include <private/hpack_p.h>
#include <variant>
#include <optional>
#include <type_traits>
#include <limits>
QT_BEGIN_NAMESPACE
template <typename T, typename Err>
class QH2Expected
{
static_assert(!std::is_same_v<T, Err>, "T and Err must be different types");
public:
QH2Expected(T &&value) : m_data(std::move(value)) { }
QH2Expected(const T &value) : m_data(value) { }
QH2Expected(Err &&error) : m_data(std::move(error)) { }
QH2Expected(const Err &error) : m_data(error) { }
QH2Expected(const QH2Expected &) = default;
QH2Expected(QH2Expected &&) = default;
QH2Expected &operator=(const QH2Expected &) = default;
QH2Expected &operator=(QH2Expected &&) = default;
~QH2Expected() = default;
QH2Expected &operator=(T &&value)
{
m_data = std::move(value);
return *this;
}
QH2Expected &operator=(const T &value)
{
m_data = value;
return *this;
}
QH2Expected &operator=(Err &&error)
{
m_data = std::move(error);
return *this;
}
QH2Expected &operator=(const Err &error)
{
m_data = error;
return *this;
}
T unwrap() const
{
Q_ASSERT(ok());
return std::get<T>(m_data);
}
Err error() const
{
Q_ASSERT(has_error());
return std::get<Err>(m_data);
}
bool ok() const noexcept { return std::holds_alternative<T>(m_data); }
bool has_value() const noexcept { return ok(); }
bool has_error() const noexcept { return std::holds_alternative<Err>(m_data); }
void clear() noexcept { m_data.reset(); }
private:
std::variant<T, Err> m_data;
};
class QHttp2Connection;
class Q_NETWORK_EXPORT QHttp2Stream : public QObject
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(QHttp2Stream)
public:
enum class State { Idle, ReservedRemote, Open, HalfClosedLocal, HalfClosedRemote, Closed };
Q_ENUM(State)
constexpr static quint8 DefaultPriority = 127;
~QHttp2Stream() noexcept;
// HTTP2 things
quint32 streamID() const noexcept { return m_streamID; }
// Are we waiting for a larger send window before sending more data?
bool isUploadBlocked() const noexcept;
bool isUploadingDATA() const noexcept { return m_uploadByteDevice != nullptr; }
State state() const noexcept { return m_state; }
bool isActive() const noexcept { return m_state != State::Closed && m_state != State::Idle; }
bool isPromisedStream() const noexcept { return m_isReserved; }
bool wasReset() const noexcept { return m_RST_STREAM_code.has_value(); }
quint32 RST_STREAM_code() const noexcept { return m_RST_STREAM_code.value_or(0); }
// Just the list of headers, as received, may contain duplicates:
HPack::HttpHeader receivedHeaders() const noexcept { return m_headers; }
QByteDataBuffer downloadBuffer() const noexcept { return m_downloadBuffer; }
Q_SIGNALS:
void headersReceived(const HPack::HttpHeader &headers, bool endStream);
void headersUpdated();
void errorOccurred(quint32 errorCode, const QString &errorString);
void stateChanged(State newState);
void promisedStreamReceived(quint32 newStreamID);
void uploadBlocked();
void dataReceived(const QByteArray &data, bool endStream);
void bytesWritten(qint64 bytesWritten);
void uploadDeviceError(const QString &errorString);
void uploadFinished();
public Q_SLOTS:
bool sendRST_STREAM(quint32 errorCode);
bool sendHEADERS(const HPack::HttpHeader &headers, bool endStream,
quint8 priority = DefaultPriority);
void sendDATA(QIODevice *device, bool endStream);
void sendDATA(QNonContiguousByteDevice *device, bool endStream);
void sendWINDOW_UPDATE(quint32 delta);
void uploadDeviceDestroyed();
private Q_SLOTS:
void maybeResumeUpload();
void uploadDeviceReadChannelFinished();
private:
friend class QHttp2Connection;
QHttp2Stream(QHttp2Connection *connection, quint32 streamID) noexcept;
[[nodiscard]] QHttp2Connection *getConnection() const
{
return qobject_cast<QHttp2Connection *>(parent());
}
enum class StateTransition {
Open,
CloseLocal,
CloseRemote,
RST,
};
void setState(State newState);
void transitionState(StateTransition transition);
void internalSendDATA();
void finishSendDATA();
void handleDATA(const Http2::Frame &inboundFrame);
void handleHEADERS(Http2::FrameFlags frameFlags, const HPack::HttpHeader &headers);
void handleRST_STREAM(const Http2::Frame &inboundFrame);
void handleWINDOW_UPDATE(const Http2::Frame &inboundFrame);
void finishWithError(quint32 errorCode, const QString &message);
void finishWithError(quint32 errorCode);
// Keep it const since it never changes after creation
const quint32 m_streamID = 0;
qint32 m_recvWindow = 0;
qint32 m_sendWindow = 0;
bool m_endStreamAfterDATA = false;
std::optional<quint32> m_RST_STREAM_code;
QIODevice *m_uploadDevice = nullptr;
QNonContiguousByteDevice *m_uploadByteDevice = nullptr;
QByteDataBuffer m_downloadBuffer;
State m_state = State::Idle;
HPack::HttpHeader m_headers;
bool m_isReserved = false;
};
class Q_NETWORK_EXPORT QHttp2Connection : public QObject
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(QHttp2Connection)
public:
enum class CreateStreamError {
MaxConcurrentStreamsReached,
ReceivedGOAWAY,
};
Q_ENUM(CreateStreamError)
// For a pre-established connection:
[[nodiscard]] static QHttp2Connection *
createUpgradedConnection(QIODevice *socket, const QHttp2Configuration &config);
// For a new connection, potential TLS handshake must already be finished:
[[nodiscard]] static QHttp2Connection *createDirectConnection(QIODevice *socket,
const QHttp2Configuration &config);
[[nodiscard]] static QHttp2Connection *
createDirectServerConnection(QIODevice *socket, const QHttp2Configuration &config);
~QHttp2Connection();
[[nodiscard]] QH2Expected<QHttp2Stream *, CreateStreamError> createStream();
QHttp2Stream *getStream(quint32 streamId) const;
QHttp2Stream *promisedStream(const QUrl &streamKey) const
{
if (quint32 id = m_promisedStreams.value(streamKey, 0); id)
return m_streams.value(id);
return nullptr;
}
void close() { sendGOAWAY(Http2::HTTP2_NO_ERROR); }
bool isGoingAway() const noexcept { return m_goingAway; }
quint32 maxConcurrentStreams() const noexcept { return m_maxConcurrentStreams; }
quint32 maxHeaderListSize() const noexcept { return m_maxHeaderListSize; }
bool isUpgradedConnection() const noexcept { return m_upgradedConnection; }
Q_SIGNALS:
void newIncomingStream(QHttp2Stream *stream);
void newPromisedStream(QHttp2Stream *stream);
void errorReceived(/*@future: add as needed?*/); // Connection errors only, no stream-specific errors
void connectionClosed();
void settingsFrameReceived();
void errorOccurred(Http2::Http2Error errorCode, const QString &errorString);
void receivedGOAWAY(quint32 errorCode, quint32 lastStreamID);
public Q_SLOTS:
void handleReadyRead();
void handleConnectionClosure();
private:
friend class QHttp2Stream;
[[nodiscard]] QIODevice *getSocket() const { return qobject_cast<QIODevice *>(parent()); }
QH2Expected<QHttp2Stream *, QHttp2Connection::CreateStreamError> createStreamInternal();
QHttp2Stream *createStreamInternal_impl(quint32 streamID);
bool isInvalidStream(quint32 streamID) noexcept;
bool streamWasReset(quint32 streamID) noexcept;
void connectionError(Http2::Http2Error errorCode,
const char *message); // Connection failed to be established?
void setH2Configuration(QHttp2Configuration config);
void closeSession();
qsizetype numActiveStreams() const noexcept;
bool sendClientPreface();
bool sendSETTINGS();
bool sendServerPreface();
bool serverCheckClientPreface();
bool sendWINDOW_UPDATE(quint32 streamID, quint32 delta);
bool sendGOAWAY(quint32 errorCode);
bool sendSETTINGS_ACK();
void handleDATA();
void handleHEADERS();
void handlePRIORITY();
void handleRST_STREAM();
void handleSETTINGS();
void handlePUSH_PROMISE();
void handlePING();
void handleGOAWAY();
void handleWINDOW_UPDATE();
void handleCONTINUATION();
void handleContinuedHEADERS();
bool acceptSetting(Http2::Settings identifier, quint32 newValue);
bool readClientPreface();
explicit QHttp2Connection(QIODevice *socket);
enum class Type { Client, Server } m_connectionType = Type::Client;
bool waitingForSettingsACK = false;
static constexpr quint32 maxAcceptableTableSize = 16 * HPack::FieldLookupTable::DefaultSize;
// HTTP/2 4.3: Header compression is stateful. One compression context and
// one decompression context are used for the entire connection.
HPack::Decoder decoder = HPack::Decoder(HPack::FieldLookupTable::DefaultSize);
HPack::Encoder encoder = HPack::Encoder(HPack::FieldLookupTable::DefaultSize, true);
QHttp2Configuration m_config;
QHash<quint32, QPointer<QHttp2Stream>> m_streams;
QHash<QUrl, quint32> m_promisedStreams;
QVarLengthArray<quint32> m_resetStreamIDs;
quint32 m_nextStreamID = 1;
// Peer's max frame size (this min is the default value
// we start with, that can be updated by SETTINGS frame):
quint32 maxFrameSize = Http2::minPayloadLimit;
Http2::FrameReader frameReader;
Http2::Frame inboundFrame;
Http2::FrameWriter frameWriter;
// Temporary storage to assemble HEADERS' block
// from several CONTINUATION frames ...
bool continuationExpected = false;
std::vector<Http2::Frame> continuedFrames;
// Control flow:
// This is how many concurrent streams our peer allows us, 100 is the
// initial value, can be updated by the server's SETTINGS frame(s):
quint32 m_maxConcurrentStreams = Http2::maxConcurrentStreams;
// While we allow sending SETTTINGS_MAX_CONCURRENT_STREAMS to limit our peer,
// it's just a hint and we do not actually enforce it (and we can continue
// sending requests and creating streams while maxConcurrentStreams allows).
// This is our (client-side) maximum possible receive window size, we set
// it in a ctor from QHttp2Configuration, it does not change after that.
// The default is 64Kb:
qint32 maxSessionReceiveWindowSize = Http2::defaultSessionWindowSize;
// Our session current receive window size, updated in a ctor from
// QHttp2Configuration. Signed integer since it can become negative
// (it's still a valid window size).
qint32 sessionReceiveWindowSize = Http2::defaultSessionWindowSize;
// Our per-stream receive window size, default is 64 Kb, will be updated
// from QHttp2Configuration. Again, signed - can become negative.
qint32 streamInitialReceiveWindowSize = Http2::defaultSessionWindowSize;
// These are our peer's receive window sizes, they will be updated by the
// peer's SETTINGS and WINDOW_UPDATE frames, defaults presumed to be 64Kb.
qint32 sessionSendWindowSize = Http2::defaultSessionWindowSize;
qint32 streamInitialSendWindowSize = Http2::defaultSessionWindowSize;
// Our peer's header size limitations. It's unlimited by default, but can
// be changed via peer's SETTINGS frame.
quint32 m_maxHeaderListSize = (std::numeric_limits<quint32>::max)();
// While we can send SETTINGS_MAX_HEADER_LIST_SIZE value (our limit on
// the headers size), we never enforce it, it's just a hint to our peer.
bool m_upgradedConnection = false;
bool m_goingAway = false;
bool pushPromiseEnabled = false;
quint32 lastPromisedID = Http2::connectionStreamID;
quint32 m_lastIncomingStreamID = Http2::connectionStreamID;
// Server-side only:
bool m_waitingForClientPreface = false;
};
QT_END_NAMESPACE
#endif // HTTP2CONNECTION_P_H

View File

@ -13,6 +13,7 @@ add_subdirectory(qnetworkcachemetadata)
add_subdirectory(qabstractnetworkcache)
add_subdirectory(qrestaccessmanager)
if(QT_FEATURE_private_tests)
add_subdirectory(qhttp2connection)
add_subdirectory(qhttpheaderparser)
add_subdirectory(qhttpnetworkconnection)
add_subdirectory(qhttpnetworkreply)

View File

@ -0,0 +1,16 @@
# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT)
cmake_minimum_required(VERSION 3.16)
project(tst_qhttp2connection LANGUAGES CXX)
find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST)
endif()
qt_internal_add_test(tst_qhttp2connection
SOURCES
tst_qhttp2connection.cpp
LIBRARIES
Qt::NetworkPrivate
Qt::Test
)

View File

@ -0,0 +1,364 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#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 connectToServer();
void WINDOW_UPDATE();
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_STREAM_code(), 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::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 };
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);
}
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);
}
QTEST_MAIN(tst_QHttp2Connection)
#include "tst_qhttp2connection.moc"