diff options
Diffstat (limited to 'tests/auto/network/access/http2')
-rw-r--r-- | tests/auto/network/access/http2/certs/fluke.cert | 75 | ||||
-rw-r--r-- | tests/auto/network/access/http2/certs/fluke.key | 15 | ||||
-rw-r--r-- | tests/auto/network/access/http2/http2.pro | 8 | ||||
-rw-r--r-- | tests/auto/network/access/http2/http2srv.cpp | 695 | ||||
-rw-r--r-- | tests/auto/network/access/http2/http2srv.h | 172 | ||||
-rw-r--r-- | tests/auto/network/access/http2/tst_http2.cpp | 539 |
6 files changed, 1504 insertions, 0 deletions
diff --git a/tests/auto/network/access/http2/certs/fluke.cert b/tests/auto/network/access/http2/certs/fluke.cert new file mode 100644 index 0000000000..ace4e4f0eb --- /dev/null +++ b/tests/auto/network/access/http2/certs/fluke.cert @@ -0,0 +1,75 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 0 (0x0) + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=NO, ST=Oslo, L=Nydalen, O=Nokia Corporation and/or its subsidiary(-ies), OU=Development, CN=fluke.troll.no/emailAddress=ahanssen@trolltech.com + Validity + Not Before: Dec 4 01:10:32 2007 GMT + Not After : Apr 21 01:10:32 2035 GMT + Subject: C=NO, ST=Oslo, O=Nokia Corporation and/or its subsidiary(-ies), OU=Development, CN=fluke.troll.no + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (1024 bit) + Modulus (1024 bit): + 00:a7:c8:a0:4a:c4:19:05:1b:66:ba:32:e2:d2:f1: + 1c:6f:17:82:e4:39:2e:01:51:90:db:04:34:32:11: + 21:c2:0d:6f:59:d8:53:90:54:3f:83:8f:a9:d3:b3: + d5:ee:1a:9b:80:ae:c3:25:c9:5e:a5:af:4b:60:05: + aa:a0:d1:91:01:1f:ca:04:83:e3:58:1c:99:32:45: + 84:70:72:58:03:98:4a:63:8b:41:f5:08:49:d2:91: + 02:60:6b:e4:64:fe:dd:a0:aa:74:08:e9:34:4c:91: + 5f:12:3d:37:4d:54:2c:ad:7f:5b:98:60:36:02:8c: + 3b:f6:45:f3:27:6a:9b:94:9d + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 21:85:04:3D:23:01:66:E5:F7:9F:1A:84:24:8A:AF:0A:79:F4:E5:AC + X509v3 Authority Key Identifier: + DirName:/C=NO/ST=Oslo/L=Nydalen/O=Nokia Corporation and/or its subsidiary(-ies)/OU=Development/CN=fluke.troll.no/emailAddress=ahanssen@trolltech.com + serial:8E:A8:B4:E8:91:B7:54:2E + + Signature Algorithm: sha1WithRSAEncryption + 6d:57:5f:d1:05:43:f0:62:05:ec:2a:71:a5:dc:19:08:f2:c4: + a6:bd:bb:25:d9:ca:89:01:0e:e4:cf:1f:c1:8c:c8:24:18:35: + 53:59:7b:c0:43:b4:32:e6:98:b2:a6:ef:15:05:0b:48:5f:e1: + a0:0c:97:a9:a1:77:d8:35:18:30:bc:a9:8f:d3:b7:54:c7:f1: + a9:9e:5d:e6:19:bf:f6:3c:5b:2b:d8:e4:3e:62:18:88:8b:d3: + 24:e1:40:9b:0c:e6:29:16:62:ab:ea:05:24:70:36:aa:55:93: + ef:02:81:1b:23:10:a2:04:eb:56:95:75:fc:f8:94:b1:5d:42: + c5:3f:36:44:85:5d:3a:2e:90:46:8a:a2:b9:6f:87:ae:0c:15: + 40:19:31:90:fc:3b:25:bb:ae:f1:66:13:0d:85:90:d9:49:34: + 8f:f2:5d:f9:7a:db:4d:5d:27:f6:76:9d:35:8c:06:a6:4c:a3: + b1:b2:b6:6f:1d:d7:a3:00:fd:72:eb:9e:ea:44:a1:af:21:34: + 7d:c7:42:e2:49:91:19:8b:c0:ad:ba:82:80:a8:71:70:f4:35: + 31:91:63:84:20:95:e9:60:af:64:8b:cc:ff:3d:8a:76:74:3d: + c8:55:6d:e4:8e:c3:2b:1c:e8:42:18:ae:9f:e6:6b:9c:34:06: + ec:6a:f2:c3 +-----BEGIN CERTIFICATE----- +MIIEEzCCAvugAwIBAgIBADANBgkqhkiG9w0BAQUFADCBnDELMAkGA1UEBhMCTk8x +DTALBgNVBAgTBE9zbG8xEDAOBgNVBAcTB055ZGFsZW4xFjAUBgNVBAoTDVRyb2xs +dGVjaCBBU0ExFDASBgNVBAsTC0RldmVsb3BtZW50MRcwFQYDVQQDEw5mbHVrZS50 +cm9sbC5ubzElMCMGCSqGSIb3DQEJARYWYWhhbnNzZW5AdHJvbGx0ZWNoLmNvbTAe +Fw0wNzEyMDQwMTEwMzJaFw0zNTA0MjEwMTEwMzJaMGMxCzAJBgNVBAYTAk5PMQ0w +CwYDVQQIEwRPc2xvMRYwFAYDVQQKEw1Ucm9sbHRlY2ggQVNBMRQwEgYDVQQLEwtE +ZXZlbG9wbWVudDEXMBUGA1UEAxMOZmx1a2UudHJvbGwubm8wgZ8wDQYJKoZIhvcN +AQEBBQADgY0AMIGJAoGBAKfIoErEGQUbZroy4tLxHG8XguQ5LgFRkNsENDIRIcIN +b1nYU5BUP4OPqdOz1e4am4CuwyXJXqWvS2AFqqDRkQEfygSD41gcmTJFhHByWAOY +SmOLQfUISdKRAmBr5GT+3aCqdAjpNEyRXxI9N01ULK1/W5hgNgKMO/ZF8ydqm5Sd +AgMBAAGjggEaMIIBFjAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NM +IEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUIYUEPSMBZuX3nxqEJIqv +Cnn05awwgbsGA1UdIwSBszCBsKGBoqSBnzCBnDELMAkGA1UEBhMCTk8xDTALBgNV +BAgTBE9zbG8xEDAOBgNVBAcTB055ZGFsZW4xFjAUBgNVBAoTDVRyb2xsdGVjaCBB +U0ExFDASBgNVBAsTC0RldmVsb3BtZW50MRcwFQYDVQQDEw5mbHVrZS50cm9sbC5u +bzElMCMGCSqGSIb3DQEJARYWYWhhbnNzZW5AdHJvbGx0ZWNoLmNvbYIJAI6otOiR +t1QuMA0GCSqGSIb3DQEBBQUAA4IBAQBtV1/RBUPwYgXsKnGl3BkI8sSmvbsl2cqJ +AQ7kzx/BjMgkGDVTWXvAQ7Qy5piypu8VBQtIX+GgDJepoXfYNRgwvKmP07dUx/Gp +nl3mGb/2PFsr2OQ+YhiIi9Mk4UCbDOYpFmKr6gUkcDaqVZPvAoEbIxCiBOtWlXX8 ++JSxXULFPzZEhV06LpBGiqK5b4euDBVAGTGQ/Dslu67xZhMNhZDZSTSP8l35ettN +XSf2dp01jAamTKOxsrZvHdejAP1y657qRKGvITR9x0LiSZEZi8CtuoKAqHFw9DUx +kWOEIJXpYK9ki8z/PYp2dD3IVW3kjsMrHOhCGK6f5mucNAbsavLD +-----END CERTIFICATE----- diff --git a/tests/auto/network/access/http2/certs/fluke.key b/tests/auto/network/access/http2/certs/fluke.key new file mode 100644 index 0000000000..9d1664d609 --- /dev/null +++ b/tests/auto/network/access/http2/certs/fluke.key @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQCnyKBKxBkFG2a6MuLS8RxvF4LkOS4BUZDbBDQyESHCDW9Z2FOQ +VD+Dj6nTs9XuGpuArsMlyV6lr0tgBaqg0ZEBH8oEg+NYHJkyRYRwclgDmEpji0H1 +CEnSkQJga+Rk/t2gqnQI6TRMkV8SPTdNVCytf1uYYDYCjDv2RfMnapuUnQIDAQAB +AoGANFzLkanTeSGNFM0uttBipFT9F4a00dqHz6JnO7zXAT26I5r8sU1pqQBb6uLz +/+Qz5Zwk8RUAQcsMRgJetuPQUb0JZjF6Duv24hNazqXBCu7AZzUenjafwmKC/8ri +KpX3fTwqzfzi//FKGgbXQ80yykSSliDL3kn/drATxsLCgQECQQDXhEFWLJ0vVZ1s +1Ekf+3NITE+DR16X+LQ4W6vyEHAjTbaNWtcTKdAWLA2l6N4WAAPYSi6awm+zMxx4 +VomVTsjdAkEAx0z+e7natLeFcrrq8pbU+wa6SAP1VfhQWKitxL1e7u/QO90NCpxE +oQYKzMkmmpOOFjQwEMAy1dvFMbm4LHlewQJAC/ksDBaUcQHHqjktCtrUb8rVjAyW +A8lscckeB2fEYyG5J6dJVaY4ClNOOs5yMDS2Afk1F6H/xKvtQ/5CzInA/QJATDub +K+BPU8jO9q+gpuIi3VIZdupssVGmCgObVCHLakG4uO04y9IyPhV9lA9tALtoIf4c +VIvv5fWGXBrZ48kZAQJBAJmVCdzQxd9LZI5vxijUCj5EI4e+x5DRqVUvyP8KCZrC +AiNyoDP85T+hBZaSXK3aYGpVwelyj3bvo1GrTNwNWLw= +-----END RSA PRIVATE KEY----- diff --git a/tests/auto/network/access/http2/http2.pro b/tests/auto/network/access/http2/http2.pro new file mode 100644 index 0000000000..e130f30784 --- /dev/null +++ b/tests/auto/network/access/http2/http2.pro @@ -0,0 +1,8 @@ +QT += core core-private network network-private testlib + +CONFIG += testcase parallel_test c++11 +TARGET = tst_http2 +HEADERS += http2srv.h +SOURCES += tst_http2.cpp http2srv.cpp + +DEFINES += SRCDIR=\\\"$$PWD/\\\" diff --git a/tests/auto/network/access/http2/http2srv.cpp b/tests/auto/network/access/http2/http2srv.cpp new file mode 100644 index 0000000000..9d68b5c798 --- /dev/null +++ b/tests/auto/network/access/http2/http2srv.cpp @@ -0,0 +1,695 @@ +/**************************************************************************** +** +** 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 <QtTest/QtTest> + +#include <QtNetwork/private/http2protocol_p.h> +#include <QtNetwork/private/bitstreams_p.h> + +#include "http2srv.h" + +#ifndef QT_NO_SSL +#include <QtNetwork/qsslconfiguration.h> +#include <QtNetwork/qsslsocket.h> +#include <QtNetwork/qsslkey.h> +#endif + +#include <QtNetwork/qtcpsocket.h> + +#include <QtCore/qdebug.h> +#include <QtCore/qlist.h> +#include <QtCore/qfile.h> + +#include <cstdlib> +#include <cstring> +#include <limits> + +QT_BEGIN_NAMESPACE + +using namespace Http2; +using namespace HPack; + +namespace +{ + +inline bool is_valid_client_stream(quint32 streamID) +{ + // A valid client stream ID is an odd integer number in the range [1, INT_MAX]. + return (streamID & 0x1) && streamID <= std::numeric_limits<qint32>::max(); +} + +void fill_push_header(const HttpHeader &originalRequest, HttpHeader &promisedRequest) +{ + for (const auto &field : originalRequest) { + if (field.name == QByteArray(":authority") || + field.name == QByteArray(":scheme")) { + promisedRequest.push_back(field); + } + } +} + +} + +Http2Server::Http2Server(bool h2c, const Http2Settings &ss, const Http2Settings &cs) + : serverSettings(ss), + clearTextHTTP2(h2c) +{ + for (const auto &s : cs) + expectedClientSettings[quint16(s.identifier)] = s.value; + + responseBody = "<html>\n" + "<head>\n" + "<title>Sample \"Hello, World\" Application</title>\n" + "</head>\n" + "<body bgcolor=white>\n" + "<table border=\"0\" cellpadding=\"10\">\n" + "<tr>\n" + "<td>\n" + "<img src=\"images/springsource.png\">\n" + "</td>\n" + "<td>\n" + "<h1>Sample \"Hello, World\" Application</h1>\n" + "</td>\n" + "</tr>\n" + "</table>\n" + "<p>This is the home page for the HelloWorld Web application. </p>\n" + "</body>\n" + "</html>"; +} + +Http2Server::~Http2Server() +{ +} + +void Http2Server::enablePushPromise(bool pushEnabled, const QByteArray &path) +{ + pushPromiseEnabled = pushEnabled; + pushPath = path; +} + +void Http2Server::setResponseBody(const QByteArray &body) +{ + responseBody = body; +} + +void Http2Server::startServer() +{ +#ifdef QT_NO_SSL + // Let the test fail with timeout. + if (!clearTextHTTP2) + return; +#endif + if (listen()) + emit serverStarted(serverPort()); +} + +void Http2Server::sendServerSettings() +{ + Q_ASSERT(socket); + + if (!serverSettings.size()) + return; + + writer.start(FrameType::SETTINGS, FrameFlag::EMPTY, connectionStreamID); + for (const auto &s : serverSettings) { + writer.append(s.identifier); + writer.append(s.value); + if (s.identifier == Settings::INITIAL_WINDOW_SIZE_ID) + streamRecvWindowSize = s.value; + } + writer.write(*socket); + // Now, let's update our peer on a session recv window size: + const quint32 updatedSize = 10 * streamRecvWindowSize; + if (sessionRecvWindowSize < updatedSize) { + const quint32 delta = updatedSize - sessionRecvWindowSize; + sessionRecvWindowSize = updatedSize; + sessionCurrRecvWindow = updatedSize; + sendWINDOW_UPDATE(connectionStreamID, delta); + } + + waitingClientAck = true; + settingsSent = true; +} + +void Http2Server::sendGOAWAY(quint32 streamID, quint32 error, quint32 lastStreamID) +{ + Q_ASSERT(socket); + + writer.start(FrameType::GOAWAY, FrameFlag::EMPTY, streamID); + writer.append(lastStreamID); + writer.append(error); + writer.write(*socket); +} + +void Http2Server::sendRST_STREAM(quint32 streamID, quint32 error) +{ + Q_ASSERT(socket); + + writer.start(FrameType::RST_STREAM, FrameFlag::EMPTY, streamID); + writer.append(error); + writer.write(*socket); +} + +void Http2Server::sendDATA(quint32 streamID, quint32 windowSize) +{ + Q_ASSERT(socket); + + const auto it = suspendedStreams.find(streamID); + Q_ASSERT(it != suspendedStreams.end()); + + const quint32 offset = it->second; + Q_ASSERT(offset < quint32(responseBody.size())); + + const quint32 bytes = std::min<quint32>(windowSize, responseBody.size() - offset); + const quint32 frameSizeLimit(clientSetting(Settings::MAX_FRAME_SIZE_ID, Http2::maxFrameSize)); + const uchar *src = reinterpret_cast<const uchar *>(responseBody.constData() + offset); + const bool last = offset + bytes == quint32(responseBody.size()); + + writer.start(FrameType::DATA, FrameFlag::EMPTY, streamID); + writer.writeDATA(*socket, frameSizeLimit, src, bytes); + + if (last) { + writer.start(FrameType::DATA, FrameFlag::END_STREAM, streamID); + writer.setPayloadSize(0); + writer.write(*socket); + suspendedStreams.erase(it); + activeRequests.erase(streamID); + + Q_ASSERT(closedStreams.find(streamID) == closedStreams.end()); + closedStreams.insert(streamID); + } else { + it->second += bytes; + } +} + +void Http2Server::sendWINDOW_UPDATE(quint32 streamID, quint32 delta) +{ + Q_ASSERT(socket); + + writer.start(FrameType::WINDOW_UPDATE, FrameFlag::EMPTY, streamID); + writer.append(delta); + writer.write(*socket); +} + +void Http2Server::incomingConnection(qintptr socketDescriptor) +{ + if (clearTextHTTP2) { + socket.reset(new QTcpSocket); + const bool set = socket->setSocketDescriptor(socketDescriptor); + Q_ASSERT(set); + // Stop listening: + close(); + QMetaObject::invokeMethod(this, "connectionEstablished", + Qt::QueuedConnection); + } else { +#ifndef QT_NO_SSL + socket.reset(new QSslSocket); + QSslSocket *sslSocket = static_cast<QSslSocket *>(socket.data()); + // Add HTTP2 as supported protocol: + auto conf = QSslConfiguration::defaultConfiguration(); + auto protos = conf.allowedNextProtocols(); + protos.prepend(QSslConfiguration::ALPNProtocolHTTP2); + conf.setAllowedNextProtocols(protos); + sslSocket->setSslConfiguration(conf); + // SSL-related setup ... + sslSocket->setPeerVerifyMode(QSslSocket::VerifyNone); + sslSocket->setProtocol(QSsl::TlsV1_2OrLater); + connect(sslSocket, SIGNAL(sslErrors(QList<QSslError>)), + this, SLOT(ignoreErrorSlot())); + QFile file(SRCDIR "certs/fluke.key"); + file.open(QIODevice::ReadOnly); + QSslKey key(file.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey); + sslSocket->setPrivateKey(key); + auto localCert = QSslCertificate::fromPath(SRCDIR "certs/fluke.cert"); + sslSocket->setLocalCertificateChain(localCert); + sslSocket->setSocketDescriptor(socketDescriptor, QAbstractSocket::ConnectedState); + // Stop listening. + close(); + // Start SSL handshake and ALPN: + connect(sslSocket, SIGNAL(encrypted()), this, SLOT(connectionEstablished())); + sslSocket->startServerEncryption(); +#else + Q_UNREACHABLE(); +#endif + } +} + +quint32 Http2Server::clientSetting(Http2::Settings identifier, quint32 defaultValue) +{ + const auto it = expectedClientSettings.find(quint16(identifier)); + if (it != expectedClientSettings.end()) + return it->second; + return defaultValue; +} + +void Http2Server::connectionEstablished() +{ + using namespace Http2; + + connect(socket.data(), SIGNAL(readyRead()), + this, SLOT(readReady())); + + waitingClientPreface = true; + waitingClientAck = false; + waitingClientSettings = false; + settingsSent = false; + // We immediately send our settings so that our client + // can use flow control correctly. + sendServerSettings(); + + if (socket->bytesAvailable()) + readReady(); +} + +void Http2Server::ignoreErrorSlot() +{ +#ifndef QT_NO_SSL + static_cast<QSslSocket *>(socket.data())->ignoreSslErrors(); +#endif +} + +// Now HTTP2 "server" part: +/* +This code is overly simplified but it tests the basic HTTP2 expected behavior: +1. CONNECTION PREFACE +2. SETTINGS +3. sends our own settings (to modify the flow control) +4. collects and reports requests +5. if asked - sends responds to those requests +6. does some very basic error handling +7. tests frames validity/stream logic at the very basic level. +*/ + +void Http2Server::readReady() +{ + if (connectionError) + return; + + if (waitingClientPreface) { + handleConnectionPreface(); + } else { + const auto status = reader.read(*socket); + switch (status) { + case FrameStatus::incompleteFrame: + break; + case FrameStatus::goodFrame: + handleIncomingFrame(); + break; + default: + connectionError = true; + sendGOAWAY(connectionStreamID, PROTOCOL_ERROR, connectionStreamID); + } + } + + if (socket->bytesAvailable()) + QMetaObject::invokeMethod(this, "readReady", Qt::QueuedConnection); +} + +void Http2Server::handleConnectionPreface() +{ + Q_ASSERT(waitingClientPreface); + + if (socket->bytesAvailable() < clientPrefaceLength) + return; // Wait for more data ... + + char buf[clientPrefaceLength] = {}; + socket->read(buf, clientPrefaceLength); + if (std::memcmp(buf, Http2clientPreface, clientPrefaceLength)) { + sendGOAWAY(connectionStreamID, PROTOCOL_ERROR, connectionStreamID); + emit clientPrefaceError(); + connectionError = true; + return; + } + + waitingClientPreface = false; + waitingClientSettings = true; +} + +void Http2Server::handleIncomingFrame() +{ + // Frames that our implementation can send include: + // 1. SETTINGS (happens only during connection preface, + // handled already by this point) + // 2. SETTIGNS with ACK should be sent only as a response + // to a server's SETTINGS + // 3. HEADERS + // 4. CONTINUATION + // 5. DATA + // 6. PING + // 7. RST_STREAM + // 8. GOAWAY + + inboundFrame = std::move(reader.inboundFrame()); + + if (continuedRequest.size()) { + if (inboundFrame.type() != FrameType::CONTINUATION || + inboundFrame.streamID() != continuedRequest.front().streamID()) { + sendGOAWAY(connectionStreamID, PROTOCOL_ERROR, connectionStreamID); + emit invalidFrame(); + connectionError = true; + return; + } + } + + switch (inboundFrame.type()) { + case FrameType::SETTINGS: + handleSETTINGS(); + break; + case FrameType::HEADERS: + case FrameType::CONTINUATION: + continuedRequest.push_back(std::move(inboundFrame)); + processRequest(); + break; + case FrameType::DATA: + handleDATA(); + break; + case FrameType::RST_STREAM: + // TODO: this is not tested for now. + break; + case FrameType::PING: + // TODO: this is not tested for now. + break; + case FrameType::GOAWAY: + // TODO: this is not tested for now. + break; + case FrameType::WINDOW_UPDATE: + handleWINDOW_UPDATE(); + break; + default:; + } +} + +void Http2Server::handleSETTINGS() +{ + // SETTINGS is either a part of the connection preface, + // or a SETTINGS ACK. + Q_ASSERT(inboundFrame.type() == FrameType::SETTINGS); + + if (inboundFrame.flags().testFlag(FrameFlag::ACK)) { + if (!waitingClientAck || inboundFrame.dataSize()) { + emit invalidFrame(); + connectionError = true; + waitingClientAck = false; + return; + } + + waitingClientAck = false; + emit serverSettingsAcked(); + return; + } + + // QHttp2ProtocolHandler always sends some settings, + // and the size is a multiple of 6. + if (!inboundFrame.dataSize() || inboundFrame.dataSize() % 6) { + sendGOAWAY(connectionStreamID, FRAME_SIZE_ERROR, connectionStreamID); + emit clientPrefaceError(); + connectionError = true; + return; + } + + const uchar *src = inboundFrame.dataBegin(); + const uchar *end = src + inboundFrame.dataSize(); + + const auto notFound = expectedClientSettings.end(); + + while (src != end) { + const auto id = qFromBigEndian<quint16>(src); + const auto value = qFromBigEndian<quint32>(src + 2); + if (expectedClientSettings.find(id) == notFound || + expectedClientSettings[id] != value) { + emit clientPrefaceError(); + connectionError = true; + return; + } + + src += 6; + } + + // Send SETTINGS ACK: + writer.start(FrameType::SETTINGS, FrameFlag::ACK, connectionStreamID); + writer.write(*socket); + waitingClientSettings = false; + emit clientPrefaceOK(); +} + +void Http2Server::handleDATA() +{ + Q_ASSERT(inboundFrame.type() == FrameType::DATA); + + const auto streamID = inboundFrame.streamID(); + + if (!is_valid_client_stream(streamID) || + closedStreams.find(streamID) != closedStreams.end()) { + emit invalidFrame(); + connectionError = true; + sendGOAWAY(connectionStreamID, PROTOCOL_ERROR, connectionStreamID); + return; + } + + const auto payloadSize = inboundFrame.payloadSize(); + if (sessionCurrRecvWindow < payloadSize) { + // Client does not respect our session window size! + emit invalidRequest(streamID); + connectionError = true; + sendGOAWAY(connectionStreamID, FLOW_CONTROL_ERROR, connectionStreamID); + return; + } + + auto it = streamWindows.find(streamID); + if (it == streamWindows.end()) + it = streamWindows.insert(std::make_pair(streamID, streamRecvWindowSize)).first; + + + if (it->second < payloadSize) { + emit invalidRequest(streamID); + connectionError = true; + sendGOAWAY(connectionStreamID, FLOW_CONTROL_ERROR, connectionStreamID); + return; + } + + it->second -= payloadSize; + if (it->second < streamRecvWindowSize / 2) { + sendWINDOW_UPDATE(streamID, streamRecvWindowSize / 2); + it->second += streamRecvWindowSize / 2; + } + + sessionCurrRecvWindow -= payloadSize; + + if (sessionCurrRecvWindow < sessionRecvWindowSize / 2) { + // This is some quite naive and trivial logic on when to update. + + sendWINDOW_UPDATE(connectionStreamID, sessionRecvWindowSize / 2); + sessionCurrRecvWindow += sessionRecvWindowSize / 2; + } + + if (inboundFrame.flags().testFlag(FrameFlag::END_STREAM)) { + closedStreams.insert(streamID); // Enter "half-closed remote" state. + streamWindows.erase(it); + emit receivedData(streamID); + } +} + +void Http2Server::handleWINDOW_UPDATE() +{ + const auto streamID = inboundFrame.streamID(); + if (!streamID) // We ignore this for now to keep things simple. + return; + + if (streamID && suspendedStreams.find(streamID) == suspendedStreams.end()) { + if (closedStreams.find(streamID) == closedStreams.end()) { + sendRST_STREAM(streamID, PROTOCOL_ERROR); + emit invalidFrame(); + connectionError = true; + } + + return; + } + + const quint32 delta = qFromBigEndian<quint32>(inboundFrame.dataBegin()); + if (!delta || delta > quint32(std::numeric_limits<qint32>::max())) { + sendRST_STREAM(streamID, PROTOCOL_ERROR); + emit invalidFrame(); + connectionError = true; + return; + } + + emit windowUpdate(streamID); + sendDATA(streamID, delta); +} + +void Http2Server::sendResponse(quint32 streamID, bool emptyBody) +{ + Q_ASSERT(activeRequests.find(streamID) != activeRequests.end()); + + const quint32 maxFrameSize(clientSetting(Settings::MAX_FRAME_SIZE_ID, + Http2::maxFrameSize)); + + if (pushPromiseEnabled) { + // A real server supporting PUSH_PROMISE will probably first send + // PUSH_PROMISE and then a normal response (to a real request), + // so that a client parsing this response and discovering another + // resource it needs, will _already_ have this additional resource + // in PUSH_PROMISE. + lastPromisedStream += 2; + + writer.start(FrameType::PUSH_PROMISE, FrameFlag::END_HEADERS, streamID); + writer.append(lastPromisedStream); + + HttpHeader pushHeader; + fill_push_header(activeRequests[streamID], pushHeader); + pushHeader.push_back(HeaderField(":method", "GET")); + pushHeader.push_back(HeaderField(":path", pushPath)); + + // Now interesting part, let's make it into 'stream': + activeRequests[lastPromisedStream] = pushHeader; + + HPack::BitOStream ostream(writer.outboundFrame().buffer); + const bool result = encoder.encodeRequest(ostream, pushHeader); + Q_ASSERT(result); + + // Well, it's not HEADERS, it's PUSH_PROMISE with ... HEADERS block. + // Should work. + writer.writeHEADERS(*socket, maxFrameSize); + qDebug() << "server sent a PUSH_PROMISE on" << lastPromisedStream; + + if (responseBody.isEmpty()) + responseBody = QByteArray("I PROMISE (AND PUSH) YOU ..."); + + // Now we send this promised data as a normal response on our reserved + // stream (disabling PUSH_PROMISE for the moment to avoid recursion): + pushPromiseEnabled = false; + sendResponse(lastPromisedStream, false); + pushPromiseEnabled = true; + // Now we'll continue with _normal_ response. + } + + writer.start(FrameType::HEADERS, FrameFlag::END_HEADERS, streamID); + if (emptyBody) + writer.addFlag(FrameFlag::END_STREAM); + + HttpHeader header = {{":status", "200"}}; + if (!emptyBody) { + header.push_back(HPack::HeaderField("content-length", + QString("%1").arg(responseBody.size()).toLatin1())); + } + + HPack::BitOStream ostream(writer.outboundFrame().buffer); + const bool result = encoder.encodeResponse(ostream, header); + Q_ASSERT(result); + + writer.writeHEADERS(*socket, maxFrameSize); + + if (!emptyBody) { + Q_ASSERT(suspendedStreams.find(streamID) == suspendedStreams.end()); + + const quint32 windowSize = clientSetting(Settings::INITIAL_WINDOW_SIZE_ID, + Http2::defaultSessionWindowSize); + // Suspend to immediately resume it. + suspendedStreams[streamID] = 0; // start sending from offset 0 + sendDATA(streamID, windowSize); + } else { + activeRequests.erase(streamID); + closedStreams.insert(streamID); + } +} + +void Http2Server::processRequest() +{ + Q_ASSERT(continuedRequest.size()); + + if (!continuedRequest.back().flags().testFlag(FrameFlag::END_HEADERS)) + return; + + // We test here: + // 1. stream is 'idle'. + // 2. has priority set and dependency (it's 0x0 at the moment). + // 3. header can be decompressed. + const auto &headersFrame = continuedRequest.front(); + const auto streamID = headersFrame.streamID(); + if (!is_valid_client_stream(streamID)) { + emit invalidRequest(streamID); + connectionError = true; + sendGOAWAY(connectionStreamID, PROTOCOL_ERROR, connectionStreamID); + return; + } + + if (closedStreams.find(streamID) != closedStreams.end()) { + emit invalidFrame(); + connectionError = true; + sendGOAWAY(connectionStreamID, PROTOCOL_ERROR, connectionStreamID); + return; + } + + quint32 dep = 0; + uchar w = 0; + if (!headersFrame.priority(&dep, &w)) { + emit invalidFrame(); + sendRST_STREAM(streamID, PROTOCOL_ERROR); + return; + } + + // Assemble headers ... + quint32 totalSize = 0; + for (const auto &frame : continuedRequest) { + if (std::numeric_limits<quint32>::max() - frame.dataSize() < totalSize) { + // Resulted in overflow ... + emit invalidFrame(); + connectionError = true; + sendGOAWAY(connectionStreamID, PROTOCOL_ERROR, connectionStreamID); + return; + } + totalSize += frame.dataSize(); + } + + std::vector<uchar> hpackBlock(totalSize); + auto dst = hpackBlock.begin(); + for (const auto &frame : continuedRequest) { + if (!frame.dataSize()) + continue; + std::copy(frame.dataBegin(), frame.dataBegin() + frame.dataSize(), dst); + dst += frame.dataSize(); + } + + HPack::BitIStream inputStream{&hpackBlock[0], &hpackBlock[0] + hpackBlock.size()}; + + if (!decoder.decodeHeaderFields(inputStream)) { + emit decompressionFailed(streamID); + sendRST_STREAM(streamID, COMPRESSION_ERROR); + closedStreams.insert(streamID); + return; + } + + // Actually, if needed, we can do a comparison here. + activeRequests[streamID] = decoder.decodedHeader(); + if (headersFrame.flags().testFlag(FrameFlag::END_STREAM)) + emit receivedRequest(streamID); + // else - we're waiting for incoming DATA frames ... + continuedRequest.clear(); +} + +QT_END_NAMESPACE diff --git a/tests/auto/network/access/http2/http2srv.h b/tests/auto/network/access/http2/http2srv.h new file mode 100644 index 0000000000..15a4f212c9 --- /dev/null +++ b/tests/auto/network/access/http2/http2srv.h @@ -0,0 +1,172 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef HTTP2SRV_H +#define HTTP2SRV_H + +#include <QtNetwork/private/http2protocol_p.h> +#include <QtNetwork/private/http2frames_p.h> +#include <QtNetwork/private/hpack_p.h> + +#include <QtNetwork/qabstractsocket.h> +#include <QtCore/qscopedpointer.h> +#include <QtNetwork/qtcpserver.h> +#include <QtCore/qbytearray.h> +#include <QtCore/qglobal.h> + +#include <vector> +#include <map> +#include <set> + +QT_BEGIN_NAMESPACE + +struct Http2Setting +{ + Http2::Settings identifier; + quint32 value = 0; + + Http2Setting(Http2::Settings ident, quint32 v) + : identifier(ident), + value(v) + {} +}; + +using Http2Settings = std::vector<Http2Setting>; + +class Http2Server : public QTcpServer +{ + Q_OBJECT +public: + Http2Server(bool clearText, const Http2Settings &serverSettings, + const Http2Settings &clientSettings); + + ~Http2Server(); + + // To be called before server started: + void enablePushPromise(bool enabled, const QByteArray &path = QByteArray()); + void setResponseBody(const QByteArray &body); + + // Invokables, since we can call them from the main thread, + // but server (can) work on its own thread. + Q_INVOKABLE void startServer(); + Q_INVOKABLE void sendServerSettings(); + Q_INVOKABLE void sendGOAWAY(quint32 streamID, quint32 error, + quint32 lastStreamID); + Q_INVOKABLE void sendRST_STREAM(quint32 streamID, quint32 error); + Q_INVOKABLE void sendDATA(quint32 streamID, quint32 windowSize); + Q_INVOKABLE void sendWINDOW_UPDATE(quint32 streamID, quint32 delta); + + Q_INVOKABLE void handleConnectionPreface(); + Q_INVOKABLE void handleIncomingFrame(); + Q_INVOKABLE void handleSETTINGS(); + Q_INVOKABLE void handleDATA(); + Q_INVOKABLE void handleWINDOW_UPDATE(); + + Q_INVOKABLE void sendResponse(quint32 streamID, bool emptyBody); + +private: + void processRequest(); + +Q_SIGNALS: + void serverStarted(quint16 port); + // Error/success notifications: + 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 windowUpdate(quint32 streamID); + +private slots: + void connectionEstablished(); + void readReady(); + +private: + void incomingConnection(qintptr socketDescriptor) Q_DECL_OVERRIDE; + + quint32 clientSetting(Http2::Settings identifier, quint32 defaultValue); + + QScopedPointer<QAbstractSocket> socket; + + // Connection preface: + bool waitingClientPreface = false; + bool waitingClientSettings = false; + bool settingsSent = false; + bool waitingClientAck = false; + + Http2Settings serverSettings; + std::map<quint16, quint32> expectedClientSettings; + + bool connectionError = false; + + Http2::FrameReader reader; + Http2::Frame inboundFrame; + Http2::FrameWriter writer; + + using FrameSequence = std::vector<Http2::Frame>; + FrameSequence continuedRequest; + + std::map<quint32, quint32> streamWindows; + + HPack::Decoder decoder{HPack::FieldLookupTable::DefaultSize}; + HPack::Encoder encoder{HPack::FieldLookupTable::DefaultSize, true}; + + using Http2Requests = std::map<quint32, HPack::HttpHeader>; + Http2Requests activeRequests; + // 'remote half-closed' streams to keep + // track of streams with END_STREAM set: + std::set<quint32> closedStreams; + // streamID + offset in response body to send. + std::map<quint32, quint32> suspendedStreams; + + // We potentially reset this once (see sendServerSettings) + // and do not change later: + quint32 sessionRecvWindowSize = Http2::defaultSessionWindowSize; + // This changes in the range [0, sessionRecvWindowSize] + // while handling DATA frames: + quint32 sessionCurrRecvWindow = sessionRecvWindowSize; + // This we potentially update only once (sendServerSettings). + quint32 streamRecvWindowSize = Http2::defaultSessionWindowSize; + + QByteArray responseBody; + bool clearTextHTTP2 = false; + bool pushPromiseEnabled = false; + quint32 lastPromisedStream = 0; + QByteArray pushPath; + +protected slots: + void ignoreErrorSlot(); +}; + +QT_END_NAMESPACE + +#endif + diff --git a/tests/auto/network/access/http2/tst_http2.cpp b/tests/auto/network/access/http2/tst_http2.cpp new file mode 100644 index 0000000000..771ddb01be --- /dev/null +++ b/tests/auto/network/access/http2/tst_http2.cpp @@ -0,0 +1,539 @@ +/**************************************************************************** +** +** 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 <QtTest/QtTest> + +#include "http2srv.h" + +#include <QtNetwork/qnetworkaccessmanager.h> +#include <QtNetwork/qnetworkrequest.h> +#include <QtNetwork/qnetworkreply.h> +#include <QtCore/qglobal.h> +#include <QtCore/qobject.h> +#include <QtCore/qthread.h> +#include <QtCore/qurl.h> + +#ifndef QT_NO_SSL +#ifndef QT_NO_OPENSSL +#include <QtNetwork/private/qsslsocket_openssl_symbols_p.h> +#endif // NO_OPENSSL +#endif // NO_SSL + +#include <cstdlib> +#include <string> + +// At the moment our HTTP/2 imlpementation requires ALPN and this means OpenSSL. +#if !defined(QT_NO_OPENSSL) && OPENSSL_VERSION_NUMBER >= 0x10002000L && !defined(OPENSSL_NO_TLSEXT) +const bool clearTextHTTP2 = false; +#else +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(); + +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(); + +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()); + + 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::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<Http2Server, ServerDeleter>; + +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); + + const QString urlAsString(clearTextHTTP2 ? QString("http://127.0.0.1:%1/index.html") + : QString("https://127.0.0.1:%1/index.html")); + const QUrl url(urlAsString.arg(serverPort)); + + 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::defaultSessionWindowSize * 50), '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)}, + {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); + + const QString urlAsString((clearTextHTTP2 ? QString("http://127.0.0.1:%1/") + : QString("https://127.0.0.1:%1/")).arg(serverPort)); + const QUrl requestUrl(urlAsString + "index.html"); + + QNetworkRequest request(requestUrl); + 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): + + const QUrl promisedUrl(urlAsString + "script.js"); + QNetworkRequest promisedRequest(promisedUrl); + 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::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) +{ + static const QString urlAsString(clearTextHTTP2 ? "http://127.0.0.1:%1/stream%2.html" + : "https://127.0.0.1:%1/stream%2.html"); + + const QUrl url(urlAsString.arg(serverPort).arg(streamNumber)); + QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, QVariant(true)); + 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); +} + +void tst_Http2::clientPrefaceOK() +{ + prefaceOK = true; +} + +void tst_Http2::clientPrefaceError() +{ + prefaceOK = false; +} + +void tst_Http2::serverSettingsAcked() +{ + serverGotSettingsACK = true; +} + +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, 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())) + QCOMPARE(reply->error(), QNetworkReply::NoError); + + --nRequests; + if (!nRequests) + stopEventLoop(); +} + +QT_END_NAMESPACE + +QTEST_MAIN(tst_Http2) + +#include "tst_http2.moc" |