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>
This commit is contained in:
parent
589d96b9b0
commit
e4c1feae5c
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user