From 53357f01561d7c2b50e0a656ca250f5e3c1af923 Mon Sep 17 00:00:00 2001 From: Timur Pocheptsov Date: Thu, 27 Jul 2017 14:34:39 +0200 Subject: 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 Reviewed-by: Timur Pocheptsov --- src/network/access/http2/http2frames.cpp | 6 + src/network/access/http2/http2frames_p.h | 2 + src/network/access/http2/http2protocol.cpp | 75 +++++++- src/network/access/http2/http2protocol_p.h | 6 + src/network/access/qhttp2protocolhandler.cpp | 38 ++-- src/network/access/qhttp2protocolhandler_p.h | 4 +- src/network/access/qhttpnetworkconnection.cpp | 35 +++- src/network/access/qhttpnetworkconnection_p.h | 2 + .../access/qhttpnetworkconnectionchannel.cpp | 77 ++++++-- .../access/qhttpnetworkconnectionchannel_p.h | 1 + src/network/access/qhttpnetworkheader_p.h | 2 +- tests/auto/network/access/http2/http2srv.cpp | 195 +++++++++++++++++++-- tests/auto/network/access/http2/http2srv.h | 33 ++++ tests/auto/network/access/http2/tst_http2.cpp | 5 +- 14 files changed, 430 insertions(+), 51 deletions(-) diff --git a/src/network/access/http2/http2frames.cpp b/src/network/access/http2/http2frames.cpp index 5a684c2f41..e695b4dd9e 100644 --- a/src/network/access/http2/http2frames.cpp +++ b/src/network/access/http2/http2frames.cpp @@ -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; diff --git a/src/network/access/http2/http2frames_p.h b/src/network/access/http2/http2frames_p.h index e5f6d46c67..4bdc775806 100644 --- a/src/network/access/http2/http2frames_p.h +++ b/src/network/access/http2/http2frames_p.h @@ -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); diff --git a/src/network/access/http2/http2protocol.cpp b/src/network/access/http2/http2protocol.cpp index 7f788a6f42..54811aeab0 100644 --- a/src/network/access/http2/http2protocol.cpp +++ b/src/network/access/http2/http2protocol.cpp @@ -37,9 +37,14 @@ ** ****************************************************************************/ -#include - #include "http2protocol_p.h" +#include "http2frames_p.h" + +#include "private/qhttpnetworkrequest_p.h" +#include "private/qhttpnetworkreply_p.h" + +#include +#include 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(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 &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 diff --git a/src/network/access/http2/http2protocol_p.h b/src/network/access/http2/http2protocol_p.h index 5d730404bb..b26ff0e9f4 100644 --- a/src/network/access/http2/http2protocol_p.h +++ b/src/network/access/http2/http2protocol_p.h @@ -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(); } diff --git a/src/network/access/qhttp2protocolhandler.cpp b/src/network/access/qhttp2protocolhandler.cpp index 44ab637da8..5032f6017f 100644 --- a/src/network/access/qhttp2protocolhandler.cpp +++ b/src/network/access/qhttp2protocolhandler.cpp @@ -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); diff --git a/src/network/access/qhttp2protocolhandler_p.h b/src/network/access/qhttp2protocolhandler_p.h index df0cf6a288..82eea21818 100644 --- a/src/network/access/qhttp2protocolhandler_p.h +++ b/src/network/access/qhttp2protocolhandler_p.h @@ -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(); diff --git a/src/network/access/qhttpnetworkconnection.cpp b/src/network/access/qhttpnetworkconnection.cpp index ae30d3a8cf..0b474ba116 100644 --- a/src/network/access/qhttpnetworkconnection.cpp +++ b/src/network/access/qhttpnetworkconnection.cpp @@ -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); diff --git a/src/network/access/qhttpnetworkconnection_p.h b/src/network/access/qhttpnetworkconnection_p.h index 3dd9bde9bd..f01a2318a5 100644 --- a/src/network/access/qhttpnetworkconnection_p.h +++ b/src/network/access/qhttpnetworkconnection_p.h @@ -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); diff --git a/src/network/access/qhttpnetworkconnectionchannel.cpp b/src/network/access/qhttpnetworkconnectionchannel.cpp index 6b2018ef86..b1ae29427e 100644 --- a/src/network/access/qhttpnetworkconnectionchannel.cpp +++ b/src/network/access/qhttpnetworkconnectionchannel.cpp @@ -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 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(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; diff --git a/src/network/access/qhttpnetworkconnectionchannel_p.h b/src/network/access/qhttpnetworkconnectionchannel_p.h index 584d52ddb7..844a7d5d15 100644 --- a/src/network/access/qhttpnetworkconnectionchannel_p.h +++ b/src/network/access/qhttpnetworkconnectionchannel_p.h @@ -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 spdyRequestsToSend; + bool switchedToHttp2 = false; #ifndef QT_NO_SSL bool ignoreAllSslErrors; QList ignoreSslErrorsList; diff --git a/src/network/access/qhttpnetworkheader_p.h b/src/network/access/qhttpnetworkheader_p.h index 89169b9331..46aec1dd8c 100644 --- a/src/network/access/qhttpnetworkheader_p.h +++ b/src/network/access/qhttpnetworkheader_p.h @@ -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; diff --git a/tests/auto/network/access/http2/http2srv.cpp b/tests/auto/network/access/http2/http2srv.cpp index d0686eb01c..663f40cbae 100644 --- a/tests/auto/network/access/http2/http2srv.cpp +++ b/tests/auto/network/access/http2/http2srv.cpp @@ -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()) { diff --git a/tests/auto/network/access/http2/http2srv.h b/tests/auto/network/access/http2/http2srv.h index 63a4a4c8e9..10d0e86736 100644 --- a/tests/auto/network/access/http2/http2srv.h +++ b/tests/auto/network/access/http2/http2srv.h @@ -29,11 +29,14 @@ #ifndef HTTP2SRV_H #define HTTP2SRV_H +#include +#include #include #include #include #include +#include #include #include #include @@ -58,6 +61,19 @@ struct Http2Setting using Http2Settings = std::vector; +// 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 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 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(); }; diff --git a/tests/auto/network/access/http2/tst_http2.cpp b/tests/auto/network/access/http2/tst_http2.cpp index d7a57f5e26..e433293a2c 100644 --- a/tests/auto/network/access/http2/tst_http2.cpp +++ b/tests/auto/network/access/http2/tst_http2.cpp @@ -47,10 +47,12 @@ #include #include -// 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; -- cgit v1.2.3