diff options
Diffstat (limited to 'src/network/access')
28 files changed, 772 insertions, 176 deletions
diff --git a/src/network/access/http2/http2protocol_p.h b/src/network/access/http2/http2protocol_p.h index fb5ff199c5..f0f18d1dd5 100644 --- a/src/network/access/http2/http2protocol_p.h +++ b/src/network/access/http2/http2protocol_p.h @@ -117,7 +117,7 @@ const qint32 maxSessionReceiveWindowSize((quint32(1) << 31) - 1); // Presumably, we never use up to 100 streams so let it be 10 simultaneous: const qint32 qtDefaultStreamReceiveWindowSize = maxSessionReceiveWindowSize / 10; -struct Frame configurationToSettingsFrame(const QHttp2Configuration &configuration); +struct Frame Q_AUTOTEST_EXPORT configurationToSettingsFrame(const QHttp2Configuration &configuration); QByteArray settingsFrameToBase64(const Frame &settingsFrame); void appendProtocolUpgradeHeaders(const QHttp2Configuration &configuration, QHttpNetworkRequest *request); std::vector<uchar> assemble_hpack_block(const std::vector<Frame> &frames); diff --git a/src/network/access/qformdatabuilder.cpp b/src/network/access/qformdatabuilder.cpp new file mode 100644 index 0000000000..1d2054c956 --- /dev/null +++ b/src/network/access/qformdatabuilder.cpp @@ -0,0 +1,363 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qformdatabuilder.h" + +#include <QtCore/private/qstringconverter_p.h> +#if QT_CONFIG(mimetype) +#include "QtCore/qmimedatabase.h" +#endif + +#include <vector> + +QT_BEGIN_NAMESPACE + +/*! + \class QFormDataPartBuilder + \brief The QFormDataPartBuilder class is a convenience class to simplify + the construction of QHttpPart objects. + \since 6.8 + + \ingroup network + \ingroup shared + \inmodule QtNetwork + + The QFormDataPartBuilder class can be used to build a QHttpPart object with + the content disposition header set to be form-data by default. Then the + generated object can be used as part of a multipart message (which is + represented by the QHttpMultiPart class). + + \sa QHttpPart, QHttpMultiPart, QFormDataBuilder +*/ + +static QByteArray nameToByteArray(QStringView view) +{ + return view.toUtf8(); +} + +static QByteArray nameToByteArray(QLatin1StringView view) +{ + if (!QtPrivate::isAscii(view)) + return view.toString().toUtf8(); // ### optimize + + return QByteArray::fromRawData(view.data(), view.size()); +} + +static QByteArray nameToByteArray(QUtf8StringView view) +{ + return QByteArray::fromRawData(view.data(), view.size()); +} + +static void escapeNameAndAppend(QByteArray &dst, QByteArrayView src) +{ + for (auto c : src) { + if (c == '"' || c == '\\') + dst += '\\'; + dst += c; + } +} + +/*! + Constructs a QFormDataPartBuilder object and sets \a name as the name + parameter of the form-data. +*/ +QFormDataPartBuilder::QFormDataPartBuilder(QAnyStringView name, PrivateConstructor /*unused*/) +{ + static_assert(std::is_nothrow_move_constructible_v<decltype(m_body)>); + static_assert(std::is_nothrow_move_assignable_v<decltype(m_body)>); + + const auto enc = name.visit([](auto name) { return nameToByteArray(name); }); + + m_headerValue += "form-data; name=\""; + escapeNameAndAppend(m_headerValue, enc); + m_headerValue += "\""; +} + +/*! + \fn QFormDataPartBuilder::QFormDataPartBuilder(QFormDataPartBuilder &&other) noexcept + + Move-constructs a QFormDataPartBuilder instance, making it point at the same + object that \a other was pointing to. +*/ + +/*! + \fn QFormDataPartBuilder &QFormDataPartBuilder::operator=(QFormDataPartBuilder &&other) + + Move-assigns \a other to this QFormDataPartBuilder instance. +*/ + +/*! + Destroys the QFormDataPartBuilder object. +*/ + +QFormDataPartBuilder::~QFormDataPartBuilder() + = default; + +static void convertInto_impl(QByteArray &dst, QUtf8StringView in) +{ + dst.clear(); + dst += QByteArrayView{in}; // it's ASCII, anyway +} + +static void convertInto_impl(QByteArray &dst, QLatin1StringView in) +{ + dst.clear(); + dst += QByteArrayView{in}; // it's ASCII, anyway +} + +static void convertInto_impl(QByteArray &dst, QStringView in) +{ + dst.resize(in.size()); + (void)QLatin1::convertFromUnicode(dst.data(), in); +} + +static void convertInto(QByteArray &dst, QAnyStringView in) +{ + in.visit([&dst](auto in) { convertInto_impl(dst, in); }); +} + +QFormDataPartBuilder &QFormDataPartBuilder::setBodyHelper(const QByteArray &data, + QAnyStringView name, + QAnyStringView mimeType) +{ + m_originalBodyName = name.toString(); + convertInto(m_mimeType, mimeType); + m_body = data; + return *this; +} + +/*! + Sets \a data as the body of this MIME part and, if given, \a fileName as the + file name parameter in the content disposition header. + + If \a mimeType is not given (is empty), then QFormDataPartBuilder tries to + auto-detect the mime-type of \a data using QMimeDatabase. + + A subsequent call to setBodyDevice() discards the body and the device will + be used instead. + + For a large amount of data (e.g. an image), setBodyDevice() is preferred, + which will not copy the data internally. + + \sa setBodyDevice() +*/ + +QFormDataPartBuilder &QFormDataPartBuilder::setBody(QByteArrayView data, + QAnyStringView fileName, + QAnyStringView mimeType) +{ + return setBody(data.toByteArray(), fileName, mimeType); +} + +/*! + Sets \a body as the body device of this part and \a fileName as the file + name parameter in the content disposition header. + + If \a mimeType is not given (is empty), then QFormDataPartBuilder tries to + auto-detect the mime-type of \a body using QMimeDatabase. + + A subsequent call to setBody() discards the body device and the data set by + setBody() will be used instead. + + For large amounts of data this method should be preferred over setBody(), + because the content is not copied when using this method, but read + directly from the device. + + \a body must be open and readable. QFormDataPartBuilder does not take + ownership of \a body, i.e. the device must be closed and destroyed if + necessary. + + \sa setBody(), QHttpPart::setBodyDevice() + */ + +QFormDataPartBuilder &QFormDataPartBuilder::setBodyDevice(QIODevice *body, QAnyStringView fileName, + QAnyStringView mimeType) +{ + m_originalBodyName = fileName.toString(); + convertInto(m_mimeType, mimeType); + m_body = body; + return *this; +} + +/*! + Sets the headers specified in \a headers. + + \note The "content-type" and "content-disposition" headers, if any are + specified in \a headers, will be overwritten by the class. +*/ + +QFormDataPartBuilder &QFormDataPartBuilder::setHeaders(const QHttpHeaders &headers) +{ + m_httpHeaders = headers; + return *this; +} + +/*! + \internal + + Generates a QHttpPart and sets the content disposition header as form-data. + + When this function called, it uses the MIME database to deduce the type the + body based on its name and then sets the deduced type as the content type + header. +*/ + +QHttpPart QFormDataPartBuilder::build() +{ + QHttpPart httpPart; + + if (!m_originalBodyName.isNull()) { + const bool utf8 = !QtPrivate::isAscii(m_originalBodyName); + const auto enc = utf8 ? m_originalBodyName.toUtf8() : m_originalBodyName.toLatin1(); + m_headerValue += "; filename=\""; + escapeNameAndAppend(m_headerValue, enc); + m_headerValue += "\""; + if (utf8) { + // For 'filename*' production see + // https://datatracker.ietf.org/doc/html/rfc5987#section-3.2.1 + // For providing both filename and filename* parameters see + // https://datatracker.ietf.org/doc/html/rfc6266#section-4.3 and + // https://datatracker.ietf.org/doc/html/rfc8187#section-4.2 + m_headerValue += "; filename*=UTF-8''" + enc.toPercentEncoding(); + } + } + +#if QT_CONFIG(mimetype) + if (m_mimeType.isEmpty()) { + // auto-detect + QMimeDatabase db; + convertInto(m_mimeType, std::visit([&](auto &arg) { + return db.mimeTypeForFileNameAndData(m_originalBodyName, arg); + }, m_body).name()); + } +#endif + + for (qsizetype i = 0; i < m_httpHeaders.size(); i++) { + httpPart.setRawHeader(QByteArrayView(m_httpHeaders.nameAt(i)).toByteArray(), + m_httpHeaders.valueAt(i).toByteArray()); + } + + if (!m_mimeType.isEmpty()) + httpPart.setHeader(QNetworkRequest::ContentTypeHeader, m_mimeType); + + httpPart.setHeader(QNetworkRequest::ContentDispositionHeader, m_headerValue); + + if (auto d = std::get_if<QIODevice*>(&m_body)) + httpPart.setBodyDevice(*d); + else if (auto b = std::get_if<QByteArray>(&m_body)) + httpPart.setBody(*b); + else + Q_UNREACHABLE(); + + return httpPart; +} + +/*! + \class QFormDataBuilder + \brief The QFormDataBuilder class is a convenience class to simplify + the construction of QHttpMultiPart objects. + \since 6.8 + + \ingroup network + \ingroup shared + \inmodule QtNetwork + + The QFormDataBuilder class can be used to build a QHttpMultiPart object + with the content type set to be FormDataType by default. + + The snippet below demonstrates how to build a multipart message with + QFormDataBuilder: + + \code + QFormDataBuilder builder; + QFile image(u"../../pic.png"_s); image.open(QFile::ReadOnly); + QFile mask(u"../../mask.png"_s); mask.open(QFile::ReadOnly); + + builder.part("image"_L1).setBodyDevice(&image, "the actual image"); + builder.part("mask"_L1).setBodyDevice(&mask, "the mask image"); + builder.part("prompt"_L1).setBody("Lobster wearing a beret"); + builder.part("n"_L1).setBody("2"); + builder.part("size"_L1).setBody("512x512"); + + std::unique_ptr<QHttpMultiPart> mp = builder.buildMultiPart(); + \endcode + + \sa QHttpPart, QHttpMultiPart, QFormDataPartBuilder +*/ + +class QFormDataBuilderPrivate +{ +public: + std::vector<QFormDataPartBuilder> parts; +}; + +/*! + Constructs an empty QFormDataBuilder object. +*/ + +QFormDataBuilder::QFormDataBuilder() + : d_ptr(new QFormDataBuilderPrivate()) +{ + +} + +/*! + Destroys the QFormDataBuilder object. +*/ + +QFormDataBuilder::~QFormDataBuilder() +{ + delete d_ptr; +} + +/*! + \fn QFormDataBuilder::QFormDataBuilder(QFormDataBuilder &&other) noexcept + + Move-constructs a QFormDataBuilder instance, making it point at the same + object that \a other was pointing to. +*/ + +/*! + \fn QFormDataBuilder &QFormDataBuilder::operator=(QFormDataBuilder &&other) noexcept + + Move-assigns \a other to this QFormDataBuilder instance. +*/ + +/*! + Constructs and returns a reference to a QFormDataPartBuilder object and sets + \a name as the name parameter of the form-data. The returned reference is + valid until the next call to this function. + + Limiting \a name characters to US-ASCII is + \l {https://datatracker.ietf.org/doc/html/rfc7578#section-5.1.1}{strongly recommended} + for interoperability reasons. + + \sa QFormDataPartBuilder, QHttpPart +*/ + +QFormDataPartBuilder &QFormDataBuilder::part(QAnyStringView name) +{ + Q_D(QFormDataBuilder); + + return d->parts.emplace_back(name, QFormDataPartBuilder::PrivateConstructor()); +} + +/*! + Constructs and returns a pointer to a QHttpMultipart object. + + \sa QHttpMultiPart +*/ + +std::unique_ptr<QHttpMultiPart> QFormDataBuilder::buildMultiPart() +{ + Q_D(QFormDataBuilder); + + auto multiPart = std::make_unique<QHttpMultiPart>(QHttpMultiPart::FormDataType); + + for (auto &part : d->parts) + multiPart->append(part.build()); + + return multiPart; +} + +QT_END_NAMESPACE diff --git a/src/network/access/qformdatabuilder.h b/src/network/access/qformdatabuilder.h new file mode 100644 index 0000000000..4305ff8cb7 --- /dev/null +++ b/src/network/access/qformdatabuilder.h @@ -0,0 +1,117 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QFORMDATABUILDER_H +#define QFORMDATABUILDER_H + +#include <QtNetwork/qtnetworkglobal.h> +#include <QtNetwork/qhttpheaders.h> +#include <QtNetwork/qhttpmultipart.h> + +#include <QtCore/qbytearray.h> +#include <QtCore/qiodevice.h> +#include <QtCore/qstring.h> + +#include <memory> +#include <variant> + +#ifndef Q_OS_WASM +QT_REQUIRE_CONFIG(http); +#endif + +QT_BEGIN_NAMESPACE + +class QHttpPartPrivate; +class QHttpMultiPart; +class QDebug; + +class QFormDataBuilderPrivate; + +class QFormDataPartBuilder +{ + struct PrivateConstructor { explicit PrivateConstructor() = default; }; +public: + Q_NETWORK_EXPORT explicit QFormDataPartBuilder(QAnyStringView name, PrivateConstructor); + + QFormDataPartBuilder(QFormDataPartBuilder &&other) noexcept + : m_headerValue(std::move(other.m_headerValue)), + m_originalBodyName(std::move(other.m_originalBodyName)), + m_httpHeaders(std::move(other.m_httpHeaders)), + m_body(std::move(other.m_body)), + m_reserved(std::exchange(other.m_reserved, nullptr)) + { + + } + + QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_PURE_SWAP(QFormDataPartBuilder) + void swap(QFormDataPartBuilder &other) noexcept + { + m_headerValue.swap(other.m_headerValue); + m_originalBodyName.swap(other.m_originalBodyName); + m_httpHeaders.swap(other.m_httpHeaders); + m_body.swap(other.m_body); + qt_ptr_swap(m_reserved, other.m_reserved); + } + + Q_NETWORK_EXPORT ~QFormDataPartBuilder(); + + Q_WEAK_OVERLOAD QFormDataPartBuilder &setBody(const QByteArray &data, + QAnyStringView fileName = {}, + QAnyStringView mimeType = {}) + { return setBodyHelper(data, fileName, mimeType); } + + Q_NETWORK_EXPORT QFormDataPartBuilder &setBody(QByteArrayView data, + QAnyStringView fileName = {}, + QAnyStringView mimeType = {}); + Q_NETWORK_EXPORT QFormDataPartBuilder &setBodyDevice(QIODevice *body, + QAnyStringView fileName = {}, + QAnyStringView mimeType = {}); + Q_NETWORK_EXPORT QFormDataPartBuilder &setHeaders(const QHttpHeaders &headers); +private: + Q_DISABLE_COPY(QFormDataPartBuilder) + + Q_NETWORK_EXPORT QFormDataPartBuilder &setBodyHelper(const QByteArray &data, + QAnyStringView fileName, + QAnyStringView mimeType); + QHttpPart build(); + + QByteArray m_headerValue; + QByteArray m_mimeType; + QString m_originalBodyName; + QHttpHeaders m_httpHeaders; + std::variant<QIODevice*, QByteArray> m_body; + void *m_reserved = nullptr; + + friend class QFormDataBuilder; + friend void swap(QFormDataPartBuilder &lhs, QFormDataPartBuilder &rhs) noexcept + { lhs.swap(rhs); } +}; + +class QFormDataBuilder +{ +public: + Q_NETWORK_EXPORT QFormDataBuilder(); + + QFormDataBuilder(QFormDataBuilder &&other) noexcept : d_ptr(std::exchange(other.d_ptr, nullptr)) {} + + QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_PURE_SWAP(QFormDataBuilder) + void swap(QFormDataBuilder &other) noexcept + { + qt_ptr_swap(d_ptr, other.d_ptr); + } + + Q_NETWORK_EXPORT ~QFormDataBuilder(); + Q_NETWORK_EXPORT QFormDataPartBuilder &part(QAnyStringView name); + Q_NETWORK_EXPORT std::unique_ptr<QHttpMultiPart> buildMultiPart(); +private: + QFormDataBuilderPrivate *d_ptr; + + Q_DECLARE_PRIVATE(QFormDataBuilder) + Q_DISABLE_COPY(QFormDataBuilder) +}; + +Q_DECLARE_SHARED(QFormDataBuilder) + +QT_END_NAMESPACE + +#endif // QFORMDATABUILDER_H diff --git a/src/network/access/qhttp2connection.cpp b/src/network/access/qhttp2connection.cpp index 8560e0da38..2d92684863 100644 --- a/src/network/access/qhttp2connection.cpp +++ b/src/network/access/qhttp2connection.cpp @@ -17,7 +17,7 @@ QT_BEGIN_NAMESPACE -Q_LOGGING_CATEGORY(qHttp2ConnectionLog, "qt.network.http2.connection", QtCriticalMsg) +Q_STATIC_LOGGING_CATEGORY(qHttp2ConnectionLog, "qt.network.http2.connection", QtCriticalMsg) using namespace Qt::StringLiterals; using namespace Http2; diff --git a/src/network/access/qhttp2protocolhandler.cpp b/src/network/access/qhttp2protocolhandler.cpp index d9341dc643..3d55cee1eb 100644 --- a/src/network/access/qhttp2protocolhandler.cpp +++ b/src/network/access/qhttp2protocolhandler.cpp @@ -1341,8 +1341,8 @@ quint32 QHttp2ProtocolHandler::createNewStream(const HttpMessagePair &message, b replyPrivate->connectionChannel = m_channel; reply->setHttp2WasUsed(true); streamIDs.insert(reply, newStreamID); - connect(reply, SIGNAL(destroyed(QObject*)), - this, SLOT(_q_replyDestroyed(QObject*))); + connect(reply, &QHttpNetworkReply::destroyed, + this, &QHttp2ProtocolHandler::_q_replyDestroyed); const Stream newStream(message, newStreamID, streamInitialSendWindowSize, @@ -1350,8 +1350,8 @@ quint32 QHttp2ProtocolHandler::createNewStream(const HttpMessagePair &message, b if (!uploadDone) { if (auto src = newStream.data()) { - connect(src, SIGNAL(readyRead()), this, - SLOT(_q_uploadDataReadyRead()), Qt::QueuedConnection); + connect(src, &QNonContiguousByteDevice::readyRead, this, + &QHttp2ProtocolHandler::_q_uploadDataReadyRead, Qt::QueuedConnection); connect(src, &QHttp2ProtocolHandler::destroyed, this, &QHttp2ProtocolHandler::_q_uploadDataDestroyed); streamIDs.insert(src, newStreamID); diff --git a/src/network/access/qhttpheaders.cpp b/src/network/access/qhttpheaders.cpp index c63da899a8..a4ec7b422d 100644 --- a/src/network/access/qhttpheaders.cpp +++ b/src/network/access/qhttpheaders.cpp @@ -18,7 +18,7 @@ QT_BEGIN_NAMESPACE -Q_LOGGING_CATEGORY(lcQHttpHeaders, "qt.network.http.headers"); +Q_STATIC_LOGGING_CATEGORY(lcQHttpHeaders, "qt.network.http.headers"); /*! \class QHttpHeaders @@ -988,50 +988,6 @@ QDebug operator<<(QDebug debug, const QHttpHeaders &headers) } #endif -// A clarification on string encoding: -// Setters and getters only accept names and values that are Latin-1 representable: -// Either they are directly ASCII/Latin-1, or if they are UTF-X, they only use first 256 -// of the unicode points. For example using a '€' (U+20AC) in value would yield a warning -// and the call is ignored. -// Furthermore the 'name' has more strict rules than the 'value' - -// TODO FIXME REMOVEME once this is merged: -// https://codereview.qt-project.org/c/qt/qtbase/+/508829 -static bool isUtf8Latin1Representable(QUtf8StringView s) noexcept -{ - // L1 encoded in UTF8 has at most the form - // - 0b0XXX'XXXX - US-ASCII - // - 0b1100'00XX 0b10XX'XXXX - at most 8 non-zero LSB bits allowed in L1 - bool inMultibyte = false; - for (unsigned char c : s) { - if (c < 128) { // US-ASCII - if (inMultibyte) - return false; // invalid sequence - } else { - // decode as UTF-8: - if ((c & 0b1110'0000) == 0b1100'0000) { // two-octet UTF-8 leader - if (inMultibyte) - return false; // invalid sequence - inMultibyte = true; - const auto bits_7_to_11 = c & 0b0001'1111; - if (bits_7_to_11 < 0b10) - return false; // invalid sequence (US-ASCII encoded in two octets) - if (bits_7_to_11 > 0b11) // more than the two LSB - return false; // outside L1 - } else if ((c & 0b1100'0000) == 0b1000'0000) { // trailing UTF-8 octet - if (!inMultibyte) - return false; // invalid sequence - inMultibyte = false; // only one continuation allowed - } else { - return false; // invalid sequence or outside of L1 - } - } - } - if (inMultibyte) - return false; // invalid sequence: premature end - return true; -} - static constexpr auto isValidHttpHeaderNameChar = [](uchar c) noexcept { // RFC 9110 Chapters "5.1 Field Names" and "5.6.2 Tokens" @@ -1106,8 +1062,10 @@ static bool headerValueValidImpl(QLatin1StringView value) noexcept static bool headerValueValidImpl(QUtf8StringView value) noexcept { - if (!isUtf8Latin1Representable(value)) // TODO FIXME see the function - return false; + // UTF-8 byte sequences are also used as values directly + // => allow them as such. UTF-8 byte sequences for characters + // outside of ASCII should all fit into obs-text (>= 0x80) + // (see isValidHttpHeaderValueChar) return std::all_of(value.begin(), value.end(), isValidHttpHeaderValueChar); } diff --git a/src/network/access/qhttpmultipart.cpp b/src/network/access/qhttpmultipart.cpp index a695969f00..711d89544c 100644 --- a/src/network/access/qhttpmultipart.cpp +++ b/src/network/access/qhttpmultipart.cpp @@ -409,6 +409,14 @@ QHttpMultiPartPrivate::QHttpMultiPartPrivate() : contentType(QHttpMultiPart::Mix Q_ASSERT(boundary.size() <= 70); } +QHttpMultiPartPrivate::~QHttpMultiPartPrivate() +{ + delete device; +} + +QHttpMultiPartIODevice::~QHttpMultiPartIODevice() + = default; + qint64 QHttpMultiPartIODevice::size() const { // if not done yet, we calculate the size and the offsets of each part, diff --git a/src/network/access/qhttpmultipart_p.h b/src/network/access/qhttpmultipart_p.h index 7a12ce8424..39c147d2bc 100644 --- a/src/network/access/qhttpmultipart_p.h +++ b/src/network/access/qhttpmultipart_p.h @@ -16,10 +16,14 @@ // #include <QtNetwork/private/qtnetworkglobal_p.h> +#include <QtNetwork/qhttpmultipart.h> + #include "QtCore/qshareddata.h" #include "qnetworkrequest_p.h" // for deriving QHttpPartPrivate from QNetworkHeadersPrivate #include "qhttpheadershelper_p.h" + #include "private/qobject_p.h" +#include <QtCore/qiodevice.h> #ifndef Q_OS_WASM QT_REQUIRE_CONFIG(http); @@ -92,8 +96,7 @@ public: QIODevice(), multiPart(parentMultiPart), readPointer(0), deviceSize(-1) { } - ~QHttpMultiPartIODevice() { - } + ~QHttpMultiPartIODevice() override; virtual bool atEnd() const override { return readPointer == size(); @@ -128,15 +131,17 @@ public: -class QHttpMultiPartPrivate: public QObjectPrivate +class Q_AUTOTEST_EXPORT QHttpMultiPartPrivate: public QObjectPrivate { public: QHttpMultiPartPrivate(); + ~QHttpMultiPartPrivate() override; - ~QHttpMultiPartPrivate() + static QHttpMultiPartPrivate *get(QHttpMultiPart *message) { return message->d_func(); } + static const QHttpMultiPartPrivate *get(const QHttpMultiPart *message) { - delete device; + return message->d_func(); } QList<QHttpPart> parts; diff --git a/src/network/access/qhttpnetworkconnection.cpp b/src/network/access/qhttpnetworkconnection.cpp index 419491a711..3ef07c6993 100644 --- a/src/network/access/qhttpnetworkconnection.cpp +++ b/src/network/access/qhttpnetworkconnection.cpp @@ -52,10 +52,11 @@ static int getPreferredActiveChannelCount(QHttpNetworkConnection::ConnectionType QHttpNetworkConnectionPrivate::QHttpNetworkConnectionPrivate( quint16 connectionCount, const QString &hostName, quint16 port, bool encrypt, - QHttpNetworkConnection::ConnectionType type) + bool isLocalSocket, QHttpNetworkConnection::ConnectionType type) : hostName(hostName), port(port), encrypt(encrypt), + isLocalSocket(isLocalSocket), activeChannelCount(getPreferredActiveChannelCount(type, connectionCount)), channelCount(connectionCount), channels(new QHttpNetworkConnectionChannel[channelCount]), @@ -64,6 +65,8 @@ QHttpNetworkConnectionPrivate::QHttpNetworkConnectionPrivate( #endif connectionType(type) { + if (isLocalSocket) // Don't try to do host lookup for local sockets + networkLayerState = IPv4; // We allocate all 6 channels even if it's an HTTP/2-enabled // connection: in case the protocol negotiation via NPN/ALPN fails, // we will have normally working HTTP/1.1. @@ -102,13 +105,18 @@ void QHttpNetworkConnectionPrivate::pauseConnection() // Disable all socket notifiers for (int i = 0; i < activeChannelCount; i++) { - if (channels[i].socket) { + if (auto *absSocket = qobject_cast<QAbstractSocket *>(channels[i].socket)) { #ifndef QT_NO_SSL if (encrypt) - QSslSocketPrivate::pauseSocketNotifiers(static_cast<QSslSocket*>(channels[i].socket)); + QSslSocketPrivate::pauseSocketNotifiers(static_cast<QSslSocket*>(absSocket)); else #endif - QAbstractSocketPrivate::pauseSocketNotifiers(channels[i].socket); + QAbstractSocketPrivate::pauseSocketNotifiers(absSocket); + } else if (qobject_cast<QLocalSocket *>(channels[i].socket)) { + // @todo how would we do this? +#if 0 // @todo Enable this when there is a debug category for this + qDebug() << "Should pause socket but there is no way to do it for local sockets"; +#endif } } } @@ -118,17 +126,21 @@ void QHttpNetworkConnectionPrivate::resumeConnection() state = RunningState; // Enable all socket notifiers for (int i = 0; i < activeChannelCount; i++) { - if (channels[i].socket) { + if (auto *absSocket = qobject_cast<QAbstractSocket *>(channels[i].socket)) { #ifndef QT_NO_SSL if (encrypt) - QSslSocketPrivate::resumeSocketNotifiers(static_cast<QSslSocket*>(channels[i].socket)); + QSslSocketPrivate::resumeSocketNotifiers(static_cast<QSslSocket*>(absSocket)); else #endif - QAbstractSocketPrivate::resumeSocketNotifiers(channels[i].socket); + QAbstractSocketPrivate::resumeSocketNotifiers(absSocket); // Resume pending upload if needed if (channels[i].state == QHttpNetworkConnectionChannel::WritingState) QMetaObject::invokeMethod(&channels[i], "_q_uploadDataReadyRead", Qt::QueuedConnection); + } else if (qobject_cast<QLocalSocket *>(channels[i].socket)) { +#if 0 // @todo Enable this when there is a debug category for this + qDebug() << "Should resume socket but there is no way to do it for local sockets"; +#endif } } @@ -292,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)) { @@ -524,7 +541,9 @@ QHttpNetworkConnectionPrivate::parseRedirectResponse(QHttpNetworkReply *reply) // Check redirect url protocol const QUrl priorUrl(reply->request().url()); - if (redirectUrl.scheme() == "http"_L1 || redirectUrl.scheme() == "https"_L1) { + const QString targetUrlScheme = redirectUrl.scheme(); + if (targetUrlScheme == "http"_L1 || targetUrlScheme == "https"_L1 + || targetUrlScheme.startsWith("unix"_L1)) { switch (reply->request().redirectPolicy()) { case QNetworkRequest::NoLessSafeRedirectPolicy: // Here we could handle https->http redirects as InsecureProtocolError. @@ -535,7 +554,7 @@ QHttpNetworkConnectionPrivate::parseRedirectResponse(QHttpNetworkReply *reply) break; case QNetworkRequest::SameOriginRedirectPolicy: if (priorUrl.host() != redirectUrl.host() - || priorUrl.scheme() != redirectUrl.scheme() + || priorUrl.scheme() != targetUrlScheme || priorUrl.port() != redirectUrl.port()) { return {{}, QNetworkReply::InsecureRedirectError}; } @@ -1024,7 +1043,7 @@ void QHttpNetworkConnectionPrivate::_q_startNextRequest() for (int i = 0; i < activeChannelCount; ++i) { if (channels[i].resendCurrent && (channels[i].state != QHttpNetworkConnectionChannel::ClosingState)) { if (!channels[i].socket - || channels[i].socket->state() == QAbstractSocket::UnconnectedState) { + || QSocketAbstraction::socketState(channels[i].socket) == QAbstractSocket::UnconnectedState) { if (!channels[i].ensureConnection()) continue; } @@ -1048,7 +1067,9 @@ void QHttpNetworkConnectionPrivate::_q_startNextRequest() // try to get a free AND connected socket for (int i = 0; i < activeChannelCount; ++i) { if (channels[i].socket) { - if (!channels[i].reply && !channels[i].isSocketBusy() && channels[i].socket->state() == QAbstractSocket::ConnectedState) { + if (!channels[i].reply && !channels[i].isSocketBusy() + && QSocketAbstraction::socketState(channels[i].socket) + == QAbstractSocket::ConnectedState) { if (dequeueRequest(channels[i].socket)) channels[i].sendRequest(); } @@ -1068,7 +1089,8 @@ void QHttpNetworkConnectionPrivate::_q_startNextRequest() else if (networkLayerState == IPv6) channels[0].networkLayerPreference = QAbstractSocket::IPv6Protocol; channels[0].ensureConnection(); - if (channels[0].socket && channels[0].socket->state() == QAbstractSocket::ConnectedState + if (auto *s = channels[0].socket; s + && QSocketAbstraction::socketState(s) == QAbstractSocket::ConnectedState && !channels[0].pendingEncrypt) { if (channels[0].h2RequestsToSend.size()) { channels[0].sendRequest(); @@ -1095,9 +1117,13 @@ void QHttpNetworkConnectionPrivate::_q_startNextRequest() // return fast if there is nothing to pipeline if (highPriorityQueue.isEmpty() && lowPriorityQueue.isEmpty()) return; - for (int i = 0; i < activeChannelCount; i++) - if (channels[i].socket && channels[i].socket->state() == QAbstractSocket::ConnectedState) + for (int i = 0; i < activeChannelCount; i++) { + if (channels[i].socket + && QSocketAbstraction::socketState(channels[i].socket) + == QAbstractSocket::ConnectedState) { fillPipeline(channels[i].socket); + } + } // If there is not already any connected channels we need to connect a new one. // We do not pair the channel with the request until we know if it is @@ -1122,15 +1148,16 @@ void QHttpNetworkConnectionPrivate::_q_startNextRequest() if (!channels[i].socket) continue; - if ((channels[i].socket->state() == QAbstractSocket::ConnectingState) - || (channels[i].socket->state() == QAbstractSocket::HostLookupState) + using State = QAbstractSocket::SocketState; + if ((QSocketAbstraction::socketState(channels[i].socket) == State::ConnectingState) + || (QSocketAbstraction::socketState(channels[i].socket) == State::HostLookupState) || channels[i].pendingEncrypt) { // pendingEncrypt == "EncryptingState" neededOpenChannels--; continue; } if (!channels[i].reply && !channels[i].isSocketBusy() - && (channels[i].socket->state() == QAbstractSocket::UnconnectedState)) { + && (QSocketAbstraction::socketState(channels[i].socket) == State::UnconnectedState)) { channelsToConnect.push_back(i); neededOpenChannels--; } @@ -1329,9 +1356,9 @@ void QHttpNetworkConnectionPrivate::_q_connectDelayedChannel() } QHttpNetworkConnection::QHttpNetworkConnection(quint16 connectionCount, const QString &hostName, - quint16 port, bool encrypt, QObject *parent, + quint16 port, bool encrypt, bool isLocalSocket, QObject *parent, QHttpNetworkConnection::ConnectionType connectionType) - : QObject(*(new QHttpNetworkConnectionPrivate(connectionCount, hostName, port, encrypt, + : QObject(*(new QHttpNetworkConnectionPrivate(connectionCount, hostName, port, encrypt, isLocalSocket, connectionType)), parent) { Q_D(QHttpNetworkConnection); diff --git a/src/network/access/qhttpnetworkconnection_p.h b/src/network/access/qhttpnetworkconnection_p.h index c2d062fb16..5e4bce5eb0 100644 --- a/src/network/access/qhttpnetworkconnection_p.h +++ b/src/network/access/qhttpnetworkconnection_p.h @@ -64,7 +64,8 @@ public: }; QHttpNetworkConnection(quint16 channelCount, const QString &hostName, quint16 port = 80, - bool encrypt = false, QObject *parent = nullptr, + bool encrypt = false, bool isLocalSocket = false, + QObject *parent = nullptr, ConnectionType connectionType = ConnectionTypeHTTP); ~QHttpNetworkConnection(); @@ -155,7 +156,8 @@ public: }; QHttpNetworkConnectionPrivate(quint16 connectionCount, const QString &hostName, quint16 port, - bool encrypt, QHttpNetworkConnection::ConnectionType type); + bool encrypt, bool isLocalSocket, + QHttpNetworkConnection::ConnectionType type); ~QHttpNetworkConnectionPrivate(); void init(); @@ -205,6 +207,7 @@ public: QString hostName; quint16 port; bool encrypt; + bool isLocalSocket; bool delayIpv4 = true; // Number of channels we are trying to use at the moment: diff --git a/src/network/access/qhttpnetworkconnectionchannel.cpp b/src/network/access/qhttpnetworkconnectionchannel.cpp index e178d65356..8688e4b8d7 100644 --- a/src/network/access/qhttpnetworkconnectionchannel.cpp +++ b/src/network/access/qhttpnetworkconnectionchannel.cpp @@ -12,6 +12,7 @@ #include <private/qhttp2protocolhandler_p.h> #include <private/qhttpprotocolhandler_p.h> #include <private/http2protocol_p.h> +#include <private/qsocketabstraction_p.h> #ifndef QT_NO_SSL # include <private/qsslsocket_p.h> @@ -78,6 +79,8 @@ void QHttpNetworkConnectionChannel::init() #ifndef QT_NO_SSL if (connection->d_func()->encrypt) socket = new QSslSocket; + else if (connection->d_func()->isLocalSocket) + socket = new QLocalSocket; else socket = new QTcpSocket; #else @@ -85,7 +88,8 @@ void QHttpNetworkConnectionChannel::init() #endif #ifndef QT_NO_NETWORKPROXY // Set by QNAM anyway, but let's be safe here - socket->setProxy(QNetworkProxy::NoProxy); + if (auto s = qobject_cast<QAbstractSocket *>(socket)) + s->setProxy(QNetworkProxy::NoProxy); #endif // After some back and forth in all the last years, this is now a DirectConnection because otherwise @@ -94,32 +98,48 @@ void QHttpNetworkConnectionChannel::init() QObject::connect(socket, &QIODevice::bytesWritten, this, &QHttpNetworkConnectionChannel::_q_bytesWritten, Qt::DirectConnection); - QObject::connect(socket, &QAbstractSocket::connected, - this, &QHttpNetworkConnectionChannel::_q_connected, - Qt::DirectConnection); QObject::connect(socket, &QIODevice::readyRead, this, &QHttpNetworkConnectionChannel::_q_readyRead, Qt::DirectConnection); - // The disconnected() and error() signals may already come - // while calling connectToHost(). - // In case of a cached hostname or an IP this - // will then emit a signal to the user of QNetworkReply - // but cannot be caught because the user did not have a chance yet - // to connect to QNetworkReply's signals. - qRegisterMetaType<QAbstractSocket::SocketError>(); - QObject::connect(socket, &QAbstractSocket::disconnected, - this, &QHttpNetworkConnectionChannel::_q_disconnected, - Qt::DirectConnection); - QObject::connect(socket, &QAbstractSocket::errorOccurred, - this, &QHttpNetworkConnectionChannel::_q_error, - Qt::DirectConnection); + + QSocketAbstraction::visit([this](auto *socket){ + using SocketType = std::remove_pointer_t<decltype(socket)>; + QObject::connect(socket, &SocketType::connected, + this, &QHttpNetworkConnectionChannel::_q_connected, + Qt::DirectConnection); + + // The disconnected() and error() signals may already come + // while calling connectToHost(). + // In case of a cached hostname or an IP this + // will then emit a signal to the user of QNetworkReply + // but cannot be caught because the user did not have a chance yet + // to connect to QNetworkReply's signals. + QObject::connect(socket, &SocketType::disconnected, + this, &QHttpNetworkConnectionChannel::_q_disconnected, + Qt::DirectConnection); + if constexpr (std::is_same_v<SocketType, QAbstractSocket>) { + QObject::connect(socket, &QAbstractSocket::errorOccurred, + this, &QHttpNetworkConnectionChannel::_q_error, + Qt::DirectConnection); + } else if constexpr (std::is_same_v<SocketType, QLocalSocket>) { + auto convertAndForward = [this](QLocalSocket::LocalSocketError error) { + _q_error(static_cast<QAbstractSocket::SocketError>(error)); + }; + QObject::connect(socket, &SocketType::errorOccurred, + this, std::move(convertAndForward), + Qt::DirectConnection); + } + }, socket); + #ifndef QT_NO_NETWORKPROXY - QObject::connect(socket, &QAbstractSocket::proxyAuthenticationRequired, - this, &QHttpNetworkConnectionChannel::_q_proxyAuthenticationRequired, - Qt::DirectConnection); + if (auto *s = qobject_cast<QAbstractSocket *>(socket)) { + QObject::connect(s, &QAbstractSocket::proxyAuthenticationRequired, + this, &QHttpNetworkConnectionChannel::_q_proxyAuthenticationRequired, + Qt::DirectConnection); + } #endif #ifndef QT_NO_SSL @@ -156,8 +176,10 @@ void QHttpNetworkConnectionChannel::init() #endif #ifndef QT_NO_NETWORKPROXY - if (proxy.type() != QNetworkProxy::NoProxy) - socket->setProxy(proxy); + if (auto *s = qobject_cast<QAbstractSocket *>(socket); + s && proxy.type() != QNetworkProxy::NoProxy) { + s->setProxy(proxy); + } #endif isInitialized = true; } @@ -170,7 +192,7 @@ void QHttpNetworkConnectionChannel::close() if (!socket) state = QHttpNetworkConnectionChannel::IdleState; - else if (socket->state() == QAbstractSocket::UnconnectedState) + else if (QSocketAbstraction::socketState(socket) == QAbstractSocket::UnconnectedState) state = QHttpNetworkConnectionChannel::IdleState; else state = QHttpNetworkConnectionChannel::ClosingState; @@ -190,7 +212,7 @@ void QHttpNetworkConnectionChannel::abort() { if (!socket) state = QHttpNetworkConnectionChannel::IdleState; - else if (socket->state() == QAbstractSocket::UnconnectedState) + else if (QSocketAbstraction::socketState(socket) == QAbstractSocket::UnconnectedState) state = QHttpNetworkConnectionChannel::IdleState; else state = QHttpNetworkConnectionChannel::ClosingState; @@ -201,7 +223,10 @@ void QHttpNetworkConnectionChannel::abort() if (socket) { // socket can be 0 since the host lookup is done from qhttpnetworkconnection.cpp while // there is no socket yet. - socket->abort(); + auto callAbort = [](auto *s) { + s->abort(); + }; + QSocketAbstraction::visit(callAbort, socket); } } @@ -268,7 +293,7 @@ bool QHttpNetworkConnectionChannel::ensureConnection() if (!isInitialized) init(); - QAbstractSocket::SocketState socketState = socket->state(); + QAbstractSocket::SocketState socketState = QSocketAbstraction::socketState(socket); // resend this request after we receive the disconnected signal // If !socket->isOpen() then we have already called close() on the socket, but there was still a @@ -335,7 +360,8 @@ bool QHttpNetworkConnectionChannel::ensureConnection() connectHost = connection->d_func()->networkProxy.hostName(); connectPort = connection->d_func()->networkProxy.port(); } - if (socket->proxy().type() == QNetworkProxy::HttpProxy) { + if (auto *abSocket = qobject_cast<QAbstractSocket *>(socket); + abSocket && abSocket->proxy().type() == QNetworkProxy::HttpProxy) { // Make user-agent field available to HTTP proxy socket engine (QTBUG-17223) QByteArray value; // ensureConnection is called before any request has been assigned, but can also be @@ -353,11 +379,11 @@ bool QHttpNetworkConnectionChannel::ensureConnection() value = request.headerField("user-agent"); } if (!value.isEmpty()) { - QNetworkProxy proxy(socket->proxy()); + QNetworkProxy proxy(abSocket->proxy()); auto h = proxy.headers(); h.replaceOrAppend(QHttpHeaders::WellKnownHeader::UserAgent, value); proxy.setHeaders(std::move(h)); - socket->setProxy(proxy); + abSocket->setProxy(proxy); } } #endif @@ -380,7 +406,7 @@ bool QHttpNetworkConnectionChannel::ensureConnection() // limit the socket read buffer size. we will read everything into // the QHttpNetworkReply anyway, so let's grow only that and not // here and there. - socket->setReadBufferSize(64*1024); + sslSocket->setReadBufferSize(64*1024); #else // Need to dequeue the request so that we can emit the error. if (!reply) @@ -394,17 +420,24 @@ bool QHttpNetworkConnectionChannel::ensureConnection() && connection->cacheProxy().type() == QNetworkProxy::NoProxy && connection->transparentProxy().type() == QNetworkProxy::NoProxy) { #endif - socket->connectToHost(connectHost, connectPort, QIODevice::ReadWrite | QIODevice::Unbuffered, networkLayerPreference); - // For an Unbuffered QTcpSocket, the read buffer size has a special meaning. - socket->setReadBufferSize(1*1024); + if (auto *s = qobject_cast<QAbstractSocket *>(socket)) { + s->connectToHost(connectHost, connectPort, + QIODevice::ReadWrite | QIODevice::Unbuffered, + networkLayerPreference); + // For an Unbuffered QTcpSocket, the read buffer size has a special meaning. + s->setReadBufferSize(1 * 1024); + } else if (auto *s = qobject_cast<QLocalSocket *>(socket)) { + s->connectToServer(connectHost); + } #ifndef QT_NO_NETWORKPROXY } else { - socket->connectToHost(connectHost, connectPort, QIODevice::ReadWrite, networkLayerPreference); - + auto *s = qobject_cast<QAbstractSocket *>(socket); + Q_ASSERT(s); // limit the socket read buffer size. we will read everything into // the QHttpNetworkReply anyway, so let's grow only that and not // here and there. - socket->setReadBufferSize(64*1024); + s->connectToHost(connectHost, connectPort, QIODevice::ReadWrite, networkLayerPreference); + s->setReadBufferSize(64 * 1024); } #endif } @@ -507,7 +540,7 @@ void QHttpNetworkConnectionChannel::allDone() // move next from pipeline to current request if (!alreadyPipelinedRequests.isEmpty()) { - if (resendCurrent || connectionCloseEnabled || socket->state() != QAbstractSocket::ConnectedState) { + if (resendCurrent || connectionCloseEnabled || QSocketAbstraction::socketState(socket) != QAbstractSocket::ConnectedState) { // move the pipelined ones back to the main queue requeueCurrentlyPipelinedRequests(); close(); @@ -538,7 +571,7 @@ void QHttpNetworkConnectionChannel::allDone() QMetaObject::invokeMethod(connection, "_q_startNextRequest", Qt::QueuedConnection); } else if (alreadyPipelinedRequests.isEmpty()) { if (connectionCloseEnabled) - if (socket->state() != QAbstractSocket::UnconnectedState) + if (QSocketAbstraction::socketState(socket) != QAbstractSocket::UnconnectedState) close(); if (qobject_cast<QHttpNetworkConnection*>(connection)) QMetaObject::invokeMethod(connection, "_q_startNextRequest", Qt::QueuedConnection); @@ -556,7 +589,7 @@ void QHttpNetworkConnectionChannel::detectPipeliningSupport() // check for not having connection close && (!reply->d_func()->isConnectionCloseEnabled()) // check if it is still connected - && (socket->state() == QAbstractSocket::ConnectedState) + && (QSocketAbstraction::socketState(socket) == QAbstractSocket::ConnectedState) // check for broken servers in server reply header // this is adapted from http://mxr.mozilla.org/firefox/ident?i=SupportsPipelining && (serverHeaderField = reply->headerField("Server"), !serverHeaderField.contains("Microsoft-IIS/4.")) @@ -679,8 +712,8 @@ bool QHttpNetworkConnectionChannel::resetUploadData() void QHttpNetworkConnectionChannel::setProxy(const QNetworkProxy &networkProxy) { - if (socket) - socket->setProxy(networkProxy); + if (auto *s = qobject_cast<QAbstractSocket *>(socket)) + s->setProxy(networkProxy); proxy = networkProxy; } @@ -841,7 +874,7 @@ void QHttpNetworkConnectionChannel::_q_disconnected() } -void QHttpNetworkConnectionChannel::_q_connected() +void QHttpNetworkConnectionChannel::_q_connected_abstract_socket(QAbstractSocket *absSocket) { // For the Happy Eyeballs we need to check if this is the first channel to connect. if (connection->d_func()->networkLayerState == QHttpNetworkConnectionPrivate::HostLookupPending || connection->d_func()->networkLayerState == QHttpNetworkConnectionPrivate::IPv4or6) { @@ -852,7 +885,7 @@ void QHttpNetworkConnectionChannel::_q_connected() else if (networkLayerPreference == QAbstractSocket::IPv6Protocol) connection->d_func()->networkLayerState = QHttpNetworkConnectionPrivate::IPv6; else { - if (socket->peerAddress().protocol() == QAbstractSocket::IPv4Protocol) + if (absSocket->peerAddress().protocol() == QAbstractSocket::IPv4Protocol) connection->d_func()->networkLayerState = QHttpNetworkConnectionPrivate::IPv4; else connection->d_func()->networkLayerState = QHttpNetworkConnectionPrivate::IPv6; @@ -875,7 +908,7 @@ void QHttpNetworkConnectionChannel::_q_connected() } // improve performance since we get the request sent by the kernel ASAP - //socket->setSocketOption(QAbstractSocket::LowDelayOption, 1); + //absSocket->setSocketOption(QAbstractSocket::LowDelayOption, 1); // We have this commented out now. It did not have the effect we wanted. If we want to // do this properly, Qt has to combine multiple HTTP requests into one buffer // and send this to the kernel in one syscall and then the kernel immediately sends @@ -884,7 +917,7 @@ void QHttpNetworkConnectionChannel::_q_connected() // the requests into one TCP packet. // not sure yet if it helps, but it makes sense - socket->setSocketOption(QAbstractSocket::KeepAliveOption, 1); + absSocket->setSocketOption(QAbstractSocket::KeepAliveOption, 1); pipeliningSupported = QHttpNetworkConnectionChannel::PipeliningSupportUnknown; @@ -893,7 +926,7 @@ void QHttpNetworkConnectionChannel::_q_connected() if (!connectionPrivate->connectionMonitor.isMonitoring()) { // Now that we have a pair of addresses, we can start monitoring the // connection status to handle its loss properly. - if (connectionPrivate->connectionMonitor.setTargets(socket->localAddress(), socket->peerAddress())) + if (connectionPrivate->connectionMonitor.setTargets(absSocket->localAddress(), absSocket->peerAddress())) connectionPrivate->connectionMonitor.startMonitoring(); } } @@ -905,7 +938,7 @@ void QHttpNetworkConnectionChannel::_q_connected() if (!connection->sslContext()) { // this socket is making the 1st handshake for this connection, // we need to set the SSL context so new sockets can reuse it - if (auto socketSslContext = QSslSocketPrivate::sslContext(static_cast<QSslSocket*>(socket))) + if (auto socketSslContext = QSslSocketPrivate::sslContext(static_cast<QSslSocket*>(absSocket))) connection->setSslContext(std::move(socketSslContext)); } #endif @@ -927,7 +960,7 @@ void QHttpNetworkConnectionChannel::_q_connected() switchedToHttp2 = false; if (!reply) - connection->d_func()->dequeueRequest(socket); + connection->d_func()->dequeueRequest(absSocket); if (reply) { if (tryProtocolUpgrade) { @@ -940,6 +973,22 @@ void QHttpNetworkConnectionChannel::_q_connected() } } +void QHttpNetworkConnectionChannel::_q_connected_local_socket(QLocalSocket *localSocket) +{ + state = QHttpNetworkConnectionChannel::IdleState; + if (!reply) // No reply object, try to dequeue a request (which is paired with a reply): + connection->d_func()->dequeueRequest(localSocket); + if (reply) + sendRequest(); +} + +void QHttpNetworkConnectionChannel::_q_connected() +{ + if (auto *s = qobject_cast<QAbstractSocket *>(socket)) + _q_connected_abstract_socket(s); + else if (auto *s = qobject_cast<QLocalSocket *>(socket)) + _q_connected_local_socket(s); +} void QHttpNetworkConnectionChannel::_q_error(QAbstractSocket::SocketError socketError) { @@ -1116,7 +1165,7 @@ void QHttpNetworkConnectionChannel::_q_error(QAbstractSocket::SocketError socket //signal emission triggered event loop if (!socket) state = QHttpNetworkConnectionChannel::IdleState; - else if (socket->state() == QAbstractSocket::UnconnectedState) + else if (QSocketAbstraction::socketState(socket) == QAbstractSocket::UnconnectedState) state = QHttpNetworkConnectionChannel::IdleState; else state = QHttpNetworkConnectionChannel::ClosingState; diff --git a/src/network/access/qhttpnetworkconnectionchannel_p.h b/src/network/access/qhttpnetworkconnectionchannel_p.h index c42290feca..853b647ecc 100644 --- a/src/network/access/qhttpnetworkconnectionchannel_p.h +++ b/src/network/access/qhttpnetworkconnectionchannel_p.h @@ -19,6 +19,7 @@ #include <QtNetwork/qnetworkrequest.h> #include <QtNetwork/qnetworkreply.h> #include <QtNetwork/qabstractsocket.h> +#include <QtNetwork/qlocalsocket.h> #include <private/qobject_p.h> #include <qauthenticator.h> @@ -71,7 +72,7 @@ public: ClosingState = 16, BusyState = (ConnectingState|WritingState|WaitingState|ReadingState|ClosingState) }; - QAbstractSocket *socket; + QIODevice *socket; bool ssl; bool isInitialized; ChannelState state; @@ -156,6 +157,8 @@ public: void _q_bytesWritten(qint64 bytes); // proceed sending void _q_readyRead(); // pending data to read void _q_disconnected(); // disconnected from host + void _q_connected_abstract_socket(QAbstractSocket *socket); + void _q_connected_local_socket(QLocalSocket *socket); void _q_connected(); // start sending request void _q_error(QAbstractSocket::SocketError); // error from socket #ifndef QT_NO_NETWORKPROXY diff --git a/src/network/access/qhttpnetworkrequest.cpp b/src/network/access/qhttpnetworkrequest.cpp index 7a4ffb1684..06cc0b4464 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 131885f6d2..4444020402 100644 --- a/src/network/access/qhttpnetworkrequest_p.h +++ b/src/network/access/qhttpnetworkrequest_p.h @@ -117,6 +117,9 @@ public: QString peerVerifyName() const; void setPeerVerifyName(const QString &peerName); + QString fullLocalServerName() const; + void setFullLocalServerName(const QString &fullServerName); + private: QSharedDataPointer<QHttpNetworkRequestPrivate> d; friend class QHttpNetworkRequestPrivate; @@ -140,6 +143,7 @@ public: 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 b0ae0dcf44..4e5cf05aef 100644 --- a/src/network/access/qhttpthreaddelegate.cpp +++ b/src/network/access/qhttpthreaddelegate.cpp @@ -95,7 +95,9 @@ static QByteArray makeCacheKey(QUrl &url, QNetworkProxy *proxy, const QString &p QUrl copy = url; QString scheme = copy.scheme(); bool isEncrypted = scheme == "https"_L1 || scheme == "preconnect-https"_L1; - copy.setPort(copy.port(isEncrypted ? 443 : 80)); + const bool isLocalSocket = scheme.startsWith("unix"_L1); + if (!isLocalSocket) + copy.setPort(copy.port(isEncrypted ? 443 : 80)); if (scheme == "preconnect-http"_L1) copy.setScheme("http"_L1); else if (scheme == "preconnect-https"_L1) @@ -145,9 +147,9 @@ class QNetworkAccessCachedHttpConnection: public QHttpNetworkConnection, { // Q_OBJECT public: - QNetworkAccessCachedHttpConnection(quint16 connectionCount, const QString &hostName, quint16 port, bool encrypt, + QNetworkAccessCachedHttpConnection(quint16 connectionCount, const QString &hostName, quint16 port, bool encrypt, bool isLocalSocket, QHttpNetworkConnection::ConnectionType connectionType) - : QHttpNetworkConnection(connectionCount, hostName, port, encrypt, /*parent=*/nullptr, connectionType) + : QHttpNetworkConnection(connectionCount, hostName, port, encrypt, isLocalSocket, /*parent=*/nullptr, connectionType) { setExpires(true); setShareable(true); @@ -244,7 +246,9 @@ void QHttpThreadDelegate::startRequest() // check if we have an open connection to this host QUrl urlCopy = httpRequest.url(); - urlCopy.setPort(urlCopy.port(ssl ? 443 : 80)); + const bool isLocalSocket = urlCopy.scheme().startsWith("unix"_L1); + if (!isLocalSocket) + urlCopy.setPort(urlCopy.port(ssl ? 443 : 80)); QHttpNetworkConnection::ConnectionType connectionType = httpRequest.isHTTP2Allowed() ? QHttpNetworkConnection::ConnectionTypeHTTP2 @@ -279,10 +283,19 @@ void QHttpThreadDelegate::startRequest() } else #endif // QT_CONFIG(ssl) { - urlCopy.setScheme(QStringLiteral("h2")); + if (isLocalSocket) + urlCopy.setScheme(QStringLiteral("unix+h2")); + else + urlCopy.setScheme(QStringLiteral("h2")); } } + 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()); @@ -295,10 +308,19 @@ void QHttpThreadDelegate::startRequest() // the http object is actually a QHttpNetworkConnection httpConnection = static_cast<QNetworkAccessCachedHttpConnection *>(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, - connectionType); + httpConnection = new QNetworkAccessCachedHttpConnection( + http1Parameters.numberOfConnectionsPerHost(), host, urlCopy.port(), ssl, + isLocalSocket, connectionType); if (connectionType == QHttpNetworkConnection::ConnectionTypeHTTP2 || connectionType == QHttpNetworkConnection::ConnectionTypeHTTP2Direct) { httpConnection->setHttp2Parameters(http2Parameters); diff --git a/src/network/access/qnetworkaccessmanager.cpp b/src/network/access/qnetworkaccessmanager.cpp index 7ef062a54d..b371730c3c 100644 --- a/src/network/access/qnetworkaccessmanager.cpp +++ b/src/network/access/qnetworkaccessmanager.cpp @@ -857,22 +857,15 @@ QNetworkReply *QNetworkAccessManager::post(const QNetworkRequest &request, const } /*! - \overload + \fn QNetworkReply *QNetworkAccessManager::post(const QNetworkRequest &request, std::nullptr_t nptr) \since 6.8 + \overload + Sends the POST request specified by \a request without a body and returns a new QNetworkReply object. */ -QNetworkReply *QNetworkAccessManager::post(const QNetworkRequest &request, std::nullptr_t nptr) -{ - Q_UNUSED(nptr); - QIODevice *dev = nullptr; - - return d_func()->postProcess(createRequest(QNetworkAccessManager::PostOperation, - request, - dev)); -} #if QT_CONFIG(http) || defined(Q_OS_WASM) /*! @@ -958,22 +951,16 @@ QNetworkReply *QNetworkAccessManager::put(const QNetworkRequest &request, const } /*! + \since 6.8 + \overload - \since 6.8 + \fn QNetworkReply *QNetworkAccessManager::put(const QNetworkRequest &request, std::nullptr_t nptr) Sends the PUT request specified by \a request without a body and returns a new QNetworkReply object. */ -QNetworkReply *QNetworkAccessManager::put(const QNetworkRequest &request, std::nullptr_t nptr) -{ - Q_UNUSED(nptr); - QIODevice *dev = nullptr; - - return d_func()->postProcess(createRequest(QNetworkAccessManager::PutOperation, request, dev)); -} - /*! \since 4.6 @@ -1220,6 +1207,13 @@ QNetworkReply *QNetworkAccessManager::createRequest(QNetworkAccessManager::Opera bool isLocalFile = req.url().isLocalFile(); QString scheme = req.url().scheme(); + // Remap local+http to unix+http to make further processing easier + if (scheme == "local+http"_L1) { + scheme = u"unix+http"_s; + QUrl url = req.url(); + url.setScheme(scheme); + req.setUrl(url); + } // fast path for GET on file:// URLs // The QNetworkAccessFileBackend will right now only be used for PUT @@ -1296,11 +1290,15 @@ QNetworkReply *QNetworkAccessManager::createRequest(QNetworkAccessManager::Opera u"https", u"preconnect-https", #endif + u"unix+http", }; // Since Qt 5 we use the new QNetworkReplyHttpImpl if (std::find(std::begin(httpSchemes), std::end(httpSchemes), scheme) != std::end(httpSchemes)) { + #ifndef QT_NO_SSL - if (isStrictTransportSecurityEnabled() && d->stsCache.isKnownHost(request.url())) { + const bool isLocalSocket = scheme.startsWith("unix"_L1); + if (!isLocalSocket && isStrictTransportSecurityEnabled() + && d->stsCache.isKnownHost(request.url())) { QUrl stsUrl(request.url()); // RFC6797, 8.3: // The UA MUST replace the URI scheme with "https" [RFC2818], @@ -1391,6 +1389,8 @@ QStringList QNetworkAccessManager::supportedSchemesImplementation() const // Those ones don't exist in backends #if QT_CONFIG(http) schemes << QStringLiteral("http"); + schemes << QStringLiteral("unix+http"); + schemes << QStringLiteral("local+http"); #ifndef QT_NO_SSL if (QSslSocket::supportsSsl()) schemes << QStringLiteral("https"); diff --git a/src/network/access/qnetworkaccessmanager.h b/src/network/access/qnetworkaccessmanager.h index 0d069b2a9b..4bae05772f 100644 --- a/src/network/access/qnetworkaccessmanager.h +++ b/src/network/access/qnetworkaccessmanager.h @@ -84,10 +84,18 @@ public: QNetworkReply *get(const QNetworkRequest &request, const QByteArray &data); QNetworkReply *post(const QNetworkRequest &request, QIODevice *data); QNetworkReply *post(const QNetworkRequest &request, const QByteArray &data); - QNetworkReply *post(const QNetworkRequest &request, std::nullptr_t nptr); + QNetworkReply *post(const QNetworkRequest &request, std::nullptr_t) + { + return post(request, static_cast<QIODevice*>(nullptr)); + } + QNetworkReply *put(const QNetworkRequest &request, QIODevice *data); QNetworkReply *put(const QNetworkRequest &request, const QByteArray &data); - QNetworkReply *put(const QNetworkRequest &request, std::nullptr_t nptr); + QNetworkReply *put(const QNetworkRequest &request, std::nullptr_t) + { + return put(request, static_cast<QIODevice*>(nullptr)); + } + QNetworkReply *deleteResource(const QNetworkRequest &request); QNetworkReply *sendCustomRequest(const QNetworkRequest &request, const QByteArray &verb, QIODevice *data = nullptr); QNetworkReply *sendCustomRequest(const QNetworkRequest &request, const QByteArray &verb, const QByteArray &data); diff --git a/src/network/access/qnetworkreplyhttpimpl.cpp b/src/network/access/qnetworkreplyhttpimpl.cpp index 3e1fe761ee..89458825e9 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<QString>()) { + httpRequest.setFullLocalServerName(path.toString()); + } + } + // Create the HTTP thread delegate QHttpThreadDelegate *delegate = new QHttpThreadDelegate; // Propagate Http/2 settings: @@ -1228,7 +1235,8 @@ void QNetworkReplyHttpImplPrivate::onRedirected(const QUrl &redirectUrl, int htt if (httpRequest.isFollowRedirects()) // update the reply's url as it could've changed url = redirectUrl; - if (managerPrivate->stsEnabled && managerPrivate->stsCache.isKnownHost(url)) { + const bool wasLocalSocket = schemeBefore.startsWith("unix"_L1); + if (!wasLocalSocket && managerPrivate->stsEnabled && managerPrivate->stsCache.isKnownHost(url)) { // RFC6797, 8.3: // The UA MUST replace the URI scheme with "https" [RFC2818], // and if the URI contains an explicit port component of "80", @@ -1242,9 +1250,12 @@ void QNetworkReplyHttpImplPrivate::onRedirected(const QUrl &redirectUrl, int htt url.setPort(443); } - const bool isLessSafe = schemeBefore == "https"_L1 && url.scheme() == "http"_L1; - if (httpRequest.redirectPolicy() == QNetworkRequest::NoLessSafeRedirectPolicy - && isLessSafe) { + // Just to be on the safe side for local sockets, any changes to the scheme + // are considered less safe + const bool changingLocalScheme = wasLocalSocket && url.scheme() != schemeBefore; + const bool isLessSafe = changingLocalScheme + || (schemeBefore == "https"_L1 && url.scheme() == "http"_L1); + if (httpRequest.redirectPolicy() == QNetworkRequest::NoLessSafeRedirectPolicy && isLessSafe) { error(QNetworkReply::InsecureRedirectError, QCoreApplication::translate("QHttp", "Insecure redirect")); return; diff --git a/src/network/access/qnetworkreplywasmimpl.cpp b/src/network/access/qnetworkreplywasmimpl.cpp index 7d2b6a701e..ccf1542958 100644 --- a/src/network/access/qnetworkreplywasmimpl.cpp +++ b/src/network/access/qnetworkreplywasmimpl.cpp @@ -4,7 +4,6 @@ #include "qnetworkreplywasmimpl_p.h" #include "qnetworkrequest.h" -#include <QtCore/qtimer.h> #include <QtCore/qdatetime.h> #include <QtCore/qcoreapplication.h> #include <QtCore/qfileinfo.h> diff --git a/src/network/access/qnetworkrequest.cpp b/src/network/access/qnetworkrequest.cpp index 7a1b5426d2..2fb467d3a8 100644 --- a/src/network/access/qnetworkrequest.cpp +++ b/src/network/access/qnetworkrequest.cpp @@ -144,7 +144,8 @@ QT_IMPL_METATYPE_EXTERN_TAGGED(QNetworkRequest::RedirectPolicy, QNetworkRequest_ server (like "Ok", "Found", "Not Found", "Access Denied", etc.) This is the human-readable representation of the status code (see above). If the connection was not HTTP-based, this - attribute will not be present. + attribute will not be present. \e{Note:} The reason phrase is + not used when using HTTP/2. \value RedirectionTargetAttribute Replies only, type: QMetaType::QUrl (no default) @@ -324,6 +325,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 e281c74834..368eb99d95 100644 --- a/src/network/access/qnetworkrequest.h +++ b/src/network/access/qnetworkrequest.h @@ -70,6 +70,7 @@ public: ConnectionCacheExpiryTimeoutSecondsAttribute, Http2CleartextAllowedAttribute, UseCredentialsAttribute, + FullLocalServerNameAttribute, User = 1000, UserMax = 32767 diff --git a/src/network/access/qnetworkrequestfactory.cpp b/src/network/access/qnetworkrequestfactory.cpp index d9c536cef2..4666891b2e 100644 --- a/src/network/access/qnetworkrequestfactory.cpp +++ b/src/network/access/qnetworkrequestfactory.cpp @@ -17,7 +17,7 @@ QT_DEFINE_QESDP_SPECIALIZATION_DTOR(QNetworkRequestFactoryPrivate) using namespace Qt::StringLiterals; -Q_LOGGING_CATEGORY(lcQrequestfactory, "qt.network.access.request.factory") +Q_STATIC_LOGGING_CATEGORY(lcQrequestfactory, "qt.network.access.request.factory") /*! \class QNetworkRequestFactory @@ -28,8 +28,6 @@ Q_LOGGING_CATEGORY(lcQrequestfactory, "qt.network.access.request.factory") \brief Convenience class for grouping remote server endpoints that share common network request properties. - \preliminary - REST servers often have endpoints that require the same headers and other data. Grouping such endpoints with a QNetworkRequestFactory makes it more convenient to issue requests to these endpoints; only the typically diff --git a/src/network/access/qnetworkrequestfactory.h b/src/network/access/qnetworkrequestfactory.h index 9d955a51e7..c170b75c8e 100644 --- a/src/network/access/qnetworkrequestfactory.h +++ b/src/network/access/qnetworkrequestfactory.h @@ -25,7 +25,7 @@ class QSslConfiguration; class QNetworkRequestFactoryPrivate; QT_DECLARE_QESDP_SPECIALIZATION_DTOR_WITH_EXPORT(QNetworkRequestFactoryPrivate, Q_NETWORK_EXPORT) -class QT_TECH_PREVIEW_API QNetworkRequestFactory +class QNetworkRequestFactory { public: Q_NETWORK_EXPORT QNetworkRequestFactory(); diff --git a/src/network/access/qrestaccessmanager.cpp b/src/network/access/qrestaccessmanager.cpp index 7ef682e955..9e0182c7cb 100644 --- a/src/network/access/qrestaccessmanager.cpp +++ b/src/network/access/qrestaccessmanager.cpp @@ -31,8 +31,6 @@ Q_LOGGING_CATEGORY(lcQrest, "qt.network.access.rest") \inmodule QtNetwork \reentrant - \preliminary - QRestAccessManager is a convenience wrapper on top of QNetworkAccessManager. It amends datatypes and HTTP methods that are useful for typical RESTful client applications. diff --git a/src/network/access/qrestaccessmanager.h b/src/network/access/qrestaccessmanager.h index 3245b41785..d72c075bf8 100644 --- a/src/network/access/qrestaccessmanager.h +++ b/src/network/access/qrestaccessmanager.h @@ -74,7 +74,7 @@ QNetworkReply *customWithDataImpl(const QNetworkRequest& request, const QByteArr /* end */ class QRestAccessManagerPrivate; -class QT_TECH_PREVIEW_API Q_NETWORK_EXPORT QRestAccessManager : public QObject +class Q_NETWORK_EXPORT QRestAccessManager : public QObject { Q_OBJECT using CallbackPrototype = void(*)(QRestReply&); diff --git a/src/network/access/qrestaccessmanager_p.h b/src/network/access/qrestaccessmanager_p.h index 2e6c1afb90..5fa2cc9e5a 100644 --- a/src/network/access/qrestaccessmanager_p.h +++ b/src/network/access/qrestaccessmanager_p.h @@ -20,6 +20,7 @@ #include <QtNetwork/qnetworkaccessmanager.h> +#include <QtCore/qloggingcategory.h> #include <QtCore/qjsonarray.h> #include <QtCore/qhash.h> #include <QtCore/qjsondocument.h> @@ -28,6 +29,8 @@ QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQrest) + class QRestReply; class QRestAccessManagerPrivate : public QObjectPrivate { diff --git a/src/network/access/qrestreply.cpp b/src/network/access/qrestreply.cpp index 2d8d101084..204ecf6553 100644 --- a/src/network/access/qrestreply.cpp +++ b/src/network/access/qrestreply.cpp @@ -5,6 +5,7 @@ #include "qrestreply_p.h" #include <QtNetwork/private/qnetworkreply_p.h> +#include <QtNetwork/private/qrestaccessmanager_p.h> #include <QtCore/qbytearrayview.h> #include <QtCore/qjsondocument.h> @@ -18,7 +19,6 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; -Q_DECLARE_LOGGING_CATEGORY(lcQrest) /*! \class QRestReply @@ -29,8 +29,6 @@ Q_DECLARE_LOGGING_CATEGORY(lcQrest) \ingroup network \inmodule QtNetwork - \preliminary - QRestReply wraps a QNetworkReply and provides convenience methods for data and status handling. The methods provide convenience for typical RESTful client applications. diff --git a/src/network/access/qrestreply.h b/src/network/access/qrestreply.h index c32fff1d4e..61aa7a6788 100644 --- a/src/network/access/qrestreply.h +++ b/src/network/access/qrestreply.h @@ -20,7 +20,7 @@ class QJsonDocument; class QString; class QRestReplyPrivate; -class QT_TECH_PREVIEW_API QRestReply +class QRestReply { public: Q_NETWORK_EXPORT explicit QRestReply(QNetworkReply *reply); |