Handle informational HTTP replies (1xx) for HTTP/2

Make QHttp2ProtocolHandler discard all informational (1xx) replies with
the exception of 101.

According to RFC 9110:

"A client MUST be able to parse one or more 1xx responses received
prior to a final response, even if the client does not expect one.
A user agent MAY ignore unexpected 1xx responses."

Fixes: QTBUG-121755
Change-Id: I8b8d578f23d4fbe28929f8c54b3607bcaf85405f
Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
Reviewed-by: Juha Vuolle <juha.vuolle@qt.io>
bb10
Mate Barany 2024-02-20 14:55:08 +01:00
parent c89b1bbddc
commit 6d08db8666
4 changed files with 101 additions and 0 deletions

View File

@ -1101,6 +1101,13 @@ void QHttp2ProtocolHandler::updateStream(Stream &stream, const HPack::HttpHeader
}
}
// Discard all informational (1xx) replies with the exception of 101.
// Also see RFC 9110 (Chapter 15.2)
if (statusCode == 100 || (102 <= statusCode && statusCode <= 199)) {
httpReplyPrivate->clearHttpLayerInformation();
return;
}
if (QHttpNetworkReply::isHttpRedirect(statusCode) && httpRequest.isFollowRedirects()) {
QHttpNetworkConnectionPrivate::ParseRedirectResult result =
m_connection->d_func()->parseRedirectResponse(httpReply);

View File

@ -84,6 +84,12 @@ Http2Server::~Http2Server()
{
}
void Http2Server::setInformationalStatusCode(int code)
{
if (code == 100 || (102 <= code && code <= 199))
informationalStatusCode = code;
}
void Http2Server::enablePushPromise(bool pushEnabled, const QByteArray &path)
{
pushPromiseEnabled = pushEnabled;
@ -837,6 +843,25 @@ void Http2Server::sendResponse(quint32 streamID, bool emptyBody)
// Now we'll continue with _normal_ response.
}
// Create a header with an informational status code and some random header
// fields. The setter ensures that the value is 100 or is between 102 and 199
// (inclusive) if set - otherwise it is 0
if (informationalStatusCode > 0) {
writer.start(FrameType::HEADERS, FrameFlag::END_HEADERS, streamID);
HttpHeader informationalHeader;
informationalHeader.push_back({":status", QByteArray::number(informationalStatusCode)});
informationalHeader.push_back(HeaderField("a_random_header_field", "it_will_be_dropped"));
informationalHeader.push_back(HeaderField("another_random_header_field", "drop_this_too"));
HPack::BitOStream ostream(writer.outboundFrame().buffer);
const bool result = encoder.encodeResponse(ostream, informationalHeader);
Q_ASSERT(result);
writer.writeHEADERS(*socket, maxFrameSize);
}
writer.start(FrameType::HEADERS, FrameFlag::END_HEADERS, streamID);
if (emptyBody)
writer.addFlag(FrameFlag::END_STREAM);

View File

@ -58,6 +58,8 @@ public:
~Http2Server();
// To send responses with status code 1xx
void setInformationalStatusCode(int code);
// To be called before server started:
void enablePushPromise(bool enabled, const QByteArray &path = QByteArray());
void setResponseBody(const QByteArray &body);
@ -210,6 +212,7 @@ private:
int redirectCount = 0;
bool sendTrailingHEADERS = false;
int informationalStatusCode = 0;
protected slots:
void ignoreErrorSlot();
};

View File

@ -72,6 +72,8 @@ private slots:
void defaultQnamHttp2Configuration();
void singleRequest_data();
void singleRequest();
void informationalRequest_data();
void informationalRequest();
void multipleRequests();
void flowControlClientSide();
void flowControlServerSide();
@ -300,6 +302,70 @@ void tst_Http2::singleRequest()
#endif // QT_CONFIG(ssl)
}
void tst_Http2::informationalRequest_data()
{
QTest::addColumn<int>("statusCode");
// 'Clear text' that should always work, either via the protocol upgrade
// or as direct.
QTest::addRow("statusCode-100") << 100;
QTest::addRow("statusCode-125") << 125;
QTest::addRow("statusCode-150") << 150;
QTest::addRow("statusCode-175") << 175;
}
void tst_Http2::informationalRequest()
{
clearHTTP2State();
serverPort = 0;
nRequests = 1;
ServerPtr srv(newServer(defaultServerSettings, defaultConnectionType()));
QFETCH(const int, statusCode);
srv->setInformationalStatusCode(statusCode);
QMetaObject::invokeMethod(srv.data(), "startServer", Qt::QueuedConnection);
runEventLoop();
QVERIFY(serverPort != 0);
auto url = requestUrl(defaultConnectionType());
url.setPath("/index.html");
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
auto reply = manager->get(request);
connect(reply, &QNetworkReply::finished, this, &tst_Http2::replyFinished);
// Since we're using self-signed certificates,
// ignore SSL errors:
reply->ignoreSslErrors();
runEventLoop();
STOP_ON_FAILURE
QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
QCOMPARE(reply->error(), QNetworkReply::NoError);
QVERIFY(reply->isFinished());
const QVariant code(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute));
// We are discarding informational headers if the status code is in the range of
// 102-199 or if it is 100. As these header fields were part of the informational
// header used for this test case, we should not see them at this point and the
// status code should be 200.
QCOMPARE(code.value<int>(), 200);
QVERIFY(!reply->hasRawHeader("a_random_header_field"));
QVERIFY(!reply->hasRawHeader("another_random_header_field"));
}
void tst_Http2::multipleRequests()
{
clearHTTP2State();