HTTP/2 - implement the proper 'h2c' (protocol upgrade)

Without TLS (and thus ALPN/NPN negotiation) HTTP/2 requires
a protocol upgrade procedure, as described in RFC 7540, 3.2.
We start as HTTP/1.1 (and thus we create QHttpProtocolHandler first),
augmenting the headers we send with 'Upgrade: h2c'. In case
we receive HTTP/1.1 response with status code 101 ('Switching
Protocols'), we continue as HTTP/2 session, creating QHttp2ProtocolHandler
and pretending the first request we sent was HTTP/2 request
on a real HTTP/2 stream. If the first response is something different
from 101, we continue as HTTP/1.1. This change also required
auto-test update: our toy-server now has to respond to
the initial HTTP/1.1 request on a platform without ALPN/NPN.
As a bonus a subtle flakyness in 'goaway' auto-test went
away (well, it was fixed).

[ChangeLog][QtNetwork][HTTP/2] In case of clear text HTTP/2 we
now initiate a required protocol upgrade procedure instead of
'H2Direct' connection.

Task-number: QTBUG-61397
Change-Id: I573fa304fdaf661490159037dc47775d97c8ea5b
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
Reviewed-by: Timur Pocheptsov <timur.pocheptsov@qt.io>
This commit is contained in:
Timur Pocheptsov 2017-07-27 14:34:39 +02:00
parent fabedd399e
commit 53357f0156
14 changed files with 430 additions and 51 deletions

View File

@ -361,6 +361,12 @@ FrameWriter::FrameWriter(FrameType type, FrameFlags flags, quint32 streamID)
start(type, flags, streamID);
}
void FrameWriter::setOutboundFrame(Frame &&newFrame)
{
frame = std::move(newFrame);
updatePayloadSize();
}
void FrameWriter::start(FrameType type, FrameFlags flags, quint32 streamID)
{
auto &buffer = frame.buffer;

View File

@ -129,6 +129,8 @@ public:
return frame;
}
void setOutboundFrame(Frame &&newFrame);
// Frame 'builders':
void start(FrameType type, FrameFlags flags, quint32 streamID);
void setPayloadSize(quint32 size);

View File

@ -37,9 +37,14 @@
**
****************************************************************************/
#include <QtCore/qstring.h>
#include "http2protocol_p.h"
#include "http2frames_p.h"
#include "private/qhttpnetworkrequest_p.h"
#include "private/qhttpnetworkreply_p.h"
#include <QtCore/qbytearray.h>
#include <QtCore/qstring.h>
QT_BEGIN_NAMESPACE
@ -57,6 +62,38 @@ const char Http2clientPreface[clientPrefaceLength] =
0x2e, 0x30, 0x0d, 0x0a, 0x0d, 0x0a,
0x53, 0x4d, 0x0d, 0x0a, 0x0d, 0x0a};
QByteArray default_SETTINGS_to_Base64()
{
Frame frame(default_SETTINGS_frame());
// SETTINGS frame's payload consists of pairs:
// 2-byte-identifier | 4-byte-value == multiple of 6.
Q_ASSERT(frame.payloadSize() && !(frame.payloadSize() % 6));
const char *src = reinterpret_cast<const char *>(frame.dataBegin());
const QByteArray wrapper(QByteArray::fromRawData(src, int(frame.dataSize())));
// 3.2.1
// The content of the HTTP2-Settings header field is the payload
// of a SETTINGS frame (Section 6.5), encoded as a base64url string
// (that is, the URL- and filename-safe Base64 encoding described in
// Section 5 of [RFC4648], with any trailing '=' characters omitted).
return wrapper.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
}
void prepare_for_protocol_upgrade(QHttpNetworkRequest &request)
{
// RFC 2616, 14.10
// RFC 7540, 3.2
QByteArray value(request.headerField("Connection"));
// We _append_ 'Upgrade':
if (value.size())
value += ", ";
value += "Upgrade, HTTP2-Settings";
request.setHeaderField("Connection", value);
// This we just (re)write.
request.setHeaderField("Upgrade", "h2c");
// This we just (re)write.
request.setHeaderField("HTTP2-Settings", default_SETTINGS_to_Base64());
}
void qt_error(quint32 errorCode, QNetworkReply::NetworkError &error,
QString &errorMessage)
@ -151,6 +188,40 @@ QNetworkReply::NetworkError qt_error(quint32 errorCode)
return error;
}
bool is_PUSH_PROMISE_enabled()
{
bool ok = false;
const int env = qEnvironmentVariableIntValue("QT_HTTP2_ENABLE_PUSH_PROMISE", &ok);
return ok && env;
}
bool is_protocol_upgraded(const QHttpNetworkReply &reply)
{
if (reply.statusCode() == 101) {
// Do some minimal checks here - we expect 'Upgrade: h2c' to be found.
const auto &header = reply.header();
for (const QPair<QByteArray, QByteArray> &field : header) {
if (field.first.toLower() == "upgrade" && field.second.toLower() == "h2c")
return true;
}
}
return false;
}
Frame default_SETTINGS_frame()
{
// 6.5 SETTINGS
FrameWriter builder(FrameType::SETTINGS, FrameFlag::EMPTY, connectionStreamID);
// MAX frame size (16 kb), disable/enable PUSH_PROMISE
builder.append(Settings::MAX_FRAME_SIZE_ID);
builder.append(quint32(maxFrameSize));
builder.append(Settings::ENABLE_PUSH_ID);
builder.append(quint32(is_PUSH_PROMISE_enabled()));
return builder.outboundFrame();
}
} // namespace Http2
QT_END_NAMESPACE

View File

@ -59,6 +59,8 @@
QT_BEGIN_NAMESPACE
class QHttpNetworkRequest;
class QHttpNetworkReply;
class QString;
namespace Http2
@ -132,6 +134,7 @@ enum Http2PredefinedParameters
const quint32 lastValidStreamID((quint32(1) << 31) - 1); // HTTP/2, 5.1.1
extern const Q_AUTOTEST_EXPORT char Http2clientPreface[clientPrefaceLength];
void prepare_for_protocol_upgrade(QHttpNetworkRequest &request);
enum class FrameStatus
{
@ -169,6 +172,9 @@ enum Http2Error
void qt_error(quint32 errorCode, QNetworkReply::NetworkError &error, QString &errorString);
QString qt_error_string(quint32 errorCode);
QNetworkReply::NetworkError qt_error(quint32 errorCode);
bool is_PUSH_PROMISE_enabled();
bool is_protocol_upgraded(const QHttpNetworkReply &reply);
struct Frame default_SETTINGS_frame();
}

View File

@ -170,10 +170,22 @@ QHttp2ProtocolHandler::QHttp2ProtocolHandler(QHttpNetworkConnectionChannel *chan
decoder(HPack::FieldLookupTable::DefaultSize),
encoder(HPack::FieldLookupTable::DefaultSize, true)
{
Q_ASSERT(channel);
continuedFrames.reserve(20);
bool ok = false;
const int env = qEnvironmentVariableIntValue("QT_HTTP2_ENABLE_PUSH_PROMISE", &ok);
pushPromiseEnabled = ok && env;
pushPromiseEnabled = is_PUSH_PROMISE_enabled();
if (!channel->ssl) {
// We upgraded from HTTP/1.1 to HTTP/2. channel->request was already sent
// as HTTP/1.1 request. The response with status code 101 triggered
// protocol switch and now we are waiting for the real response, sent
// as HTTP/2 frames.
Q_ASSERT(channel->reply);
const quint32 initialStreamID = createNewStream(HttpMessagePair(channel->request, channel->reply),
true /* uploaded by HTTP/1.1 */);
Q_ASSERT(initialStreamID == 1);
Stream &stream = activeStreams[initialStreamID];
stream.state = Stream::halfClosedLocal;
}
}
void QHttp2ProtocolHandler::_q_uploadDataReadyRead()
@ -356,12 +368,8 @@ bool QHttp2ProtocolHandler::sendClientPreface()
return false;
// 6.5 SETTINGS
frameWriter.start(FrameType::SETTINGS, FrameFlag::EMPTY, Http2::connectionStreamID);
// MAX frame size (16 kb), enable/disable PUSH
frameWriter.append(Settings::MAX_FRAME_SIZE_ID);
frameWriter.append(quint32(Http2::maxFrameSize));
frameWriter.append(Settings::ENABLE_PUSH_ID);
frameWriter.append(quint32(pushPromiseEnabled));
frameWriter.setOutboundFrame(default_SETTINGS_frame());
Q_ASSERT(frameWriter.outboundFrame().payloadSize());
if (!frameWriter.write(*m_socket))
return false;
@ -1157,7 +1165,7 @@ void QHttp2ProtocolHandler::finishStreamWithError(Stream &stream, QNetworkReply:
<< "finished with error:" << message;
}
quint32 QHttp2ProtocolHandler::createNewStream(const HttpMessagePair &message)
quint32 QHttp2ProtocolHandler::createNewStream(const HttpMessagePair &message, bool uploadDone)
{
const qint32 newStreamID = allocateStreamID();
if (!newStreamID)
@ -1178,10 +1186,12 @@ quint32 QHttp2ProtocolHandler::createNewStream(const HttpMessagePair &message)
streamInitialSendWindowSize,
streamInitialRecvWindowSize);
if (auto src = newStream.data()) {
connect(src, SIGNAL(readyRead()), this,
SLOT(_q_uploadDataReadyRead()), Qt::QueuedConnection);
src->setProperty("HTTP2StreamID", newStreamID);
if (!uploadDone) {
if (auto src = newStream.data()) {
connect(src, SIGNAL(readyRead()), this,
SLOT(_q_uploadDataReadyRead()), Qt::QueuedConnection);
src->setProperty("HTTP2StreamID", newStreamID);
}
}
activeStreams.insert(newStreamID, newStream);

View File

@ -98,7 +98,7 @@ private:
using Stream = Http2::Stream;
void _q_readyRead() override;
void _q_receiveReply() override;
Q_INVOKABLE void _q_receiveReply() override;
Q_INVOKABLE bool sendRequest() override;
bool sendClientPreface();
@ -136,7 +136,7 @@ private:
const QString &message);
// Stream's lifecycle management:
quint32 createNewStream(const HttpMessagePair &message);
quint32 createNewStream(const HttpMessagePair &message, bool uploadDone = false);
void addToSuspended(Stream &stream);
void markAsReset(quint32 streamID);
quint32 popStreamToResume();

View File

@ -627,7 +627,8 @@ QHttpNetworkReply* QHttpNetworkConnectionPrivate::queueRequest(const QHttpNetwor
if (request.isPreConnect())
preConnectRequests++;
if (connectionType == QHttpNetworkConnection::ConnectionTypeHTTP) {
if (connectionType == QHttpNetworkConnection::ConnectionTypeHTTP
|| (!encrypt && connectionType == QHttpNetworkConnection::ConnectionTypeHTTP2 && !channels[0].switchedToHttp2)) {
switch (request.priority()) {
case QHttpNetworkRequest::HighPriority:
highPriorityQueue.prepend(pair);
@ -638,7 +639,7 @@ QHttpNetworkReply* QHttpNetworkConnectionPrivate::queueRequest(const QHttpNetwor
break;
}
}
else { // SPDY, HTTP/2
else { // SPDY, HTTP/2 ('h2' mode)
if (!pair.second->d_func()->requestIsPrepared)
prepareRequest(pair);
channels[0].spdyRequestsToSend.insertMulti(request.priority(), pair);
@ -672,6 +673,25 @@ QHttpNetworkReply* QHttpNetworkConnectionPrivate::queueRequest(const QHttpNetwor
return reply;
}
void QHttpNetworkConnectionPrivate::fillHttp2Queue()
{
for (auto &pair : highPriorityQueue) {
if (!pair.second->d_func()->requestIsPrepared)
prepareRequest(pair);
channels[0].spdyRequestsToSend.insertMulti(QHttpNetworkRequest::HighPriority, pair);
}
highPriorityQueue.clear();
for (auto &pair : lowPriorityQueue) {
if (!pair.second->d_func()->requestIsPrepared)
prepareRequest(pair);
channels[0].spdyRequestsToSend.insertMulti(pair.first.priority(), pair);
}
lowPriorityQueue.clear();
}
void QHttpNetworkConnectionPrivate::requeueRequest(const HttpMessagePair &pair)
{
Q_Q(QHttpNetworkConnection);
@ -1047,8 +1067,7 @@ void QHttpNetworkConnectionPrivate::_q_startNextRequest()
}
case QHttpNetworkConnection::ConnectionTypeHTTP2:
case QHttpNetworkConnection::ConnectionTypeSPDY: {
if (channels[0].spdyRequestsToSend.isEmpty())
if (channels[0].spdyRequestsToSend.isEmpty() && channels[0].switchedToHttp2)
return;
if (networkLayerState == IPv4)
@ -1057,7 +1076,7 @@ void QHttpNetworkConnectionPrivate::_q_startNextRequest()
channels[0].networkLayerPreference = QAbstractSocket::IPv6Protocol;
channels[0].ensureConnection();
if (channels[0].socket && channels[0].socket->state() == QAbstractSocket::ConnectedState
&& !channels[0].pendingEncrypt)
&& !channels[0].pendingEncrypt && channels[0].spdyRequestsToSend.size())
channels[0].sendRequest();
break;
}
@ -1355,6 +1374,12 @@ QHttpNetworkReply* QHttpNetworkConnection::sendRequest(const QHttpNetworkRequest
return d->queueRequest(request);
}
void QHttpNetworkConnection::fillHttp2Queue()
{
Q_D(QHttpNetworkConnection);
d->fillHttp2Queue();
}
bool QHttpNetworkConnection::isSsl() const
{
Q_D(const QHttpNetworkConnection);

View File

@ -122,6 +122,7 @@ public:
//add a new HTTP request through this connection
QHttpNetworkReply* sendRequest(const QHttpNetworkRequest &request);
void fillHttp2Queue();
#ifndef QT_NO_NETWORKPROXY
//set the proxy for this connection
@ -208,6 +209,7 @@ public:
QHttpNetworkReply *queueRequest(const QHttpNetworkRequest &request);
void requeueRequest(const HttpMessagePair &pair); // e.g. after pipeline broke
void fillHttp2Queue();
bool dequeueRequest(QAbstractSocket *socket);
void prepareRequest(HttpMessagePair &request);
void updateChannel(int i, const HttpMessagePair &messagePair);

View File

@ -63,6 +63,20 @@
QT_BEGIN_NAMESPACE
namespace
{
class ProtocolHandlerDeleter : public QObject
{
public:
explicit ProtocolHandlerDeleter(QAbstractProtocolHandler *h) : handler(h) {}
~ProtocolHandlerDeleter() { delete handler; }
private:
QAbstractProtocolHandler *handler = nullptr;
};
}
// TODO: Put channel specific stuff here so it does not polute qhttpnetworkconnection.cpp
// Because in-flight when sending a request, the server might close our connection (because the persistent HTTP
@ -424,6 +438,40 @@ void QHttpNetworkConnectionChannel::allDone()
return;
}
if (connection->connectionType() == QHttpNetworkConnection::ConnectionTypeHTTP2
&& !ssl && !switchedToHttp2) {
if (Http2::is_protocol_upgraded(*reply)) {
switchedToHttp2 = true;
protocolHandler->setReply(nullptr);
// As allDone() gets called from the protocol handler, it's not yet
// safe to delete it. There is no 'deleteLater', since
// QAbstractProtocolHandler is not a QObject. Instead we do this
// trick with ProtocolHandlerDeleter, a QObject-derived class.
// These dances below just make it somewhat exception-safe.
// 1. Create a new owner:
QAbstractProtocolHandler *oldHandler = protocolHandler.data();
QScopedPointer<ProtocolHandlerDeleter> deleter(new ProtocolHandlerDeleter(oldHandler));
// 2. Retire the old one:
protocolHandler.take();
// 3. Call 'deleteLater':
deleter->deleteLater();
// 3. Give up the ownerthip:
deleter.take();
connection->fillHttp2Queue();
protocolHandler.reset(new QHttp2ProtocolHandler(this));
QHttp2ProtocolHandler *h2c = static_cast<QHttp2ProtocolHandler *>(protocolHandler.data());
QMetaObject::invokeMethod(h2c, "_q_receiveReply", Qt::QueuedConnection);
QMetaObject::invokeMethod(connection, "_q_startNextRequest", Qt::QueuedConnection);
return;
} else {
// Ok, whatever happened, we do not try HTTP/2 anymore ...
connection->setConnectionType(QHttpNetworkConnection::ConnectionTypeHTTP);
connection->d_func()->activeChannelCount = connection->d_func()->channelCount;
}
}
// while handling 401 & 407, we might reset the status code, so save this.
bool emitFinished = reply->d_func()->shouldEmitSignals();
bool connectionCloseEnabled = reply->d_func()->isConnectionCloseEnabled();
@ -838,19 +886,23 @@ void QHttpNetworkConnectionChannel::_q_connected()
#endif
} else {
state = QHttpNetworkConnectionChannel::IdleState;
if (connection->connectionType() == QHttpNetworkConnection::ConnectionTypeHTTP2) {
// We have to reset QHttp2ProtocolHandler's state machine, it's a new
// connection and the handler's state is unique per connection.
protocolHandler.reset(new QHttp2ProtocolHandler(this));
if (spdyRequestsToSend.count() > 0) {
// wait for data from the server first (e.g. initial window, max concurrent requests)
QMetaObject::invokeMethod(connection, "_q_startNextRequest", Qt::QueuedConnection);
const bool tryProtocolUpgrade = connection->connectionType() == QHttpNetworkConnection::ConnectionTypeHTTP2;
if (tryProtocolUpgrade) {
// For HTTP/1.1 it's already created and never reset.
protocolHandler.reset(new QHttpProtocolHandler(this));
}
switchedToHttp2 = false;
if (!reply)
connection->d_func()->dequeueRequest(socket);
if (reply) {
if (tryProtocolUpgrade) {
// Let's augment our request with some magic headers and try to
// switch to HTTP/2.
Http2::prepare_for_protocol_upgrade(request);
}
} else {
if (!reply)
connection->d_func()->dequeueRequest(socket);
if (reply)
sendRequest();
sendRequest();
}
}
}
@ -1078,6 +1130,7 @@ void QHttpNetworkConnectionChannel::_q_encrypted()
// has gone to the SPDY queue already
break;
} else if (nextProtocol == QSslConfiguration::ALPNProtocolHTTP2) {
switchedToHttp2 = true;
protocolHandler.reset(new QHttp2ProtocolHandler(this));
connection->setConnectionType(QHttpNetworkConnection::ConnectionTypeHTTP2);
break;

View File

@ -127,6 +127,7 @@ public:
// HTTP/2 can be cleartext also, that's why it's
// outside of QT_NO_SSL section. Sorted by priority:
QMultiMap<int, HttpMessagePair> spdyRequestsToSend;
bool switchedToHttp2 = false;
#ifndef QT_NO_SSL
bool ignoreAllSslErrors;
QList<QSslError> ignoreSslErrorsList;

View File

@ -78,7 +78,7 @@ public:
virtual void setHeaderField(const QByteArray &name, const QByteArray &data) = 0;
};
class QHttpNetworkHeaderPrivate : public QSharedData
class Q_AUTOTEST_EXPORT QHttpNetworkHeaderPrivate : public QSharedData
{
public:
QUrl url;

View File

@ -132,8 +132,23 @@ void Http2Server::startServer()
if (!clearTextHTTP2)
return;
#endif
if (listen())
if (listen()) {
if (clearTextHTTP2)
authority = QStringLiteral("127.0.0.1:%1").arg(serverPort()).toLatin1();
emit serverStarted(serverPort());
}
}
bool Http2Server::sendProtocolSwitchReply()
{
Q_ASSERT(socket);
Q_ASSERT(clearTextHTTP2 && upgradeProtocol);
// 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()
@ -232,6 +247,7 @@ void Http2Server::incomingConnection(qintptr socketDescriptor)
Q_ASSERT(set);
// Stop listening:
close();
upgradeProtocol = true;
QMetaObject::invokeMethod(this, "connectionEstablished",
Qt::QueuedConnection);
} else {
@ -275,19 +291,77 @@ quint32 Http2Server::clientSetting(Http2::Settings identifier, quint32 defaultVa
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->fields) {
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) {
auto timer = new QTimer(this);
timer->setSingleShot(true);
connect(timer, &QTimer::timeout, [this]() {
sendGOAWAY(quint32(connectionStreamID), quint32(INTERNAL_ERROR), 0);
});
timer->start(goawayTimeout);
return;
}
if (testingGOAWAY && !clearTextHTTP2)
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()));
@ -296,9 +370,17 @@ void Http2Server::connectionEstablished()
waitingClientAck = false;
waitingClientSettings = false;
settingsSent = false;
// We immediately send our settings so that our client
// can use flow control correctly.
sendServerSettings();
if (clearTextHTTP2) {
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();
@ -328,7 +410,9 @@ void Http2Server::readReady()
if (connectionError)
return;
if (waitingClientPreface) {
if (upgradeProtocol) {
handleProtocolUpgrade();
} else if (waitingClientPreface) {
handleConnectionPreface();
} else {
const auto status = reader.read(*socket);
@ -348,6 +432,79 @@ void Http2Server::readReady()
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);
@ -382,6 +539,16 @@ void Http2Server::handleIncomingFrame()
// 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()) {

View File

@ -29,11 +29,14 @@
#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>
@ -58,6 +61,19 @@ struct Http2Setting
using Http2Settings = std::vector<Http2Setting>;
// 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)
};
class Http2Server : public QTcpServer
{
Q_OBJECT
@ -75,6 +91,7 @@ public:
// 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);
@ -82,6 +99,7 @@ public:
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();
@ -114,6 +132,9 @@ private:
void incomingConnection(qintptr socketDescriptor) Q_DECL_OVERRIDE;
quint32 clientSetting(Http2::Settings identifier, quint32 defaultValue);
bool readMethodLine();
bool verifyProtocolUpgradeRequest();
void triggerGOAWAYEmulation();
QScopedPointer<QAbstractSocket> socket;
@ -166,6 +187,18 @@ private:
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;
protected slots:
void ignoreErrorSlot();
};

View File

@ -47,10 +47,12 @@
#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)
// HTTP/2 over TLS requires ALPN/NPN to negotiate the protocol version.
const bool clearTextHTTP2 = false;
#else
// No ALPN/NPN support to negotiate HTTP/2, we'll use cleartext 'h2c' with
// a protocol upgrade procedure.
const bool clearTextHTTP2 = true;
#endif
@ -507,6 +509,7 @@ void tst_Http2::sendRequest(int streamNumber,
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, QVariant(true));
request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("text/plain"));
request.setPriority(priority);
QNetworkReply *reply = nullptr;