summaryrefslogtreecommitdiffstats
path: root/tests/auto/network/access/http2
diff options
context:
space:
mode:
Diffstat (limited to 'tests/auto/network/access/http2')
-rw-r--r--tests/auto/network/access/http2/certs/fluke.cert75
-rw-r--r--tests/auto/network/access/http2/certs/fluke.key15
-rw-r--r--tests/auto/network/access/http2/http2.pro8
-rw-r--r--tests/auto/network/access/http2/http2srv.cpp695
-rw-r--r--tests/auto/network/access/http2/http2srv.h172
-rw-r--r--tests/auto/network/access/http2/tst_http2.cpp539
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"