Revision control
Copy as Markdown
// 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>
#include <QScopeGuard>
#include <QRandomGenerator>
#include <QSignalSpy>
#include "http2srv.h"
#include <QtNetwork/private/qhttpnetworkconnection_p.h>
#include <QtNetwork/private/qhttpnetworkreply_p.h>
#include <QtNetwork/private/http2protocol_p.h>
#include <QtNetwork/private/qhttp2protocolhandler_p.h>
#include <QtNetwork/qnetworkaccessmanager.h>
#include <QtNetwork/qhttp2configuration.h>
#include <QtNetwork/private/qhttp2configuration_p.h>
#include <QtNetwork/qnetworkrequest.h>
#include <QtNetwork/qnetworkreply.h>
#if QT_CONFIG(ssl)
#include <QtNetwork/qsslsocket.h>
#endif
#include <QtCore/private/qnoncontiguousbytedevice_p.h>
#include <QtCore/qsemaphore.h>
#include <QtCore/qglobal.h>
#include <QtCore/qobject.h>
#include <QtCore/qthread.h>
#include <QtCore/qurl.h>
#include <QtCore/qset.h>
#include <cstdlib>
#include <memory>
#include <string>
#include <QtTest/private/qemulationdetector_p.h>
Q_DECLARE_METATYPE(H2Type)
Q_DECLARE_METATYPE(QNetworkRequest::Attribute)
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
using namespace std::chrono_literals;
QHttp2Configuration qt_defaultH2Configuration()
{
QHttp2Configuration config;
config.setStreamReceiveWindowSize(Http2::qtDefaultStreamReceiveWindowSize);
config.setSessionReceiveWindowSize(Http2::maxSessionReceiveWindowSize);
config.setServerPushEnabled(false);
return config;
}
RawSettings qt_H2ConfigurationToSettings(const QHttp2Configuration &config = qt_defaultH2Configuration())
{
RawSettings settings;
settings[Http2::Settings::ENABLE_PUSH_ID] = config.serverPushEnabled();
settings[Http2::Settings::INITIAL_WINDOW_SIZE_ID] = config.streamReceiveWindowSize();
if (config.maxFrameSize() != Http2::minPayloadLimit)
settings[Http2::Settings::MAX_FRAME_SIZE_ID] = config.maxFrameSize();
if (const quint32 maxHeaderListSize =
QHttp2ConfigurationPrivate::get(config)->maxHeaderListSize;
maxHeaderListSize != (std::numeric_limits<quint32>::max)())
settings[Http2::Settings::MAX_HEADER_LIST_SIZE_ID] = maxHeaderListSize;
return settings;
}
class tst_Http2 : public QObject
{
Q_OBJECT
public:
tst_Http2();
~tst_Http2();
public slots:
void init();
void cleanup();
private slots:
// Tests:
void defaultQnamHttp2Configuration();
void singleRequest_data();
void singleRequest();
void informationalRequest_data();
void informationalRequest();
void multipleRequests();
void flowControlClientSide();
void flowControlServerSide();
void pushPromise();
void goaway_data();
void goaway();
void goawayNoErrorContentReSend();
void goawayDisconnectQueuedRequest();
void earlyResponse();
void earlyError();
void abortReply();
void abortReplyThenReceiveHeaders();
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 ntlmAuthenticateChallengeEmitsFinished();
void h2cAllowedAttribute_data();
void h2cAllowedAttribute();
void redirect_data();
void redirect();
void trailingHEADERS();
void dataFrameAfterEndStream();
void duplicateRequestsWithAborts();
void abortOnEncrypted();
void limitedConcurrentStreamsAllowed();
void maxHeaderTableSize();
protected slots:
// Slots to listen to our in-process server:
void serverStarted(quint16 port);
void clientPrefaceOK();
void clientPrefaceError();
void serverSettingsAcked();
void invalidFrame();
void invalidRequest(quint32 streamID);
void decompressionFailed(quint32 streamID);
void receivedRequest(quint32 streamID);
void receivedData(quint32 streamID);
void windowUpdated(quint32 streamID);
void replyFinished();
void replyFinishedWithError();
private:
std::function<void()> m_temporaryKeyChainRollback;
[[nodiscard]] std::function<void()> useTemporaryKeychain()
{
#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.
auto value = qEnvironmentVariable("QT_SSL_USE_TEMPORARY_KEYCHAIN");
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", "1");
auto envRollback = [value](){
if (value.isEmpty())
qunsetenv("QT_SSL_USE_TEMPORARY_KEYCHAIN");
else
qputenv("QT_SSL_USE_TEMPORARY_KEYCHAIN", value.toUtf8());
};
return envRollback;
#else
// avoid maybe-unused warnings from callers
return {};
#endif // QT_CONFIG(securetransport)
}
void clearHTTP2State();
// Run event for 'ms' milliseconds.
// The default value '5000' is enough for
// small payload.
void runEventLoop(int ms = 5000);
void stopEventLoop();
Http2Server *newServer(const RawSettings &serverSettings, H2Type connectionType,
const RawSettings &clientSettings = qt_H2ConfigurationToSettings());
// Send a get or post request, depending on a payload (empty or not).
void sendRequest(int streamNumber,
QNetworkRequest::Priority priority = QNetworkRequest::NormalPriority,
const QByteArray &payload = QByteArray(),
const QHttp2Configuration &clientConfiguration = qt_defaultH2Configuration());
QUrl requestUrl(H2Type connnectionType) const;
quint16 serverPort = 0;
QThread *workerThread = nullptr;
std::unique_ptr<QNetworkAccessManager> manager;
QTestEventLoop eventLoop;
int nRequests = 0;
int nSentRequests = 0;
int windowUpdates = 0;
bool prefaceOK = false;
bool serverGotSettingsACK = false;
bool POSTResponseHEADOnly = true;
static const RawSettings defaultServerSettings;
};
#define STOP_ON_FAILURE \
if (QTest::currentTestFailed()) \
return;
const RawSettings tst_Http2::defaultServerSettings{{Http2::Settings::MAX_CONCURRENT_STREAMS_ID, 100}};
namespace {
// Our server lives/works on a different thread so we invoke its 'deleteLater'
// instead of simple 'delete'.
struct ServerDeleter
{
void operator()(Http2Server *srv)
{
if (srv) {
srv->stopSendingDATAFrames();
QMetaObject::invokeMethod(srv, "deleteLater", Qt::QueuedConnection);
}
}
};
bool clearTextHTTP2 = false;
using ServerPtr = std::unique_ptr<Http2Server, ServerDeleter>;
H2Type defaultConnectionType()
{
return clearTextHTTP2 ? H2Type::h2c : H2Type::h2Alpn;
}
} // unnamed namespace
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();
}
tst_Http2::~tst_Http2()
{
workerThread->quit();
workerThread->wait(5000);
if (workerThread->isFinished()) {
delete workerThread;
} else {
connect(workerThread, &QThread::finished,
workerThread, &QThread::deleteLater);
}
}
void tst_Http2::init()
{
manager.reset(new QNetworkAccessManager);
m_temporaryKeyChainRollback = useTemporaryKeychain();
}
void tst_Http2::cleanup()
{
if (m_temporaryKeyChainRollback)
m_temporaryKeyChainRollback();
m_temporaryKeyChainRollback = {};
}
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");
QTest::addColumn<H2Type>("connectionType");
// 'Clear text' that should always work, either via the protocol upgrade
// or as direct.
QTest::addRow("h2c-upgrade") << QNetworkRequest::Http2AllowedAttribute << H2Type::h2c;
QTest::addRow("h2c-direct") << QNetworkRequest::Http2DirectAttribute << H2Type::h2cDirect;
if (!clearTextHTTP2) {
// Qt with TLS where TLS-backend supports ALPN.
QTest::addRow("h2-ALPN") << QNetworkRequest::Http2AllowedAttribute << H2Type::h2Alpn;
}
#if QT_CONFIG(ssl)
if (QSslSocket::supportsSsl())
QTest::addRow("h2-direct") << QNetworkRequest::Http2DirectAttribute << H2Type::h2Direct;
#endif
}
void tst_Http2::singleRequest()
{
clearHTTP2State();
serverPort = 0;
nRequests = 1;
QFETCH(const H2Type, connectionType);
ServerPtr srv(newServer(defaultServerSettings, connectionType));
QMetaObject::invokeMethod(srv.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
QVERIFY(serverPort != 0);
auto url = requestUrl(connectionType);
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.get(), "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();
serverPort = 0;
nRequests = 10;
ServerPtr srv(newServer(defaultServerSettings, defaultConnectionType()));
QMetaObject::invokeMethod(srv.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
QVERIFY(serverPort != 0);
// Just to make the order a bit more interesting
// we'll index this randomly:
const QNetworkRequest::Priority priorities[] = {
QNetworkRequest::HighPriority,
QNetworkRequest::NormalPriority,
QNetworkRequest::LowPriority
};
for (int i = 0; i < nRequests; ++i)
sendRequest(i, priorities[QRandomGenerator::global()->bounded(3)]);
runEventLoop();
STOP_ON_FAILURE
QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
}
void tst_Http2::flowControlClientSide()
{
// Create a server but impose limits:
// 1. Small client receive windows so server's responses cause client
// streams to suspend and protocol handler has to send WINDOW_UPDATE
// frames.
// 2. Few concurrent streams supported by the server, to test protocol
// handler in the client can suspend and then resume streams.
using namespace Http2;
clearHTTP2State();
serverPort = 0;
nRequests = 10;
windowUpdates = 0;
QHttp2Configuration params;
// A small window size for a session, and even a smaller one per stream -
// this will result in WINDOW_UPDATE frames both on connection stream and
// per stream.
params.setSessionReceiveWindowSize(Http2::defaultSessionWindowSize * 5);
params.setStreamReceiveWindowSize(Http2::defaultSessionWindowSize);
const RawSettings serverSettings = {{Settings::MAX_CONCURRENT_STREAMS_ID, quint32(3)}};
ServerPtr srv(newServer(serverSettings, defaultConnectionType(), qt_H2ConfigurationToSettings(params)));
const QByteArray respond(int(Http2::defaultSessionWindowSize * 10), 'x');
srv->setResponseBody(respond);
QMetaObject::invokeMethod(srv.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
QVERIFY(serverPort != 0);
for (int i = 0; i < nRequests; ++i)
sendRequest(i, QNetworkRequest::NormalPriority, {}, params);
runEventLoop(120000);
STOP_ON_FAILURE
QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
QVERIFY(windowUpdates > 0);
}
void tst_Http2::flowControlServerSide()
{
// Quite aggressive test:
// low MAX_FRAME_SIZE forces a lot of small DATA frames,
// payload size exceedes stream/session RECV window sizes
// so that our implementation should deal with WINDOW_UPDATE
// on a session/stream level correctly + resume/suspend streams
// to let all replies finish without any error.
using namespace Http2;
if (QTestPrivate::isRunningArmOnX86())
QSKIP("Test is too slow to run on emulator");
clearHTTP2State();
serverPort = 0;
nRequests = 10;
const RawSettings serverSettings = {{Settings::MAX_CONCURRENT_STREAMS_ID, 7}};
ServerPtr srv(newServer(serverSettings, defaultConnectionType()));
const QByteArray payload(int(Http2::defaultSessionWindowSize * 500), 'x');
QMetaObject::invokeMethod(srv.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
QVERIFY(serverPort != 0);
for (int i = 0; i < nRequests; ++i)
sendRequest(i, QNetworkRequest::NormalPriority, payload);
runEventLoop(120000);
STOP_ON_FAILURE
QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
}
void tst_Http2::pushPromise()
{
// We will first send some request, the server should reply and also emulate
// PUSH_PROMISE sending us another response as promised.
using namespace Http2;
clearHTTP2State();
serverPort = 0;
nRequests = 1;
QHttp2Configuration params;
// Defaults are good, except ENABLE_PUSH:
params.setServerPushEnabled(true);
ServerPtr srv(newServer(defaultServerSettings, defaultConnectionType(), qt_H2ConfigurationToSettings(params)));
srv->enablePushPromise(true, QByteArray("/script.js"));
QMetaObject::invokeMethod(srv.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
QVERIFY(serverPort != 0);
auto url = requestUrl(defaultConnectionType());
url.setPath("/index.html");
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, QVariant(true));
request.setHttp2Configuration(params);
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());
// Now, the most interesting part!
nSentRequests = 0;
nRequests = 1;
// Create an additional request (let's say, we parsed reply and realized we
// need another resource):
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);
reply->ignoreSslErrors();
runEventLoop();
// Let's check that NO request was actually made:
QCOMPARE(nSentRequests, 0);
// Decreased by replyFinished():
QCOMPARE(nRequests, 0);
QCOMPARE(reply->error(), QNetworkReply::NoError);
QVERIFY(reply->isFinished());
}
void tst_Http2::goaway_data()
{
// For now we test only basic things in two very simple scenarios:
// - server sends GOAWAY immediately or
// - server waits for some time (enough for ur to init several streams on a
// client side); then suddenly it replies with GOAWAY, never processing any
// request.
if (clearTextHTTP2)
QSKIP("This test requires TLS with ALPN to work");
QTest::addColumn<int>("responseTimeoutMS");
QTest::addColumn<int>("goawayCode");
QTest::addColumn<QNetworkReply::NetworkError>("expectedError");
QTest::newRow("ImmediateGOAWAY") << 0 << int(Http2::INTERNAL_ERROR) << QNetworkReply::InternalServerError;
QTest::newRow("DelayedGOAWAY") << 1000 << int(Http2::INTERNAL_ERROR) << QNetworkReply::InternalServerError;
QTest::newRow("Graceful") << 0 << int(Http2::HTTP2_NO_ERROR) << QNetworkReply::ContentReSendError;
QTest::newRow("Unknown") << 0 << 500000 << QNetworkReply::ProtocolFailure;
}
void tst_Http2::goaway()
{
using namespace Http2;
QFETCH(const int, responseTimeoutMS);
QFETCH(const int, goawayCode);
QFETCH(const QNetworkReply::NetworkError, expectedError);
clearHTTP2State();
serverPort = 0;
nRequests = 3;
ServerPtr srv(newServer(defaultServerSettings, defaultConnectionType()));
srv->emulateGOAWAY(goawayCode, responseTimeoutMS);
QMetaObject::invokeMethod(srv.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
QVERIFY(serverPort != 0);
auto url = requestUrl(defaultConnectionType());
// We have to store these replies, so that we can check errors later.
std::vector<QNetworkReply *> replies(nRequests);
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);
connect(replies[i], &QNetworkReply::errorOccurred, this, &tst_Http2::replyFinishedWithError);
// Since we're using self-signed certificates, ignore SSL errors:
replies[i]->ignoreSslErrors();
}
runEventLoop(5000 + responseTimeoutMS);
STOP_ON_FAILURE
// No request processed, no 'replyFinished' slot calls:
QCOMPARE(nRequests, 0);
for (const auto &reply : replies)
QCOMPARE(reply->error(), expectedError);
// Our server did not bother to send anything except a single GOAWAY frame:
QVERIFY(!prefaceOK);
QVERIFY(!serverGotSettingsACK);
}
void tst_Http2::goawayNoErrorContentReSend()
{
// QTBUG-147237: when a server sends a graceful GOAWAY (NO_ERROR) with lastStreamID
// that is less than a stream we already started, the stream was not processed by the
// server and is safe to resend. The reply should carry ContentReSendError, not
// RemoteHostClosedError.
clearHTTP2State();
serverPort = 0;
nRequests = 1;
ServerPtr srv(newServer(defaultServerSettings, defaultConnectionType()));
// The 500ms delay gives the client time to open stream 1 before the GOAWAY arrives.
srv->emulateGOAWAY(int(Http2::HTTP2_NO_ERROR), 500);
QMetaObject::invokeMethod(srv.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
QVERIFY(serverPort != 0);
auto url = requestUrl(defaultConnectionType());
url.setPath("/0"_L1);
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
std::unique_ptr<QNetworkReply> reply(manager->get(request));
reply->ignoreSslErrors();
connect(reply.get(), &QNetworkReply::errorOccurred, this, &tst_Http2::replyFinishedWithError);
runEventLoop(5000);
STOP_ON_FAILURE
QCOMPARE(reply->error(), QNetworkReply::ContentReSendError);
}
void tst_Http2::goawayDisconnectQueuedRequest()
{
// QTBUG-147237: a request queued while the channel is reconnecting after a graceful
// GOAWAY + TCP disconnect should receive ContentReSendError rather than hanging until
// a transferTimeout fires.
clearHTTP2State();
serverPort = 0;
nRequests = 1;
ServerPtr srv(newServer(defaultServerSettings, defaultConnectionType()));
// The 500ms delay gives the client time to open stream 1 before the GOAWAY arrives.
srv->emulateGOAWAY(int(Http2::HTTP2_NO_ERROR), 500, true);
QMetaObject::invokeMethod(srv.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
QCOMPARE_NE(serverPort, quint16(0));
auto makeRequest = [this](int index) {
auto url = requestUrl(defaultConnectionType());
url.setPath(u"/%1"_s.arg(index));
QNetworkRequest req(url);
req.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
req.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
// A request queued on a going-away channel that never reconnects is never sent at
// all, so bound the wait instead of letting the test block until it is killed.
req.setTransferTimeout(3s);
return req;
};
// First request: establishes the H2 session and receives ContentReSendError because
// the server sends GOAWAY with lastStreamID 0, so stream 1 was never processed.
std::unique_ptr<QNetworkReply> initial(manager->get(makeRequest(0)));
initial->ignoreSslErrors();
// Queue a second request once the first one has errored.
std::unique_ptr<QNetworkReply> retry;
connect(initial.get(), &QNetworkReply::errorOccurred, this,
[&](QNetworkReply::NetworkError) {
retry.reset(manager->get(makeRequest(1)));
retry->ignoreSslErrors();
connect(retry.get(), &QNetworkReply::errorOccurred, this,
[this](QNetworkReply::NetworkError) {
--nRequests;
if (!nRequests)
stopEventLoop();
});
});
runEventLoop(10000);
STOP_ON_FAILURE
// Stream 1 had ID > lastStreamID 0, so the server never processed it.
QCOMPARE(initial->error(), QNetworkReply::ContentReSendError);
// The retry was held while the going-away connection closed, then retried on a new
// connection. The server stopped accepting new connections, so the reconnect fails.
QList<QNetworkReply::NetworkError> expectedErrors = { QNetworkReply::ConnectionRefusedError };
#ifdef Q_OS_WIN
// Windows does not promptly refuse a connection to a just-closed port, so instead of
// failing fast the reconnect stalls until the transfer timeout set above fires.
expectedErrors += QNetworkReply::TimeoutError;
#endif
QVERIFY2(expectedErrors.contains(retry->error()), qPrintable(retry->errorString()));
}
void tst_Http2::earlyResponse()
{
// In this test we'd like to verify client side can handle HEADERS frame while
// its stream is in 'open' state. To achieve this, we send a POST request
// with some payload, so that the client is first sending HEADERS and then
// DATA frames without END_STREAM flag set yet (thus the stream is in Stream::open
// state). Upon receiving the client's HEADERS frame our server ('redirector')
// immediately (without trying to read any DATA frames) responds with status
// code 308. The client should properly handle this.
clearHTTP2State();
serverPort = 0;
nRequests = 1;
ServerPtr targetServer(newServer(defaultServerSettings, defaultConnectionType()));
QMetaObject::invokeMethod(targetServer.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
QVERIFY(serverPort != 0);
const quint16 targetPort = serverPort;
serverPort = 0;
ServerPtr redirector(newServer(defaultServerSettings, defaultConnectionType()));
redirector->redirectOpenStream(targetPort);
QMetaObject::invokeMethod(redirector.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
QVERIFY(serverPort);
sendRequest(1, QNetworkRequest::NormalPriority, {1000000, Qt::Uninitialized});
runEventLoop();
STOP_ON_FAILURE
QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
}
/*
Have the server return an error before we are done writing out POST data,
of course we should not crash if this happens. It's not guaranteed to
reproduce, so we run the request a few times to try to make it happen.
This is a race-condition, so the test is written using QHttpNetworkConnection
to have more influence over the timing.
*/
void tst_Http2::earlyError()
{
clearHTTP2State();
serverPort = 0;
const auto serverConnectionType = defaultConnectionType() == H2Type::h2c ? H2Type::h2Direct
: H2Type::h2Alpn;
ServerPtr server(newServer(defaultServerSettings, serverConnectionType));
server->enableSendEarlyError(true);
QMetaObject::invokeMethod(server.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
QCOMPARE_NE(serverPort, 0);
// SETUP create QHttpNetworkConnection primed for http2 usage
const auto connectionType = serverConnectionType == H2Type::h2Direct
? QHttpNetworkConnection::ConnectionTypeHTTP2Direct
: QHttpNetworkConnection::ConnectionTypeHTTP2;
QHttpNetworkConnection connection(1, "127.0.0.1", serverPort, true, false, nullptr,
connectionType);
#if QT_CONFIG(ssl)
QSslConfiguration config = QSslConfiguration::defaultConfiguration();
config.setAllowedNextProtocols({"h2"});
connection.setSslConfiguration(config);
connection.ignoreSslErrors();
#endif
// SETUP manually setup the QHttpNetworkRequest
QHttpNetworkRequest req;
req.setSsl(true);
req.setHTTP2Allowed(true);
if (defaultConnectionType() == H2Type::h2c)
req.setH2cAllowed(true);
req.setOperation(QHttpNetworkRequest::Post);
req.setUrl(requestUrl(defaultConnectionType()));
// ^ All the above is set-up, the real code starts below v
// We need a sufficiently large payload so it can't be instantly transmitted
const QByteArray payload(1 * 1024 * 1024, 'a');
auto byteDevice = std::unique_ptr<QNonContiguousByteDevice>(
QNonContiguousByteDeviceFactory::create(payload));
req.setUploadByteDevice(byteDevice.get());
// Start sending the request. It needs to establish encryption so nothing
// happens right away (at time of writing...)
std::unique_ptr<QHttpNetworkReply> reply{connection.sendRequest(req)};
QVERIFY(reply);
QSemaphore sem;
int statusCode = 0;
QObject::connect(reply.get(), &QHttpNetworkReply::finished, reply.get(), [&](){
statusCode = reply->statusCode();
// Here we forcibly replicate what happens when we get into the bad
// state:
// 1. The reply is aborted & deleted, but was not removed from internal
// container.
reply.reset();
// 2. The byte-device is deleted afterwards, which would lead to
// use-after-free when we try to signal an error on the reply object.
byteDevice.reset();
// Let the main thread continue
sem.release();
});
using namespace std::chrono_literals;
QDeadlineTimer timer(5s);
while (!sem.tryAcquire() && !timer.hasExpired())
QCoreApplication::processEvents();
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
QVERIFY(!reply);
QCOMPARE(statusCode, 403);
QVERIFY(prefaceOK);
QTRY_VERIFY(serverGotSettingsACK);
}
/*
As above this test relies a bit on timing so we are
using QHttpNetworkRequest directly.
*/
void tst_Http2::abortReply()
{
clearHTTP2State();
serverPort = 0;
const auto serverConnectionType = defaultConnectionType() == H2Type::h2c ? H2Type::h2Direct
: H2Type::h2Alpn;
ServerPtr targetServer(newServer(defaultServerSettings, serverConnectionType));
QMetaObject::invokeMethod(targetServer.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
QVERIFY(serverPort != 0);
nRequests = 1;
// SETUP create QHttpNetworkConnection primed for http2 usage
const auto connectionType = serverConnectionType == H2Type::h2Direct
? QHttpNetworkConnection::ConnectionTypeHTTP2Direct
: QHttpNetworkConnection::ConnectionTypeHTTP2;
QHttpNetworkConnection connection(1, "127.0.0.1", serverPort, true, false, nullptr,
connectionType);
#if QT_CONFIG(ssl)
QSslConfiguration config = QSslConfiguration::defaultConfiguration();
config.setAllowedNextProtocols({"h2"});
connection.setSslConfiguration(config);
connection.ignoreSslErrors();
#endif
// SETUP manually setup the QHttpNetworkRequest
QHttpNetworkRequest req;
req.setSsl(true);
req.setHTTP2Allowed(true);
if (defaultConnectionType() == H2Type::h2c)
req.setH2cAllowed(true);
req.setOperation(QHttpNetworkRequest::Post);
req.setUrl(requestUrl(defaultConnectionType()));
// ^ All the above is set-up, the real code starts below v
std::unique_ptr<QHttpNetworkReply> reply{connection.sendRequest(req)};
QVERIFY(reply);
QSemaphore sem;
QObject::connect(reply.get(), &QHttpNetworkReply::requestSent, reply.get(), [&](){
reply.reset();
sem.release();
});
// failOnWarning doesn't work for qCritical, so we set this env-var:
const char envvar[] = "QT_FATAL_CRITICALS";
auto restore = qScopeGuard([envvar, prev = qgetenv(envvar)]() {
qputenv(envvar, prev);
});
qputenv(envvar, "1");
QTest::failOnWarning(QRegularExpression("HEADERS on invalid stream"));
QVERIFY(QTest::qWaitFor([&sem]() { return sem.tryAcquire(); }));
using namespace std::chrono_literals;
// Process some extra events in case they trigger an error:
QTest::qWait(100ms);
}
void tst_Http2::abortReplyThenReceiveHeaders()
{
clearHTTP2State();
serverPort = 0;
const auto serverConnectionType = defaultConnectionType() == H2Type::h2c ? H2Type::h2Direct
: H2Type::h2Alpn;
ServerPtr targetServer(newServer(defaultServerSettings, serverConnectionType));
QMetaObject::invokeMethod(targetServer.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
QVERIFY(serverPort != 0);
// SETUP create QHttpNetworkConnection primed for http2 usage
const auto connectionType = serverConnectionType == H2Type::h2Direct
? QHttpNetworkConnection::ConnectionTypeHTTP2Direct
: QHttpNetworkConnection::ConnectionTypeHTTP2;
QHttpNetworkConnection connection(1, "127.0.0.1", serverPort, true, false, nullptr,
connectionType);
#if QT_CONFIG(ssl)
QSslConfiguration config = QSslConfiguration::defaultConfiguration();
config.setAllowedNextProtocols({"h2"});
connection.setSslConfiguration(config);
connection.ignoreSslErrors();
#endif
// SETUP manually setup the QHttpNetworkRequest
QHttpNetworkRequest req;
req.setSsl(true);
req.setHTTP2Allowed(true);
if (defaultConnectionType() == H2Type::h2c)
req.setH2cAllowed(true);
req.setOperation(QHttpNetworkRequest::Get);
req.setUrl(requestUrl(defaultConnectionType()));
// ^ All the above is set-up, the real code starts below v
std::unique_ptr<QHttpNetworkReply> reply{connection.sendRequest(req)};
QVERIFY(reply);
QSemaphore sem;
connect(reply.get(), &QHttpNetworkReply::finished, reply.get(), [&sem] {
sem.release();
});
using namespace std::chrono_literals;
QDeadlineTimer timer(5s);
// We purposefully do not use any of the qWait* functions because we don't want to process
// deleteLater's yet
while (!sem.tryAcquire() && !timer.hasExpired())
QCoreApplication::processEvents();
QCOMPARE_GT(timer.remainingTime(), 0);
auto *handler = static_cast<QHttp2ProtocolHandler *>(
connection.channels()->protocolHandler.get());
QVERIFY(handler);
QCOMPARE(handler->requestReplyPairs.size(), 0);
// Ensure deleteLater() for the stream has executed before reply destruction.
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
QTest::qWait(50ms);
reply.reset();
}
void tst_Http2::connectToHost_data()
{
// The attribute to set on a new request:
QTest::addColumn<QNetworkRequest::Attribute>("requestAttribute");
// The corresponding (to the attribute above) connection type the
// server will use:
QTest::addColumn<H2Type>("connectionType");
#if QT_CONFIG(ssl)
if (QSslSocket::supportsSsl()) {
QTest::addRow("encrypted-h2-direct") << QNetworkRequest::Http2DirectAttribute << H2Type::h2Direct;
if (!clearTextHTTP2)
QTest::addRow("encrypted-h2-ALPN") << QNetworkRequest::Http2AllowedAttribute << H2Type::h2Alpn;
}
#endif // QT_CONFIG(ssl)
// This works for all configurations, tests 'preconnect-http' scheme:
// h2 with protocol upgrade is not working for now (the logic is a bit
// complicated there ...).
QTest::addRow("h2-direct") << QNetworkRequest::Http2DirectAttribute << H2Type::h2cDirect;
}
void tst_Http2::connectToHost()
{
// QNetworkAccessManager::connectToHostEncrypted() and connectToHost()
// creates a special request with 'preconnect-https' or 'preconnect-http'
// schemes. At the level of the protocol handler we are supposed to report
// these requests as finished and wait for the real requests. This test will
// connect to a server with the first reply 'finished' signal meaning we
// indeed connected. At this point we check that a client preface was not
// sent yet, and no response received. Then we send the second (the real)
// request and do our usual checks. Since our server closes its listening
// socket on the first incoming connection (would not accept a new one),
// the successful completion of the second requests also means we were able
// to find a cached connection and re-use it.
QFETCH(const QNetworkRequest::Attribute, requestAttribute);
QFETCH(const H2Type, connectionType);
clearHTTP2State();
serverPort = 0;
nRequests = 2;
ServerPtr targetServer(newServer(defaultServerSettings, connectionType));
#if QT_CONFIG(ssl)
if (QSslSocket::supportsSsl())
{
Q_ASSERT(!clearTextHTTP2 || connectionType != H2Type::h2Alpn);
} else
#endif
{
Q_ASSERT(connectionType == H2Type::h2c || connectionType == H2Type::h2cDirect);
Q_ASSERT(targetServer->isClearText());
}
QMetaObject::invokeMethod(targetServer.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
QVERIFY(serverPort != 0);
auto url = requestUrl(connectionType);
url.setPath("/index.html");
QNetworkReply *reply = nullptr;
// Here some mess with how we create this first reply:
#if QT_CONFIG(ssl)
if (!targetServer->isClearText()) {
// Let's emulate what QNetworkAccessManager::connectToHostEncrypted() does.
// Alas, we cannot use it directly, since it does not return the reply and
// also does not know the difference between H2 with ALPN or direct.
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:
reply->ignoreSslErrors();
} else
#endif // QT_CONFIG(ssl)
{
// Emulating what QNetworkAccessManager::connectToHost() does with
// additional information that it cannot provide (the attribute).
auto copyUrl = url;
copyUrl.setScheme(QLatin1String("preconnect-http"));
QNetworkRequest request(copyUrl);
request.setAttribute(QNetworkRequest::Http2CleartextAllowedAttribute, true);
request.setAttribute(requestAttribute, true);
reply = manager->get(request);
}
connect(reply, &QNetworkReply::finished, [this, reply]() {
--nRequests;
eventLoop.exitLoop();
QCOMPARE(reply->error(), QNetworkReply::NoError);
QVERIFY(reply->isFinished());
// Nothing received back:
QVERIFY(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isNull());
QCOMPARE(reply->readAll().size(), 0);
});
runEventLoop();
STOP_ON_FAILURE
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);
// Note, unlike the first request, when the connection is ecnrytped, we
// do not ignore TLS errors on this reply - we should re-use existing
// connection, there TLS errors were already ignored.
runEventLoop();
STOP_ON_FAILURE
QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
QCOMPARE(reply->error(), QNetworkReply::NoError);
QVERIFY(reply->isFinished());
}
void tst_Http2::maxFrameSize()
{
#if !QT_CONFIG(ssl)
QSKIP("TLS support is needed for this test");
#endif // QT_CONFIG(ssl)
// Here we test we send 'MAX_FRAME_SIZE' setting in our
// 'SETTINGS'. If done properly, our server will not chunk
// the payload into several DATA frames.
auto connectionType = H2Type::h2Alpn;
auto attribute = QNetworkRequest::Http2AllowedAttribute;
if (clearTextHTTP2) {
connectionType = H2Type::h2Direct;
attribute = QNetworkRequest::Http2DirectAttribute;
}
auto h2Config = qt_defaultH2Configuration();
h2Config.setMaxFrameSize(Http2::minPayloadLimit * 3);
serverPort = 0;
nRequests = 1;
ServerPtr srv(newServer(defaultServerSettings, connectionType,
qt_H2ConfigurationToSettings(h2Config)));
srv->setResponseBody(QByteArray(Http2::minPayloadLimit * 2, 'q'));
QMetaObject::invokeMethod(srv.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
QVERIFY(serverPort != 0);
const QSignalSpy frameCounter(srv.get(), &Http2Server::sendingData);
auto url = requestUrl(connectionType);
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);
QNetworkReply *reply = manager->get(request);
reply->ignoreSslErrors();
connect(reply, &QNetworkReply::finished, this, &tst_Http2::replyFinished);
runEventLoop();
STOP_ON_FAILURE
// Normally, with a 16kb limit, our server would split such
// a response into 3 'DATA' frames (16kb + 16kb + 0|END_STREAM).
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)
if (QSslSocket::supportsSsl()) {
QTest::addRow("h2-direct")
<< QNetworkRequest::Http2DirectAttribute << H2Type::h2Direct;
}
#endif
}
void tst_Http2::moreActivitySignals()
{
clearHTTP2State();
serverPort = 0;
QFETCH(H2Type, connectionType);
ServerPtr srv(newServer(defaultServerSettings, connectionType));
QMetaObject::invokeMethod(srv.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
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
QCOMPARE(nRequests, 0);
QVERIFY(prefaceOK);
QVERIFY(serverGotSettingsACK);
QVERIFY(reply->error() == QNetworkReply::NoError);
QVERIFY(reply->isFinished());
}
void tst_Http2::contentEncoding_data()
{
QTest::addColumn<QByteArray>("encoding");
QTest::addColumn<QByteArray>("body");
QTest::addColumn<QByteArray>("expected");
QTest::addColumn<QNetworkRequest::Attribute>("h2Attribute");
QTest::addColumn<H2Type>("connectionType");
struct ContentEncodingData
{
ContentEncodingData(QByteArray &&ce, QByteArray &&body, QByteArray &&ex)
: contentEncoding(ce), body(body), expected(ex)
{
}
QByteArray contentEncoding;
QByteArray body;
QByteArray expected;
};
QList<ContentEncodingData> contentEncodingData;
contentEncodingData.emplace_back(
"gzip", QByteArray::fromBase64("H4sIAAAAAAAAA8tIzcnJVyjPL8pJAQCFEUoNCwAAAA=="),
"hello world");
contentEncodingData.emplace_back(
"deflate", QByteArray::fromBase64("eJzLSM3JyVcozy/KSQEAGgsEXQ=="), "hello world");
#if QT_CONFIG(brotli)
contentEncodingData.emplace_back("br", QByteArray::fromBase64("DwWAaGVsbG8gd29ybGQD"),
"hello world");
#endif
#if QT_CONFIG(zstd)
contentEncodingData.emplace_back(
"zstd", QByteArray::fromBase64("KLUv/QRYWQAAaGVsbG8gd29ybGRoaR6y"), "hello world");
#endif
// 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)
if (QSslSocket::supportsSsl()) {
QTest::addRow("%s-h2-direct", name)
<< data.contentEncoding << data.body << data.expected
<< QNetworkRequest::Http2DirectAttribute << H2Type::h2Direct;
}
#endif
}
}
void tst_Http2::contentEncoding()
{
clearHTTP2State();
QFETCH(H2Type, connectionType);
ServerPtr targetServer(newServer(defaultServerSettings, connectionType));
QFETCH(QByteArray, body);
targetServer->setResponseBody(body);
QFETCH(QByteArray, encoding);
targetServer->setContentEncoding(encoding);
QMetaObject::invokeMethod(targetServer.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
QVERIFY(serverPort != 0);
nRequests = 1;
auto url = requestUrl(connectionType);
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);
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());
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.get(), "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");
auto reply = std::unique_ptr<QNetworkReply>(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 {
// finished() guarantees errorOccurred() was already delivered.
connect(reply.get(), &QNetworkReply::finished, this, &tst_Http2::replyFinishedWithError);
}
// 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.get(), "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");
auto reply = std::unique_ptr<QNetworkReply>(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);
});
// finished() guarantees errorOccurred() was already delivered.
connect(reply.get(), &QNetworkReply::finished, this, &tst_Http2::replyFinishedWithError);
// 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::ntlmAuthenticateChallengeEmitsFinished()
{
// QTBUG-143926: a server returning NTLM/Negotiate must still emit finished()
clearHTTP2State();
serverPort = 0;
if (defaultConnectionType() == H2Type::h2c)
QSKIP("This test requires TLS with ALPN to work");
ServerPtr targetServer(newServer(defaultServerSettings, defaultConnectionType()));
targetServer->setAuthenticationHeader("NTLM");
QMetaObject::invokeMethod(targetServer.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
QVERIFY(serverPort != 0);
nRequests = 1;
QUrl url = requestUrl(defaultConnectionType());
url.setPath("/index.html");
QNetworkRequest request(url);
auto reply = std::unique_ptr<QNetworkReply>(manager->get(request));
bool finishedReceived = false;
connect(reply.get(), &QNetworkReply::finished, reply.get(),
[&]() { finishedReceived = true; });
// finished() guarantees errorOccurred() was already delivered.
connect(reply.get(), &QNetworkReply::finished, this, &tst_Http2::replyFinishedWithError);
reply->ignoreSslErrors();
runEventLoop();
STOP_ON_FAILURE
QVERIFY2(reply->isFinished(), "QNetworkReply::finished must be emitted for NTLM challenges");
QCOMPARE(reply->error(), QNetworkReply::AuthenticationRequiredError);
QVERIFY(finishedReceived);
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.get(), "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"); });
auto reply = std::unique_ptr<QNetworkReply>(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.get(), "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"); });
auto reply = std::unique_ptr<QNetworkReply>(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.get(), "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::dataFrameAfterEndStream()
{
// A non-conformant server (observed with VK's "kittenx") may send a stray,
// empty DATA frame with END_STREAM after the stream was already closed by
// a previous END_STREAM. Per RFC 9113 6.1 we answer with RST_STREAM, but an
// already fully-received reply must not be turned into an error, and no data
// that was received must be dropped.
// See QHttp2ProtocolHandler::finishStream().
clearHTTP2State();
serverPort = 0;
const QByteArray responseBody = "The complete and correct response body.";
ServerPtr targetServer(newServer(defaultServerSettings, defaultConnectionType()));
targetServer->setResponseBody(responseBody);
targetServer->setSendRedundantEndStreamDATA(true);
QMetaObject::invokeMethod(targetServer.get(), "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);
QCOMPARE(reply->readAll(), responseBody);
QTRY_VERIFY(serverGotSettingsACK);
}
void tst_Http2::duplicateRequestsWithAborts()
{
clearHTTP2State();
serverPort = 0;
H2Type connectionType = H2Type::h2Direct;
ServerPtr targetServer(newServer(defaultServerSettings, connectionType));
QMetaObject::invokeMethod(targetServer.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
QVERIFY(serverPort != 0);
constexpr int ExpectedSuccessfulRequests = 1;
nRequests = ExpectedSuccessfulRequests;
const auto url = requestUrl(connectionType);
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::Http2DirectAttribute, 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::abortOnEncrypted()
{
#if !QT_CONFIG(ssl)
QSKIP("TLS support is needed for this test");
#else
if (!QSslSocket::supportsSsl())
QSKIP("TLS support is needed for this test");
clearHTTP2State();
serverPort = 0;
ServerPtr targetServer(newServer(defaultServerSettings, H2Type::h2Direct));
QMetaObject::invokeMethod(targetServer.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
nRequests = 1;
nSentRequests = 0;
const auto url = requestUrl(H2Type::h2Direct);
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::Http2DirectAttribute, true);
std::unique_ptr<QNetworkReply> reply{manager->get(request)};
reply->ignoreSslErrors();
connect(reply.get(), &QNetworkReply::encrypted, reply.get(), [reply = reply.get()](){
reply->abort();
});
connect(reply.get(), &QNetworkReply::errorOccurred, this, &tst_Http2::replyFinishedWithError);
runEventLoop();
STOP_ON_FAILURE
QCOMPARE(nRequests, 0);
QCOMPARE(reply->error(), QNetworkReply::OperationCanceledError);
const bool res = QTest::qWaitFor(
[this, server = targetServer.get()]() {
return serverGotSettingsACK || prefaceOK || nSentRequests > 0;
},
500);
QVERIFY(!res);
#endif // QT_CONFIG(ssl)
}
/*
While the standard heavily recommends allowing at _least_ 100 streams, let's
test how we cope with a very small number of streams allowed.
Basically we are just testing how we would handle the situation where we are
up against the limit of active streams, which should be well-behaved to
avoid having the server to close the connection.
*/
void tst_Http2::limitedConcurrentStreamsAllowed()
{
clearHTTP2State();
serverPort = 0;
H2Type connectionType = H2Type::h2Direct;
RawSettings oneConcurrentStream{ { Http2::Settings::MAX_CONCURRENT_STREAMS_ID, 1 } };
ServerPtr targetServer(newServer(oneConcurrentStream, connectionType));
QMetaObject::invokeMethod(targetServer.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
QVERIFY(serverPort != 0);
constexpr qint32 TotalRequests = 3;
nRequests = TotalRequests;
const auto url = requestUrl(connectionType);
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::Http2DirectAttribute, true);
qint32 finishedCount = 0;
qint32 errorCount = 0;
const auto onFinished = [&](QNetworkReply *reply) {
++finishedCount;
if (reply->error() == QNetworkReply::NoError)
replyFinished();
else
++errorCount;
};
std::vector<QNetworkReply *> replies;
for (qint32 i = 0; i < TotalRequests; ++i) {
auto *reply = replies.emplace_back(manager->get(request));
reply->ignoreSslErrors();
connect(reply, &QNetworkReply::finished, reply, [&,reply](){ onFinished(reply); });
}
runEventLoop();
STOP_ON_FAILURE
QCOMPARE(nRequests, 0);
QCOMPARE(errorCount, 0);
QCOMPARE(finishedCount, TotalRequests);
}
void tst_Http2::maxHeaderTableSize()
{
clearHTTP2State();
serverPort = 0;
H2Type connectionType = H2Type::h2Direct;
RawSettings maxHeaderTableSize{ { Http2::Settings::HEADER_TABLE_SIZE_ID, 0 } };
ServerPtr targetServer(newServer(maxHeaderTableSize, connectionType));
QMetaObject::invokeMethod(targetServer.get(), "startServer", Qt::QueuedConnection);
runEventLoop();
QVERIFY(serverPort != 0);
nRequests = 1;
const auto url = requestUrl(connectionType);
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::Http2DirectAttribute, true);
constexpr int extraRequests = 5;
std::array<std::unique_ptr<QNetworkReply>, extraRequests> replies;
for (qint32 i = 0; i < 1 + extraRequests; ++i) {
for (qint32 j = 0; j < 100; ++j) {
request.setRawHeader("x-test" + QByteArray::number(j),
"Hello World" + QByteArray::number(i));
}
std::unique_ptr<QNetworkReply> reply{ manager->get(request) };
reply->ignoreSslErrors();
connect(reply.get(), &QNetworkReply::finished, this, &tst_Http2::replyFinished);
if (i == 0) {
runEventLoop();
STOP_ON_FAILURE
QCOMPARE(reply->error(), QNetworkReply::NoError);
nRequests = extraRequests;
} else {
replies[i - 1] = std::move(reply);
}
}
runEventLoop();
STOP_ON_FAILURE
QCOMPARE(nRequests, 0);
for (const auto &reply : replies)
QCOMPARE(reply->error(), QNetworkReply::NoError);
}
void tst_Http2::serverStarted(quint16 port)
{
serverPort = port;
stopEventLoop();
}
void tst_Http2::clearHTTP2State()
{
windowUpdates = 0;
prefaceOK = false;
serverGotSettingsACK = false;
POSTResponseHEADOnly = true;
}
void tst_Http2::runEventLoop(int ms)
{
eventLoop.enterLoopMSecs(ms);
}
void tst_Http2::stopEventLoop()
{
eventLoop.exitLoop();
}
Http2Server *tst_Http2::newServer(const RawSettings &serverSettings, H2Type connectionType,
const RawSettings &clientSettings)
{
using namespace Http2;
auto srv = new Http2Server(connectionType, serverSettings, clientSettings);
using Srv = Http2Server;
using Cl = tst_Http2;
connect(srv, &Srv::serverStarted, this, &Cl::serverStarted);
connect(srv, &Srv::clientPrefaceOK, this, &Cl::clientPrefaceOK);
connect(srv, &Srv::clientPrefaceError, this, &Cl::clientPrefaceError);
connect(srv, &Srv::serverSettingsAcked, this, &Cl::serverSettingsAcked);
connect(srv, &Srv::invalidFrame, this, &Cl::invalidFrame);
connect(srv, &Srv::invalidRequest, this, &Cl::invalidRequest);
connect(srv, &Srv::receivedRequest, this, &Cl::receivedRequest);
connect(srv, &Srv::receivedData, this, &Cl::receivedData);
connect(srv, &Srv::windowUpdate, this, &Cl::windowUpdated);
srv->moveToThread(workerThread);
return srv;
}
void tst_Http2::sendRequest(int streamNumber,
QNetworkRequest::Priority priority,
const QByteArray &payload,
const QHttp2Configuration &h2Config)
{
auto url = requestUrl(defaultConnectionType());
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"));
request.setPriority(priority);
request.setHttp2Configuration(h2Config);
QNetworkReply *reply = nullptr;
if (payload.size())
reply = manager->post(request, payload);
else
reply = manager->get(request);
reply->ignoreSslErrors();
connect(reply, &QNetworkReply::finished, this, &tst_Http2::replyFinished);
}
QUrl tst_Http2::requestUrl(H2Type connectionType) const
{
#if !QT_CONFIG(ssl)
Q_ASSERT(connectionType != H2Type::h2Alpn && connectionType != H2Type::h2Direct);
#endif
url.setPort(serverPort);
// Clear text may mean no-TLS-at-all or crappy-TLS-without-ALPN.
switch (connectionType) {
case H2Type::h2Alpn:
case H2Type::h2Direct:
url.setScheme(QStringLiteral("https"));
break;
case H2Type::h2c:
case H2Type::h2cDirect:
url.setScheme(QStringLiteral("http"));
break;
}
return url;
}
void tst_Http2::clientPrefaceOK()
{
prefaceOK = true;
}
void tst_Http2::clientPrefaceError()
{
prefaceOK = false;
}
void tst_Http2::serverSettingsAcked()
{
serverGotSettingsACK = true;
if (!nRequests)
stopEventLoop();
}
void tst_Http2::invalidFrame()
{
}
void tst_Http2::invalidRequest(quint32 streamID)
{
Q_UNUSED(streamID);
}
void tst_Http2::decompressionFailed(quint32 streamID)
{
Q_UNUSED(streamID);
}
void tst_Http2::receivedRequest(quint32 streamID)
{
++nSentRequests;
qDebug() << " server got a request on stream" << streamID;
Http2Server *srv = qobject_cast<Http2Server *>(sender());
Q_ASSERT(srv);
QMetaObject::invokeMethod(srv, "sendResponse", Qt::QueuedConnection,
Q_ARG(quint32, streamID),
Q_ARG(bool, false /*non-empty body*/));
}
void tst_Http2::receivedData(quint32 streamID)
{
qDebug() << " server got a 'POST' request on stream" << streamID;
Http2Server *srv = qobject_cast<Http2Server *>(sender());
Q_ASSERT(srv);
QMetaObject::invokeMethod(srv, "sendResponse", Qt::QueuedConnection,
Q_ARG(quint32, streamID),
Q_ARG(bool, POSTResponseHEADOnly /*true = HEADERS only*/));
}
void tst_Http2::windowUpdated(quint32 streamID)
{
Q_UNUSED(streamID);
++windowUpdates;
}
void tst_Http2::replyFinished()
{
QVERIFY(nRequests);
if (const auto reply = qobject_cast<QNetworkReply *>(sender())) {
if (reply->error() != QNetworkReply::NoError)
stopEventLoop();
QCOMPARE(reply->error(), QNetworkReply::NoError);
const QVariant http2Used(reply->attribute(QNetworkRequest::Http2WasUsedAttribute));
if (!http2Used.isValid() || !http2Used.toBool())
stopEventLoop();
QVERIFY(http2Used.isValid());
QVERIFY(http2Used.toBool());
const QVariant code(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute));
if (!code.isValid() || !code.canConvert<int>() || code.value<int>() != 200)
stopEventLoop();
QVERIFY(code.isValid());
QVERIFY(code.canConvert<int>());
QCOMPARE(code.value<int>(), 200);
}
--nRequests;
if (!nRequests && serverGotSettingsACK)
stopEventLoop();
}
void tst_Http2::replyFinishedWithError()
{
QVERIFY(nRequests);
if (const auto reply = qobject_cast<QNetworkReply *>(sender())) {
// For now this is a 'generic' code, it just verifies some error was
// reported without testing its type.
if (reply->error() == QNetworkReply::NoError)
stopEventLoop();
QVERIFY(reply->error() != QNetworkReply::NoError);
}
--nRequests;
if (!nRequests)
stopEventLoop();
}
QT_END_NAMESPACE
QTEST_MAIN(tst_Http2)
#include "tst_http2.moc"