diff options
Diffstat (limited to 'tests/auto/network/access/http2/tst_http2.cpp')
-rw-r--r-- | tests/auto/network/access/http2/tst_http2.cpp | 426 |
1 files changed, 370 insertions, 56 deletions
diff --git a/tests/auto/network/access/http2/tst_http2.cpp b/tests/auto/network/access/http2/tst_http2.cpp index 432b35e5c6..b624f6e436 100644 --- a/tests/auto/network/access/http2/tst_http2.cpp +++ b/tests/auto/network/access/http2/tst_http2.cpp @@ -1,30 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the test suite of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:GPL-EXCEPT$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 3 as published by the Free Software -** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only #include <QtNetwork/qtnetworkglobal.h> @@ -62,6 +37,8 @@ Q_DECLARE_METATYPE(QNetworkRequest::Attribute) QT_BEGIN_NAMESPACE +using namespace Qt::StringLiterals; + QHttp2Configuration qt_defaultH2Configuration() { QHttp2Configuration config; @@ -92,8 +69,11 @@ public slots: void init(); private slots: // Tests: + void defaultQnamHttp2Configuration(); void singleRequest_data(); void singleRequest(); + void informationalRequest_data(); + void informationalRequest(); void multipleRequests(); void flowControlClientSide(); void flowControlServerSide(); @@ -115,6 +95,16 @@ private slots: void authenticationRequired_data(); void authenticationRequired(); + 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); @@ -225,6 +215,12 @@ void tst_Http2::init() manager.reset(new QNetworkAccessManager); } +void tst_Http2::defaultQnamHttp2Configuration() +{ + // The configuration we also implicitly use in QNAM. + QCOMPARE(qt_defaultH2Configuration(), QNetworkRequest().http2Configuration()); +} + void tst_Http2::singleRequest_data() { QTest::addColumn<QNetworkRequest::Attribute>("h2Attribute"); @@ -255,7 +251,7 @@ void tst_Http2::singleRequest() // we have to use TLS sockets (== private key) and thus suppress a // keychain UI asking for permission to use a private key. // Our CI has this, but somebody testing locally - will have a problem. - qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1")); + qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1"); auto envRollback = qScopeGuard([](){ qunsetenv("QT_SSL_USE_TEMPORARY_KEYCHAIN"); }); @@ -276,6 +272,7 @@ void tst_Http2::singleRequest() url.setPath("/index.html"); QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true); QFETCH(const QNetworkRequest::Attribute, h2Attribute); request.setAttribute(h2Attribute, QVariant(true)); @@ -292,7 +289,7 @@ void tst_Http2::singleRequest() runEventLoop(); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); @@ -301,10 +298,74 @@ void tst_Http2::singleRequest() #if QT_CONFIG(ssl) if (connectionType == H2Type::h2Alpn || connectionType == H2Type::h2Direct) - QCOMPARE(encSpy.count(), 1); + QCOMPARE(encSpy.size(), 1); #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(); @@ -333,7 +394,7 @@ void tst_Http2::multipleRequests() runEventLoop(); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); } @@ -378,7 +439,7 @@ void tst_Http2::flowControlClientSide() runEventLoop(120000); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); QVERIFY(windowUpdates > 0); @@ -419,7 +480,7 @@ void tst_Http2::flowControlServerSide() runEventLoop(120000); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); } @@ -451,6 +512,7 @@ void tst_Http2::pushPromise() url.setPath("/index.html"); QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true); request.setAttribute(QNetworkRequest::Http2AllowedAttribute, QVariant(true)); request.setHttp2Configuration(params); @@ -462,7 +524,7 @@ void tst_Http2::pushPromise() runEventLoop(); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); @@ -477,6 +539,7 @@ void tst_Http2::pushPromise() url.setPath("/script.js"); QNetworkRequest promisedRequest(url); + request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true); promisedRequest.setAttribute(QNetworkRequest::Http2AllowedAttribute, QVariant(true)); reply = manager->get(promisedRequest); connect(reply, &QNetworkReply::finished, this, &tst_Http2::replyFinished); @@ -531,6 +594,7 @@ void tst_Http2::goaway() for (int i = 0; i < nRequests; ++i) { url.setPath(QString("/%1").arg(i)); QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true); request.setAttribute(QNetworkRequest::Http2AllowedAttribute, QVariant(true)); replies[i] = manager->get(request); QCOMPARE(replies[i]->error(), QNetworkReply::NoError); @@ -586,7 +650,7 @@ void tst_Http2::earlyResponse() runEventLoop(); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); } @@ -643,7 +707,7 @@ void tst_Http2::connectToHost() // we have to use TLS sockets (== private key) and thus suppress a // keychain UI asking for permission to use a private key. // Our CI has this, but somebody testing locally - will have a problem. - qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1")); + qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1"); auto envRollback = qScopeGuard([](){ qunsetenv("QT_SSL_USE_TEMPORARY_KEYCHAIN"); }); @@ -672,6 +736,7 @@ void tst_Http2::connectToHost() auto copyUrl = url; copyUrl.setScheme(QLatin1String("preconnect-https")); QNetworkRequest request(copyUrl); + request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true); request.setAttribute(requestAttribute, true); reply = manager->get(request); // Since we're using self-signed certificates, ignore SSL errors: @@ -684,6 +749,7 @@ void tst_Http2::connectToHost() auto copyUrl = url; copyUrl.setScheme(QLatin1String("preconnect-http")); QNetworkRequest request(copyUrl); + request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true); request.setAttribute(requestAttribute, true); reply = manager->get(request); } @@ -704,6 +770,7 @@ void tst_Http2::connectToHost() QCOMPARE(nRequests, 1); QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true); request.setAttribute(requestAttribute, QVariant(true)); reply = manager->get(request); connect(reply, &QNetworkReply::finished, this, &tst_Http2::replyFinished); @@ -714,7 +781,7 @@ void tst_Http2::connectToHost() runEventLoop(); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); @@ -738,7 +805,7 @@ void tst_Http2::maxFrameSize() // we have to use TLS sockets (== private key) and thus suppress a // keychain UI asking for permission to use a private key. // Our CI has this, but somebody testing locally - will have a problem. - qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1")); + qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1"); auto envRollback = qScopeGuard([](){ qunsetenv("QT_SSL_USE_TEMPORARY_KEYCHAIN"); }); @@ -769,6 +836,7 @@ void tst_Http2::maxFrameSize() url.setPath(QString("/stream1.html")); QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true); request.setAttribute(attribute, QVariant(true)); request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("text/plain")); request.setHttp2Configuration(h2Config); @@ -782,9 +850,9 @@ void tst_Http2::maxFrameSize() // Normally, with a 16kb limit, our server would split such // a response into 3 'DATA' frames (16kb + 16kb + 0|END_STREAM). - QCOMPARE(frameCounter.count(), 1); + QCOMPARE(frameCounter.size(), 1); - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); } @@ -902,7 +970,7 @@ void tst_Http2::moreActivitySignals() // we have to use TLS sockets (== private key) and thus suppress a // keychain UI asking for permission to use a private key. // Our CI has this, but somebody testing locally - will have a problem. - qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1")); + qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1"); auto envRollback = qScopeGuard([]() { qunsetenv("QT_SSL_USE_TEMPORARY_KEYCHAIN"); }); #endif @@ -915,13 +983,14 @@ void tst_Http2::moreActivitySignals() auto url = requestUrl(connectionType); url.setPath(QString("/stream1.html")); QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true); QFETCH(const QNetworkRequest::Attribute, h2Attribute); request.setAttribute(h2Attribute, QVariant(true)); request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("text/plain")); QSharedPointer<QNetworkReply> reply(manager->get(request)); nRequests = 1; connect(reply.data(), &QNetworkReply::finished, this, &tst_Http2::replyFinished); - QSignalSpy spy1(reply.data(), SIGNAL(socketConnecting())); + QSignalSpy spy1(reply.data(), SIGNAL(socketStartedConnecting())); QSignalSpy spy2(reply.data(), SIGNAL(requestSent())); QSignalSpy spy3(reply.data(), SIGNAL(metaDataChanged())); // Since we're using self-signed certificates, @@ -935,7 +1004,7 @@ void tst_Http2::moreActivitySignals() runEventLoop(); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); @@ -1012,7 +1081,7 @@ void tst_Http2::contentEncoding() // we have to use TLS sockets (== private key) and thus suppress a // keychain UI asking for permission to use a private key. // Our CI has this, but somebody testing locally - will have a problem. - qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", QByteArray("1")); + qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1"); auto envRollback = qScopeGuard([]() { qunsetenv("QT_SSL_USE_TEMPORARY_KEYCHAIN"); }); #endif @@ -1035,6 +1104,7 @@ void tst_Http2::contentEncoding() url.setPath("/index.html"); QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true); QFETCH(const QNetworkRequest::Attribute, h2Attribute); request.setAttribute(h2Attribute, QVariant(true)); @@ -1047,7 +1117,7 @@ void tst_Http2::contentEncoding() runEventLoop(); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); @@ -1060,13 +1130,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 +1152,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(); @@ -1092,6 +1172,7 @@ void tst_Http2::authenticationRequired() auto url = requestUrl(defaultConnectionType()); url.setPath("/index.html"); QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true); QByteArray expectedBody = "Hello, World!"; request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); @@ -1115,24 +1196,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: @@ -1140,12 +1227,238 @@ 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::h2cAllowedAttribute_data() +{ + QTest::addColumn<bool>("h2cAllowed"); + QTest::addColumn<bool>("useAttribute"); // true: use attribute, false: use environment variable + QTest::addColumn<bool>("success"); + + QTest::addRow("h2c-not-allowed") << false << false << false; + // Use the attribute to enable/disable the H2C: + QTest::addRow("attribute") << true << true << true; + // Use the QT_NETWORK_H2C_ALLOWED environment variable to enable/disable the H2C: + QTest::addRow("environment-variable") << true << false << true; +} + +void tst_Http2::h2cAllowedAttribute() +{ + QFETCH(const bool, h2cAllowed); + QFETCH(const bool, useAttribute); + QFETCH(const bool, success); + + clearHTTP2State(); + serverPort = 0; + + ServerPtr targetServer(newServer(defaultServerSettings, H2Type::h2c)); + targetServer->setResponseBody("Hello"); + + QMetaObject::invokeMethod(targetServer.data(), "startServer", Qt::QueuedConnection); + runEventLoop(); + + QVERIFY(serverPort != 0); + + nRequests = 1; + + auto url = requestUrl(H2Type::h2c); + url.setPath("/index.html"); + QNetworkRequest request(url); + if (h2cAllowed) { + if (useAttribute) + request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true); + else + qputenv("QT_NETWORK_H2C_ALLOWED", "1"); + } + auto envCleanup = qScopeGuard([]() { qunsetenv("QT_NETWORK_H2C_ALLOWED"); }); + + QScopedPointer<QNetworkReply> reply; + reply.reset(manager->get(request)); + + if (success) + connect(reply.get(), &QNetworkReply::finished, this, &tst_Http2::replyFinished); + else + connect(reply.get(), &QNetworkReply::errorOccurred, this, &tst_Http2::replyFinishedWithError); + + // Since we're using self-signed certificates, + // ignore SSL errors: + reply->ignoreSslErrors(); + + runEventLoop(); + STOP_ON_FAILURE + + if (!success) { + QCOMPARE(reply->error(), QNetworkReply::ConnectionRefusedError); + } else { + QCOMPARE(reply->readAll(), QByteArray("Hello")); + QTRY_VERIFY(serverGotSettingsACK); + } +} + +void tst_Http2::redirect_data() +{ + QTest::addColumn<int>("maxRedirects"); + QTest::addColumn<int>("redirectCount"); + QTest::addColumn<bool>("success"); + + QTest::addRow("1-redirects-none-allowed-failure") << 0 << 1 << false; + QTest::addRow("1-redirects-success") << 1 << 1 << true; + QTest::addRow("2-redirects-1-allowed-failure") << 1 << 2 << false; +} + +void tst_Http2::redirect() +{ + QFETCH(const int, maxRedirects); + QFETCH(const int, redirectCount); + QFETCH(const bool, success); + const QByteArray redirectUrl = "/b.html"_ba; + + clearHTTP2State(); + serverPort = 0; + + ServerPtr targetServer(newServer(defaultServerSettings, defaultConnectionType())); + targetServer->setRedirect(redirectUrl, redirectCount); + + QMetaObject::invokeMethod(targetServer.data(), "startServer", Qt::QueuedConnection); + runEventLoop(); + + QVERIFY(serverPort != 0); + + nRequests = 1; + + auto originalUrl = requestUrl(defaultConnectionType()); + auto url = originalUrl; + url.setPath("/index.html"); + QNetworkRequest request(url); + request.setMaximumRedirectsAllowed(maxRedirects); + // H2C might be used on macOS where SecureTransport doesn't support server-side ALPN + qputenv("QT_NETWORK_H2C_ALLOWED", "1"); + auto envCleanup = qScopeGuard([]() { qunsetenv("QT_NETWORK_H2C_ALLOWED"); }); + + QScopedPointer<QNetworkReply> reply; + reply.reset(manager->get(request)); + + if (success) { + connect(reply.get(), &QNetworkReply::finished, this, &tst_Http2::replyFinished); + } else { + connect(reply.get(), &QNetworkReply::errorOccurred, this, + &tst_Http2::replyFinishedWithError); + } + + // Since we're using self-signed certificates, + // ignore SSL errors: + reply->ignoreSslErrors(); + + runEventLoop(); + STOP_ON_FAILURE + + QCOMPARE(nRequests, 0); + if (success) { + QCOMPARE(reply->error(), QNetworkReply::NoError); + QCOMPARE(reply->url().toString(), + originalUrl.resolved(QString::fromLatin1(redirectUrl)).toString()); + } else if (maxRedirects < redirectCount) { + QCOMPARE(reply->error(), QNetworkReply::TooManyRedirectsError); + } + 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; @@ -1203,6 +1516,7 @@ void tst_Http2::sendRequest(int streamNumber, url.setPath(QString("/stream%1.html").arg(streamNumber)); QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true); request.setAttribute(QNetworkRequest::Http2AllowedAttribute, QVariant(true)); request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy); request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("text/plain")); |