diff --git a/tests/auto/network/access/http2/http2srv.cpp b/tests/auto/network/access/http2/http2srv.cpp index a8eebf5a24..9513744476 100644 --- a/tests/auto/network/access/http2/http2srv.cpp +++ b/tests/auto/network/access/http2/http2srv.cpp @@ -120,6 +120,11 @@ void Http2Server::setResponseBody(const QByteArray &body) responseBody = body; } +void Http2Server::setContentEncoding(const QByteArray &encoding) +{ + contentEncoding = encoding; +} + void Http2Server::emulateGOAWAY(int timeout) { Q_ASSERT(timeout >= 0); @@ -841,6 +846,9 @@ void Http2Server::sendResponse(quint32 streamID, bool emptyBody) QString("%1").arg(responseBody.size()).toLatin1())); } + if (!contentEncoding.isEmpty()) + header.push_back(HPack::HeaderField("content-encoding", contentEncoding)); + HPack::BitOStream ostream(writer.outboundFrame().buffer); const bool result = encoder.encodeResponse(ostream, header); Q_ASSERT(result); diff --git a/tests/auto/network/access/http2/http2srv.h b/tests/auto/network/access/http2/http2srv.h index 3105684d59..baf0155988 100644 --- a/tests/auto/network/access/http2/http2srv.h +++ b/tests/auto/network/access/http2/http2srv.h @@ -86,6 +86,8 @@ public: // 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); void emulateGOAWAY(int timeout); void redirectOpenStream(quint16 targetPort); @@ -211,6 +213,8 @@ private: bool redirectSent = false; quint16 targetPort = 0; QAtomicInt interrupted; + + QByteArray contentEncoding; 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 a92c948ade..5a250c6226 100644 --- a/tests/auto/network/access/http2/tst_http2.cpp +++ b/tests/auto/network/access/http2/tst_http2.cpp @@ -111,6 +111,9 @@ private slots: void connectToHost(); void maxFrameSize(); + void contentEncoding_data(); + void contentEncoding(); + protected slots: // Slots to listen to our in-process server: void serverStarted(quint16 port); @@ -767,6 +770,109 @@ void tst_Http2::maxFrameSize() QVERIFY(serverGotSettingsACK); } +void tst_Http2::contentEncoding_data() +{ + QTest::addColumn("encoding"); + QTest::addColumn("body"); + QTest::addColumn("expected"); + QTest::addColumn("h2Attribute"); + QTest::addColumn("connectionType"); + + struct ContentEncodingData + { + ContentEncodingData(QByteArray &&ce, QByteArray &&body, QByteArray &&ex) + : contentEncoding(ce), body(body), expected(ex) + { + } + QByteArray contentEncoding; + QByteArray body; + QByteArray expected; + }; + + QVector contentEncodingData; + contentEncodingData.emplace_back( + "gzip", QByteArray::fromBase64("H4sIAAAAAAAAA8tIzcnJVyjPL8pJAQCFEUoNCwAAAA=="), + "hello world"); + contentEncodingData.emplace_back( + "deflate", QByteArray::fromBase64("eJzLSM3JyVcozy/KSQEAGgsEXQ=="), "hello world"); + + // Loop through and add the data... + for (const auto &data : contentEncodingData) { + const char *name = data.contentEncoding.data(); + QTest::addRow("%s-h2c-upgrade", name) + << data.contentEncoding << data.body << data.expected + << QNetworkRequest::Http2AllowedAttribute << H2Type::h2c; + QTest::addRow("%s-h2c-direct", name) + << data.contentEncoding << data.body << data.expected + << QNetworkRequest::Http2DirectAttribute << H2Type::h2cDirect; + + if (!clearTextHTTP2) + QTest::addRow("%s-h2-ALPN", name) + << data.contentEncoding << data.body << data.expected + << QNetworkRequest::Http2AllowedAttribute << H2Type::h2Alpn; + +#if QT_CONFIG(ssl) + QTest::addRow("%s-h2-direct", name) + << data.contentEncoding << data.body << data.expected + << QNetworkRequest::Http2DirectAttribute << H2Type::h2Direct; +#endif + } +} + +void tst_Http2::contentEncoding() +{ + 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", QByteArray("1")); + auto envRollback = qScopeGuard([]() { qunsetenv("QT_SSL_USE_TEMPORARY_KEYCHAIN"); }); +#endif + + QFETCH(H2Type, connectionType); + + ServerPtr targetServer(newServer(defaultServerSettings, connectionType)); + QFETCH(QByteArray, body); + targetServer->setResponseBody(body); + QFETCH(QByteArray, encoding); + targetServer->setContentEncoding(encoding); + + QMetaObject::invokeMethod(targetServer.data(), "startServer", Qt::QueuedConnection); + runEventLoop(); + + QVERIFY(serverPort != 0); + + nRequests = 1; + + auto url = requestUrl(connectionType); + url.setPath("/index.html"); + + QNetworkRequest request(url); + QFETCH(const QNetworkRequest::Attribute, h2Attribute); + request.setAttribute(h2Attribute, QVariant(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 + + QVERIFY(nRequests == 0); + QVERIFY(prefaceOK); + QVERIFY(serverGotSettingsACK); + + QCOMPARE(reply->error(), QNetworkReply::NoError); + QVERIFY(reply->isFinished()); + QTEST(reply->readAll(), "expected"); +} + void tst_Http2::serverStarted(quint16 port) { serverPort = port; diff --git a/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp b/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp index 915926e488..2f1e97f853 100644 --- a/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp +++ b/tests/auto/network/access/qnetworkreply/tst_qnetworkreply.cpp @@ -503,6 +503,10 @@ private Q_SLOTS: void getWithTimeout(); void postWithTimeout(); + + void contentEncoding_data(); + void contentEncoding(); + // NOTE: This test must be last! void parentingRepliesToTheApp(); private: @@ -9175,6 +9179,57 @@ void tst_QNetworkReply::postWithTimeout() manager.setTransferTimeout(0); } +void tst_QNetworkReply::contentEncoding_data() +{ + QTest::addColumn("encoding"); + QTest::addColumn("body"); + QTest::addColumn("expected"); + + QTest::newRow("gzip-hello-world") + << QByteArray("gzip") + << QByteArray::fromBase64("H4sIAAAAAAAAA8tIzcnJVyjPL8pJAQCFEUoNCwAAAA==") + << QByteArray("hello world"); + QTest::newRow("deflate-hello-world") + << QByteArray("deflate") << QByteArray::fromBase64("eJzLSM3JyVcozy/KSQEAGgsEXQ==") + << QByteArray("hello world"); +} + +void tst_QNetworkReply::contentEncoding() +{ + QFETCH(QByteArray, encoding); + QFETCH(QByteArray, body); + QString header("HTTP/1.0 200 OK\r\nContent-Encoding: %1\r\nContent-Length: %2\r\n\r\n"); + header = header.arg(encoding, QString::number(body.size())); + + MiniHttpServer server(header.toLatin1() + body); + + QNetworkRequest request(QUrl("http://localhost:" + QString::number(server.serverPort()))); + QNetworkReplyPtr reply(manager.get(request)); + + QVERIFY2(waitForFinish(reply) == Success, msgWaitForFinished(reply)); + QCOMPARE(reply->error(), QNetworkReply::NoError); + + { + // Check that we included the content encoding method in our Accept-Encoding header + const QByteArray &receivedData = server.receivedData; + int start = receivedData.indexOf("Accept-Encoding"); + QVERIFY(start != -1); + int end = receivedData.indexOf("\r\n", start); + QVERIFY(end != -1); + QByteArray acceptedEncoding = receivedData.mid(start, end - start); + acceptedEncoding = acceptedEncoding.mid(acceptedEncoding.indexOf(':') + 1).trimmed(); + QByteArrayList list = acceptedEncoding.split(','); + for (QByteArray &encoding : list) + encoding = encoding.trimmed(); + QVERIFY2(list.contains(encoding), acceptedEncoding.data()); + } + + QFETCH(QByteArray, expected); + + QCOMPARE(reply->bytesAvailable(), expected.size()); + QCOMPARE(reply->readAll(), expected); +} + // NOTE: This test must be last testcase in tst_qnetworkreply! void tst_QNetworkReply::parentingRepliesToTheApp() {