// 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE template class QH2Expected { static_assert(!std::is_same_v, "T and Err must be different types"); public: // Rule Of Zero applies 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 &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(m_data); } Err error() const { Q_ASSERT(has_error()); return std::get(m_data); } bool ok() const noexcept { return std::holds_alternative(m_data); } bool has_value() const noexcept { return ok(); } bool has_error() const noexcept { return std::holds_alternative(m_data); } void clear() noexcept { m_data.reset(); } private: std::variant 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(QHttp2Stream::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); private Q_SLOTS: void maybeResumeUpload(); void uploadDeviceReadChannelFinished(); void uploadDeviceDestroyed(); private: friend class QHttp2Connection; QHttp2Stream(QHttp2Connection *connection, quint32 streamID) noexcept; [[nodiscard]] QHttp2Connection *getConnection() const { return qobject_cast(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 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, StreamIdsExhausted, ReceivedGOAWAY, }; Q_ENUM(CreateStreamError) enum class PingState { Ping, PongSignatureIdentical, PongSignatureChanged, PongNoPingSent, // We got an ACKed ping but had not sent any }; // 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 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 pingFrameRecived(QHttp2Connection::PingState state); void errorOccurred(Http2::Http2Error errorCode, const QString &errorString); void receivedGOAWAY(quint32 errorCode, quint32 lastStreamID); public Q_SLOTS: bool sendPing(); bool sendPing(QByteArrayView data); void handleReadyRead(); void handleConnectionClosure(); private: friend class QHttp2Stream; [[nodiscard]] QIODevice *getSocket() const { return qobject_cast(parent()); } QH2Expected 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 numActiveStreamsImpl(quint32 mask) const noexcept; qsizetype numActiveRemoteStreams() const noexcept; qsizetype numActiveLocalStreams() 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> m_streams; QHash m_promisedStreams; QVarLengthArray m_resetStreamIDs; std::optional m_lastPingSignature = std::nullopt; 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 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::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 m_lastIncomingStreamID = Http2::connectionStreamID; // Server-side only: bool m_waitingForClientPreface = false; }; QT_END_NAMESPACE #endif // HTTP2CONNECTION_P_H