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>bb10
parent
c2f4705f23
commit
205ff27260
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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-----
|
||||
|
|
@ -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-----
|
||||
|
|
@ -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/\\\"
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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"
|
||||
Loading…
Reference in New Issue