diff --git a/src/network/access/qhttp2connection.cpp b/src/network/access/qhttp2connection.cpp index 81f7e2da61b..ed2874c55a6 100644 --- a/src/network/access/qhttp2connection.cpp +++ b/src/network/access/qhttp2connection.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -922,6 +923,32 @@ bool QHttp2Connection::serverCheckClientPreface() return true; } +bool QHttp2Connection::sendPing() +{ + std::array data; + + QRandomGenerator gen; + gen.generate(data.begin(), data.end()); + return sendPing(data); +} + +bool QHttp2Connection::sendPing(QByteArrayView data) +{ + frameWriter.start(FrameType::PING, FrameFlag::EMPTY, connectionStreamID); + + Q_ASSERT(data.length() == 8); + if (!m_lastPingSignature) { + m_lastPingSignature = data.toByteArray(); + } else { + qCWarning(qHttp2ConnectionLog, "[%p] No PING is sent while waiting for the previous PING.", this); + return false; + } + + frameWriter.append((uchar*)data.data(), (uchar*)data.end()); + frameWriter.write(*getSocket()); + return true; +} + /*! This function must be called when you have received a readyRead signal (or equivalent) from the QIODevice. It will read and process any incoming @@ -1389,18 +1416,30 @@ void QHttp2Connection::handlePUSH_PROMISE() void QHttp2Connection::handlePING() { - // @future[server] - // Since we're implementing a client and not - // a server, we only reply to a PING, ACKing it. Q_ASSERT(inboundFrame.type() == FrameType::PING); + Q_ASSERT(inboundFrame.dataSize() == 8); if (inboundFrame.streamID() != connectionStreamID) return connectionError(PROTOCOL_ERROR, "PING on invalid stream"); - if (inboundFrame.flags() & FrameFlag::ACK) - return connectionError(PROTOCOL_ERROR, "unexpected PING ACK"); + if (inboundFrame.flags() & FrameFlag::ACK) { + QByteArrayView pingSignature(reinterpret_cast(inboundFrame.dataBegin()), 8); + if (!m_lastPingSignature.has_value()) { + emit pingFrameRecived(PingState::PongNoPingSent); + qCWarning(qHttp2ConnectionLog, "[%p] PING with ACK received but no PING was sent.", this); + } else if (pingSignature != m_lastPingSignature) { + emit pingFrameRecived(PingState::PongSignatureChanged); + qCWarning(qHttp2ConnectionLog, "[%p] PING signature does not match the last PING.", this); + } else { + emit pingFrameRecived(PingState::PongSignatureIdentical); + } + m_lastPingSignature.reset(); + return; + } else { + emit pingFrameRecived(PingState::Ping); + + } - Q_ASSERT(inboundFrame.dataSize() == 8); frameWriter.start(FrameType::PING, FrameFlag::ACK, connectionStreamID); frameWriter.append(inboundFrame.dataBegin(), inboundFrame.dataBegin() + 8); diff --git a/src/network/access/qhttp2connection_p.h b/src/network/access/qhttp2connection_p.h index a8193f74384..219e93cedc2 100644 --- a/src/network/access/qhttp2connection_p.h +++ b/src/network/access/qhttp2connection_p.h @@ -197,6 +197,13 @@ public: }; 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); @@ -232,9 +239,12 @@ Q_SIGNALS: void errorReceived(/*@future: add as needed?*/); // Connection errors only, no stream-specific errors void connectionClosed(); void settingsFrameReceived(); + void pingFrameRecived(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(); @@ -295,6 +305,7 @@ private: 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 diff --git a/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp b/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp index d9641c7391d..92077efa429 100644 --- a/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp +++ b/tests/auto/network/access/qhttp2connection/tst_qhttp2connection.cpp @@ -20,6 +20,7 @@ private slots: void construct(); void constructStream(); void testSETTINGSFrame(); + void testPING(); void connectToServer(); void WINDOW_UPDATE(); @@ -257,6 +258,35 @@ void tst_QHttp2Connection::testSETTINGSFrame() } } +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::connectToServer() { auto [client, server] = makeFakeConnectedSockets();