From 48aad482a87b5000db6139abf991367013b9aa19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A5rten=20Nordheim?= Date: Wed, 13 Mar 2024 18:30:36 +0100 Subject: [PATCH] Http: Add support for full localsocket paths [ChangeLog][QtNetwork][QNetworkAccessManager] QNetworkAccessManager now supports using full local server name, as in, named pipes on Windows or path to socket objects on Unix. Task-number: QTBUG-102855 Change-Id: Ifc743f5025b3d8d0b558ecffff437881897915d9 Reviewed-by: Timur Pocheptsov --- src/network/access/qhttpnetworkconnection.cpp | 7 ++- src/network/access/qhttpnetworkrequest.cpp | 10 +++ src/network/access/qhttpnetworkrequest_p.h | 4 ++ src/network/access/qhttpthreaddelegate.cpp | 16 ++++- src/network/access/qnetworkreplyhttpimpl.cpp | 7 +++ src/network/access/qnetworkrequest.cpp | 10 +++ src/network/access/qnetworkrequest.h | 1 + .../tst_qnetworkreply_local.cpp | 63 +++++++++++++++++++ 8 files changed, 116 insertions(+), 2 deletions(-) diff --git a/src/network/access/qhttpnetworkconnection.cpp b/src/network/access/qhttpnetworkconnection.cpp index 501a410ae7c..3ef07c69939 100644 --- a/src/network/access/qhttpnetworkconnection.cpp +++ b/src/network/access/qhttpnetworkconnection.cpp @@ -304,7 +304,12 @@ void QHttpNetworkConnectionPrivate::prepareRequest(HttpMessagePair &messagePair) request.setHeaderField("User-Agent", "Mozilla/5.0"); // set the host value = request.headerField("host"); - if (value.isEmpty()) { + if (isLocalSocket && value.isEmpty()) { + // The local socket connections might have a full file path, and that + // may not be suitable for the Host header. But we can use whatever the + // user has set in the URL. + request.prependHeaderField("Host", request.url().host().toLocal8Bit()); + } else if (value.isEmpty()) { QHostAddress add; QByteArray host; if (add.setAddress(hostName)) { diff --git a/src/network/access/qhttpnetworkrequest.cpp b/src/network/access/qhttpnetworkrequest.cpp index 7a4ffb16849..06cc0b44649 100644 --- a/src/network/access/qhttpnetworkrequest.cpp +++ b/src/network/access/qhttpnetworkrequest.cpp @@ -381,5 +381,15 @@ void QHttpNetworkRequest::setPeerVerifyName(const QString &peerName) d->peerVerifyName = peerName; } +QString QHttpNetworkRequest::fullLocalServerName() const +{ + return d->fullLocalServerName; +} + +void QHttpNetworkRequest::setFullLocalServerName(const QString &fullServerName) +{ + d->fullLocalServerName = fullServerName; +} + QT_END_NAMESPACE diff --git a/src/network/access/qhttpnetworkrequest_p.h b/src/network/access/qhttpnetworkrequest_p.h index 131885f6d22..44440204023 100644 --- a/src/network/access/qhttpnetworkrequest_p.h +++ b/src/network/access/qhttpnetworkrequest_p.h @@ -117,6 +117,9 @@ class Q_AUTOTEST_EXPORT QHttpNetworkRequest: public QHttpNetworkHeader QString peerVerifyName() const; void setPeerVerifyName(const QString &peerName); + QString fullLocalServerName() const; + void setFullLocalServerName(const QString &fullServerName); + private: QSharedDataPointer d; friend class QHttpNetworkRequestPrivate; @@ -140,6 +143,7 @@ class QHttpNetworkRequestPrivate : public QHttpNetworkHeaderPrivate QHttpNetworkRequest::Operation operation; QByteArray customVerb; + QString fullLocalServerName; // for local sockets QHttpNetworkRequest::Priority priority; mutable QNonContiguousByteDevice* uploadByteDevice; bool autoDecompress; diff --git a/src/network/access/qhttpthreaddelegate.cpp b/src/network/access/qhttpthreaddelegate.cpp index 7dddd1369e9..4e5cf05aef4 100644 --- a/src/network/access/qhttpthreaddelegate.cpp +++ b/src/network/access/qhttpthreaddelegate.cpp @@ -290,6 +290,12 @@ void QHttpThreadDelegate::startRequest() } } + QString extraData = httpRequest.peerVerifyName(); + if (isLocalSocket) { + if (QString path = httpRequest.fullLocalServerName(); !path.isEmpty()) + extraData = path; + } + #ifndef QT_NO_NETWORKPROXY if (transparentProxy.type() != QNetworkProxy::NoProxy) cacheKey = makeCacheKey(urlCopy, &transparentProxy, httpRequest.peerVerifyName()); @@ -302,10 +308,18 @@ void QHttpThreadDelegate::startRequest() // the http object is actually a QHttpNetworkConnection httpConnection = static_cast(connections.localData()->requestEntryNow(cacheKey)); if (!httpConnection) { + + QString host = urlCopy.host(); + // Update the host if a unix socket path or named pipe is used: + if (isLocalSocket) { + if (QString path = httpRequest.fullLocalServerName(); !path.isEmpty()) + host = path; + } + // no entry in cache; create an object // the http object is actually a QHttpNetworkConnection httpConnection = new QNetworkAccessCachedHttpConnection( - http1Parameters.numberOfConnectionsPerHost(), urlCopy.host(), urlCopy.port(), ssl, + http1Parameters.numberOfConnectionsPerHost(), host, urlCopy.port(), ssl, isLocalSocket, connectionType); if (connectionType == QHttpNetworkConnection::ConnectionTypeHTTP2 || connectionType == QHttpNetworkConnection::ConnectionTypeHTTP2Direct) { diff --git a/src/network/access/qnetworkreplyhttpimpl.cpp b/src/network/access/qnetworkreplyhttpimpl.cpp index b4aca940a70..89458825e95 100644 --- a/src/network/access/qnetworkreplyhttpimpl.cpp +++ b/src/network/access/qnetworkreplyhttpimpl.cpp @@ -809,6 +809,13 @@ void QNetworkReplyHttpImplPrivate::postRequest(const QNetworkRequest &newHttpReq httpRequest.setPeerVerifyName(newHttpRequest.peerVerifyName()); + if (scheme.startsWith(("unix"_L1))) { + if (QVariant path = newHttpRequest.attribute(QNetworkRequest::FullLocalServerNameAttribute); + path.isValid() && path.canConvert()) { + httpRequest.setFullLocalServerName(path.toString()); + } + } + // Create the HTTP thread delegate QHttpThreadDelegate *delegate = new QHttpThreadDelegate; // Propagate Http/2 settings: diff --git a/src/network/access/qnetworkrequest.cpp b/src/network/access/qnetworkrequest.cpp index 7a1b5426d26..0e1172b15c8 100644 --- a/src/network/access/qnetworkrequest.cpp +++ b/src/network/access/qnetworkrequest.cpp @@ -324,6 +324,16 @@ QT_IMPL_METATYPE_EXTERN_TAGGED(QNetworkRequest::RedirectPolicy, QNetworkRequest_ same-origin requests. This only affects the WebAssembly platform. (This value was introduced in 6.5.) + \value FullLocalServerNameAttribute + Requests only, type: QMetaType::String + Holds the full local server name to be used for the underlying + QLocalSocket. This attribute is used by the QNetworkAccessManager + to connect to a specific local server, when QLocalSocket's behavior for + a simple name isn't enough. The URL in the QNetworkRequest must still + use unix+http: or local+http: scheme. And the hostname in the URL will + be used for the Host header in the HTTP request. + (This value was introduced in 6.8.) + \value User Special type. Additional information can be passed in QVariants with types ranging from User to UserMax. The default diff --git a/src/network/access/qnetworkrequest.h b/src/network/access/qnetworkrequest.h index e281c748345..368eb99d955 100644 --- a/src/network/access/qnetworkrequest.h +++ b/src/network/access/qnetworkrequest.h @@ -70,6 +70,7 @@ class Q_NETWORK_EXPORT QNetworkRequest ConnectionCacheExpiryTimeoutSecondsAttribute, Http2CleartextAllowedAttribute, UseCredentialsAttribute, + FullLocalServerNameAttribute, User = 1000, UserMax = 32767 diff --git a/tests/auto/network/access/qnetworkreply_local/tst_qnetworkreply_local.cpp b/tests/auto/network/access/qnetworkreply_local/tst_qnetworkreply_local.cpp index 729605d9c86..6d78c815930 100644 --- a/tests/auto/network/access/qnetworkreply_local/tst_qnetworkreply_local.cpp +++ b/tests/auto/network/access/qnetworkreply_local/tst_qnetworkreply_local.cpp @@ -24,6 +24,11 @@ private slots: void get(); void post(); + +#if QT_CONFIG(localserver) + void fullServerName_data(); + void fullServerName(); +#endif }; void tst_QNetworkReply_local::initTestCase_data() @@ -108,6 +113,64 @@ void tst_QNetworkReply_local::post() QCOMPARE(firstRequest.receivedData.last(payload.size() + 4), "\r\n\r\n" + payload); } +#if QT_CONFIG(localserver) +void tst_QNetworkReply_local::fullServerName_data() +{ +#if defined(Q_OS_ANDROID) || defined(QT_PLATFORM_UIKIT) + QSKIP("While partially supported, the test as-is doesn't make sense on this platform."); +#else + + QTest::addColumn("hostAndPath"); + + QTest::newRow("dummy-host") << u"://irrelevant/test"_s; + QTest::newRow("no-host") << u":///test"_s; +#endif +} + +void tst_QNetworkReply_local::fullServerName() +{ + QFETCH_GLOBAL(QString, scheme); + if (!scheme.startsWith("unix"_L1) && !scheme.startsWith("local"_L1)) + return; // only relevant for local sockets + + MiniHttpServerV2 server; + QLocalServer localServer; + + QString path; +#ifdef Q_OS_WIN + path = uR"(\\.\pipe\qt_networkreply_test_fullServerName)"_s + % QString::number(QCoreApplication::applicationPid()); +#else + path = u"/tmp/qt_networkreply_test_fullServerName"_s + % QString::number(QCoreApplication::applicationPid()) % u".sock"_s; +#endif + + QVERIFY(localServer.listen(path)); + server.bind(&localServer); + + QFETCH(QString, hostAndPath); + QUrl url(scheme % hostAndPath); + QNetworkRequest req(url); + req.setAttribute(QNetworkRequest::FullLocalServerNameAttribute, path); + + QNetworkAccessManager manager; + std::unique_ptr reply(manager.get(req)); + + const bool res = QTest::qWaitFor([reply = reply.get()] { return reply->isFinished(); }); + QVERIFY(res); + + QCOMPARE(reply->readAll(), QByteArray("Hello World!")); + QCOMPARE(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(), 200); + + const QByteArray receivedData = server.peerStates().at(0).receivedData; + const QByteArray expectedGet = "GET " % url.path().toUtf8() % " HTTP/1.1\r\n"; + QVERIFY(receivedData.startsWith(expectedGet)); + + const QByteArray expectedHost = "host: " % url.host().toUtf8() % "\r\n"; + QVERIFY(receivedData.contains(expectedHost)); +} +#endif + QTEST_MAIN(tst_QNetworkReply_local) #include "tst_qnetworkreply_local.moc"