diff options
Diffstat (limited to 'tests/auto/network/access/http2')
-rw-r--r-- | tests/auto/network/access/http2/BLACKLIST | 2 | ||||
-rw-r--r-- | tests/auto/network/access/http2/CMakeLists.txt | 14 | ||||
-rw-r--r-- | tests/auto/network/access/http2/http2srv.cpp | 145 | ||||
-rw-r--r-- | tests/auto/network/access/http2/http2srv.h | 52 | ||||
-rw-r--r-- | tests/auto/network/access/http2/tst_http2.cpp | 776 |
5 files changed, 856 insertions, 133 deletions
diff --git a/tests/auto/network/access/http2/BLACKLIST b/tests/auto/network/access/http2/BLACKLIST new file mode 100644 index 0000000000..3de8d6d448 --- /dev/null +++ b/tests/auto/network/access/http2/BLACKLIST @@ -0,0 +1,2 @@ +[duplicateRequestsWithAborts] +qnx ci # QTBUG-119616 diff --git a/tests/auto/network/access/http2/CMakeLists.txt b/tests/auto/network/access/http2/CMakeLists.txt index 735f95deff..7ea559940b 100644 --- a/tests/auto/network/access/http2/CMakeLists.txt +++ b/tests/auto/network/access/http2/CMakeLists.txt @@ -1,18 +1,24 @@ -# Generated from http2.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## tst_http2 Test: ##################################################################### +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_http2 LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + qt_internal_add_test(tst_http2 SOURCES http2srv.cpp http2srv.h tst_http2.cpp - DEFINES - SRCDIR=\\\"${CMAKE_CURRENT_SOURCE_DIR}/\\\" - PUBLIC_LIBRARIES + LIBRARIES Qt::CorePrivate Qt::Network Qt::NetworkPrivate Qt::TestPrivate + BUNDLE_ANDROID_OPENSSL_LIBS ) diff --git a/tests/auto/network/access/http2/http2srv.cpp b/tests/auto/network/access/http2/http2srv.cpp index d09779bb8f..b52ea5527b 100644 --- a/tests/auto/network/access/http2/http2srv.cpp +++ b/tests/auto/network/access/http2/http2srv.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 <QTest> @@ -109,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; @@ -125,6 +106,28 @@ void Http2Server::setContentEncoding(const QByteArray &encoding) contentEncoding = encoding; } +void Http2Server::setAuthenticationHeader(const QByteArray &authentication) +{ + authenticationHeader = authentication; +} + +void Http2Server::setAuthenticationRequired(bool enable) +{ + Q_ASSERT(!enable || authenticationHeader.isEmpty()); + authenticationRequired = enable; +} + +void Http2Server::setRedirect(const QByteArray &url, int count) +{ + redirectUrl = url; + redirectCount = count; +} + +void Http2Server::setSendTrailingHEADERS(bool enable) +{ + sendTrailingHEADERS = enable; +} + void Http2Server::emulateGOAWAY(int timeout) { Q_ASSERT(timeout >= 0); @@ -143,6 +146,17 @@ bool Http2Server::isClearText() const return connectionType == H2Type::h2c || connectionType == H2Type::h2cDirect; } +QByteArray Http2Server::requestAuthorizationHeader() +{ + const auto isAuthHeader = [](const HeaderField &field) { + return field.name == "authorization"; + }; + const auto requestHeaders = decoder.decodedHeader(); + const auto authentication = + std::find_if(requestHeaders.cbegin(), requestHeaders.cend(), isAuthHeader); + return authentication == requestHeaders.cend() ? QByteArray() : authentication->value; +} + void Http2Server::startServer() { if (listen()) { @@ -251,9 +265,20 @@ void Http2Server::sendDATA(quint32 streamID, quint32 windowSize) return; if (last) { - writer.start(FrameType::DATA, FrameFlag::END_STREAM, streamID); - writer.setPayloadSize(0); - writer.write(*socket); + if (sendTrailingHEADERS) { + writer.start(FrameType::HEADERS, + FrameFlag::PRIORITY | FrameFlag::END_HEADERS | FrameFlag::END_STREAM, streamID); + const quint32 maxFrameSize(clientSetting(Settings::MAX_FRAME_SIZE_ID, + Http2::maxPayloadSize)); + // 5 bytes for PRIORITY data: + writer.append(quint32(0)); // streamID 0 (32-bit) + writer.append(quint8(0)); // + weight 0 (8-bit) + writer.writeHEADERS(*socket, maxFrameSize); + } else { + writer.start(FrameType::DATA, FrameFlag::END_STREAM, streamID); + writer.setPayloadSize(0); + writer.write(*socket); + } suspendedStreams.erase(it); activeRequests.erase(streamID); @@ -302,11 +327,12 @@ void Http2Server::incomingConnection(qintptr socketDescriptor) sslSocket->setProtocol(QSsl::TlsV1_2OrLater); connect(sslSocket, SIGNAL(sslErrors(QList<QSslError>)), this, SLOT(ignoreErrorSlot())); - QFile file(SRCDIR "certs/fluke.key"); - file.open(QIODevice::ReadOnly); + QFile file(QT_TESTCASE_SOURCEDIR "/certs/fluke.key"); + if (!file.open(QIODevice::ReadOnly)) + qFatal("Cannot open certificate file %s", qPrintable(file.fileName())); QSslKey key(file.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey); sslSocket->setPrivateKey(key); - auto localCert = QSslCertificate::fromPath(SRCDIR "certs/fluke.cert"); + auto localCert = QSslCertificate::fromPath(QT_TESTCASE_SOURCEDIR "/certs/fluke.cert"); sslSocket->setLocalCertificateChain(localCert); sslSocket->setSocketDescriptor(socketDescriptor, QAbstractSocket::ConnectedState); // Stop listening. @@ -364,16 +390,12 @@ bool Http2Server::verifyProtocolUpgradeRequest() bool settingsOk = false; QHttpNetworkReplyPrivate *firstRequestReader = protocolUpgradeHandler->d_func(); + const auto headers = firstRequestReader->headers(); // That's how we append them, that's what I expect to find: - for (const auto &header : firstRequestReader->fields) { - if (header.first == "Connection") - connectionOk = header.second.contains("Upgrade, HTTP2-Settings"); - else if (header.first == "Upgrade") - upgradeOk = header.second.contains("h2c"); - else if (header.first == "HTTP2-Settings") - settingsOk = true; - } + connectionOk = headers.combinedValue(QHttpHeaders::WellKnownHeader::Connection).contains("Upgrade, HTTP2-Settings"); + upgradeOk = headers.combinedValue(QHttpHeaders::WellKnownHeader::Upgrade).contains("h2c"); + settingsOk = headers.contains("HTTP2-Settings"); return connectionOk && upgradeOk && settingsOk; } @@ -737,10 +759,15 @@ void Http2Server::handleDATA() } if (inboundFrame.flags().testFlag(FrameFlag::END_STREAM)) { - closedStreams.insert(streamID); // Enter "half-closed remote" state. - streamWindows.erase(it); + if (responseBody.isEmpty()) { + closedStreams.insert(streamID); // Enter "half-closed remote" state. + streamWindows.erase(it); + } emit receivedData(streamID); } + emit receivedDATAFrame(streamID, + QByteArray(reinterpret_cast<const char *>(inboundFrame.dataBegin()), + inboundFrame.dataSize())); } void Http2Server::handleWINDOW_UPDATE() @@ -817,10 +844,32 @@ 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); + // We assume any auth is correct. Leaves the checking to the test itself + const bool hasAuth = !requestAuthorizationHeader().isEmpty(); + HttpHeader header; if (redirectWhileReading) { if (redirectSent) { @@ -836,7 +885,15 @@ void Http2Server::sendResponse(quint32 streamID, bool emptyBody) const QString url("%1://localhost:%2/"); header.push_back({"location", url.arg(isClearText() ? QStringLiteral("http") : QStringLiteral("https"), QString::number(targetPort)).toLatin1()}); - + } else if (redirectCount > 0) { // Not redirecting while reading, unlike above + --redirectCount; + header.push_back({":status", "308"}); + header.push_back({"location", redirectUrl}); + } else if (!authenticationHeader.isEmpty() && !hasAuth) { + header.push_back({ ":status", "401" }); + header.push_back(HPack::HeaderField("www-authenticate", authenticationHeader)); + } else if (authenticationRequired) { + header.push_back({ ":status", "401" }); } else { header.push_back({":status", "200"}); } diff --git a/tests/auto/network/access/http2/http2srv.h b/tests/auto/network/access/http2/http2srv.h index baf0155988..dc94318527 100644 --- a/tests/auto/network/access/http2/http2srv.h +++ b/tests/auto/network/access/http2/http2srv.h @@ -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 #ifndef HTTP2SRV_H #define HTTP2SRV_H @@ -83,16 +58,29 @@ 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); // No content encoding is actually performed, call setResponseBody with already encoded data void setContentEncoding(const QByteArray &contentEncoding); + // No authentication data is generated for the method, the full header value must be set + void setAuthenticationHeader(const QByteArray &authentication); + // Authentication always required, no challenge provided + void setAuthenticationRequired(bool enable); + // Set the redirect URL and count. The server will return a redirect response with the url + // 'count' amount of times + void setRedirect(const QByteArray &redirectUrl, int count); + // Send a trailing HEADERS frame with PRIORITY and END_STREAM flag + void setSendTrailingHEADERS(bool enable); void emulateGOAWAY(int timeout); void redirectOpenStream(quint16 targetPort); bool isClearText() const; + QByteArray requestAuthorizationHeader(); + // Invokables, since we can call them from the main thread, // but server (can) work on its own thread. Q_INVOKABLE void startServer(); @@ -129,6 +117,8 @@ Q_SIGNALS: void decompressionFailed(quint32 streamID); void receivedRequest(quint32 streamID); void receivedData(quint32 streamID); + // Emitted for every DATA frame. Includes the content of the frame as \a body. + void receivedDATAFrame(quint32 streamID, const QByteArray &body); void windowUpdate(quint32 streamID); void sendingData(); @@ -215,6 +205,14 @@ private: QAtomicInt interrupted; QByteArray contentEncoding; + QByteArray authenticationHeader; + bool authenticationRequired = false; + + QByteArray redirectUrl; + int redirectCount = 0; + + bool sendTrailingHEADERS = false; + int informationalStatusCode = 0; protected slots: void ignoreErrorSlot(); }; diff --git a/tests/auto/network/access/http2/tst_http2.cpp b/tests/auto/network/access/http2/tst_http2.cpp index 1aa012c6ac..d9e82330b2 100644 --- a/tests/auto/network/access/http2/tst_http2.cpp +++ b/tests/auto/network/access/http2/tst_http2.cpp @@ -1,30 +1,7 @@ -/**************************************************************************** -** -** 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> #include <QTest> #include <QTestEventLoop> @@ -40,16 +17,15 @@ #include <QtNetwork/qnetworkrequest.h> #include <QtNetwork/qnetworkreply.h> +#if QT_CONFIG(ssl) +#include <QtNetwork/qsslsocket.h> +#endif + #include <QtCore/qglobal.h> #include <QtCore/qobject.h> #include <QtCore/qthread.h> #include <QtCore/qurl.h> - -#ifndef QT_NO_SSL -#ifndef QT_NO_OPENSSL -#include <QtNetwork/private/qsslsocket_openssl_symbols_p.h> -#endif // NO_OPENSSL -#endif // NO_SSL +#include <QtCore/qset.h> #include <cstdlib> #include <memory> @@ -57,21 +33,13 @@ #include <QtTest/private/qemulationdetector_p.h> -#if (!defined(QT_NO_OPENSSL) && OPENSSL_VERSION_NUMBER >= 0x10002000L && !defined(OPENSSL_NO_TLSEXT)) \ - || QT_CONFIG(schannel) -// HTTP/2 over TLS requires ALPN/NPN to negotiate the protocol version. -const bool clearTextHTTP2 = false; -#else -// No ALPN/NPN support to negotiate HTTP/2, we'll use cleartext 'h2c' with -// a protocol upgrade procedure. -const bool clearTextHTTP2 = true; -#endif - Q_DECLARE_METATYPE(H2Type) Q_DECLARE_METATYPE(QNetworkRequest::Attribute) QT_BEGIN_NAMESPACE +using namespace Qt::StringLiterals; + QHttp2Configuration qt_defaultH2Configuration() { QHttp2Configuration config; @@ -102,8 +70,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(); @@ -114,10 +85,29 @@ private slots: void connectToHost_data(); void connectToHost(); void maxFrameSize(); + void http2DATAFrames(); + + void moreActivitySignals_data(); + void moreActivitySignals(); void contentEncoding_data(); void contentEncoding(); + 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); @@ -161,6 +151,7 @@ private: int windowUpdates = 0; bool prefaceOK = false; bool serverGotSettingsACK = false; + bool POSTResponseHEADOnly = true; static const RawSettings defaultServerSettings; }; @@ -186,6 +177,8 @@ struct ServerDeleter } }; +bool clearTextHTTP2 = false; + using ServerPtr = QScopedPointer<Http2Server, ServerDeleter>; H2Type defaultConnectionType() @@ -198,6 +191,12 @@ H2Type defaultConnectionType() tst_Http2::tst_Http2() : workerThread(new QThread) { +#if QT_CONFIG(ssl) + const auto features = QSslSocket::supportedFeatures(); + clearTextHTTP2 = !features.contains(QSsl::SupportedFeature::ServerSideAlpn); +#else + clearTextHTTP2 = true; +#endif workerThread->start(); } @@ -219,6 +218,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"); @@ -249,7 +254,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"); }); @@ -270,10 +275,73 @@ 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)); auto reply = manager->get(request); +#if QT_CONFIG(ssl) + QSignalSpy encSpy(reply, &QNetworkReply::encrypted); +#endif // QT_CONFIG(ssl) + + 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()); + +#if QT_CONFIG(ssl) + if (connectionType == H2Type::h2Alpn || connectionType == H2Type::h2Direct) + 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: @@ -282,12 +350,23 @@ void tst_Http2::singleRequest() runEventLoop(); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + 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() @@ -318,7 +397,7 @@ void tst_Http2::multipleRequests() runEventLoop(); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); } @@ -363,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); @@ -404,7 +483,7 @@ void tst_Http2::flowControlServerSide() runEventLoop(120000); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); } @@ -436,6 +515,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); @@ -447,7 +527,7 @@ void tst_Http2::pushPromise() runEventLoop(); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); @@ -462,6 +542,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); @@ -516,6 +597,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); @@ -571,7 +653,7 @@ void tst_Http2::earlyResponse() runEventLoop(); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); } @@ -628,7 +710,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"); }); @@ -657,6 +739,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: @@ -669,6 +752,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); } @@ -689,6 +773,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); @@ -699,7 +784,7 @@ void tst_Http2::connectToHost() runEventLoop(); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); @@ -723,7 +808,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"); }); @@ -754,6 +839,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); @@ -767,11 +853,166 @@ 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); + + QCOMPARE(nRequests, 0); + QVERIFY(prefaceOK); + QVERIFY(serverGotSettingsACK); +} + +void tst_Http2::http2DATAFrames() +{ + using namespace Http2; + + { + // 0. DATA frame with payload, no padding. + + FrameWriter writer(FrameType::DATA, FrameFlag::EMPTY, 1); + writer.append('a'); + writer.append('b'); + writer.append('c'); + + const Frame frame = writer.outboundFrame(); + const auto &buffer = frame.buffer; + // Frame's header is 9 bytes + 3 bytes of payload + // (+ 0 bytes of padding and no padding length): + QCOMPARE(int(buffer.size()), 12); + + QVERIFY(!frame.padding()); + QCOMPARE(int(frame.payloadSize()), 3); + QCOMPARE(int(frame.dataSize()), 3); + QCOMPARE(frame.dataBegin() - buffer.data(), 9); + QCOMPARE(char(*frame.dataBegin()), 'a'); + } + + { + // 1. DATA with padding. + + const int padLength = 10; + FrameWriter writer(FrameType::DATA, FrameFlag::END_STREAM | FrameFlag::PADDED, 1); + writer.append(uchar(padLength)); // The length of padding is 1 byte long. + writer.append('a'); + for (int i = 0; i < padLength; ++i) + writer.append('b'); + + const Frame frame = writer.outboundFrame(); + const auto &buffer = frame.buffer; + // Frame's header is 9 bytes + 1 byte for padding length + // + 1 byte of data + 10 bytes of padding: + QCOMPARE(int(buffer.size()), 21); + + QCOMPARE(frame.padding(), padLength); + QCOMPARE(int(frame.payloadSize()), 12); // Includes padding, its length + data. + QCOMPARE(int(frame.dataSize()), 1); + + // Skipping 9 bytes long header and padding length: + QCOMPARE(frame.dataBegin() - buffer.data(), 10); + + QCOMPARE(char(frame.dataBegin()[0]), 'a'); + QCOMPARE(char(frame.dataBegin()[1]), 'b'); + + QVERIFY(frame.flags().testFlag(FrameFlag::END_STREAM)); + QVERIFY(frame.flags().testFlag(FrameFlag::PADDED)); + } + { + // 2. DATA with PADDED flag, but 0 as padding length. + + FrameWriter writer(FrameType::DATA, FrameFlag::END_STREAM | FrameFlag::PADDED, 1); + + writer.append(uchar(0)); // Number of padding bytes is 1 byte long. + writer.append('a'); + + const Frame frame = writer.outboundFrame(); + const auto &buffer = frame.buffer; + + // Frame's header is 9 bytes + 1 byte for padding length + 1 byte of data + // + 0 bytes of padding: + QCOMPARE(int(buffer.size()), 11); + + QCOMPARE(frame.padding(), 0); + QCOMPARE(int(frame.payloadSize()), 2); // Includes padding (0 bytes), its length + data. + QCOMPARE(int(frame.dataSize()), 1); + + // Skipping 9 bytes long header and padding length: + QCOMPARE(frame.dataBegin() - buffer.data(), 10); + + QCOMPARE(char(*frame.dataBegin()), 'a'); + + QVERIFY(frame.flags().testFlag(FrameFlag::END_STREAM)); + QVERIFY(frame.flags().testFlag(FrameFlag::PADDED)); + } +} + +void tst_Http2::moreActivitySignals_data() +{ + QTest::addColumn<QNetworkRequest::Attribute>("h2Attribute"); + QTest::addColumn<H2Type>("connectionType"); + + QTest::addRow("h2c-upgrade") + << QNetworkRequest::Http2AllowedAttribute << H2Type::h2c; + QTest::addRow("h2c-direct") + << QNetworkRequest::Http2DirectAttribute << H2Type::h2cDirect; + + if (!clearTextHTTP2) + QTest::addRow("h2-ALPN") + << QNetworkRequest::Http2AllowedAttribute << H2Type::h2Alpn; + +#if QT_CONFIG(ssl) + QTest::addRow("h2-direct") + << QNetworkRequest::Http2DirectAttribute << H2Type::h2Direct; +#endif +} + +void tst_Http2::moreActivitySignals() +{ + clearHTTP2State(); + +#if QT_CONFIG(securetransport) + // Normally on macOS we use plain text only for SecureTransport + // does not support ALPN on the server side. With 'direct encrytped' + // 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", "1"); + auto envRollback = qScopeGuard([]() { qunsetenv("QT_SSL_USE_TEMPORARY_KEYCHAIN"); }); +#endif + + serverPort = 0; + QFETCH(H2Type, connectionType); + ServerPtr srv(newServer(defaultServerSettings, connectionType)); + QMetaObject::invokeMethod(srv.data(), "startServer", Qt::QueuedConnection); + runEventLoop(100); + QVERIFY(serverPort != 0); + 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(socketStartedConnecting())); + QSignalSpy spy2(reply.data(), SIGNAL(requestSent())); + QSignalSpy spy3(reply.data(), SIGNAL(metaDataChanged())); + // Since we're using self-signed certificates, + // ignore SSL errors: + reply->ignoreSslErrors(); + + spy1.wait(); + spy2.wait(); + spy3.wait(); + + runEventLoop(); + STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); + + QVERIFY(reply->error() == QNetworkReply::NoError); + QVERIFY(reply->isFinished()); } void tst_Http2::contentEncoding_data() @@ -843,7 +1084,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 @@ -866,6 +1107,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)); @@ -878,7 +1120,7 @@ void tst_Http2::contentEncoding() runEventLoop(); STOP_ON_FAILURE - QVERIFY(nRequests == 0); + QCOMPARE(nRequests, 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); @@ -887,6 +1129,422 @@ void tst_Http2::contentEncoding() QTEST(reply->readAll(), "expected"); } +void tst_Http2::authenticationRequired_data() +{ + QTest::addColumn<bool>("success"); + QTest::addColumn<bool>("responseHEADOnly"); + QTest::addColumn<bool>("withChallenge"); + + 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 << 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() +{ + clearHTTP2State(); + serverPort = 0; + QFETCH(const bool, responseHEADOnly); + POSTResponseHEADOnly = responseHEADOnly; + + QFETCH(const bool, success); + QFETCH(const bool, withChallenge); + + ServerPtr targetServer(newServer(defaultServerSettings, defaultConnectionType())); + 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(); + + QVERIFY(serverPort != 0); + + nRequests = 1; + + 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"); + QScopedPointer<QNetworkReply> reply; + reply.reset(manager->post(request, expectedBody)); + + bool authenticationRequested = false; + connect(manager.get(), &QNetworkAccessManager::authenticationRequired, reply.get(), + [&](QNetworkReply *, QAuthenticator *auth) { + authenticationRequested = true; + if (success) { + auth->setUser("admin"); + auth->setPassword("admin"); + } + }); + + QByteArray receivedBody; + connect(targetServer.get(), &Http2Server::receivedDATAFrame, reply.get(), + [&receivedBody](quint32 streamID, const QByteArray &body) { + if (streamID == 3) // The expected body is on the retry, so streamID == 3 + receivedBody += body; + }); + + if (success) { + connect(reply.get(), &QNetworkReply::finished, this, &tst_Http2::replyFinished); + } 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 || !withChallenge); + + 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: + auto reqAuthHeader = targetServer->requestAuthorizationHeader(); + 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 *auth) { + 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"); + 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; @@ -898,6 +1556,7 @@ void tst_Http2::clearHTTP2State() windowUpdates = 0; prefaceOK = false; serverGotSettingsACK = false; + POSTResponseHEADOnly = true; } void tst_Http2::runEventLoop(int ms) @@ -943,6 +1602,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")); @@ -1030,7 +1690,7 @@ void tst_Http2::receivedData(quint32 streamID) Q_ASSERT(srv); QMetaObject::invokeMethod(srv, "sendResponse", Qt::QueuedConnection, Q_ARG(quint32, streamID), - Q_ARG(bool, true /*HEADERS only*/)); + Q_ARG(bool, POSTResponseHEADOnly /*true = HEADERS only*/)); } void tst_Http2::windowUpdated(quint32 streamID) |