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:
parent
6972b8deea
commit
c9a9469f55
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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();
|
||||
|
1409
src/network/access/qhttp2connection.cpp
Normal file
1409
src/network/access/qhttp2connection.cpp
Normal file
File diff suppressed because it is too large
Load Diff
362
src/network/access/qhttp2connection_p.h
Normal file
362
src/network/access/qhttp2connection_p.h
Normal 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
|
@ -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)
|
||||
|
16
tests/auto/network/access/qhttp2connection/CMakeLists.txt
Normal file
16
tests/auto/network/access/qhttp2connection/CMakeLists.txt
Normal 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
|
||||
)
|
@ -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"
|
Loading…
x
Reference in New Issue
Block a user