summaryrefslogtreecommitdiffstats
path: root/src/network
diff options
context:
space:
mode:
Diffstat (limited to 'src/network')
-rw-r--r--src/network/access/qhttp2connection.cpp222
-rw-r--r--src/network/access/qhttp2connection_p.h22
2 files changed, 192 insertions, 52 deletions
diff --git a/src/network/access/qhttp2connection.cpp b/src/network/access/qhttp2connection.cpp
index c0b07ddd652..62afd172d57 100644
--- a/src/network/access/qhttp2connection.cpp
+++ b/src/network/access/qhttp2connection.cpp
@@ -15,6 +15,7 @@
#include <algorithm>
#include <memory>
+#include <chrono>
QT_BEGIN_NAMESPACE
@@ -646,6 +647,8 @@ void QHttp2Stream::setState(State newState)
streamID(), int(m_state), int(newState));
m_state = newState;
emit stateChanged(newState);
+ if (m_state == State::Closed)
+ getConnection()->maybeCloseOnGoingAway();
}
// Changes the state as appropriate given the current state and the transition.
@@ -1033,6 +1036,27 @@ QHttp2Stream *QHttp2Connection::getStream(quint32 streamID) const
return m_streams.value(streamID, nullptr).get();
}
+/*!
+ Initiates connection shutdown. When \a errorCode is \c{NO_ERROR}, graceful
+ shutdown is initiated, allowing existing streams to complete. Otherwise the
+ connection is closed immediately with an error.
+*/
+void QHttp2Connection::close(Http2::Http2Error errorCode)
+{
+ if (m_connectionAborted)
+ return;
+
+ if (errorCode == Http2::HTTP2_NO_ERROR) {
+ if (m_connectionType == Type::Server)
+ sendInitialServerGracefulShutdownGoaway();
+ else
+ sendClientGracefulShutdownGoaway();
+ } else {
+ // RFC 9113, 5.4.1: After sending the GOAWAY frame for an error
+ // condition, the endpoint MUST close the TCP connection
+ connectionError(errorCode, "Connection closed with error", false);
+ }
+}
/*!
\fn QHttp2Stream *QHttp2Connection::promisedStream(const QUrl &streamKey) const
@@ -1042,13 +1066,6 @@ QHttp2Stream *QHttp2Connection::getStream(quint32 streamID) const
*/
/*!
- \fn void QHttp2Connection::close()
-
- This sends a GOAWAY frame on the connection stream, gracefully closing the
- connection.
-*/
-
-/*!
\fn bool QHttp2Connection::isGoingAway() const noexcept
Returns \c true if the connection is in the process of being closed, or
@@ -1154,13 +1171,6 @@ void QHttp2Connection::handleReadyRead()
if (m_connectionType == Type::Server && !serverCheckClientPreface())
return;
- const auto streamIsActive = [](const QPointer<QHttp2Stream> &stream) {
- return stream && stream->isActive();
- };
- if (m_goingAway && std::none_of(m_streams.cbegin(), m_streams.cend(), streamIsActive)) {
- close();
- return;
- }
QIODevice *socket = getSocket();
qCDebug(qHttp2ConnectionLog, "[%p] Receiving data, %lld bytes available", this,
@@ -1170,13 +1180,13 @@ void QHttp2Connection::handleReadyRead()
if (!m_prefaceSent)
return;
- while (!m_goingAway || std::any_of(m_streams.cbegin(), m_streams.cend(), streamIsActive)) {
+ while (!m_connectionAborted) {
const auto result = frameReader.read(*socket);
if (result != FrameStatus::goodFrame)
qCDebug(qHttp2ConnectionLog, "[%p] Tried to read frame, got %d", this, int(result));
switch (result) {
case FrameStatus::incompleteFrame:
- return;
+ return; // No more complete frames available
case FrameStatus::protocolError:
return connectionError(PROTOCOL_ERROR, "invalid frame");
case FrameStatus::sizeError: {
@@ -1297,24 +1307,35 @@ void QHttp2Connection::setH2Configuration(QHttp2Configuration config)
encoder.setCompressStrings(m_config.huffmanCompressionEnabled());
}
-void QHttp2Connection::connectionError(Http2Error errorCode, const char *message)
+void QHttp2Connection::connectionError(Http2Error errorCode, const char *message, bool logAsError)
{
Q_ASSERT(message);
- // RFC 9113, 6.8: An endpoint MAY send multiple GOAWAY frames if circumstances change.
+ if (m_connectionAborted)
+ return;
+ m_connectionAborted = true;
- qCCritical(qHttp2ConnectionLog, "[%p] Connection error: %s (%d)", this, message,
- int(errorCode));
+ if (logAsError) {
+ qCCritical(qHttp2ConnectionLog, "[%p] Connection error: %s (%d)", this, message,
+ int(errorCode));
+ } else {
+ qCDebug(qHttp2ConnectionLog, "[%p] Closing connection: %s (%d)", this, message,
+ int(errorCode));
+ }
- // RFC 9113, 6.8: Endpoints SHOULD always send a GOAWAY frame before closing a connection so
- // that the remote peer can know whether a stream has been partially processed or not.
- sendGOAWAY(errorCode);
+ // Mark going away so other code paths will stop creating new streams
+ m_goingAway = true;
+ // RFC 9113 5.4.1: An endpoint that encounters a connection error SHOULD
+ // first send a GOAWAY frame with the last incoming stream ID.
+ m_lastStreamToProcess = std::min(m_lastIncomingStreamID, m_lastStreamToProcess);
+ sendGOAWAYFrame(errorCode, m_lastStreamToProcess);
auto messageView = QLatin1StringView(message);
for (QHttp2Stream *stream : std::as_const(m_streams)) {
if (stream && stream->isActive())
stream->finishWithError(errorCode, messageView);
}
-
+ // RFC 9113 5.4.1: After sending the GOAWAY frame for an error condition,
+ // the endpoint MUST close the TCP connection
closeSession();
}
@@ -1423,20 +1444,99 @@ bool QHttp2Connection::sendWINDOW_UPDATE(quint32 streamID, quint32 delta)
return frameWriter.write(*getSocket());
}
-bool QHttp2Connection::sendGOAWAY(Http2::Http2Error errorCode)
+void QHttp2Connection::sendClientGracefulShutdownGoaway()
{
+ // Clients send a single GOAWAY. No race condition since they control stream creation
+ Q_ASSERT(m_connectionType == Type::Client);
+
+ if (m_connectionAborted || m_goingAway) {
+ qCWarning(qHttp2ConnectionLog, "[%p] Client graceful shutdown already in progress", this);
+ return;
+ }
+
m_goingAway = true;
- // If this is the first time, start the timer:
- if (m_lastStreamToProcess == Http2::lastValidStreamID)
- m_goawayGraceTimer.setRemainingTime(GoawayGracePeriod);
- m_lastStreamToProcess = std::min(m_lastIncomingStreamID, m_lastStreamToProcess);
+ m_gracefulShutdownState = GracefulShutdownState::FinalGOAWAYSent;
+ m_lastStreamToProcess = m_lastIncomingStreamID;
+ sendGOAWAYFrame(Http2::HTTP2_NO_ERROR, m_lastStreamToProcess);
+
+ maybeCloseOnGoingAway();
+}
+
+void QHttp2Connection::sendInitialServerGracefulShutdownGoaway()
+{
+ Q_ASSERT(m_connectionType == Type::Server);
+ // RFC 9113, 6.8: A server that is attempting to gracefully shut down a
+ // connection SHOULD send an initial GOAWAY frame with the last stream
+ // identifier set to 2^31-1 and a NO_ERROR code.
+ if (m_connectionAborted || m_goingAway) {
+ qCWarning(qHttp2ConnectionLog, "[%p] Server graceful shutdown already in progress", this);
+ return;
+ }
+
+ m_goingAway = true;
+ m_goawayGraceTimer.setRemainingTime(GoawayGracePeriod);
+ sendGOAWAYFrame(Http2::HTTP2_NO_ERROR, Http2::lastValidStreamID);
+
+ // Send PING to measure RTT; handlePING() continues the shutdown on ACK.
+ // RFC 9113 6.8: After allowing time for any in-flight stream creation
+ // (at least one round-trip time)
+ if (sendPing())
+ m_gracefulShutdownState = GracefulShutdownState::AwaitingShutdownPing;
+ else
+ m_gracefulShutdownState = GracefulShutdownState::AwaitingPriorPing;
+}
+
+void QHttp2Connection::sendFinalServerGracefulShutdownGoaway()
+{
+ if (m_connectionAborted || !m_goingAway) {
+ qCWarning(qHttp2ConnectionLog, "[%p] Server graceful shutdown not in progress", this);
+ return;
+ }
+ m_gracefulShutdownState = GracefulShutdownState::FinalGOAWAYSent;
+ m_lastStreamToProcess = m_lastIncomingStreamID;
+ sendGOAWAYFrame(Http2::HTTP2_NO_ERROR, m_lastStreamToProcess);
+ maybeCloseOnGoingAway();
+}
+
+bool QHttp2Connection::sendGOAWAYFrame(Http2::Http2Error errorCode, quint32 lastStreamID)
+{
+ QIODevice *socket = getSocket();
+ if (!socket || !socket->isOpen())
+ return false;
+
qCDebug(qHttp2ConnectionLog, "[%p] Sending GOAWAY frame, error code %u, last stream %u", this,
- errorCode, m_lastStreamToProcess);
+ errorCode, lastStreamID);
+
frameWriter.start(FrameType::GOAWAY, FrameFlag::EMPTY,
Http2PredefinedParameters::connectionStreamID);
- frameWriter.append(m_lastStreamToProcess);
+ frameWriter.append(lastStreamID);
frameWriter.append(quint32(errorCode));
- return frameWriter.write(*getSocket());
+ return frameWriter.write(*socket);
+}
+
+void QHttp2Connection::maybeCloseOnGoingAway()
+{
+ // Only close if we've reached the final phase of graceful shutdown
+ // For the sender: after FinalGOAWAYSent
+ // For the receiver: after receiving GOAWAY and all our streams are done
+ if (m_connectionAborted || !m_goingAway) {
+ qCDebug(qHttp2ConnectionLog, "[%p] Connection close deferred, graceful shutdown not active",
+ this);
+ return;
+ }
+
+ // For graceful shutdown initiator, only close after final GOAWAY is sent
+ if (m_gracefulShutdownState == GracefulShutdownState::AwaitingShutdownPing)
+ return; // Still waiting for RTT measurement before final GOAWAY
+
+ const auto streamIsActive = [](const QPointer<QHttp2Stream> &stream) {
+ return stream && stream->isActive();
+ };
+
+ if (std::none_of(m_streams.cbegin(), m_streams.cend(), streamIsActive)) {
+ qCDebug(qHttp2ConnectionLog, "[%p] All streams closed, closing connection", this);
+ closeSession();
+ }
}
bool QHttp2Connection::sendSETTINGS_ACK()
@@ -1571,8 +1671,6 @@ void QHttp2Connection::handleHEADERS()
qCDebug(qHttp2ConnectionLog, "[%p] HEADERS frame on stream %d has PRIORITY flag", this,
streamID);
handlePRIORITY();
- if (m_goingAway)
- return;
}
const bool endHeaders = flags.testFlag(FrameFlag::END_HEADERS);
@@ -1834,6 +1932,17 @@ void QHttp2Connection::handlePING()
emit pingFrameReceived(PingState::PongSignatureIdentical);
}
m_lastPingSignature.reset();
+
+ // Handle sendInitialServerGracefulShutdownGoaway()
+ if (m_gracefulShutdownState == GracefulShutdownState::AwaitingShutdownPing) {
+ sendFinalServerGracefulShutdownGoaway();
+ } else if (m_gracefulShutdownState == GracefulShutdownState::AwaitingPriorPing) {
+ // Prior PING completed, now send our RTT measurement PING. This shouldn't fail!
+ m_gracefulShutdownState = GracefulShutdownState::AwaitingShutdownPing;
+ [[maybe_unused]] const bool ok = sendPing();
+ Q_ASSERT(ok);
+ }
+
return;
} else {
emit pingFrameReceived(PingState::Ping);
@@ -1876,27 +1985,44 @@ void QHttp2Connection::handleGOAWAY()
if (lastStreamID != 0 && (lastStreamID & 0x1) != LocalMask)
return connectionError(PROTOCOL_ERROR, "GOAWAY with invalid last stream ID");
+ // 6.8 - An endpoint MAY send multiple GOAWAY frames if circumstances
+ // change. Endpoints MUST NOT increase the value they send in the last
+ // stream identifier
+ if (m_lastGoAwayLastStreamID && lastStreamID > *m_lastGoAwayLastStreamID)
+ return connectionError(PROTOCOL_ERROR, "Repeated GOAWAY with invalid last stream ID");
+ m_lastGoAwayLastStreamID = lastStreamID;
+
qCDebug(qHttp2ConnectionLog, "[%p] Received GOAWAY frame, error code %u, last stream %u",
this, errorCode, lastStreamID);
m_goingAway = true;
emit receivedGOAWAY(errorCode, lastStreamID);
- // Since the embedded stream ID is the last one that was or _might be_ processed,
- // we cancel anything that comes after it. 0 can be used in the special case that
- // no streams at all were or will be processed.
- const quint32 firstPossibleStream = m_connectionType == Type::Client ? 1 : 2;
- const quint32 firstCancelledStream = lastStreamID ? lastStreamID + 2 : firstPossibleStream;
- Q_ASSERT((firstCancelledStream & 0x1) == LocalMask);
- for (quint32 id = firstCancelledStream; id < m_nextStreamID; id += 2) {
- QHttp2Stream *stream = m_streams.value(id, nullptr);
- if (stream && stream->isActive())
- stream->finishWithError(errorCode, "Received GOAWAY"_L1);
- }
-
- const auto isActive = [](const QHttp2Stream *stream) { return stream && stream->isActive(); };
- if (std::none_of(m_streams.cbegin(), m_streams.cend(), isActive))
+ if (errorCode == HTTP2_NO_ERROR) {
+ // Graceful GOAWAY (NO_ERROR): Only cancel streams the peer explicitly won't process
+ // (those with IDs > lastStreamID). Streams with ID <= lastStreamID can still complete.
+ // '0' can be used in the special case that no streams at all were or will be processed.
+ const quint32 firstPossibleStream = m_connectionType == Type::Client ? 1 : 2;
+ const quint32 firstCancelledStream = lastStreamID ? lastStreamID + 2 : firstPossibleStream;
+ Q_ASSERT((firstCancelledStream & 0x1) == LocalMask);
+ for (quint32 id = firstCancelledStream; id < m_nextStreamID; id += 2) {
+ QHttp2Stream *stream = m_streams.value(id, nullptr);
+ if (stream && stream->isActive())
+ stream->finishWithError(errorCode, "Received GOAWAY"_L1);
+ }
+ maybeCloseOnGoingAway(); // check if we can close now
+ } else {
+ // RFC 9113, 5.4.1: After sending the GOAWAY frame for an error
+ // condition, the endpoint MUST close the TCP connection.
+ // As the peer is closing the connection immediately, they won't
+ // process any more data, so we close the connection here already.
+ m_connectionAborted = true;
+ for (QHttp2Stream *stream : std::as_const(m_streams)) {
+ if (stream && stream->isActive())
+ stream->finishWithError(errorCode, "Received GOAWAY"_L1);
+ }
closeSession();
+ }
}
void QHttp2Connection::handleWINDOW_UPDATE()
diff --git a/src/network/access/qhttp2connection_p.h b/src/network/access/qhttp2connection_p.h
index e2af7d7ab33..d9f2bccc58a 100644
--- a/src/network/access/qhttp2connection_p.h
+++ b/src/network/access/qhttp2connection_p.h
@@ -260,7 +260,7 @@ public:
return nullptr;
}
- void close(Http2::Http2Error error = Http2::HTTP2_NO_ERROR) { sendGOAWAY(error); }
+ void close(Http2::Http2Error errorCode = Http2::HTTP2_NO_ERROR);
bool isGoingAway() const noexcept { return m_goingAway; }
@@ -302,8 +302,7 @@ private:
Q_ALWAYS_INLINE
bool streamIsIgnored(quint32 streamID) const noexcept;
- void connectionError(Http2::Http2Error errorCode,
- const char *message); // Connection failed to be established?
+ void connectionError(Http2::Http2Error errorCode, const char *message, bool logAsError = true);
void setH2Configuration(QHttp2Configuration config);
void closeSession();
void registerStreamAsResetLocally(quint32 streamID);
@@ -316,7 +315,11 @@ private:
bool sendServerPreface();
bool serverCheckClientPreface();
bool sendWINDOW_UPDATE(quint32 streamID, quint32 delta);
- bool sendGOAWAY(Http2::Http2Error errorCode);
+ void sendClientGracefulShutdownGoaway();
+ void sendInitialServerGracefulShutdownGoaway();
+ void sendFinalServerGracefulShutdownGoaway();
+ bool sendGOAWAYFrame(Http2::Http2Error errorCode, quint32 lastSreamID);
+ void maybeCloseOnGoingAway();
bool sendSETTINGS_ACK();
void handleDATA();
@@ -423,6 +426,17 @@ private:
static constexpr std::chrono::duration GoawayGracePeriod = std::chrono::seconds(60);
QDeadlineTimer m_goawayGraceTimer;
+ std::optional<quint32> m_lastGoAwayLastStreamID;
+ bool m_connectionAborted = false;
+
+ enum class GracefulShutdownState {
+ None,
+ AwaitingPriorPing,
+ AwaitingShutdownPing,
+ FinalGOAWAYSent,
+ };
+ GracefulShutdownState m_gracefulShutdownState = GracefulShutdownState::None;
+
bool m_prefaceSent = false;
// Server-side only: