diff options
author | Timur Pocheptsov <timur.pocheptsov@qt.io> | 2017-07-27 14:34:39 +0200 |
---|---|---|
committer | Timur Pocheptsov <timur.pocheptsov@qt.io> | 2017-08-27 04:54:55 +0000 |
commit | 53357f01561d7c2b50e0a656ca250f5e3c1af923 (patch) | |
tree | 28ac5a34217efb50a4844d498794baedc72d66ae /src/network/access/qhttpnetworkconnectionchannel.cpp | |
parent | fabedd399ebe4d28d6eb62c8a863f1bbcce78d3a (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.cpp | 77 |
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; |