summaryrefslogtreecommitdiffstats
path: root/src/network/access/qhttpnetworkconnectionchannel.cpp
diff options
context:
space:
mode:
authorTimur Pocheptsov <timur.pocheptsov@qt.io>2017-07-27 14:34:39 +0200
committerTimur Pocheptsov <timur.pocheptsov@qt.io>2017-08-27 04:54:55 +0000
commit53357f01561d7c2b50e0a656ca250f5e3c1af923 (patch)
tree28ac5a34217efb50a4844d498794baedc72d66ae /src/network/access/qhttpnetworkconnectionchannel.cpp
parentfabedd399ebe4d28d6eb62c8a863f1bbcce78d3a (diff)
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>
Diffstat (limited to 'src/network/access/qhttpnetworkconnectionchannel.cpp')
-rw-r--r--src/network/access/qhttpnetworkconnectionchannel.cpp77
1 files changed, 65 insertions, 12 deletions
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;