diff options
author | Timur Pocheptsov <timur.pocheptsov@theqtcompany.com> | 2016-02-26 12:37:24 +0100 |
---|---|---|
committer | Timur Pocheptsov <timur.pocheptsov@theqtcompany.com> | 2016-07-14 10:20:36 +0000 |
commit | 205ff27260b03e7aa2aa60a7eb452251dfa11246 (patch) | |
tree | d03260e8f079500bbdcf98c75d30e0175ad9e361 /tests/auto/network/access | |
parent | c2f4705f23ddccf075010edb0532fd73145b8b15 (diff) |
HTTP2 - autotest
Add autotest for QHttp2ProtocolHandler. This patch contains a very simplistic
"in-process HTTP2 server" for testing the protocol's basic logic/flow control/error
handling and emulating possible scenarios.
Task-number: QTBUG-50956
Change-Id: Ie02d3329c5182277a3c7c84f1bae8d02308e945d
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
Diffstat (limited to 'tests/auto/network/access')
-rw-r--r-- | tests/auto/network/access/access.pro | 8 | ||||
-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 | 9 | ||||
-rw-r--r-- | tests/auto/network/access/http2/http2srv.cpp | 611 | ||||
-rw-r--r-- | tests/auto/network/access/http2/http2srv.h | 166 | ||||
-rw-r--r-- | tests/auto/network/access/http2/tst_http2.cpp | 449 |
7 files changed, 1332 insertions, 1 deletions
diff --git a/tests/auto/network/access/access.pro b/tests/auto/network/access/access.pro index 6ef10b7b1b..ef0aeac3c8 100644 --- a/tests/auto/network/access/access.pro +++ b/tests/auto/network/access/access.pro @@ -18,5 +18,11 @@ SUBDIRS=\ qhttpnetworkconnection \ qhttpnetworkreply \ qftp \ - hpack \ + hpack +contains(QT_CONFIG, openssl) | contains(QT_CONFIG, openssl-linked) { + contains(QT_CONFIG, private_tests) { + SUBDIRS += \ + http2 + } +} 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..5dd8bdf9ae --- /dev/null +++ b/tests/auto/network/access/http2/http2.pro @@ -0,0 +1,9 @@ +QT += core core-private network network-private testlib + +CONFIG += testcase parallel_test c++11 +TEMPLATE = app +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..eb09569cdd --- /dev/null +++ b/tests/auto/network/access/http2/http2srv.cpp @@ -0,0 +1,611 @@ +/**************************************************************************** +** +** 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" + +#include <QtNetwork/qsslconfiguration.h> +#include <QtNetwork/qhostaddress.h> +#include <QtNetwork/qsslkey.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(); +} + +} + +Http2Server::Http2Server(const Http2Settings &ss, const Http2Settings &cs) + : serverSettings(ss) +{ + 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::setResponseBody(const QByteArray &body) +{ + responseBody = body; +} + +void Http2Server::startServer() +{ + if (listen()) + emit serverStarted(serverPort()); +} + + +void Http2Server::sendServerSettings() +{ + Q_ASSERT(socket); + + if (!serverSettings.size()) + return; + + outboundFrame.start(FrameType::SETTINGS, FrameFlag::EMPTY, connectionStreamID); + for (const auto &s : serverSettings) { + outboundFrame.append(s.identifier); + outboundFrame.append(s.value); + if (s.identifier == Settings::INITIAL_WINDOW_SIZE_ID) + streamRecvWindowSize = s.value; + } + outboundFrame.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); + + outboundFrame.start(FrameType::GOAWAY, FrameFlag::EMPTY, streamID); + outboundFrame.append(lastStreamID); + outboundFrame.append(error); + outboundFrame.write(*socket); +} + +void Http2Server::sendRST_STREAM(quint32 streamID, quint32 error) +{ + Q_ASSERT(socket); + + outboundFrame.start(FrameType::RST_STREAM, FrameFlag::EMPTY, streamID); + outboundFrame.append(error); + outboundFrame.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); + outboundFrame.start(FrameType::DATA, FrameFlag::EMPTY, streamID); + const bool last = offset + bytes == quint32(responseBody.size()); + + const quint32 frameSizeLimit(clientSetting(Settings::MAX_FRAME_SIZE_ID, Http2::maxFrameSize)); + outboundFrame.writeDATA(*socket, frameSizeLimit, + reinterpret_cast<const uchar *>(responseBody.constData() + offset), + bytes); + + if (last) { + outboundFrame.start(FrameType::DATA, FrameFlag::END_STREAM, streamID); + outboundFrame.setPayloadSize(0); + outboundFrame.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); + + outboundFrame.start(FrameType::WINDOW_UPDATE, FrameFlag::EMPTY, streamID); + outboundFrame.append(delta); + outboundFrame.write(*socket); +} + +void Http2Server::incomingConnection(qintptr socketDescriptor) +{ + socket.reset(new QSslSocket); + // Add HTTP2 as supported protocol: + auto conf = QSslConfiguration::defaultConfiguration(); + auto protos = conf.allowedNextProtocols(); + protos.prepend(QSslConfiguration::ALPNProtocolHTTP2); + conf.setAllowedNextProtocols(protos); + socket->setSslConfiguration(conf); + // SSL-related setup ... + socket->setPeerVerifyMode(QSslSocket::VerifyNone); + socket->setProtocol(QSsl::TlsV1_2OrLater); + connect(socket.data(), 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); + socket->setPrivateKey(key); + auto localCert = QSslCertificate::fromPath(SRCDIR "certs/fluke.cert"); + socket->setLocalCertificateChain(localCert); + socket->setSocketDescriptor(socketDescriptor, QAbstractSocket::ConnectedState); + // Stop listening. + close(); + // Start SSL handshake and ALPN: + connect(socket.data(), SIGNAL(encrypted()), + this, SLOT(connectionEncrypted())); + socket->startServerEncryption(); +} + +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::connectionEncrypted() +{ + 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() +{ + socket->ignoreSslErrors(); +} + +// 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 = inboundFrame.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 + + 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: + outboundFrame.start(FrameType::SETTINGS, FrameFlag::ACK, connectionStreamID); + outboundFrame.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; + } + + if (sessionCurrRecvWindow < inboundFrame.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 < inboundFrame.payloadSize) { + emit invalidRequest(streamID); + connectionError = true; + sendGOAWAY(connectionStreamID, FLOW_CONTROL_ERROR, connectionStreamID); + return; + } + + it->second -= inboundFrame.payloadSize; + if (it->second < streamRecvWindowSize / 2) { + sendWINDOW_UPDATE(streamID, streamRecvWindowSize / 2); + it->second += streamRecvWindowSize / 2; + } + + sessionCurrRecvWindow -= inboundFrame.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()); + + outboundFrame.start(FrameType::HEADERS, FrameFlag::END_HEADERS, streamID); + if (emptyBody) + outboundFrame.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(outboundFrame.rawFrameBuffer()); + const bool result = encoder.encodeResponse(ostream, header); + Q_ASSERT(result); + Q_UNUSED(result) + + const quint32 maxFrameSize(clientSetting(Settings::MAX_FRAME_SIZE_ID, Http2::maxFrameSize)); + outboundFrame.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; + } + + continuedRequest.clear(); + // 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 ... +} + +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..00cfde944b --- /dev/null +++ b/tests/auto/network/access/http2/http2srv.h @@ -0,0 +1,166 @@ +/**************************************************************************** +** +** 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 <QtCore/qscopedpointer.h> +#include <QtNetwork/qtcpserver.h> +#include <QtNetwork/qsslsocket.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(const Http2Settings &serverSettings, + const Http2Settings &clientSettings); + + ~Http2Server(); + + // To be called before server started: + 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 connectionEncrypted(); + void readReady(); + +private: + void incomingConnection(qintptr socketDescriptor) Q_DECL_OVERRIDE; + + quint32 clientSetting(Http2::Settings identifier, quint32 defaultValue); + + QScopedPointer<QSslSocket> 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 inboundFrame; + Http2::FrameWriter outboundFrame; + + using FrameSequence = std::vector<Http2::FrameReader>; + 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; + +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..dbb89db0f9 --- /dev/null +++ b/tests/auto/network/access/http2/tst_http2.cpp @@ -0,0 +1,449 @@ +/**************************************************************************** +** +** 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> + +// 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) +#define QT_ALPN +#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(); + +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(); + // TODO: different parameters like client/server settings ... + Http2Server *newServer(const Http2Settings &serverSettings); + // 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 windowUpdates = 0; + bool prefaceOK = false; + bool serverGotSettingsACK = false; + + static const Http2Settings defaultServerSettings; +}; + +const Http2Settings tst_Http2::defaultServerSettings{{Http2::Settings::MAX_CONCURRENT_STREAMS_ID, 100}}; + +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() +{ +#ifndef QT_ALPN + QSKIP("This test requires ALPN support"); +#endif + clearHTTP2State(); + + serverPort = 0; + nRequests = 1; + + auto srv = newServer(defaultServerSettings); + + QMetaObject::invokeMethod(srv, "startServer", Qt::QueuedConnection); + runEventLoop(); + + QVERIFY(serverPort != 0); + + const QUrl url(QString("https://127.0.0.1:%1/index.html").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()); + + QMetaObject::invokeMethod(srv, "deleteLater", Qt::QueuedConnection); +} + +void tst_Http2::multipleRequests() +{ +#ifndef QT_ALPN + QSKIP("This test requires ALPN support"); +#endif + clearHTTP2State(); + + serverPort = 0; + nRequests = 10; + + auto srv = newServer(defaultServerSettings); + + QMetaObject::invokeMethod(srv, "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); + + QMetaObject::invokeMethod(srv, "deleteLater", Qt::QueuedConnection); +} + +void tst_Http2::flowControlClientSide() +{ +#ifndef QT_ALPN + QSKIP("This test requires ALPN support"); +#endif + // 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}}; + + auto srv = newServer(serverSettings); + + const QByteArray respond(int(Http2::defaultSessionWindowSize * 100), 'x'); + srv->setResponseBody(respond); + + QMetaObject::invokeMethod(srv, "startServer", Qt::QueuedConnection); + + runEventLoop(); + QVERIFY(serverPort != 0); + + for (int i = 0; i < nRequests; ++i) + sendRequest(i); + + runEventLoop(10000); + + QVERIFY(nRequests == 0); + QVERIFY(prefaceOK); + QVERIFY(serverGotSettingsACK); + QVERIFY(windowUpdates > 0); + + QMetaObject::invokeMethod(srv, "deleteLater", Qt::QueuedConnection); +} + +void tst_Http2::flowControlServerSide() +{ +#ifndef QT_ALPN + QSKIP("This test requires ALPN support"); +#endif + // 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}}; + + auto srv = newServer(serverSettings); + + const QByteArray payload(int(Http2::defaultSessionWindowSize * 1000), 'x'); + + QMetaObject::invokeMethod(srv, "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); + + QMetaObject::invokeMethod(srv, "deleteLater", Qt::QueuedConnection); + srv = nullptr; +} + +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) +{ + using namespace Http2; + // Client's settings are fixed by qhttp2protocolhandler. + const Http2Settings clientSettings = {{Settings::MAX_FRAME_SIZE_ID, quint32(Http2::maxFrameSize)}, + {Settings::ENABLE_PUSH_ID, quint32(0)}}; + auto srv = new Http2Server(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("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) +{ + 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" |