diff options
Diffstat (limited to 'tests/auto/network/access/http2/tst_http2.cpp')
-rw-r--r-- | tests/auto/network/access/http2/tst_http2.cpp | 311 |
1 files changed, 288 insertions, 23 deletions
diff --git a/tests/auto/network/access/http2/tst_http2.cpp b/tests/auto/network/access/http2/tst_http2.cpp index 0702a03541..396a6f2fda 100644 --- a/tests/auto/network/access/http2/tst_http2.cpp +++ b/tests/auto/network/access/http2/tst_http2.cpp @@ -1,5 +1,5 @@ // Copyright (C) 2016 The Qt Company Ltd. -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include <QtNetwork/qtnetworkglobal.h> @@ -25,6 +25,7 @@ #include <QtCore/qobject.h> #include <QtCore/qthread.h> #include <QtCore/qurl.h> +#include <QtCore/qset.h> #include <cstdlib> #include <memory> @@ -72,6 +73,8 @@ private slots: void defaultQnamHttp2Configuration(); void singleRequest_data(); void singleRequest(); + void informationalRequest_data(); + void informationalRequest(); void multipleRequests(); void flowControlClientSide(); void flowControlServerSide(); @@ -93,12 +96,18 @@ private slots: void authenticationRequired_data(); void authenticationRequired(); + void unsupportedAuthenticateChallenge(); + void h2cAllowedAttribute_data(); void h2cAllowedAttribute(); void redirect_data(); void redirect(); + void trailingHEADERS(); + + void duplicateRequestsWithAborts(); + protected slots: // Slots to listen to our in-process server: void serverStarted(quint16 port); @@ -283,7 +292,7 @@ void tst_Http2::singleRequest() runEventLoop(); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); @@ -296,6 +305,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(); @@ -324,7 +397,7 @@ void tst_Http2::multipleRequests() runEventLoop(); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); } @@ -369,7 +442,7 @@ void tst_Http2::flowControlClientSide() runEventLoop(120000); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); QVERIFY(windowUpdates > 0); @@ -410,7 +483,7 @@ void tst_Http2::flowControlServerSide() runEventLoop(120000); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); } @@ -454,7 +527,7 @@ void tst_Http2::pushPromise() runEventLoop(); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); @@ -580,7 +653,7 @@ void tst_Http2::earlyResponse() runEventLoop(); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); } @@ -711,7 +784,7 @@ void tst_Http2::connectToHost() runEventLoop(); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); @@ -782,7 +855,7 @@ void tst_Http2::maxFrameSize() // a response into 3 'DATA' frames (16kb + 16kb + 0|END_STREAM). QCOMPARE(frameCounter.size(), 1); - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); } @@ -934,7 +1007,7 @@ void tst_Http2::moreActivitySignals() runEventLoop(); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); @@ -1047,7 +1120,7 @@ void tst_Http2::contentEncoding() runEventLoop(); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); @@ -1060,13 +1133,18 @@ void tst_Http2::authenticationRequired_data() { QTest::addColumn<bool>("success"); QTest::addColumn<bool>("responseHEADOnly"); + QTest::addColumn<bool>("withChallenge"); - QTest::addRow("failed-auth") << false << true; - QTest::addRow("successful-auth") << true << true; + QTest::addRow("failed-auth") << false << true << true; + QTest::addRow("successful-auth") << true << true << true; // Include a DATA frame in the response from the remote server. An example would be receiving a // JSON response on a request along with the 401 error. - QTest::addRow("failed-auth-with-response") << false << false; - QTest::addRow("successful-auth-with-response") << true << false; + QTest::addRow("failed-auth-with-response") << false << false << true; + QTest::addRow("successful-auth-with-response") << true << false << true; + + // Don't provide a challenge header. This is valid if you are actually just + // denied access for whatever reason. + QTest::addRow("no-challenge") << false << false << false; } void tst_Http2::authenticationRequired() @@ -1077,10 +1155,15 @@ void tst_Http2::authenticationRequired() POSTResponseHEADOnly = responseHEADOnly; QFETCH(const bool, success); + QFETCH(const bool, withChallenge); ServerPtr targetServer(newServer(defaultServerSettings, defaultConnectionType())); - targetServer->setResponseBody("Hello"); - targetServer->setAuthenticationHeader("Basic realm=\"Shadow\""); + QByteArray responseBody = "Hello"_ba; + targetServer->setResponseBody(responseBody); + if (withChallenge) + targetServer->setAuthenticationHeader("Basic realm=\"Shadow\""); + else + targetServer->setAuthenticationRequired(true); QMetaObject::invokeMethod(targetServer.data(), "startServer", Qt::QueuedConnection); runEventLoop(); @@ -1116,24 +1199,30 @@ void tst_Http2::authenticationRequired() receivedBody += body; }); - if (success) + if (success) { connect(reply.get(), &QNetworkReply::finished, this, &tst_Http2::replyFinished); - else - connect(reply.get(), &QNetworkReply::errorOccurred, this, &tst_Http2::replyFinishedWithError); + } else { + // Use queued connection so that the finished signal can be emitted and the isFinished + // property can be set. + connect(reply.get(), &QNetworkReply::errorOccurred, this, + &tst_Http2::replyFinishedWithError, Qt::QueuedConnection); + } // Since we're using self-signed certificates, // ignore SSL errors: reply->ignoreSslErrors(); runEventLoop(); STOP_ON_FAILURE + QVERIFY2(reply->isFinished(), + "The reply should error out if authentication fails, or finish if it succeeds"); if (!success) QCOMPARE(reply->error(), QNetworkReply::AuthenticationRequiredError); // else: no error (is checked in tst_Http2::replyFinished) - QVERIFY(authenticationRequested); + QVERIFY(authenticationRequested || !withChallenge); - const auto isAuthenticated = [](QByteArray bv) { + const auto isAuthenticated = [](const QByteArray &bv) { return bv == "Basic YWRtaW46YWRtaW4="; // admin:admin }; // Get the "authorization" header out from the server and make sure it's as expected: @@ -1141,12 +1230,105 @@ void tst_Http2::authenticationRequired() QCOMPARE(isAuthenticated(reqAuthHeader), success); if (success) QCOMPARE(receivedBody, expectedBody); + if (responseHEADOnly) { + const QVariant contentLenHeader = reply->header(QNetworkRequest::ContentLengthHeader); + QVERIFY2(!contentLenHeader.isValid(), "We expect no DATA frames to be received"); + QCOMPARE(reply->readAll(), QByteArray()); + } else { + const qint32 contentLen = reply->header(QNetworkRequest::ContentLengthHeader).toInt(); + QCOMPARE(contentLen, responseBody.length()); + QCOMPARE(reply->bytesAvailable(), responseBody.length()); + QCOMPARE(reply->readAll(), QByteArray("Hello")); + } // In the `!success` case we need to wait for the server to emit this or it might cause issues // in the next test running after this. In the `success` case we anyway expect it to have been // received. QTRY_VERIFY(serverGotSettingsACK); } +void tst_Http2::unsupportedAuthenticateChallenge() +{ + clearHTTP2State(); + serverPort = 0; + + if (defaultConnectionType() == H2Type::h2c) + QSKIP("This test requires TLS with ALPN to work"); + + ServerPtr targetServer(newServer(defaultServerSettings, defaultConnectionType())); + QByteArray responseBody = "Hello"_ba; + targetServer->setResponseBody(responseBody); + targetServer->setAuthenticationHeader("Bearer realm=\"qt.io accounts\""); + + QMetaObject::invokeMethod(targetServer.data(), "startServer", Qt::QueuedConnection); + runEventLoop(); + + QVERIFY(serverPort != 0); + + nRequests = 1; + + QUrl url = requestUrl(defaultConnectionType()); + url.setPath("/index.html"); + QNetworkRequest request(url); + + QByteArray expectedBody = "Hello, World!"; + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + QScopedPointer<QNetworkReply> reply; + reply.reset(manager->post(request, expectedBody)); + + bool authenticationRequested = false; + connect(manager.get(), &QNetworkAccessManager::authenticationRequired, reply.get(), + [&](QNetworkReply *, QAuthenticator *) { + authenticationRequested = true; + }); + + bool finishedReceived = false; + connect(reply.get(), &QNetworkReply::finished, reply.get(), + [&]() { finishedReceived = true; }); + bool errorReceived = false; + connect(reply.get(), &QNetworkReply::errorOccurred, reply.get(), + [&]() { errorReceived = true; }); + + QSet<quint32> receivedDataOnStreams; + connect(targetServer.get(), &Http2Server::receivedDATAFrame, reply.get(), + [&receivedDataOnStreams](quint32 streamID, const QByteArray &body) { + Q_UNUSED(body); + receivedDataOnStreams.insert(streamID); + }); + + // Use queued connection so that the finished signal can be emitted and the + // isFinished property can be set. + connect(reply.get(), &QNetworkReply::errorOccurred, this, + &tst_Http2::replyFinishedWithError, Qt::QueuedConnection); + + // Since we're using self-signed certificates, ignore SSL errors: + reply->ignoreSslErrors(); + + runEventLoop(); + STOP_ON_FAILURE + QVERIFY2(reply->isFinished(), + "The reply should error out if authentication fails, or finish if it succeeds"); + + QCOMPARE(reply->error(), QNetworkReply::AuthenticationRequiredError); + QVERIFY(reply->isFinished()); + QVERIFY(errorReceived); + QVERIFY(finishedReceived); + QCOMPARE(receivedDataOnStreams.size(), 1); + QVERIFY(receivedDataOnStreams.contains(1)); // the original, failed, request + + QVERIFY(!authenticationRequested); + + // We should not have sent any authentication headers to the server, since + // we don't support the challenge. + const QByteArray reqAuthHeader = targetServer->requestAuthorizationHeader(); + QVERIFY(reqAuthHeader.isEmpty()); + + // In the `!success` case we need to wait for the server to emit this or it might cause issues + // in the next test running after this. In the `success` case we anyway expect it to have been + // received. + QTRY_VERIFY(serverGotSettingsACK); + +} + void tst_Http2::h2cAllowedAttribute_data() { QTest::addColumn<bool>("h2cAllowed"); @@ -1242,7 +1424,7 @@ void tst_Http2::redirect() QVERIFY(serverPort != 0); - nRequests = 1 + maxRedirects; + nRequests = 1; auto originalUrl = requestUrl(defaultConnectionType()); auto url = originalUrl; @@ -1270,6 +1452,7 @@ void tst_Http2::redirect() runEventLoop(); STOP_ON_FAILURE + QCOMPARE(nRequests, 0); if (success) { QCOMPARE(reply->error(), QNetworkReply::NoError); QCOMPARE(reply->url().toString(), @@ -1280,6 +1463,88 @@ void tst_Http2::redirect() QTRY_VERIFY(serverGotSettingsACK); } +void tst_Http2::trailingHEADERS() +{ + clearHTTP2State(); + serverPort = 0; + + ServerPtr targetServer(newServer(defaultServerSettings, defaultConnectionType())); + targetServer->setSendTrailingHEADERS(true); + + QMetaObject::invokeMethod(targetServer.data(), "startServer", Qt::QueuedConnection); + runEventLoop(); + + QVERIFY(serverPort != 0); + + nRequests = 1; + + const auto url = requestUrl(defaultConnectionType()); + QNetworkRequest request(url); + // H2C might be used on macOS where SecureTransport doesn't support server-side ALPN + request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true); + + std::unique_ptr<QNetworkReply> reply{ manager->get(request) }; + connect(reply.get(), &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); + + QCOMPARE(reply->error(), QNetworkReply::NoError); + QTRY_VERIFY(serverGotSettingsACK); +} + +void tst_Http2::duplicateRequestsWithAborts() +{ + clearHTTP2State(); + serverPort = 0; + + ServerPtr targetServer(newServer(defaultServerSettings, defaultConnectionType())); + + QMetaObject::invokeMethod(targetServer.data(), "startServer", Qt::QueuedConnection); + runEventLoop(); + + QVERIFY(serverPort != 0); + + constexpr int ExpectedSuccessfulRequests = 1; + nRequests = ExpectedSuccessfulRequests; + + const auto url = requestUrl(defaultConnectionType()); + QNetworkRequest request(url); + // H2C might be used on macOS where SecureTransport doesn't support server-side ALPN + request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true); + + qint32 finishedCount = 0; + auto connectToSlots = [this, &finishedCount](QNetworkReply *reply){ + const auto onFinished = [&finishedCount, reply, this]() { + ++finishedCount; + if (reply->error() == QNetworkReply::NoError) + replyFinished(); + }; + connect(reply, &QNetworkReply::finished, reply, onFinished); + }; + + std::vector<QNetworkReply *> replies; + for (qint32 i = 0; i < 3; ++i) { + auto &reply = replies.emplace_back(manager->get(request)); + connectToSlots(reply); + if (i < 2) // Delete and abort all-but-one: + reply->deleteLater(); + // Since we're using self-signed certificates, ignore SSL errors: + reply->ignoreSslErrors(); + } + + runEventLoop(); + STOP_ON_FAILURE + + QCOMPARE(nRequests, 0); + QCOMPARE(finishedCount, ExpectedSuccessfulRequests); +} + void tst_Http2::serverStarted(quint16 port) { serverPort = port; |