diff options
author | Øystein Heskestad <[email protected]> | 2025-06-03 14:45:51 +0200 |
---|---|---|
committer | Øystein Heskestad <[email protected]> | 2025-07-09 14:21:13 +0200 |
commit | ea669306b2f46be6e07a67a2cb0ace92484abc0a (patch) | |
tree | 2d1048e1f29dd02d7366e6ae4da4426a5c493a1c | |
parent | c5c80cb7b91e93dbfe873081ba4a9103a2a819f1 (diff) |
When handling HTTP(S)/1.1 clients use chunked transfer encoding
for sequential QIODevices, because sequential QIODevices don't
know their length. The failure to do so caused clients to hang.
When handling HTTP(S)/1.0 clients disconnect after write because
such clients don't support chunked transfer encoding.
Removed expect_fail from tests that now work.
Fixes: QTBUG-137330
Pick-to: 6.10 6.9 6.8
Change-Id: Ie7a0e106c5475b8298697cdee0ba080acab3ce97
Reviewed-by: Lena Biliaieva <[email protected]>
Reviewed-by: Matthias Rauter <[email protected]>
-rw-r--r-- | src/httpserver/qhttpserverhttp1protocolhandler.cpp | 86 | ||||
-rw-r--r-- | src/httpserver/qhttpserverhttp1protocolhandler_p.h | 1 | ||||
-rw-r--r-- | src/httpserver/qhttpserverparser.cpp | 7 | ||||
-rw-r--r-- | src/httpserver/qhttpserverparser_p.h | 2 | ||||
-rw-r--r-- | src/httpserver/qhttpserverrequest.cpp | 2 | ||||
-rw-r--r-- | src/httpserver/qhttpserverrequest_p.h | 2 | ||||
-rw-r--r-- | tests/auto/qhttpserver/tst_qhttpserver.cpp | 18 |
7 files changed, 76 insertions, 42 deletions
diff --git a/src/httpserver/qhttpserverhttp1protocolhandler.cpp b/src/httpserver/qhttpserverhttp1protocolhandler.cpp index b858a60..35c2d28 100644 --- a/src/httpserver/qhttpserverhttp1protocolhandler.cpp +++ b/src/httpserver/qhttpserverhttp1protocolhandler.cpp @@ -111,16 +111,23 @@ struct IOChunkedTransfer const QPointer<QIODevice> sink; const QMetaObject::Connection bytesWrittenConnection; const QMetaObject::Connection readyReadConnection; + const QMetaObject::Connection readChannelFinished; bool inRead = false; + bool gotReadChannelFinished = false; + bool writingIsComplete = false; + bool useHttp1_1 = false; - IOChunkedTransfer(QIODevice *input, QIODevice *output) : + IOChunkedTransfer(QIODevice *input, QIODevice *output, bool http1_1) : source(input), sink(output), bytesWrittenConnection(connectToBytesWritten(this, output)), readyReadConnection(QObject::connect(source.data(), &QIODevice::readyRead, source.data(), - [this]() { readFromInput(); })) + [this]() { readFromInput(); })), + readChannelFinished(QObject::connect(source.data(), &QIODevice::readChannelFinished, + source.data(), + [this]() { readChannelFinishedHandler(); })), + useHttp1_1(http1_1) { - Q_ASSERT(!source->atEnd()); // TODO error out QObject::connect(sink.data(), &QObject::destroyed, source.data(), &QObject::deleteLater); QObject::connect(source.data(), &QObject::destroyed, source.data(), [this]() { delete this; @@ -132,6 +139,7 @@ struct IOChunkedTransfer { QObject::disconnect(bytesWrittenConnection); QObject::disconnect(readyReadConnection); + QObject::disconnect(readChannelFinished); } static QMetaObject::Connection connectToBytesWritten(IOChunkedTransfer *that, QIODevice *device) @@ -152,6 +160,12 @@ struct IOChunkedTransfer return beginIndex == endIndex; } + void readChannelFinishedHandler() + { + gotReadChannelFinished = true; + readFromInput(); + } + void readFromInput() { if (inRead) @@ -167,13 +181,19 @@ struct IOChunkedTransfer beginIndex = 0; endIndex = source->read(buffer, bufferSize); if (endIndex < 0) { - endIndex = beginIndex; // Mark the buffer as empty - qCWarning(lcHttpServerHttp1Handler, "Error reading chunk: %ls", - qUtf16Printable(source->errorString())); - break; + endIndex = 0; // Mark the buffer as empty + if (!source->isSequential()) { + qCWarning(lcHttpServerHttp1Handler, "Error reading chunk: %ls", + qUtf16Printable(source->errorString())); + return; + } + } + if (endIndex == 0) { // Nothing was read + if (!writingIsComplete + && ((!source->isSequential() && source->atEnd()) || gotReadChannelFinished)) + completeWriting(); + return; } - if (endIndex == 0) - break; memset(buffer + endIndex, 0, sizeof(buffer) - std::size_t(endIndex)); writeToOutput(); } @@ -183,7 +203,6 @@ struct IOChunkedTransfer { if (sink.isNull() || source.isNull()) return; - if (isBufferEmpty()) return; @@ -200,19 +219,38 @@ struct IOChunkedTransfer } #endif - const auto writtenBytes = sink->write(buffer + beginIndex, endIndex); + if (useHttp1_1 && source->isSequential()) { + sink->write(QByteArray::number(endIndex - beginIndex, 16)); + sink->write("\r\n"); + } + const auto writtenBytes = sink->write(buffer + beginIndex, endIndex - beginIndex); if (writtenBytes < 0) { qCWarning(lcHttpServerHttp1Handler, "Error writing chunk: %ls", qUtf16Printable(sink->errorString())); return; } + if (useHttp1_1 && source->isSequential()) + sink->write("\r\n"); beginIndex += writtenBytes; - if (isBufferEmpty()) { - if (source->bytesAvailable() && !inRead) - readFromInput(); - else if (source->atEnd()) // Finishing - source->deleteLater(); + if (isBufferEmpty() && !inRead) + readFromInput(); + } + + void completeWriting() + { + if (sink.isNull()) + return; + Q_ASSERT(!source.isNull()); + Q_ASSERT(isBufferEmpty()); + + if (source->isSequential()) { + if (useHttp1_1) + sink->write("0\r\n\r\n"); + else + sink->close(); } + source->deleteLater(); + writingIsComplete = true; } }; @@ -325,6 +363,7 @@ void QHttpServerHttp1ProtocolHandler::handleReadyRead() return; // Partial read qCDebug(lcHttpServerHttp1Handler) << "Request:" << request; + useHttp1_1 = request.d->minorVersion == 1; QHttpServerResponder responder(this); @@ -442,20 +481,19 @@ void QHttpServerHttp1ProtocolHandler::write(QIODevice *data, const QHttpHeaders } QHttpHeaders allHeaders(headers); - if (!input->isSequential()) { // Non-sequential QIODevice should know its data size + if (input->isSequential()) { + if (useHttp1_1) + allHeaders.append(QHttpHeaders::WellKnownHeader::TransferEncoding, "chunked"); + else + allHeaders.append(QHttpHeaders::WellKnownHeader::Connection, "close"); + } else { // Non-sequential QIODevice should know its data size allHeaders.append(QHttpHeaders::WellKnownHeader::ContentLength, QByteArray::number(input->size())); } - writeStatusAndHeaders(status, allHeaders); - if (input->atEnd()) { - qCDebug(lcHttpServerHttp1Handler, "No more data available."); - return; - } - // input takes ownership of the IOChunkedTransfer pointer inside his constructor - new IOChunkedTransfer<>(input.release(), socket); + new IOChunkedTransfer<>(input.release(), socket, useHttp1_1); state = TransferState::Ready; } diff --git a/src/httpserver/qhttpserverhttp1protocolhandler_p.h b/src/httpserver/qhttpserverhttp1protocolhandler_p.h index a9b5ec1..bc3fd55 100644 --- a/src/httpserver/qhttpserverhttp1protocolhandler_p.h +++ b/src/httpserver/qhttpserverhttp1protocolhandler_p.h @@ -89,6 +89,7 @@ private: bool handlingRequest = false; bool protocolChanged = false; QElapsedTimer lastActiveTimer; + bool useHttp1_1 = false; }; QT_END_NAMESPACE diff --git a/src/httpserver/qhttpserverparser.cpp b/src/httpserver/qhttpserverparser.cpp index 3be9385..6042814 100644 --- a/src/httpserver/qhttpserverparser.cpp +++ b/src/httpserver/qhttpserverparser.cpp @@ -76,6 +76,8 @@ bool QHttpServerParser::parseRequestLine(QByteArrayView line) headerParser.setMajorVersion(protocol[5] - '0'); headerParser.setMinorVersion(protocol[7] - '0'); + majorVersion = protocol[5] - '0'; + minorVersion = protocol[7] - '0'; method = parseRequestMethod(requestMethod); url = QUrl::fromEncoded(requestUrl.toByteArray()); @@ -306,6 +308,9 @@ bool QHttpServerParser::parse(QHttp2Stream *socket) { clear(); + majorVersion = 2; + minorVersion = 0; + for (const auto &pair : socket->receivedHeaders()) { if (pair.name == ":method") { method = parseRequestMethod(pair.value); @@ -353,6 +358,8 @@ void QHttpServerParser::clear() currentChunkRead = 0; currentChunkSize = 0; upgrade = false; + majorVersion = 0; + minorVersion = 0; fragment.clear(); bodyBuffer.clear(); diff --git a/src/httpserver/qhttpserverparser_p.h b/src/httpserver/qhttpserverparser_p.h index ec1c0e4..cdc7580 100644 --- a/src/httpserver/qhttpserverparser_p.h +++ b/src/httpserver/qhttpserverparser_p.h @@ -80,6 +80,8 @@ public: qsizetype currentChunkRead; qsizetype currentChunkSize; bool upgrade; + int majorVersion; + int minorVersion; QByteArray fragment; QByteDataBuffer bodyBuffer; diff --git a/src/httpserver/qhttpserverrequest.cpp b/src/httpserver/qhttpserverrequest.cpp index e50dc1b..cecdbc4 100644 --- a/src/httpserver/qhttpserverrequest.cpp +++ b/src/httpserver/qhttpserverrequest.cpp @@ -306,6 +306,8 @@ QHttpServerRequest QHttpServerRequest::create(const QHttpServerParser &parser) request.d->method = parser.method; request.d->headers = parser.headers; request.d->body = parser.body; + request.d->majorVersion = parser.majorVersion; + request.d->minorVersion = parser.minorVersion; return request; } diff --git a/src/httpserver/qhttpserverrequest_p.h b/src/httpserver/qhttpserverrequest_p.h index 2c25559..044d66b 100644 --- a/src/httpserver/qhttpserverrequest_p.h +++ b/src/httpserver/qhttpserverrequest_p.h @@ -48,6 +48,8 @@ public: quint16 remotePort; QHostAddress localAddress; quint16 localPort; + int majorVersion; + int minorVersion; #if QT_CONFIG(ssl) QSslConfiguration sslConfiguration; #endif diff --git a/tests/auto/qhttpserver/tst_qhttpserver.cpp b/tests/auto/qhttpserver/tst_qhttpserver.cpp index dc090c3..62701f1 100644 --- a/tests/auto/qhttpserver/tst_qhttpserver.cpp +++ b/tests/auto/qhttpserver/tst_qhttpserver.cpp @@ -1653,12 +1653,6 @@ void tst_QHttpServer::writeSequentialDevice() QSignalSpy spy(reply.get(), &QNetworkReply::finished); spy.wait(2s); - if (!useHttp2) { - QEXPECT_FAIL( - "", - "QTBUG-137330: Writing from a Sequential QIODevice to HTTP/1.1 Hangs the Client", - Abort); - } QCOMPARE(spy.count(), 1); checkReply(reply.release(), "GoGoGo"); } @@ -1679,12 +1673,6 @@ void tst_QHttpServer::writeMuchToSequentialDevice() QSignalSpy spy(reply.get(), &QNetworkReply::finished); spy.wait(2s); - if (!useHttp2) { - QEXPECT_FAIL( - "", - "QTBUG-137330: Writing from a Sequential QIODevice to HTTP/1.1 Hangs the Client", - Abort); - } QCOMPARE(spy.count(), 1); checkReply(reply.release(), u"a"_s.repeated(bufferSize * 2)); } @@ -1704,12 +1692,6 @@ void tst_QHttpServer::writeFromEmptySequentialDevice() QSignalSpy spy(reply.get(), &QNetworkReply::finished); spy.wait(2s); - if (!useHttp2) { - QEXPECT_FAIL( - "", - "QTBUG-137330: Writing from a Sequential QIODevice to HTTP/1.1 Hangs the Client", - Abort); - } QCOMPARE(spy.count(), 1); checkReply(reply.release(), ""); } |