mirror of
https://github.com/crystalidea/qt6windows7.git
synced 2025-07-07 17:50:59 +08:00
qt 6.5.1 original
This commit is contained in:
17
tests/auto/network/access/http2/CMakeLists.txt
Normal file
17
tests/auto/network/access/http2/CMakeLists.txt
Normal file
@ -0,0 +1,17 @@
|
||||
# Copyright (C) 2022 The Qt Company Ltd.
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
#####################################################################
|
||||
## tst_http2 Test:
|
||||
#####################################################################
|
||||
|
||||
qt_internal_add_test(tst_http2
|
||||
SOURCES
|
||||
http2srv.cpp http2srv.h
|
||||
tst_http2.cpp
|
||||
LIBRARIES
|
||||
Qt::CorePrivate
|
||||
Qt::Network
|
||||
Qt::NetworkPrivate
|
||||
Qt::TestPrivate
|
||||
)
|
34
tests/auto/network/access/http2/certs/fluke.cert
Normal file
34
tests/auto/network/access/http2/certs/fluke.cert
Normal file
@ -0,0 +1,34 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
-----END CERTIFICATE-----
|
52
tests/auto/network/access/http2/certs/fluke.key
Normal file
52
tests/auto/network/access/http2/certs/fluke.key
Normal file
@ -0,0 +1,52 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
g0veFbyfzlJ0q8qzD/iffqqIa2ZSmQ==
|
||||
-----END PRIVATE KEY-----
|
985
tests/auto/network/access/http2/http2srv.cpp
Normal file
985
tests/auto/network/access/http2/http2srv.cpp
Normal file
@ -0,0 +1,985 @@
|
||||
// Copyright (C) 2016 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#include <QTest>
|
||||
|
||||
#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/qtimer.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 <= quint32(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(H2Type type, const RawSettings &ss, const RawSettings &cs)
|
||||
: connectionType(type),
|
||||
serverSettings(ss),
|
||||
expectedClientSettings(cs)
|
||||
{
|
||||
#if !QT_CONFIG(ssl)
|
||||
Q_ASSERT(type != H2Type::h2Alpn && type != H2Type::h2Direct);
|
||||
#endif
|
||||
|
||||
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::setContentEncoding(const QByteArray &encoding)
|
||||
{
|
||||
contentEncoding = encoding;
|
||||
}
|
||||
|
||||
void Http2Server::setAuthenticationHeader(const QByteArray &authentication)
|
||||
{
|
||||
authenticationHeader = authentication;
|
||||
}
|
||||
|
||||
void Http2Server::setRedirect(const QByteArray &url, int count)
|
||||
{
|
||||
redirectUrl = url;
|
||||
redirectCount = count;
|
||||
}
|
||||
|
||||
void Http2Server::setSendTrailingHEADERS(bool enable)
|
||||
{
|
||||
sendTrailingHEADERS = enable;
|
||||
}
|
||||
|
||||
void Http2Server::emulateGOAWAY(int timeout)
|
||||
{
|
||||
Q_ASSERT(timeout >= 0);
|
||||
testingGOAWAY = true;
|
||||
goawayTimeout = timeout;
|
||||
}
|
||||
|
||||
void Http2Server::redirectOpenStream(quint16 port)
|
||||
{
|
||||
redirectWhileReading = true;
|
||||
targetPort = port;
|
||||
}
|
||||
|
||||
bool Http2Server::isClearText() const
|
||||
{
|
||||
return connectionType == H2Type::h2c || connectionType == H2Type::h2cDirect;
|
||||
}
|
||||
|
||||
QByteArray Http2Server::requestAuthorizationHeader()
|
||||
{
|
||||
const auto isAuthHeader = [](const HeaderField &field) {
|
||||
return field.name == "authorization";
|
||||
};
|
||||
const auto requestHeaders = decoder.decodedHeader();
|
||||
const auto authentication =
|
||||
std::find_if(requestHeaders.cbegin(), requestHeaders.cend(), isAuthHeader);
|
||||
return authentication == requestHeaders.cend() ? QByteArray() : authentication->value;
|
||||
}
|
||||
|
||||
void Http2Server::startServer()
|
||||
{
|
||||
if (listen()) {
|
||||
if (isClearText())
|
||||
authority = QStringLiteral("127.0.0.1:%1").arg(serverPort()).toLatin1();
|
||||
emit serverStarted(serverPort());
|
||||
}
|
||||
}
|
||||
|
||||
bool Http2Server::sendProtocolSwitchReply()
|
||||
{
|
||||
Q_ASSERT(socket);
|
||||
Q_ASSERT(connectionType == H2Type::h2c);
|
||||
// The first and the last HTTP/1.1 response we send:
|
||||
const char response[] = "HTTP/1.1 101 Switching Protocols\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Upgrade: h2c\r\n\r\n";
|
||||
const qint64 size = sizeof response - 1;
|
||||
return socket->write(response, size) == size;
|
||||
}
|
||||
|
||||
void Http2Server::sendServerSettings()
|
||||
{
|
||||
Q_ASSERT(socket);
|
||||
|
||||
if (!serverSettings.size())
|
||||
return;
|
||||
|
||||
writer.start(FrameType::SETTINGS, FrameFlag::EMPTY, connectionStreamID);
|
||||
for (auto it = serverSettings.cbegin(); it != serverSettings.cend(); ++it) {
|
||||
writer.append(it.key());
|
||||
writer.append(it.value());
|
||||
if (it.key() == Settings::INITIAL_WINDOW_SIZE_ID)
|
||||
streamRecvWindowSize = it.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()));
|
||||
|
||||
quint32 bytesToSend = std::min<quint32>(windowSize, responseBody.size() - offset);
|
||||
quint32 bytesSent = 0;
|
||||
const quint32 frameSizeLimit(clientSetting(Settings::MAX_FRAME_SIZE_ID, Http2::minPayloadLimit));
|
||||
const uchar *src = reinterpret_cast<const uchar *>(responseBody.constData() + offset);
|
||||
const bool last = offset + bytesToSend == quint32(responseBody.size());
|
||||
|
||||
// The payload can significantly exceed frameSizeLimit. Internally, writer
|
||||
// will do needed fragmentation, but if some test failed, there is no need
|
||||
// to wait for writer to send all DATA frames, we check 'interrupted' and
|
||||
// stop early instead.
|
||||
const quint32 framesInChunk = 10;
|
||||
while (bytesToSend) {
|
||||
if (interrupted.loadAcquire())
|
||||
return;
|
||||
const quint32 chunkSize = std::min<quint32>(framesInChunk * frameSizeLimit, bytesToSend);
|
||||
writer.start(FrameType::DATA, FrameFlag::EMPTY, streamID);
|
||||
writer.writeDATA(*socket, frameSizeLimit, src, chunkSize);
|
||||
src += chunkSize;
|
||||
bytesToSend -= chunkSize;
|
||||
bytesSent += chunkSize;
|
||||
if (frameSizeLimit != Http2::minPayloadLimit) {
|
||||
// Our test is probably interested in how many DATA frames were sent.
|
||||
emit sendingData();
|
||||
}
|
||||
}
|
||||
|
||||
if (interrupted.loadAcquire())
|
||||
return;
|
||||
|
||||
if (last) {
|
||||
if (sendTrailingHEADERS) {
|
||||
writer.start(FrameType::HEADERS,
|
||||
FrameFlag::PRIORITY | FrameFlag::END_HEADERS | FrameFlag::END_STREAM, streamID);
|
||||
const quint32 maxFrameSize(clientSetting(Settings::MAX_FRAME_SIZE_ID,
|
||||
Http2::maxPayloadSize));
|
||||
// 5 bytes for PRIORITY data:
|
||||
writer.append(quint32(0)); // streamID 0 (32-bit)
|
||||
writer.append(quint8(0)); // + weight 0 (8-bit)
|
||||
writer.writeHEADERS(*socket, maxFrameSize);
|
||||
} else {
|
||||
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 += bytesSent;
|
||||
}
|
||||
}
|
||||
|
||||
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 (isClearText()) {
|
||||
socket.reset(new QTcpSocket);
|
||||
const bool set = socket->setSocketDescriptor(socketDescriptor);
|
||||
Q_ASSERT(set);
|
||||
// Stop listening:
|
||||
close();
|
||||
upgradeProtocol = connectionType == H2Type::h2c;
|
||||
QMetaObject::invokeMethod(this, "connectionEstablished",
|
||||
Qt::QueuedConnection);
|
||||
} else {
|
||||
#if QT_CONFIG(ssl)
|
||||
socket.reset(new QSslSocket);
|
||||
QSslSocket *sslSocket = static_cast<QSslSocket *>(socket.data());
|
||||
|
||||
if (connectionType == H2Type::h2Alpn) {
|
||||
// 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(QT_TESTCASE_SOURCEDIR "/certs/fluke.key");
|
||||
file.open(QIODevice::ReadOnly);
|
||||
QSslKey key(file.readAll(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey);
|
||||
sslSocket->setPrivateKey(key);
|
||||
auto localCert = QSslCertificate::fromPath(QT_TESTCASE_SOURCEDIR "/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_ASSERT(0);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
quint32 Http2Server::clientSetting(Http2::Settings identifier, quint32 defaultValue)
|
||||
{
|
||||
const auto it = expectedClientSettings.find(identifier);
|
||||
if (it != expectedClientSettings.end())
|
||||
return it.value();
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
bool Http2Server::readMethodLine()
|
||||
{
|
||||
// We know for sure that Qt did the right thing sending us the correct
|
||||
// Request-line with CRLF at the end ...
|
||||
// We're overly simplistic here but all we need to know - the method.
|
||||
while (socket->bytesAvailable()) {
|
||||
char c = 0;
|
||||
if (socket->read(&c, 1) != 1)
|
||||
return false;
|
||||
if (c == '\n' && requestLine.endsWith('\r')) {
|
||||
if (requestLine.startsWith("GET"))
|
||||
requestType = QHttpNetworkRequest::Get;
|
||||
else if (requestLine.startsWith("POST"))
|
||||
requestType = QHttpNetworkRequest::Post;
|
||||
else
|
||||
requestType = QHttpNetworkRequest::Custom; // 'invalid'.
|
||||
requestLine.clear();
|
||||
|
||||
return true;
|
||||
} else {
|
||||
requestLine.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Http2Server::verifyProtocolUpgradeRequest()
|
||||
{
|
||||
Q_ASSERT(protocolUpgradeHandler.data());
|
||||
|
||||
bool connectionOk = false;
|
||||
bool upgradeOk = false;
|
||||
bool settingsOk = false;
|
||||
|
||||
QHttpNetworkReplyPrivate *firstRequestReader = protocolUpgradeHandler->d_func();
|
||||
|
||||
// That's how we append them, that's what I expect to find:
|
||||
for (const auto &header : firstRequestReader->headers()) {
|
||||
if (header.first == "Connection")
|
||||
connectionOk = header.second.contains("Upgrade, HTTP2-Settings");
|
||||
else if (header.first == "Upgrade")
|
||||
upgradeOk = header.second.contains("h2c");
|
||||
else if (header.first == "HTTP2-Settings")
|
||||
settingsOk = true;
|
||||
}
|
||||
|
||||
return connectionOk && upgradeOk && settingsOk;
|
||||
}
|
||||
|
||||
void Http2Server::triggerGOAWAYEmulation()
|
||||
{
|
||||
Q_ASSERT(testingGOAWAY);
|
||||
auto timer = new QTimer(this);
|
||||
timer->setSingleShot(true);
|
||||
connect(timer, &QTimer::timeout, [this]() {
|
||||
sendGOAWAY(quint32(connectionStreamID), quint32(INTERNAL_ERROR), 0);
|
||||
});
|
||||
timer->start(goawayTimeout);
|
||||
}
|
||||
|
||||
void Http2Server::connectionEstablished()
|
||||
{
|
||||
using namespace Http2;
|
||||
|
||||
if (testingGOAWAY && !isClearText())
|
||||
return triggerGOAWAYEmulation();
|
||||
|
||||
// For clearTextHTTP2 we first have to respond with 'protocol switch'
|
||||
// and then continue with whatever logic we have (testingGOAWAY or not),
|
||||
// otherwise our 'peer' cannot process HTTP/2 frames yet.
|
||||
|
||||
connect(socket.data(), SIGNAL(readyRead()),
|
||||
this, SLOT(readReady()));
|
||||
|
||||
waitingClientPreface = true;
|
||||
waitingClientAck = false;
|
||||
waitingClientSettings = false;
|
||||
settingsSent = false;
|
||||
|
||||
if (connectionType == H2Type::h2c) {
|
||||
requestLine.clear();
|
||||
// Now we have to handle HTTP/1.1 request. We use Get/Post in our test,
|
||||
// so set requestType to something unsupported:
|
||||
requestType = QHttpNetworkRequest::Options;
|
||||
} else {
|
||||
// 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 (redirectSent) {
|
||||
// We are a "single shot" server, working in 'h2' mode,
|
||||
// responding with a redirect code. Don't bother to handle
|
||||
// anything else now.
|
||||
return;
|
||||
}
|
||||
|
||||
if (upgradeProtocol) {
|
||||
handleProtocolUpgrade();
|
||||
} else 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::handleProtocolUpgrade()
|
||||
{
|
||||
using ReplyPrivate = QHttpNetworkReplyPrivate;
|
||||
Q_ASSERT(upgradeProtocol);
|
||||
|
||||
if (!protocolUpgradeHandler.data())
|
||||
protocolUpgradeHandler.reset(new Http11Reply);
|
||||
|
||||
QHttpNetworkReplyPrivate *firstRequestReader = protocolUpgradeHandler->d_func();
|
||||
|
||||
// QHttpNetworkReplyPrivate parses ... reply. It will, unfortunately, fail
|
||||
// on the first line ... which is a part of request. So we read this line
|
||||
// and extract the method first.
|
||||
if (firstRequestReader->state == ReplyPrivate::NothingDoneState) {
|
||||
if (!readMethodLine())
|
||||
return;
|
||||
|
||||
if (requestType != QHttpNetworkRequest::Get && requestType != QHttpNetworkRequest::Post) {
|
||||
emit invalidRequest(1);
|
||||
return;
|
||||
}
|
||||
|
||||
firstRequestReader->state = ReplyPrivate::ReadingHeaderState;
|
||||
}
|
||||
|
||||
if (!socket->bytesAvailable())
|
||||
return;
|
||||
|
||||
if (firstRequestReader->state == ReplyPrivate::ReadingHeaderState)
|
||||
firstRequestReader->readHeader(socket.data());
|
||||
else if (firstRequestReader->state == ReplyPrivate::ReadingDataState)
|
||||
firstRequestReader->readBodyFast(socket.data(), &firstRequestReader->responseData);
|
||||
|
||||
switch (firstRequestReader->state) {
|
||||
case ReplyPrivate::ReadingHeaderState:
|
||||
return;
|
||||
case ReplyPrivate::ReadingDataState:
|
||||
if (requestType == QHttpNetworkRequest::Post)
|
||||
return;
|
||||
break;
|
||||
case ReplyPrivate::AllDoneState:
|
||||
break;
|
||||
default:
|
||||
socket->close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!verifyProtocolUpgradeRequest() || !sendProtocolSwitchReply()) {
|
||||
socket->close();
|
||||
return;
|
||||
}
|
||||
|
||||
upgradeProtocol = false;
|
||||
protocolUpgradeHandler.reset(nullptr);
|
||||
|
||||
if (testingGOAWAY)
|
||||
return triggerGOAWAYEmulation();
|
||||
|
||||
// HTTP/1.1 'fields' we have in firstRequestRead are useless (they are not
|
||||
// even allowed in HTTP/2 header). Let's pretend we have received
|
||||
// valid HTTP/2 headers and can extract fields we need:
|
||||
HttpHeader h2header;
|
||||
h2header.push_back(HeaderField(":scheme", "http")); // we are in clearTextHTTP2 mode.
|
||||
h2header.push_back(HeaderField(":authority", authority));
|
||||
activeRequests[1] = std::move(h2header);
|
||||
// After protocol switch we immediately send our SETTINGS.
|
||||
sendServerSettings();
|
||||
if (requestType == QHttpNetworkRequest::Get)
|
||||
emit receivedRequest(1);
|
||||
else
|
||||
emit receivedData(1);
|
||||
}
|
||||
|
||||
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 (testingGOAWAY) {
|
||||
// GOAWAY test is simplistic for now: after HTTP/2 was
|
||||
// negotiated (via ALPN/NPN or a protocol switch), send
|
||||
// a GOAWAY frame after some (probably non-zero) timeout.
|
||||
// We do not handle any frames, but timeout gives QNAM
|
||||
// more time to initiate more streams and thus make the
|
||||
// test more interesting/complex (on a client side).
|
||||
return;
|
||||
}
|
||||
|
||||
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 = Http2::Settings(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)) {
|
||||
if (responseBody.isEmpty()) {
|
||||
closedStreams.insert(streamID); // Enter "half-closed remote" state.
|
||||
streamWindows.erase(it);
|
||||
}
|
||||
emit receivedData(streamID);
|
||||
}
|
||||
emit receivedDATAFrame(streamID,
|
||||
QByteArray(reinterpret_cast<const char *>(inboundFrame.dataBegin()),
|
||||
inboundFrame.dataSize()));
|
||||
}
|
||||
|
||||
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::maxPayloadSize));
|
||||
|
||||
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);
|
||||
|
||||
// We assume any auth is correct. Leaves the checking to the test itself
|
||||
const bool hasAuth = !requestAuthorizationHeader().isEmpty();
|
||||
|
||||
HttpHeader header;
|
||||
if (redirectWhileReading) {
|
||||
if (redirectSent) {
|
||||
// This is a "single-shot" server responding with a redirect code.
|
||||
return;
|
||||
}
|
||||
|
||||
redirectSent = true;
|
||||
|
||||
qDebug("server received HEADERS frame (followed by DATA frames), redirecting ...");
|
||||
Q_ASSERT(targetPort);
|
||||
header.push_back({":status", "308"});
|
||||
const QString url("%1://localhost:%2/");
|
||||
header.push_back({"location", url.arg(isClearText() ? QStringLiteral("http") : QStringLiteral("https"),
|
||||
QString::number(targetPort)).toLatin1()});
|
||||
} else if (redirectCount > 0) { // Not redirecting while reading, unlike above
|
||||
--redirectCount;
|
||||
header.push_back({":status", "308"});
|
||||
header.push_back({"location", redirectUrl});
|
||||
} else if (!authenticationHeader.isEmpty() && !hasAuth) {
|
||||
header.push_back({ ":status", "401" });
|
||||
header.push_back(HPack::HeaderField("www-authenticate", authenticationHeader));
|
||||
authenticationHeader.clear();
|
||||
} else {
|
||||
header.push_back({":status", "200"});
|
||||
}
|
||||
|
||||
if (!emptyBody) {
|
||||
header.push_back(HPack::HeaderField("content-length",
|
||||
QString("%1").arg(responseBody.size()).toLatin1()));
|
||||
}
|
||||
|
||||
if (!contentEncoding.isEmpty())
|
||||
header.push_back(HPack::HeaderField("content-encoding", contentEncoding));
|
||||
|
||||
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::stopSendingDATAFrames()
|
||||
{
|
||||
interrupted.storeRelease(1);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (redirectWhileReading) {
|
||||
sendResponse(streamID, true);
|
||||
// Don't try to read any DATA frames ...
|
||||
socket->disconnect();
|
||||
} // else - we're waiting for incoming DATA frames ...
|
||||
|
||||
continuedRequest.clear();
|
||||
}
|
||||
|
||||
QT_END_NAMESPACE
|
217
tests/auto/network/access/http2/http2srv.h
Normal file
217
tests/auto/network/access/http2/http2srv.h
Normal file
@ -0,0 +1,217 @@
|
||||
// Copyright (C) 2016 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
|
||||
#ifndef HTTP2SRV_H
|
||||
#define HTTP2SRV_H
|
||||
|
||||
#include <QtNetwork/private/qhttpnetworkrequest_p.h>
|
||||
#include <QtNetwork/private/qhttpnetworkreply_p.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/qsharedpointer.h>
|
||||
#include <QtCore/qscopedpointer.h>
|
||||
#include <QtNetwork/qtcpserver.h>
|
||||
#include <QtCore/qbytearray.h>
|
||||
#include <QtCore/qatomic.h>
|
||||
#include <QtCore/qglobal.h>
|
||||
#include <QtCore/qmap.h>
|
||||
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
// At the moment we do not have any public API parsing HTTP headers. Even worse -
|
||||
// the code that can do this exists only in QHttpNetworkReplyPrivate class.
|
||||
// To be able to access reply's d_func() we have these classes:
|
||||
class Http11ReplyPrivate : public QHttpNetworkReplyPrivate
|
||||
{
|
||||
};
|
||||
|
||||
class Http11Reply : public QHttpNetworkReply
|
||||
{
|
||||
public:
|
||||
Q_DECLARE_PRIVATE(Http11Reply)
|
||||
};
|
||||
|
||||
enum class H2Type {
|
||||
h2Alpn, // Secure connection, ALPN to negotiate h2.
|
||||
h2c, // Clear text with protocol upgrade.
|
||||
h2Direct, // Secure connection, ALPN not supported.
|
||||
h2cDirect, // Clear text direct
|
||||
};
|
||||
|
||||
using RawSettings = QMap<Http2::Settings, quint32>;
|
||||
|
||||
class Http2Server : public QTcpServer
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
|
||||
Http2Server(H2Type type, const RawSettings &serverSettings,
|
||||
const RawSettings &clientSettings);
|
||||
|
||||
~Http2Server();
|
||||
|
||||
|
||||
// To be called before server started:
|
||||
void enablePushPromise(bool enabled, const QByteArray &path = QByteArray());
|
||||
void setResponseBody(const QByteArray &body);
|
||||
// No content encoding is actually performed, call setResponseBody with already encoded data
|
||||
void setContentEncoding(const QByteArray &contentEncoding);
|
||||
// No authentication data is generated for the method, the full header value must be set
|
||||
void setAuthenticationHeader(const QByteArray &authentication);
|
||||
// Set the redirect URL and count. The server will return a redirect response with the url
|
||||
// 'count' amount of times
|
||||
void setRedirect(const QByteArray &redirectUrl, int count);
|
||||
// Send a trailing HEADERS frame with PRIORITY and END_STREAM flag
|
||||
void setSendTrailingHEADERS(bool enable);
|
||||
void emulateGOAWAY(int timeout);
|
||||
void redirectOpenStream(quint16 targetPort);
|
||||
|
||||
bool isClearText() const;
|
||||
|
||||
QByteArray requestAuthorizationHeader();
|
||||
|
||||
// Invokables, since we can call them from the main thread,
|
||||
// but server (can) work on its own thread.
|
||||
Q_INVOKABLE void startServer();
|
||||
bool sendProtocolSwitchReply();
|
||||
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 handleProtocolUpgrade();
|
||||
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);
|
||||
|
||||
void stopSendingDATAFrames();
|
||||
|
||||
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);
|
||||
// Emitted for every DATA frame. Includes the content of the frame as \a body.
|
||||
void receivedDATAFrame(quint32 streamID, const QByteArray &body);
|
||||
void windowUpdate(quint32 streamID);
|
||||
void sendingData();
|
||||
|
||||
private slots:
|
||||
void connectionEstablished();
|
||||
void readReady();
|
||||
|
||||
private:
|
||||
void incomingConnection(qintptr socketDescriptor) override;
|
||||
|
||||
quint32 clientSetting(Http2::Settings identifier, quint32 defaultValue);
|
||||
bool readMethodLine();
|
||||
bool verifyProtocolUpgradeRequest();
|
||||
void triggerGOAWAYEmulation();
|
||||
|
||||
QScopedPointer<QAbstractSocket> socket;
|
||||
|
||||
H2Type connectionType = H2Type::h2Alpn;
|
||||
// Connection preface:
|
||||
bool waitingClientPreface = false;
|
||||
bool waitingClientSettings = false;
|
||||
bool settingsSent = false;
|
||||
bool waitingClientAck = false;
|
||||
|
||||
RawSettings serverSettings;
|
||||
RawSettings 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 pushPromiseEnabled = false;
|
||||
quint32 lastPromisedStream = 0;
|
||||
QByteArray pushPath;
|
||||
|
||||
bool testingGOAWAY = false;
|
||||
int goawayTimeout = 0;
|
||||
|
||||
// Clear text HTTP/2, we have to deal with the protocol upgrade request
|
||||
// from the initial HTTP/1.1 request.
|
||||
bool upgradeProtocol = false;
|
||||
QByteArray requestLine;
|
||||
QHttpNetworkRequest::Operation requestType;
|
||||
// We need QHttpNetworkReply (actually its private d-object) to handle the
|
||||
// first HTTP/1.1 request. QHttpNetworkReplyPrivate does parsing + in case
|
||||
// of POST it is also reading the body for us.
|
||||
QScopedPointer<Http11Reply> protocolUpgradeHandler;
|
||||
// We need it for PUSH_PROMISE, with the correct port number appended,
|
||||
// when replying to essentially 1.1 request.
|
||||
QByteArray authority;
|
||||
// Redirect, with status code 308, as soon as we've seen headers, while client
|
||||
// may still be sending DATA frames. See tst_Http2::earlyResponse().
|
||||
bool redirectWhileReading = false;
|
||||
bool redirectSent = false;
|
||||
quint16 targetPort = 0;
|
||||
QAtomicInt interrupted;
|
||||
|
||||
QByteArray contentEncoding;
|
||||
QByteArray authenticationHeader;
|
||||
|
||||
QByteArray redirectUrl;
|
||||
int redirectCount = 0;
|
||||
|
||||
bool sendTrailingHEADERS = false;
|
||||
protected slots:
|
||||
void ignoreErrorSlot();
|
||||
};
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#endif
|
||||
|
1527
tests/auto/network/access/http2/tst_http2.cpp
Normal file
1527
tests/auto/network/access/http2/tst_http2.cpp
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user