/**************************************************************************** ** ** 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$ ** ****************************************************************************/ #include #include "http2srv.h" #include #include #include #include #include #include #include #ifndef QT_NO_SSL #ifndef QT_NO_OPENSSL #include #endif // NO_OPENSSL #endif // NO_SSL #include #include #if !defined(QT_NO_OPENSSL) && OPENSSL_VERSION_NUMBER >= 0x10002000L && !defined(OPENSSL_NO_TLSEXT) // 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 QT_BEGIN_NAMESPACE class tst_Http2 : public QObject { Q_OBJECT public: tst_Http2(); ~tst_Http2(); private slots: // Tests: void singleRequest(); void multipleRequests(); void flowControlClientSide(); void flowControlServerSide(); void pushPromise(); void goaway_data(); void goaway(); 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: 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 Http2Settings &serverSettings, const Http2Settings &clientSettings = defaultClientSettings); // 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()); QUrl requestUrl() const; quint16 serverPort = 0; QThread *workerThread = nullptr; QNetworkAccessManager manager; QEventLoop eventLoop; QTimer timer; int nRequests = 0; int nSentRequests = 0; int windowUpdates = 0; bool prefaceOK = false; bool serverGotSettingsACK = false; static const Http2Settings defaultServerSettings; static const Http2Settings defaultClientSettings; }; const Http2Settings tst_Http2::defaultServerSettings{{Http2::Settings::MAX_CONCURRENT_STREAMS_ID, 100}}; const Http2Settings tst_Http2::defaultClientSettings{{Http2::Settings::MAX_FRAME_SIZE_ID, quint32(Http2::maxFrameSize)}, {Http2::Settings::INITIAL_WINDOW_SIZE_ID, quint32(Http2::initialStreamReceiveWindowSize)}, {Http2::Settings::ENABLE_PUSH_ID, quint32(0)}}; namespace { // Our server lives/works on a different thread so we invoke its 'deleteLater' // instead of simple 'delete'. struct ServerDeleter { static void cleanup(Http2Server *srv) { if (srv) QMetaObject::invokeMethod(srv, "deleteLater", Qt::QueuedConnection); } }; using ServerPtr = QScopedPointer; struct EnvVarGuard { EnvVarGuard(const char *name, const QByteArray &value) : varName(name), prevValue(qgetenv(name)) { Q_ASSERT(name); qputenv(name, value); } ~EnvVarGuard() { if (prevValue.size()) qputenv(varName.c_str(), prevValue); else qunsetenv(varName.c_str()); } const std::string varName; const QByteArray prevValue; }; } // unnamed namespace tst_Http2::tst_Http2() : workerThread(new QThread) { workerThread->start(); timer.setInterval(10000); timer.setSingleShot(true); connect(&timer, SIGNAL(timeout()), &eventLoop, SLOT(quit())); } 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::singleRequest() { clearHTTP2State(); serverPort = 0; nRequests = 1; ServerPtr srv(newServer(defaultServerSettings)); QMetaObject::invokeMethod(srv.data(), "startServer", Qt::QueuedConnection); runEventLoop(); QVERIFY(serverPort != 0); auto url = requestUrl(); url.setPath("/index.html"); QNetworkRequest request(url); request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, 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(); QVERIFY(nRequests == 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); QCOMPARE(reply->error(), QNetworkReply::NoError); QVERIFY(reply->isFinished()); } void tst_Http2::multipleRequests() { clearHTTP2State(); serverPort = 0; nRequests = 10; ServerPtr srv(newServer(defaultServerSettings)); QMetaObject::invokeMethod(srv.data(), "startServer", Qt::QueuedConnection); runEventLoop(); QVERIFY(serverPort != 0); // Just to make the order a bit more interesting // we'll index this randomly: QNetworkRequest::Priority priorities[] = {QNetworkRequest::HighPriority, QNetworkRequest::NormalPriority, QNetworkRequest::LowPriority}; for (int i = 0; i < nRequests; ++i) sendRequest(i, priorities[std::rand() % 3]); runEventLoop(); QVERIFY(nRequests == 0); QVERIFY(prefaceOK); QVERIFY(serverGotSettingsACK); } void tst_Http2::flowControlClientSide() { // Create a server but impose limits: // 1. Small MAX frame size, so we test CONTINUATION frames. // 2. Small client windows so server responses cause client streams // to suspend and server sends WINDOW_UPDATE frames. // 3. Few concurrent streams, to test protocol handler can resume // suspended requests. using namespace Http2; clearHTTP2State(); serverPort = 0; nRequests = 10; windowUpdates = 0; const Http2Settings serverSettings = {{Settings::MAX_CONCURRENT_STREAMS_ID, 3}}; ServerPtr srv(newServer(serverSettings)); const QByteArray respond(int(Http2::initialStreamReceiveWindowSize * 5), 'x'); srv->setResponseBody(respond); QMetaObject::invokeMethod(srv.data(), "startServer", Qt::QueuedConnection); runEventLoop(); QVERIFY(serverPort != 0); for (int i = 0; i < nRequests; ++i) sendRequest(i); runEventLoop(120000); QVERIFY(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; clearHTTP2State(); serverPort = 0; nRequests = 30; const Http2Settings serverSettings = {{Settings::MAX_CONCURRENT_STREAMS_ID, 7}}; ServerPtr srv(newServer(serverSettings)); const QByteArray payload(int(Http2::defaultSessionWindowSize * 500), 'x'); QMetaObject::invokeMethod(srv.data(), "startServer", Qt::QueuedConnection); runEventLoop(); QVERIFY(serverPort != 0); for (int i = 0; i < nRequests; ++i) sendRequest(i, QNetworkRequest::NormalPriority, payload); runEventLoop(120000); QVERIFY(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; const EnvVarGuard env("QT_HTTP2_ENABLE_PUSH_PROMISE", "1"); const Http2Settings clientSettings{{Settings::MAX_FRAME_SIZE_ID, quint32(Http2::maxFrameSize)}, {Http2::Settings::INITIAL_WINDOW_SIZE_ID, quint32(Http2::initialStreamReceiveWindowSize)}, {Settings::ENABLE_PUSH_ID, quint32(1)}}; ServerPtr srv(newServer(defaultServerSettings, clientSettings)); srv->enablePushPromise(true, QByteArray("/script.js")); QMetaObject::invokeMethod(srv.data(), "startServer", Qt::QueuedConnection); runEventLoop(); QVERIFY(serverPort != 0); auto url = requestUrl(); url.setPath("/index.html"); QNetworkRequest request(url); request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, 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(); QVERIFY(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); 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. QTest::addColumn("responseTimeoutMS"); QTest::newRow("ImmediateGOAWAY") << 0; QTest::newRow("DelayedGOAWAY") << 1000; } void tst_Http2::goaway() { using namespace Http2; QFETCH(const int, responseTimeoutMS); clearHTTP2State(); serverPort = 0; nRequests = 3; ServerPtr srv(newServer(defaultServerSettings, defaultClientSettings)); srv->emulateGOAWAY(responseTimeoutMS); QMetaObject::invokeMethod(srv.data(), "startServer", Qt::QueuedConnection); runEventLoop(); QVERIFY(serverPort != 0); auto url = requestUrl(); // We have to store these replies, so that we can check errors later. std::vector replies(nRequests); for (int i = 0; i < nRequests; ++i) { url.setPath(QString("/%1").arg(i)); QNetworkRequest request(url); request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, QVariant(true)); replies[i] = manager.get(request); QCOMPARE(replies[i]->error(), QNetworkReply::NoError); void (QNetworkReply::*errorSignal)(QNetworkReply::NetworkError) = &QNetworkReply::error; connect(replies[i], errorSignal, this, &tst_Http2::replyFinishedWithError); // Since we're using self-signed certificates, ignore SSL errors: replies[i]->ignoreSslErrors(); } runEventLoop(5000 + responseTimeoutMS); // No request processed, no 'replyFinished' slot calls: QCOMPARE(nRequests, 0); // Our server did not bother to send anything except a single GOAWAY frame: QVERIFY(!prefaceOK); QVERIFY(!serverGotSettingsACK); } void tst_Http2::serverStarted(quint16 port) { serverPort = port; stopEventLoop(); } void tst_Http2::clearHTTP2State() { windowUpdates = 0; prefaceOK = false; serverGotSettingsACK = false; } void tst_Http2::runEventLoop(int ms) { timer.setInterval(ms); timer.start(); eventLoop.exec(); } void tst_Http2::stopEventLoop() { timer.stop(); eventLoop.quit(); } Http2Server *tst_Http2::newServer(const Http2Settings &serverSettings, const Http2Settings &clientSettings) { using namespace Http2; auto srv = new Http2Server(clearTextHTTP2, 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) { auto url = requestUrl(); url.setPath(QString("/stream%1.html").arg(streamNumber)); QNetworkRequest request(url); request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, QVariant(true)); request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("text/plain")); request.setPriority(priority); 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() const { static auto url = QUrl(QLatin1String(clearTextHTTP2 ? "http://127.0.0.1" : "https://127.0.0.1")); url.setPort(serverPort); 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(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(sender()); Q_ASSERT(srv); QMetaObject::invokeMethod(srv, "sendResponse", Qt::QueuedConnection, Q_ARG(quint32, streamID), Q_ARG(bool, 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(sender())) { QCOMPARE(reply->error(), QNetworkReply::NoError); const QVariant http2Used(reply->attribute(QNetworkRequest::HTTP2WasUsedAttribute)); QVERIFY(http2Used.isValid()); QVERIFY(http2Used.toBool()); const QVariant spdyUsed(reply->attribute(QNetworkRequest::SpdyWasUsedAttribute)); QVERIFY(spdyUsed.isValid()); QVERIFY(!spdyUsed.toBool()); } --nRequests; if (!nRequests && serverGotSettingsACK) stopEventLoop(); } void tst_Http2::replyFinishedWithError() { QVERIFY(nRequests); if (const auto reply = qobject_cast(sender())) { // For now this is a 'generic' code, it just verifies some error was // reported without testing its type. QVERIFY(reply->error() != QNetworkReply::NoError); } --nRequests; if (!nRequests) stopEventLoop(); } QT_END_NAMESPACE QTEST_MAIN(tst_Http2) #include "tst_http2.moc"