| From fc1b8814f38c7d925d4590e9bdb16a02ca824025 Mon Sep 17 00:00:00 2001 |
| From: =?UTF-8?q?M=C3=A5rten=20Nordheim?= <marten.nordheim@qt.io> |
| Date: Tue, 25 Jun 2024 17:09:35 +0200 |
| Subject: [PATCH] HTTP2: Delay any communication until encrypted() can be |
| responded to |
| |
| We have the encrypted() signal that lets users do extra checks on the |
| established connection. It is emitted as BlockingQueued, so the HTTP |
| thread stalls until it is done emitting. Users can potentially call |
| abort() on the QNetworkReply at that point, which is passed as a Queued |
| call back to the HTTP thread. That means that any currently queued |
| signal emission will be processed before the abort() call is processed. |
| |
| In the case of HTTP2 it is a little special since it is multiplexed and |
| the code is built to start requests as they are available. This means |
| that, while the code worked fine for HTTP1, since one connection only |
| has one request, it is not working for HTTP2, since we try to send more |
| requests in-between the encrypted() signal and the abort() call. |
| |
| This patch changes the code to delay any communication until the |
| encrypted() signal has been emitted and processed, for HTTP2 only. |
| It's done by adding a few booleans, both to know that we have to return |
| early and so we can keep track of what events arose and what we need to |
| resume once enough time has passed that any abort() call must have been |
| processed. |
| |
| Fixes: QTBUG-126610 |
| Pick-to: 6.5 6.2 5.15 5.12 |
| Change-Id: Ic25a600c278203256e35f541026f34a8783235ae |
| Reviewed-by: Marc Mutz <marc.mutz@qt.io> |
| Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io> |
| (cherry picked from commit b1e75376cc3adfc7da5502a277dfe9711f3e0536) |
| Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org> |
| (cherry picked from commit 0fb43e4395da34d561814242a0186999e4956e28) |
| |
| Upstream: https://github.com/qt/qtbase/commit/2b1e36e183ce75c224305c7a94457b92f7a5cf58 |
| Signed-off-by: Roy Kollen Svendsen <roykollensvendsen@gmail.com> |
| --- |
| src/network/access/qhttp2protocolhandler.cpp | 6 +-- |
| .../access/qhttpnetworkconnectionchannel.cpp | 48 ++++++++++++++++++- |
| .../access/qhttpnetworkconnectionchannel_p.h | 6 +++ |
| tests/auto/network/access/http2/tst_http2.cpp | 44 +++++++++++++++++ |
| 4 files changed, 99 insertions(+), 5 deletions(-) |
| |
| diff --git a/src/network/access/qhttp2protocolhandler.cpp b/src/network/access/qhttp2protocolhandler.cpp |
| index 0abd99b9bc..3631b13dc8 100644 |
| --- a/src/network/access/qhttp2protocolhandler.cpp |
| +++ b/src/network/access/qhttp2protocolhandler.cpp |
| @@ -303,12 +303,12 @@ bool QHttp2ProtocolHandler::sendRequest() |
| } |
| } |
| |
| - if (!prefaceSent && !sendClientPreface()) |
| - return false; |
| - |
| if (!requests.size()) |
| return true; |
| |
| + if (!prefaceSent && !sendClientPreface()) |
| + return false; |
| + |
| m_channel->state = QHttpNetworkConnectionChannel::WritingState; |
| // Check what was promised/pushed, maybe we do not have to send a request |
| // and have a response already? |
| diff --git a/src/network/access/qhttpnetworkconnectionchannel.cpp b/src/network/access/qhttpnetworkconnectionchannel.cpp |
| index 6766989690..1e4161d1fd 100644 |
| --- a/src/network/access/qhttpnetworkconnectionchannel.cpp |
| +++ b/src/network/access/qhttpnetworkconnectionchannel.cpp |
| @@ -209,6 +209,10 @@ void QHttpNetworkConnectionChannel::abort() |
| bool QHttpNetworkConnectionChannel::sendRequest() |
| { |
| Q_ASSERT(protocolHandler); |
| + if (waitingForPotentialAbort) { |
| + needInvokeSendRequest = true; |
| + return false; // this return value is unused |
| + } |
| return protocolHandler->sendRequest(); |
| } |
| |
| @@ -221,21 +225,28 @@ bool QHttpNetworkConnectionChannel::sendRequest() |
| void QHttpNetworkConnectionChannel::sendRequestDelayed() |
| { |
| QMetaObject::invokeMethod(this, [this] { |
| - Q_ASSERT(protocolHandler); |
| if (reply) |
| - protocolHandler->sendRequest(); |
| + sendRequest(); |
| }, Qt::ConnectionType::QueuedConnection); |
| } |
| |
| void QHttpNetworkConnectionChannel::_q_receiveReply() |
| { |
| Q_ASSERT(protocolHandler); |
| + if (waitingForPotentialAbort) { |
| + needInvokeReceiveReply = true; |
| + return; |
| + } |
| protocolHandler->_q_receiveReply(); |
| } |
| |
| void QHttpNetworkConnectionChannel::_q_readyRead() |
| { |
| Q_ASSERT(protocolHandler); |
| + if (waitingForPotentialAbort) { |
| + needInvokeReadyRead = true; |
| + return; |
| + } |
| protocolHandler->_q_readyRead(); |
| } |
| |
| @@ -1239,7 +1250,18 @@ void QHttpNetworkConnectionChannel::_q_encrypted() |
| if (!h2RequestsToSend.isEmpty()) { |
| // Similar to HTTP/1.1 counterpart below: |
| const auto &pair = std::as_const(h2RequestsToSend).first(); |
| + waitingForPotentialAbort = true; |
| emit pair.second->encrypted(); |
| + |
| + // We don't send or handle any received data until any effects from |
| + // emitting encrypted() have been processed. This is necessary |
| + // because the user may have called abort(). We may also abort the |
| + // whole connection if the request has been aborted and there is |
| + // no more requests to send. |
| + QMetaObject::invokeMethod(this, |
| + &QHttpNetworkConnectionChannel::checkAndResumeCommunication, |
| + Qt::QueuedConnection); |
| + |
| // In case our peer has sent us its settings (window size, max concurrent streams etc.) |
| // let's give _q_receiveReply a chance to read them first ('invokeMethod', QueuedConnection). |
| } |
| @@ -1257,6 +1279,28 @@ void QHttpNetworkConnectionChannel::_q_encrypted() |
| QMetaObject::invokeMethod(connection, "_q_startNextRequest", Qt::QueuedConnection); |
| } |
| |
| + |
| +void QHttpNetworkConnectionChannel::checkAndResumeCommunication() |
| +{ |
| + Q_ASSERT(connection->connectionType() == QHttpNetworkConnection::ConnectionTypeHTTP2 |
| + || connection->connectionType() == QHttpNetworkConnection::ConnectionTypeHTTP2Direct); |
| + |
| + // Because HTTP/2 requires that we send a SETTINGS frame as the first thing we do, and respond |
| + // to a SETTINGS frame with an ACK, we need to delay any handling until we can ensure that any |
| + // effects from emitting encrypted() have been processed. |
| + // This function is called after encrypted() was emitted, so check for changes. |
| + |
| + if (!reply && h2RequestsToSend.isEmpty()) |
| + abort(); |
| + waitingForPotentialAbort = false; |
| + if (needInvokeReadyRead) |
| + _q_readyRead(); |
| + if (needInvokeReceiveReply) |
| + _q_receiveReply(); |
| + if (needInvokeSendRequest) |
| + sendRequest(); |
| +} |
| + |
| void QHttpNetworkConnectionChannel::requeueHttp2Requests() |
| { |
| const auto h2RequestsToSendCopy = std::exchange(h2RequestsToSend, {}); |
| diff --git a/src/network/access/qhttpnetworkconnectionchannel_p.h b/src/network/access/qhttpnetworkconnectionchannel_p.h |
| index c42290feca..061f20fd42 100644 |
| --- a/src/network/access/qhttpnetworkconnectionchannel_p.h |
| +++ b/src/network/access/qhttpnetworkconnectionchannel_p.h |
| @@ -74,6 +74,10 @@ public: |
| QAbstractSocket *socket; |
| bool ssl; |
| bool isInitialized; |
| + bool waitingForPotentialAbort = false; |
| + bool needInvokeReceiveReply = false; |
| + bool needInvokeReadyRead = false; |
| + bool needInvokeSendRequest = false; |
| ChannelState state; |
| QHttpNetworkRequest request; // current request, only used for HTTP |
| QHttpNetworkReply *reply; // current reply for this request, only used for HTTP |
| @@ -146,6 +150,8 @@ public: |
| void closeAndResendCurrentRequest(); |
| void resendCurrentRequest(); |
| |
| + void checkAndResumeCommunication(); |
| + |
| bool isSocketBusy() const; |
| bool isSocketWriting() const; |
| bool isSocketWaiting() const; |
| diff --git a/tests/auto/network/access/http2/tst_http2.cpp b/tests/auto/network/access/http2/tst_http2.cpp |
| index 00efbc9832..c02e7b7b5b 100644 |
| --- a/tests/auto/network/access/http2/tst_http2.cpp |
| +++ b/tests/auto/network/access/http2/tst_http2.cpp |
| @@ -106,6 +106,8 @@ private slots: |
| |
| void duplicateRequestsWithAborts(); |
| |
| + void abortOnEncrypted(); |
| + |
| protected slots: |
| // Slots to listen to our in-process server: |
| void serverStarted(quint16 port); |
| @@ -1479,6 +1481,48 @@ void tst_Http2::duplicateRequestsWithAborts() |
| QCOMPARE(finishedCount, ExpectedSuccessfulRequests); |
| } |
| |
| +void tst_Http2::abortOnEncrypted() |
| +{ |
| +#if !QT_CONFIG(ssl) |
| + QSKIP("TLS support is needed for this test"); |
| +#else |
| + clearHTTP2State(); |
| + serverPort = 0; |
| + |
| + ServerPtr targetServer(newServer(defaultServerSettings, H2Type::h2Direct)); |
| + |
| + QMetaObject::invokeMethod(targetServer.data(), "startServer", Qt::QueuedConnection); |
| + runEventLoop(); |
| + |
| + nRequests = 1; |
| + nSentRequests = 0; |
| + |
| + const auto url = requestUrl(H2Type::h2Direct); |
| + QNetworkRequest request(url); |
| + request.setAttribute(QNetworkRequest::Http2DirectAttribute, true); |
| + |
| + std::unique_ptr<QNetworkReply> reply{manager->get(request)}; |
| + reply->ignoreSslErrors(); |
| + connect(reply.get(), &QNetworkReply::encrypted, reply.get(), [reply = reply.get()](){ |
| + reply->abort(); |
| + }); |
| + connect(reply.get(), &QNetworkReply::errorOccurred, this, &tst_Http2::replyFinishedWithError); |
| + |
| + runEventLoop(); |
| + STOP_ON_FAILURE |
| + |
| + QCOMPARE(nRequests, 0); |
| + QCOMPARE(reply->error(), QNetworkReply::OperationCanceledError); |
| + |
| + const bool res = QTest::qWaitFor( |
| + [this, server = targetServer.get()]() { |
| + return serverGotSettingsACK || prefaceOK || nSentRequests > 0; |
| + }, |
| + 500); |
| + QVERIFY(!res); |
| +#endif // QT_CONFIG(ssl) |
| +} |
| + |
| void tst_Http2::serverStarted(quint16 port) |
| { |
| serverPort = port; |
| -- |
| 2.46.0 |
| |