diff options
author | Timur Pocheptsov <timur.pocheptsov@qt.io> | 2019-07-17 15:10:49 +0200 |
---|---|---|
committer | Timur Pocheptsov <timur.pocheptsov@qt.io> | 2019-07-24 16:03:02 +0200 |
commit | e4c1feae5c0bec21e24edbf5acacd248dd121634 (patch) | |
tree | b5285e7dd9bb4a3617fdf6db9780a70a07d74a03 | |
parent | 589d96b9b06a4a7d0dca03a06c80716318761277 (diff) |
Implement 'preconnect-https' and 'preconnect-http' for H2
QNetworkAccessManager::connectToHostEncrypted()/connectToHost()
creates 'fake' requests with pseudo-schemes 'preconnect-https'/
'preconnect-http'. QHttp2ProtocolHandler should handle this
requests in a special way - reporting them immediately as
finished (so that QNAM emits finished as it does in case of
HTTP/1.1) and not trying to send anything.
We also have to properly cache the connection - 'https' or
'http' scheme is too generic - it allows (unfortunately)
mixing H2/HTTP/1.1 in a single connection in case an attribute
was missing on a request, which is wrong.
h2c is more complicated, since it needs a real request
to negotiate the protocol switch to H2, with the current
QNetworkHttpConnection(Channel)'s design it's not possible
without large changes (aka regressions and new bugs introduced).
Auto-test extended.
Fixes: QTBUG-77082
Change-Id: I03467673a620c89784c2d36521020dc9d08aced7
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
Reviewed-by: MÃ¥rten Nordheim <marten.nordheim@qt.io>
-rw-r--r-- | src/network/access/qhttp2protocolhandler.cpp | 25 | ||||
-rw-r--r-- | src/network/access/qhttpthreaddelegate.cpp | 36 | ||||
-rw-r--r-- | src/network/access/qnetworkaccessmanager.cpp | 9 | ||||
-rw-r--r-- | tests/auto/network/access/http2/tst_http2.cpp | 123 |
4 files changed, 175 insertions, 18 deletions
diff --git a/src/network/access/qhttp2protocolhandler.cpp b/src/network/access/qhttp2protocolhandler.cpp index 87a70d8a55..5d7fc7506e 100644 --- a/src/network/access/qhttp2protocolhandler.cpp +++ b/src/network/access/qhttp2protocolhandler.cpp @@ -170,7 +170,6 @@ QHttp2ProtocolHandler::QHttp2ProtocolHandler(QHttpNetworkConnectionChannel *chan encoder(HPack::FieldLookupTable::DefaultSize, true) { Q_ASSERT(channel && m_connection); - continuedFrames.reserve(20); const ProtocolParameters params(m_connection->http2Parameters()); @@ -322,10 +321,32 @@ bool QHttp2ProtocolHandler::sendRequest() return false; } + // Process 'fake' (created by QNetworkAccessManager::connectToHostEncrypted()) + // requests first: + auto &requests = m_channel->spdyRequestsToSend; + for (auto it = requests.begin(), endIt = requests.end(); it != endIt;) { + const auto &pair = *it; + const QString scheme(pair.first.url().scheme()); + if (scheme == QLatin1String("preconnect-http") + || scheme == QLatin1String("preconnect-https")) { + m_connection->preConnectFinished(); + emit pair.second->finished(); + it = requests.erase(it); + if (!requests.size()) { + // Normally, after a connection was established and H2 + // was negotiated, we send a client preface. connectToHostEncrypted + // though is not meant to send any data, it's just a 'preconnect'. + // Thus we return early: + return true; + } + } else { + ++it; + } + } + if (!prefaceSent && !sendClientPreface()) return false; - auto &requests = m_channel->spdyRequestsToSend; if (!requests.size()) return true; diff --git a/src/network/access/qhttpthreaddelegate.cpp b/src/network/access/qhttpthreaddelegate.cpp index 0e97acdd9d..86ee6fd2eb 100644 --- a/src/network/access/qhttpthreaddelegate.cpp +++ b/src/network/access/qhttpthreaddelegate.cpp @@ -128,7 +128,8 @@ static QByteArray makeCacheKey(QUrl &url, QNetworkProxy *proxy) QString result; QUrl copy = url; QString scheme = copy.scheme(); - bool isEncrypted = scheme == QLatin1String("https"); + bool isEncrypted = scheme == QLatin1String("https") + || scheme == QLatin1String("preconnect-https"); copy.setPort(copy.port(isEncrypted ? 443 : 80)); if (scheme == QLatin1String("preconnect-http")) { copy.setScheme(QLatin1String("http")); @@ -295,17 +296,29 @@ void QHttpThreadDelegate::startRequest() connectionType = QHttpNetworkConnection::ConnectionTypeHTTP2Direct; } + const bool isH2 = httpRequest.isHTTP2Allowed() || httpRequest.isHTTP2Direct(); + if (isH2) { +#if QT_CONFIG(ssl) + if (ssl) { + if (!httpRequest.isHTTP2Direct()) { + QList<QByteArray> protocols; + protocols << QSslConfiguration::ALPNProtocolHTTP2 + << QSslConfiguration::NextProtocolHttp1_1; + incomingSslConfiguration->setAllowedNextProtocols(protocols); + } + urlCopy.setScheme(QStringLiteral("h2s")); + } else +#endif // QT_CONFIG(ssl) + { + urlCopy.setScheme(QStringLiteral("h2")); + } + } + #ifndef QT_NO_SSL if (ssl && !incomingSslConfiguration.data()) incomingSslConfiguration.reset(new QSslConfiguration); - if (httpRequest.isHTTP2Allowed() && ssl) { - // With HTTP2Direct we do not try any protocol negotiation. - QList<QByteArray> protocols; - protocols << QSslConfiguration::ALPNProtocolHTTP2 - << QSslConfiguration::NextProtocolHttp1_1; - incomingSslConfiguration->setAllowedNextProtocols(protocols); - } else if (httpRequest.isSPDYAllowed() && ssl) { + if (!isH2 && httpRequest.isSPDYAllowed() && ssl) { connectionType = QHttpNetworkConnection::ConnectionTypeSPDY; urlCopy.setScheme(QStringLiteral("spdy")); // to differentiate SPDY requests from HTTPS requests QList<QByteArray> nextProtocols; @@ -322,12 +335,11 @@ void QHttpThreadDelegate::startRequest() cacheKey = makeCacheKey(urlCopy, &cacheProxy); else #endif - cacheKey = makeCacheKey(urlCopy, 0); - + cacheKey = makeCacheKey(urlCopy, nullptr); // the http object is actually a QHttpNetworkConnection httpConnection = static_cast<QNetworkAccessCachedHttpConnection *>(connections.localData()->requestEntryNow(cacheKey)); - if (httpConnection == 0) { + if (!httpConnection) { // no entry in cache; create an object // the http object is actually a QHttpNetworkConnection #ifdef QT_NO_BEARERMANAGEMENT @@ -357,7 +369,7 @@ void QHttpThreadDelegate::startRequest() connections.localData()->addEntry(cacheKey, httpConnection); } else { if (httpRequest.withCredentials()) { - QNetworkAuthenticationCredential credential = authenticationManager->fetchCachedCredentials(httpRequest.url(), 0); + QNetworkAuthenticationCredential credential = authenticationManager->fetchCachedCredentials(httpRequest.url(), nullptr); if (!credential.user.isEmpty() && !credential.password.isEmpty()) { QAuthenticator auth; auth.setUser(credential.user); diff --git a/src/network/access/qnetworkaccessmanager.cpp b/src/network/access/qnetworkaccessmanager.cpp index 85e2c492e4..31b1dbf2a5 100644 --- a/src/network/access/qnetworkaccessmanager.cpp +++ b/src/network/access/qnetworkaccessmanager.cpp @@ -1192,10 +1192,11 @@ void QNetworkAccessManager::connectToHostEncrypted(const QString &hostName, quin if (sslConfiguration != QSslConfiguration::defaultConfiguration()) request.setSslConfiguration(sslConfiguration); - // There is no way to enable SPDY via a request, so we need to check - // the ssl configuration whether SPDY is allowed here. - if (sslConfiguration.allowedNextProtocols().contains( - QSslConfiguration::NextProtocolSpdy3_0)) + // There is no way to enable SPDY/HTTP2 via a request, so we need to check + // the ssl configuration whether SPDY/HTTP2 is allowed here. + if (sslConfiguration.allowedNextProtocols().contains(QSslConfiguration::ALPNProtocolHTTP2)) + request.setAttribute(QNetworkRequest::HTTP2AllowedAttribute, true); + else if (sslConfiguration.allowedNextProtocols().contains(QSslConfiguration::NextProtocolSpdy3_0)) request.setAttribute(QNetworkRequest::SpdyAllowedAttribute, true); get(request); diff --git a/tests/auto/network/access/http2/tst_http2.cpp b/tests/auto/network/access/http2/tst_http2.cpp index f7d74e66b2..ce685a1b1c 100644 --- a/tests/auto/network/access/http2/tst_http2.cpp +++ b/tests/auto/network/access/http2/tst_http2.cpp @@ -84,6 +84,8 @@ private slots: void goaway_data(); void goaway(); void earlyResponse(); + void connectToHost_data(); + void connectToHost(); protected slots: // Slots to listen to our in-process server: @@ -544,6 +546,127 @@ void tst_Http2::earlyResponse() QVERIFY(serverGotSettingsACK); } +void tst_Http2::connectToHost_data() +{ + // The attribute to set on a new request: + QTest::addColumn<QNetworkRequest::Attribute>("requestAttribute"); + // The corresponding (to the attribute above) connection type the + // server will use: + QTest::addColumn<H2Type>("connectionType"); + +#if QT_CONFIG(ssl) + QTest::addRow("encrypted-h2-direct") << QNetworkRequest::Http2DirectAttribute << H2Type::h2Direct; + if (!clearTextHTTP2) + QTest::addRow("encrypted-h2-ALPN") << QNetworkRequest::HTTP2AllowedAttribute << H2Type::h2Alpn; +#endif // QT_CONFIG(ssl) + // This works for all configurations, tests 'preconnect-http' scheme: + // h2 with protocol upgrade is not working for now (the logic is a bit + // complicated there ...). + QTest::addRow("h2-direct") << QNetworkRequest::Http2DirectAttribute << H2Type::h2cDirect; +} + +void tst_Http2::connectToHost() +{ + // QNetworkAccessManager::connectToHostEncrypted() and connectToHost() + // creates a special request with 'preconnect-https' or 'preconnect-http' + // schemes. At the level of the protocol handler we are supposed to report + // these requests as finished and wait for the real requests. This test will + // connect to a server with the first reply 'finished' signal meaning we + // indeed connected. At this point we check that a client preface was not + // sent yet, and no response received. Then we send the second (the real) + // request and do our usual checks. Since our server closes its listening + // socket on the first incoming connection (would not accept a new one), + // the successful completion of the second requests also means we were able + // to find a cached connection and re-use it. + + QFETCH(const QNetworkRequest::Attribute, requestAttribute); + QFETCH(const H2Type, connectionType); + + clearHTTP2State(); + + serverPort = 0; + nRequests = 2; + + ServerPtr targetServer(newServer(defaultServerSettings, connectionType)); + +#if QT_CONFIG(ssl) + Q_ASSERT(!clearTextHTTP2 || connectionType != H2Type::h2Alpn); +#else + Q_ASSERT(connectionType == H2Type::h2c || connectionType == H2Type::h2cDirect); + Q_ASSERT(targetServer->isClearText()); +#endif // QT_CONFIG(ssl) + + QMetaObject::invokeMethod(targetServer.data(), "startServer", Qt::QueuedConnection); + runEventLoop(); + + QVERIFY(serverPort != 0); + + auto url = requestUrl(connectionType); + url.setPath("/index.html"); + + QNetworkReply *reply = nullptr; + // Here some mess with how we create this first reply: +#if QT_CONFIG(ssl) + if (!targetServer->isClearText()) { + // Let's emulate what QNetworkAccessManager::connectToHostEncrypted() does. + // Alas, we cannot use it directly, since it does not return the reply and + // also does not know the difference between H2 with ALPN or direct. + auto copyUrl = url; + copyUrl.setScheme(QLatin1String("preconnect-https")); + QNetworkRequest request(copyUrl); + request.setAttribute(requestAttribute, true); + reply = manager->get(request); + // Since we're using self-signed certificates, ignore SSL errors: + reply->ignoreSslErrors(); + } else +#endif // QT_CONFIG(ssl) + { + // Emulating what QNetworkAccessManager::connectToHost() does with + // additional information that it cannot provide (the attribute). + auto copyUrl = url; + copyUrl.setScheme(QLatin1String("preconnect-http")); + QNetworkRequest request(copyUrl); + request.setAttribute(requestAttribute, true); + reply = manager->get(request); + } + + connect(reply, &QNetworkReply::finished, [this, reply]() { + --nRequests; + eventLoop.exitLoop(); + QCOMPARE(reply->error(), QNetworkReply::NoError); + QVERIFY(reply->isFinished()); + // Nothing must be sent yet: + QVERIFY(!prefaceOK); + QVERIFY(!serverGotSettingsACK); + // Nothing received back: + QVERIFY(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isNull()); + QCOMPARE(reply->readAll().size(), 0); + }); + + runEventLoop(); + STOP_ON_FAILURE + + QCOMPARE(nRequests, 1); + + QNetworkRequest request(url); + request.setAttribute(requestAttribute, QVariant(true)); + reply = manager->get(request); + connect(reply, &QNetworkReply::finished, this, &tst_Http2::replyFinished); + // Note, unlike the first request, when the connection is ecnrytped, we + // do not ignore TLS errors on this reply - we should re-use existing + // connection, there TLS errors were already ignored. + + runEventLoop(); + STOP_ON_FAILURE + + QVERIFY(nRequests == 0); + QVERIFY(prefaceOK); + QVERIFY(serverGotSettingsACK); + + QCOMPARE(reply->error(), QNetworkReply::NoError); + QVERIFY(reply->isFinished()); +} + void tst_Http2::serverStarted(quint16 port) { serverPort = port; |