summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/network/access/http2/http2frames.cpp6
-rw-r--r--src/network/access/http2/http2frames_p.h2
-rw-r--r--src/network/access/http2/http2protocol.cpp75
-rw-r--r--src/network/access/http2/http2protocol_p.h6
-rw-r--r--src/network/access/qhttp2protocolhandler.cpp38
-rw-r--r--src/network/access/qhttp2protocolhandler_p.h4
-rw-r--r--src/network/access/qhttpnetworkconnection.cpp35
-rw-r--r--src/network/access/qhttpnetworkconnection_p.h2
-rw-r--r--src/network/access/qhttpnetworkconnectionchannel.cpp77
-rw-r--r--src/network/access/qhttpnetworkconnectionchannel_p.h1
-rw-r--r--src/network/access/qhttpnetworkheader_p.h2
-rw-r--r--tests/auto/network/access/http2/http2srv.cpp195
-rw-r--r--tests/auto/network/access/http2/http2srv.h33
-rw-r--r--tests/auto/network/access/http2/tst_http2.cpp5
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 <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
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<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;
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<int, HttpMessagePair> spdyRequestsToSend;
+ bool switchedToHttp2 = false;
#ifndef QT_NO_SSL
bool ignoreAllSslErrors;
QList<QSslError> 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 <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();
};
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 <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;