diff options
Diffstat (limited to 'src/network')
58 files changed, 2718 insertions, 647 deletions
diff --git a/src/network/CMakeLists.txt b/src/network/CMakeLists.txt index e977400245..08789d89de 100644 --- a/src/network/CMakeLists.txt +++ b/src/network/CMakeLists.txt @@ -110,6 +110,7 @@ qt_internal_extend_target(Network CONDITION APPLE qt_internal_extend_target(Network CONDITION WASM SOURCES + access/qformdatabuilder.cpp access/qformdatabuilder.h access/qhttpmultipart.cpp access/qhttpmultipart.h access/qhttpmultipart_p.h access/qhttpnetworkheader.cpp access/qhttpnetworkheader_p.h access/qnetworkreplywasmimpl.cpp access/qnetworkreplywasmimpl_p.h @@ -126,6 +127,7 @@ qt_internal_extend_target(Network CONDITION QT_FEATURE_http access/http2/huffman.cpp access/http2/huffman_p.h access/qabstractprotocolhandler.cpp access/qabstractprotocolhandler_p.h access/qdecompresshelper.cpp access/qdecompresshelper_p.h + access/qformdatabuilder.cpp access/qformdatabuilder.h access/qhttp1configuration.cpp access/qhttp1configuration.h access/qhttp2configuration.cpp access/qhttp2configuration.h access/qhttp2connection.cpp access/qhttp2connection_p.h 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/qabstractnetworkcache.cpp b/src/network/access/qabstractnetworkcache.cpp index c8b940d801..3cd55d46fa 100644 --- a/src/network/access/qabstractnetworkcache.cpp +++ b/src/network/access/qabstractnetworkcache.cpp @@ -3,6 +3,8 @@ #include "qabstractnetworkcache.h" #include "qabstractnetworkcache_p.h" +#include "qnetworkrequest_p.h" +#include "qhttpheadershelper_p.h" #include <qdatastream.h> #include <qdatetime.h> @@ -28,14 +30,14 @@ public: url == other.url && lastModified == other.lastModified && expirationDate == other.expirationDate - && headers == other.headers - && saveToDisk == other.saveToDisk; + && saveToDisk == other.saveToDisk + && QHttpHeadersHelper::compareStrict(headers, other.headers); } QUrl url; QDateTime lastModified; QDateTime expirationDate; - QNetworkCacheMetaData::RawHeaderList headers; + QHttpHeaders headers; QNetworkCacheMetaData::AttributesMap attributes; bool saveToDisk; @@ -207,21 +209,45 @@ void QNetworkCacheMetaData::setUrl(const QUrl &url) Returns a list of all raw headers that are set in this meta data. The list is in the same order that the headers were set. - \sa setRawHeaders() + \sa setRawHeaders(), headers() */ QNetworkCacheMetaData::RawHeaderList QNetworkCacheMetaData::rawHeaders() const { - return d->headers; + return QNetworkHeadersPrivate::fromHttpToRaw(d->headers); } /*! Sets the raw headers to \a list. - \sa rawHeaders() + \sa rawHeaders(), setHeaders() */ void QNetworkCacheMetaData::setRawHeaders(const RawHeaderList &list) { - d->headers = list; + d->headers = QNetworkHeadersPrivate::fromRawToHttp(list); +} + +/*! + \since 6.8 + + Returns headers in form of QHttpHeaders that are set in this meta data. + + \sa setHeaders() +*/ +QHttpHeaders QNetworkCacheMetaData::headers() const +{ + return d->headers; +} + +/*! + \since 6.8 + + Sets the headers of this network cache meta data to \a headers. + + \sa headers() +*/ +void QNetworkCacheMetaData::setHeaders(const QHttpHeaders &headers) +{ + d->headers = headers; } /*! @@ -367,7 +393,8 @@ void QNetworkCacheMetaDataPrivate::load(QDataStream &in, QNetworkCacheMetaData & in >> p->lastModified; in >> p->saveToDisk; in >> p->attributes; - in >> p->headers; + QNetworkCacheMetaData::RawHeaderList list; in >> list; + metaData.setRawHeaders(list); } /*! diff --git a/src/network/access/qabstractnetworkcache.h b/src/network/access/qabstractnetworkcache.h index c70dcf737c..b12fd4f863 100644 --- a/src/network/access/qabstractnetworkcache.h +++ b/src/network/access/qabstractnetworkcache.h @@ -48,6 +48,9 @@ public: RawHeaderList rawHeaders() const; void setRawHeaders(const RawHeaderList &headers); + QHttpHeaders headers() const; + void setHeaders(const QHttpHeaders &headers); + QDateTime lastModified() const; void setLastModified(const QDateTime &dateTime); diff --git a/src/network/access/qabstractprotocolhandler_p.h b/src/network/access/qabstractprotocolhandler_p.h index ad82aae66e..da5eaeeb74 100644 --- a/src/network/access/qabstractprotocolhandler_p.h +++ b/src/network/access/qabstractprotocolhandler_p.h @@ -23,7 +23,7 @@ QT_BEGIN_NAMESPACE class QHttpNetworkConnectionChannel; class QHttpNetworkReply; -class QAbstractSocket; +class QIODevice; class QHttpNetworkConnection; class QAbstractProtocolHandler { @@ -39,7 +39,7 @@ public: protected: QHttpNetworkConnectionChannel *m_channel; QHttpNetworkReply *m_reply; - QAbstractSocket *m_socket; + QIODevice *m_socket; QHttpNetworkConnection *m_connection; }; diff --git a/src/network/access/qformdatabuilder.cpp b/src/network/access/qformdatabuilder.cpp new file mode 100644 index 0000000000..9cde45cddd --- /dev/null +++ b/src/network/access/qformdatabuilder.cpp @@ -0,0 +1,330 @@ +// 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 + +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 +*/ + +/*! + Constructs a QFormDataPartBuilder object and sets \a name as the name + parameter of the form-data. +*/ +QFormDataPartBuilder::QFormDataPartBuilder(QLatin1StringView 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)>); + + m_headerValue += "form-data; name=\""; + for (auto c : name) { + if (c == '"' || c == '\\') + m_headerValue += '\\'; + m_headerValue += c; + } + 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 auto encodeFileName(QStringView view) +{ + struct R { QByteArrayView encoding; QByteArray encoded; }; + + QByteArrayView encoding = "="; + bool needsUtf8 = false; + + for (QChar c : view) { + if (c > u'\xff') { + encoding = "*=UTF-8''"; + needsUtf8 = true; + break; + } else if (c > u'\x7f') { + encoding = "*=ISO-8859-1''"; + } + } + + return R{encoding, needsUtf8 ? view.toUtf8() : view.toLatin1()}; +} + +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; +} + +/*! + 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 auto enc = encodeFileName(m_originalBodyName); + m_headerValue += "; filename" + enc.encoding + + enc.encoded.toPercentEncoding(); // RFC 5987 Section 3.2.1 + } + +#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> = builder.buildMultiPart(); + \endcode + + \sa QHttpPart, QHttpMultiPart, QFormDataPartBuilder +*/ + +/*! + Constructs an empty QFormDataBuilder object. +*/ + +QFormDataBuilder::QFormDataBuilder() + = default; + +/*! + Destroys the QFormDataBuilder object. +*/ + +QFormDataBuilder::~QFormDataBuilder() + = default; + +/*! + \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. + + \sa QFormDataPartBuilder, QHttpPart +*/ + +QFormDataPartBuilder &QFormDataBuilder::part(QLatin1StringView name) +{ + static_assert(std::is_nothrow_move_constructible_v<decltype(m_parts)>); + static_assert(std::is_nothrow_move_assignable_v<decltype(m_parts)>); + + return m_parts.emplace_back(name, QFormDataPartBuilder::PrivateConstructor()); +} + +/*! + Constructs and returns a pointer to a QHttpMultipart object. The caller + takes ownership of the generated QHttpMultiPart object. + + \sa QHttpMultiPart +*/ + +std::unique_ptr<QHttpMultiPart> QFormDataBuilder::buildMultiPart() +{ + auto multiPart = std::make_unique<QHttpMultiPart>(QHttpMultiPart::FormDataType); + + for (auto &part : m_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..68f9f3742c --- /dev/null +++ b/src/network/access/qformdatabuilder.h @@ -0,0 +1,126 @@ +// 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> +#include <vector> + +#ifndef Q_OS_WASM +QT_REQUIRE_CONFIG(http); +#endif + +class tst_QFormDataBuilder; + +QT_BEGIN_NAMESPACE + +class QHttpPartPrivate; +class QHttpMultiPart; +class QDebug; + +class QFormDataPartBuilder +{ + struct PrivateConstructor { explicit PrivateConstructor() = default; }; +public: + Q_NETWORK_EXPORT explicit QFormDataPartBuilder(QLatin1StringView 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); + Q_NETWORK_EXPORT 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 class ::tst_QFormDataBuilder; + friend void swap(QFormDataPartBuilder &lhs, QFormDataPartBuilder &rhs) noexcept + { lhs.swap(rhs); } +}; + +class QFormDataBuilder +{ +public: + Q_NETWORK_EXPORT explicit QFormDataBuilder(); + + QFormDataBuilder(QFormDataBuilder &&other) noexcept + : m_parts(std::move(other.m_parts)), + m_reserved(std::exchange(other.m_reserved, nullptr)) + { + + } + + QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_PURE_SWAP(QFormDataBuilder) + void swap(QFormDataBuilder &other) noexcept + { + m_parts.swap(other.m_parts); + qt_ptr_swap(m_reserved, other.m_reserved); + } + + Q_NETWORK_EXPORT ~QFormDataBuilder(); + Q_NETWORK_EXPORT QFormDataPartBuilder &part(QLatin1StringView name); + Q_NETWORK_EXPORT std::unique_ptr<QHttpMultiPart> buildMultiPart(); +private: + std::vector<QFormDataPartBuilder> m_parts; + void *m_reserved = nullptr; + + friend void swap(QFormDataBuilder &lhs, QFormDataBuilder &rhs) noexcept + { lhs.swap(rhs); } + + Q_DISABLE_COPY(QFormDataBuilder) +}; + +QT_END_NAMESPACE + +#endif // QFORMDATABUILDER_H diff --git a/src/network/access/qhttpmultipart.cpp b/src/network/access/qhttpmultipart.cpp index 6d81f1b957..a695969f00 100644 --- a/src/network/access/qhttpmultipart.cpp +++ b/src/network/access/qhttpmultipart.cpp @@ -386,9 +386,12 @@ void QHttpPartPrivate::checkHeaderCreated() const { if (!headerCreated) { // copied from QHttpNetworkRequestPrivate::header() and adapted - const auto fields = allRawHeaders(); - for (const auto &[name, value] : fields) - header += name + ": " + value + "\r\n"; + const auto h = headers(); + for (qsizetype i = 0; i < h.size(); ++i) { + const auto name = h.nameAt(i); + header += QByteArrayView(name.data(), name.size()) + ": " + h.valueAt(i) + "\r\n"; + } + header += "\r\n"; headerCreated = true; } @@ -511,6 +514,48 @@ qint64 QHttpMultiPartIODevice::writeData(const char *data, qint64 maxSize) return -1; } +#ifndef QT_NO_DEBUG_STREAM + +/*! + \fn QDebug QHttpPart::operator<<(QDebug debug, const QHttpPart &part) + + Writes the \a part into the \a debug object for debugging purposes. + Unless a device is set, the size of the body is shown. + + \sa {Debugging Techniques} + \since 6.8 +*/ + +QDebug operator<<(QDebug debug, const QHttpPart &part) +{ + const QDebugStateSaver saver(debug); + debug.resetFormat().nospace().noquote(); + + debug << "QHttpPart(headers = [" + << part.d->cookedHeaders + << "], http headers = [" + << part.d->httpHeaders + << "],"; + + if (part.d->bodyDevice) { + debug << " bodydevice = [" + << part.d->bodyDevice + << ", is open: " + << part.d->bodyDevice->isOpen() + << "]"; + } else { + debug << " size of body = " + << part.d->body.size() + << " bytes"; + } + + debug << ")"; + + return debug; +} + +#endif // QT_NO_DEBUG_STREAM + QT_END_NAMESPACE diff --git a/src/network/access/qhttpmultipart.h b/src/network/access/qhttpmultipart.h index 26e5fafdf2..9732bbd99d 100644 --- a/src/network/access/qhttpmultipart.h +++ b/src/network/access/qhttpmultipart.h @@ -19,6 +19,7 @@ QT_BEGIN_NAMESPACE class QHttpPartPrivate; class QHttpMultiPart; +class QDebug; class Q_NETWORK_EXPORT QHttpPart { @@ -45,6 +46,9 @@ private: QSharedDataPointer<QHttpPartPrivate> d; friend class QHttpMultiPartIODevice; +#ifndef QT_NO_DEBUG_STREAM + friend Q_NETWORK_EXPORT QDebug operator<<(QDebug debug, const QHttpPart &httpPart); +#endif }; Q_DECLARE_SHARED(QHttpPart) diff --git a/src/network/access/qhttpmultipart_p.h b/src/network/access/qhttpmultipart_p.h index d485fcf5cd..7a12ce8424 100644 --- a/src/network/access/qhttpmultipart_p.h +++ b/src/network/access/qhttpmultipart_p.h @@ -18,6 +18,7 @@ #include <QtNetwork/private/qtnetworkglobal_p.h> #include "QtCore/qshareddata.h" #include "qnetworkrequest_p.h" // for deriving QHttpPartPrivate from QNetworkHeadersPrivate +#include "qhttpheadershelper_p.h" #include "private/qobject_p.h" #ifndef Q_OS_WASM @@ -47,8 +48,10 @@ public: inline bool operator==(const QHttpPartPrivate &other) const { - return rawHeaders == other.rawHeaders && body == other.body && - bodyDevice == other.bodyDevice && readPointer == other.readPointer; + return QHttpHeadersHelper::compareStrict(httpHeaders, other.httpHeaders) + && body == other.body + && bodyDevice == other.bodyDevice + && readPointer == other.readPointer; } void setBodyDevice(QIODevice *device) { diff --git a/src/network/access/qhttpnetworkconnection.cpp b/src/network/access/qhttpnetworkconnection.cpp index 1897380e0e..3ef07c6993 100644 --- a/src/network/access/qhttpnetworkconnection.cpp +++ b/src/network/access/qhttpnetworkconnection.cpp @@ -13,6 +13,7 @@ #include <qauthenticator.h> #include <qcoreapplication.h> #include <private/qdecompresshelper_p.h> +#include <private/qsocketabstraction_p.h> #include <qbuffer.h> #include <qpair.h> @@ -51,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]), @@ -63,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. @@ -101,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 } } } @@ -117,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 } } @@ -291,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)) { @@ -319,7 +337,7 @@ void QHttpNetworkConnectionPrivate::prepareRequest(HttpMessagePair &messagePair) -void QHttpNetworkConnectionPrivate::emitReplyError(QAbstractSocket *socket, +void QHttpNetworkConnectionPrivate::emitReplyError(QIODevice *socket, QHttpNetworkReply *reply, QNetworkReply::NetworkError errorCode) { @@ -384,7 +402,7 @@ void QHttpNetworkConnectionPrivate::copyCredentials(int fromChannel, QAuthentica // handles the authentication for one channel and eventually re-starts the other channels -bool QHttpNetworkConnectionPrivate::handleAuthenticateChallenge(QAbstractSocket *socket, QHttpNetworkReply *reply, +bool QHttpNetworkConnectionPrivate::handleAuthenticateChallenge(QIODevice *socket, QHttpNetworkReply *reply, bool isProxy, bool &resend) { Q_ASSERT(socket); @@ -486,7 +504,7 @@ bool QHttpNetworkConnectionPrivate::handleAuthenticateChallenge(QAbstractSocket } // Used by the HTTP1 code-path -QUrl QHttpNetworkConnectionPrivate::parseRedirectResponse(QAbstractSocket *socket, +QUrl QHttpNetworkConnectionPrivate::parseRedirectResponse(QIODevice *socket, QHttpNetworkReply *reply) { ParseRedirectResult result = parseRedirectResponse(reply); @@ -523,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. @@ -534,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}; } @@ -740,7 +760,7 @@ QHttpNetworkReply* QHttpNetworkConnectionPrivate::predictNextRequestsReply() con } // this is called from _q_startNextRequest and when a request has been sent down a socket from the channel -void QHttpNetworkConnectionPrivate::fillPipeline(QAbstractSocket *socket) +void QHttpNetworkConnectionPrivate::fillPipeline(QIODevice *socket) { // return fast if there is nothing to pipeline if (highPriorityQueue.isEmpty() && lowPriorityQueue.isEmpty()) @@ -768,7 +788,7 @@ void QHttpNetworkConnectionPrivate::fillPipeline(QAbstractSocket *socket) return; // check if socket is connected - if (socket->state() != QAbstractSocket::ConnectedState) + if (QSocketAbstraction::socketState(socket) != QAbstractSocket::ConnectedState) return; // check for resendCurrent @@ -862,16 +882,15 @@ bool QHttpNetworkConnectionPrivate::fillPipeline(QList<HttpMessagePair> &queue, } -QString QHttpNetworkConnectionPrivate::errorDetail(QNetworkReply::NetworkError errorCode, QAbstractSocket *socket, const QString &extraDetail) +QString QHttpNetworkConnectionPrivate::errorDetail(QNetworkReply::NetworkError errorCode, QIODevice *socket, const QString &extraDetail) { QString errorString; switch (errorCode) { - case QNetworkReply::HostNotFoundError: - if (socket) - errorString = QCoreApplication::translate("QHttp", "Host %1 not found").arg(socket->peerName()); - else - errorString = QCoreApplication::translate("QHttp", "Host %1 not found").arg(hostName); + case QNetworkReply::HostNotFoundError: { + const QString peerName = socket ? QSocketAbstraction::socketPeerName(socket) : hostName; + errorString = QCoreApplication::translate("QHttp", "Host %1 not found").arg(peerName); break; + } case QNetworkReply::ConnectionRefusedError: errorString = QCoreApplication::translate("QHttp", "Connection refused"); break; @@ -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 36234a24ba..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(); @@ -177,7 +179,7 @@ public: QHttpNetworkRequest predictNextRequest() const; QHttpNetworkReply* predictNextRequestsReply() const; - void fillPipeline(QAbstractSocket *socket); + void fillPipeline(QIODevice *socket); bool fillPipeline(QList<HttpMessagePair> &queue, QHttpNetworkConnectionChannel &channel); // read more HTTP body after the next event loop spin @@ -197,7 +199,7 @@ public: void createAuthorization(QIODevice *socket, QHttpNetworkRequest &request); - QString errorDetail(QNetworkReply::NetworkError errorCode, QAbstractSocket *socket, + QString errorDetail(QNetworkReply::NetworkError errorCode, QIODevice *socket, const QString &extraDetail = QString()); void removeReply(QHttpNetworkReply *reply); @@ -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: @@ -219,15 +222,15 @@ public: qint64 uncompressedBytesAvailableNextBlock(const QHttpNetworkReply &reply) const; - void emitReplyError(QAbstractSocket *socket, QHttpNetworkReply *reply, QNetworkReply::NetworkError errorCode); - bool handleAuthenticateChallenge(QAbstractSocket *socket, QHttpNetworkReply *reply, bool isProxy, bool &resend); + void emitReplyError(QIODevice *socket, QHttpNetworkReply *reply, QNetworkReply::NetworkError errorCode); + bool handleAuthenticateChallenge(QIODevice *socket, QHttpNetworkReply *reply, bool isProxy, bool &resend); struct ParseRedirectResult { QUrl redirectUrl; QNetworkReply::NetworkError errorCode; }; static ParseRedirectResult parseRedirectResponse(QHttpNetworkReply *reply); // Used by the HTTP1 code-path - QUrl parseRedirectResponse(QAbstractSocket *socket, QHttpNetworkReply *reply); + QUrl parseRedirectResponse(QIODevice *socket, QHttpNetworkReply *reply); #ifndef QT_NO_NETWORKPROXY QNetworkProxy networkProxy; diff --git a/src/network/access/qhttpnetworkconnectionchannel.cpp b/src/network/access/qhttpnetworkconnectionchannel.cpp index 6766989690..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,58 +88,75 @@ 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 // the state inside the *Socket classes gets messed up, also in conjunction with the socket notifiers // which behave slightly differently on Windows vs Linux - QObject::connect(socket, SIGNAL(bytesWritten(qint64)), - this, SLOT(_q_bytesWritten(qint64)), + QObject::connect(socket, &QIODevice::bytesWritten, + this, &QHttpNetworkConnectionChannel::_q_bytesWritten, Qt::DirectConnection); - QObject::connect(socket, SIGNAL(connected()), - this, SLOT(_q_connected()), - Qt::DirectConnection); - QObject::connect(socket, SIGNAL(readyRead()), - this, SLOT(_q_readyRead()), + 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, SIGNAL(disconnected()), - this, SLOT(_q_disconnected()), - Qt::DirectConnection); - QObject::connect(socket, SIGNAL(errorOccurred(QAbstractSocket::SocketError)), - this, SLOT(_q_error(QAbstractSocket::SocketError)), - 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, SIGNAL(proxyAuthenticationRequired(QNetworkProxy,QAuthenticator*)), - this, SLOT(_q_proxyAuthenticationRequired(QNetworkProxy,QAuthenticator*)), - 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 QSslSocket *sslSocket = qobject_cast<QSslSocket*>(socket); if (sslSocket) { // won't be a sslSocket if encrypt is false - QObject::connect(sslSocket, SIGNAL(encrypted()), - this, SLOT(_q_encrypted()), + QObject::connect(sslSocket, &QSslSocket::encrypted, + this, &QHttpNetworkConnectionChannel::_q_encrypted, Qt::DirectConnection); - QObject::connect(sslSocket, SIGNAL(sslErrors(QList<QSslError>)), - this, SLOT(_q_sslErrors(QList<QSslError>)), + QObject::connect(sslSocket, &QSslSocket::sslErrors, + this, &QHttpNetworkConnectionChannel::_q_sslErrors, Qt::DirectConnection); - QObject::connect(sslSocket, SIGNAL(preSharedKeyAuthenticationRequired(QSslPreSharedKeyAuthenticator*)), - this, SLOT(_q_preSharedKeyAuthenticationRequired(QSslPreSharedKeyAuthenticator*)), + QObject::connect(sslSocket, &QSslSocket::preSharedKeyAuthenticationRequired, + this, &QHttpNetworkConnectionChannel::_q_preSharedKeyAuthenticationRequired, Qt::DirectConnection); - QObject::connect(sslSocket, SIGNAL(encryptedBytesWritten(qint64)), - this, SLOT(_q_encryptedBytesWritten(qint64)), + QObject::connect(sslSocket, &QSslSocket::encryptedBytesWritten, + this, &QHttpNetworkConnectionChannel::_q_encryptedBytesWritten, Qt::DirectConnection); if (ignoreAllSslErrors) @@ -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,9 +379,11 @@ bool QHttpNetworkConnectionChannel::ensureConnection() value = request.headerField("user-agent"); } if (!value.isEmpty()) { - QNetworkProxy proxy(socket->proxy()); - proxy.setRawHeader("User-Agent", value); //detaches - socket->setProxy(proxy); + QNetworkProxy proxy(abSocket->proxy()); + auto h = proxy.headers(); + h.replaceOrAppend(QHttpHeaders::WellKnownHeader::UserAgent, value); + proxy.setHeaders(std::move(h)); + abSocket->setProxy(proxy); } } #endif @@ -378,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) @@ -392,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 } @@ -505,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(); @@ -536,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); @@ -554,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.")) @@ -677,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; } @@ -839,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) { @@ -850,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; @@ -873,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 @@ -882,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; @@ -891,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(); } } @@ -903,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 @@ -925,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) { @@ -938,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) { @@ -1114,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/qhttpprotocolhandler.cpp b/src/network/access/qhttpprotocolhandler.cpp index 28eab03890..fb584eb9cc 100644 --- a/src/network/access/qhttpprotocolhandler.cpp +++ b/src/network/access/qhttpprotocolhandler.cpp @@ -5,6 +5,7 @@ #include <private/qhttpprotocolhandler_p.h> #include <private/qnoncontiguousbytedevice_p.h> #include <private/qhttpnetworkconnectionchannel_p.h> +#include <private/qsocketabstraction_p.h> QT_BEGIN_NAMESPACE @@ -34,10 +35,8 @@ void QHttpProtocolHandler::_q_receiveReply() return; } - QAbstractSocket::SocketState socketState = m_socket->state(); - // connection might be closed to signal the end of data - if (socketState == QAbstractSocket::UnconnectedState) { + if (QSocketAbstraction::socketState(m_socket) == QAbstractSocket::UnconnectedState) { if (m_socket->bytesAvailable() <= 0) { if (m_reply->d_func()->state == QHttpNetworkReplyPrivate::ReadingDataState) { // finish this reply. this case happens when the server did not send a content length @@ -115,7 +114,7 @@ void QHttpProtocolHandler::_q_receiveReply() } case QHttpNetworkReplyPrivate::ReadingDataState: { QHttpNetworkReplyPrivate *replyPrivate = m_reply->d_func(); - if (m_socket->state() == QAbstractSocket::ConnectedState && + if (QSocketAbstraction::socketState(m_socket) == QAbstractSocket::ConnectedState && replyPrivate->downstreamLimited && !replyPrivate->responseData.isEmpty() && replyPrivate->shouldEmitSignals()) { // (only do the following when still connected, not when we have already been disconnected and there is still data) // We already have some HTTP body data. We don't read more from the socket until @@ -193,7 +192,8 @@ void QHttpProtocolHandler::_q_receiveReply() void QHttpProtocolHandler::_q_readyRead() { - if (m_socket->state() == QAbstractSocket::ConnectedState && m_socket->bytesAvailable() == 0) { + if (QSocketAbstraction::socketState(m_socket) == QAbstractSocket::ConnectedState + && m_socket->bytesAvailable() == 0) { // We got a readyRead but no bytes are available.. // This happens for the Unbuffered QTcpSocket // Also check if socket is in ConnectedState since @@ -201,7 +201,7 @@ void QHttpProtocolHandler::_q_readyRead() char c; qint64 ret = m_socket->peek(&c, 1); if (ret < 0) { - m_channel->_q_error(m_socket->error()); + m_channel->_q_error(QSocketAbstraction::socketError(m_socket)); // We still need to handle the reply so it emits its signals etc. if (m_reply) _q_receiveReply(); 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/qnetworkaccessbackend.cpp b/src/network/access/qnetworkaccessbackend.cpp index 5dbcef4bbe..3c7fee567d 100644 --- a/src/network/access/qnetworkaccessbackend.cpp +++ b/src/network/access/qnetworkaccessbackend.cpp @@ -568,6 +568,43 @@ void QNetworkAccessBackend::setRawHeader(const QByteArray &header, const QByteAr } /*! + \since 6.8 + + Returns headers that are set in this QNetworkAccessBackend instance. + + \sa setHeaders() +*/ +QHttpHeaders QNetworkAccessBackend::headers() const +{ + return d_func()->m_reply->headers(); +} + +/*! + \since 6.8 + + Sets \a newHeaders as headers, overriding any previously set headers. + + These headers are accessible on the QNetworkReply instance which was + returned when calling one of the appropriate functions on + QNetworkAccessManager. + + \sa headers() +*/ +void QNetworkAccessBackend::setHeaders(QHttpHeaders &&newHeaders) +{ + d_func()->m_reply->setHeaders(std::move(newHeaders)); +} + +/*! + \overload + \since 6.8 +*/ +void QNetworkAccessBackend::setHeaders(const QHttpHeaders &newHeaders) +{ + d_func()->m_reply->setHeaders(newHeaders); +} + +/*! Returns the operation which was requested when calling QNetworkAccessManager. */ diff --git a/src/network/access/qnetworkaccessbackend_p.h b/src/network/access/qnetworkaccessbackend_p.h index 799ae3faad..4f8d7e4372 100644 --- a/src/network/access/qnetworkaccessbackend_p.h +++ b/src/network/access/qnetworkaccessbackend_p.h @@ -104,6 +104,9 @@ public: void setHeader(QNetworkRequest::KnownHeaders header, const QVariant &value); QByteArray rawHeader(const QByteArray &header) const; void setRawHeader(const QByteArray &header, const QByteArray &value); + QHttpHeaders headers() const; + void setHeaders(const QHttpHeaders &newHeaders); + void setHeaders(QHttpHeaders &&newHeaders); QNetworkAccessManager::Operation operation() const; bool isCachingEnabled() const; diff --git a/src/network/access/qnetworkaccesscachebackend.cpp b/src/network/access/qnetworkaccesscachebackend.cpp index fd8174c143..ead5fe2ef5 100644 --- a/src/network/access/qnetworkaccesscachebackend.cpp +++ b/src/network/access/qnetworkaccesscachebackend.cpp @@ -47,21 +47,21 @@ bool QNetworkAccessCacheBackend::sendCacheContents() return false; QNetworkCacheMetaData::AttributesMap attributes = item.attributes(); - setAttribute(QNetworkRequest::HttpStatusCodeAttribute, attributes.value(QNetworkRequest::HttpStatusCodeAttribute)); - setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, attributes.value(QNetworkRequest::HttpReasonPhraseAttribute)); - - // set the raw headers - const QNetworkCacheMetaData::RawHeaderList rawHeaders = item.rawHeaders(); - for (const auto &header : rawHeaders) { - if (header.first.compare("cache-control", Qt::CaseInsensitive) == 0) { - const QLatin1StringView cacheControlValue(header.second); - if (cacheControlValue.contains("must-revalidate"_L1, Qt::CaseInsensitive) - || cacheControlValue.contains("no-cache"_L1, Qt::CaseInsensitive)) { - return false; - } - } - setRawHeader(header.first, header.second); + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, + attributes.value(QNetworkRequest::HttpStatusCodeAttribute)); + setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, + attributes.value(QNetworkRequest::HttpReasonPhraseAttribute)); + + // set the headers + auto headers = item.headers(); + const auto cacheControlValue = QLatin1StringView( + headers.value(QHttpHeaders::WellKnownHeader::CacheControl)); + // RFC 9111 Section 5.2 Cache Control + if (cacheControlValue.contains("must-revalidate"_L1, Qt::CaseInsensitive) + || cacheControlValue.contains("no-cache"_L1, Qt::CaseInsensitive)) { + return false; } + setHeaders(std::move(headers)); // handle a possible redirect QVariant redirectionTarget = attributes.value(QNetworkRequest::RedirectionTargetAttribute); diff --git a/src/network/access/qnetworkaccessdebugpipebackend.cpp b/src/network/access/qnetworkaccessdebugpipebackend.cpp index 4b12f9fe31..e0a89e3646 100644 --- a/src/network/access/qnetworkaccessdebugpipebackend.cpp +++ b/src/network/access/qnetworkaccessdebugpipebackend.cpp @@ -97,7 +97,9 @@ qint64 QNetworkAccessDebugPipeBackend::read(char *data, qint64 maxlen) if (haveRead == -1) { hasDownloadFinished = true; // this ensures a good last downloadProgress is emitted - setHeader(QNetworkRequest::ContentLengthHeader, QVariant()); + auto h = headers(); + h.removeAll(QHttpHeaders::WellKnownHeader::ContentLength); + setHeaders(std::move(h)); possiblyFinish(); return haveRead; } diff --git a/src/network/access/qnetworkaccessmanager.cpp b/src/network/access/qnetworkaccessmanager.cpp index 4e13c9924b..ae99721758 100644 --- a/src/network/access/qnetworkaccessmanager.cpp +++ b/src/network/access/qnetworkaccessmanager.cpp @@ -1220,6 +1220,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 @@ -1255,12 +1262,14 @@ QNetworkReply *QNetworkAccessManager::createRequest(QNetworkAccessManager::Opera } } QNetworkRequest request = req; + auto h = request.headers(); #ifndef Q_OS_WASM // Content-length header is not allowed to be set by user in wasm - if (!request.header(QNetworkRequest::ContentLengthHeader).isValid() && + if (!h.contains(QHttpHeaders::WellKnownHeader::ContentLength) && outgoingData && !outgoingData->isSequential()) { // request has no Content-Length // but the data that is outgoing is random-access - request.setHeader(QNetworkRequest::ContentLengthHeader, outgoingData->size()); + h.append(QHttpHeaders::WellKnownHeader::ContentLength, + QByteArray::number(outgoingData->size())); } #endif if (static_cast<QNetworkRequest::LoadControl> @@ -1269,9 +1278,11 @@ QNetworkReply *QNetworkAccessManager::createRequest(QNetworkAccessManager::Opera if (d->cookieJar) { QList<QNetworkCookie> cookies = d->cookieJar->cookiesForUrl(request.url()); if (!cookies.isEmpty()) - request.setHeader(QNetworkRequest::CookieHeader, QVariant::fromValue(cookies)); + h.replaceOrAppend(QHttpHeaders::WellKnownHeader::Cookie, + QNetworkHeadersPrivate::fromCookieList(cookies)); } } + request.setHeaders(std::move(h)); #ifdef Q_OS_WASM Q_UNUSED(isLocalFile); // Support http, https, and relative urls @@ -1292,11 +1303,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], @@ -1387,6 +1402,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"); @@ -1746,9 +1763,10 @@ QNetworkRequest QNetworkAccessManagerPrivate::prepareMultipart(const QNetworkReq { // copy the request, we probably need to add some headers QNetworkRequest newRequest(request); + auto h = newRequest.headers(); // add Content-Type header if not there already - if (!request.header(QNetworkRequest::ContentTypeHeader).isValid()) { + if (!h.contains(QHttpHeaders::WellKnownHeader::ContentType)) { QByteArray contentType; contentType.reserve(34 + multiPart->d_func()->boundary.size()); contentType += "multipart/"; @@ -1768,14 +1786,15 @@ QNetworkRequest QNetworkAccessManagerPrivate::prepareMultipart(const QNetworkReq } // putting the boundary into quotes, recommended in RFC 2046 section 5.1.1 contentType += "; boundary=\"" + multiPart->d_func()->boundary + '"'; - newRequest.setHeader(QNetworkRequest::ContentTypeHeader, QVariant(contentType)); + h.append(QHttpHeaders::WellKnownHeader::ContentType, contentType); } // add MIME-Version header if not there already (we must include the header // if the message conforms to RFC 2045, see section 4 of that RFC) - auto mimeHeader = "MIME-Version"_ba; - if (!request.hasRawHeader(mimeHeader)) - newRequest.setRawHeader(mimeHeader, "1.0"_ba); + if (!h.contains(QHttpHeaders::WellKnownHeader::MIMEVersion)) + h.append(QHttpHeaders::WellKnownHeader::MIMEVersion, "1.0"_ba); + + newRequest.setHeaders(std::move(h)); QIODevice *device = multiPart->d_func()->device; if (!device->isReadable()) { diff --git a/src/network/access/qnetworkdiskcache.cpp b/src/network/access/qnetworkdiskcache.cpp index c883a61886..b39924025e 100644 --- a/src/network/access/qnetworkdiskcache.cpp +++ b/src/network/access/qnetworkdiskcache.cpp @@ -154,15 +154,11 @@ QIODevice *QNetworkDiskCache::prepare(const QNetworkCacheMetaData &metaData) return nullptr; } - const auto headers = metaData.rawHeaders(); - for (const auto &header : headers) { - if (header.first.compare("content-length", Qt::CaseInsensitive) == 0) { - const qint64 size = header.second.toLongLong(); - if (size > (maximumCacheSize() * 3)/4) - return nullptr; - break; - } - } + const auto sizeValue = metaData.headers().value(QHttpHeaders::WellKnownHeader::ContentLength); + const qint64 size = sizeValue.toLongLong(); + if (size > (maximumCacheSize() * 3)/4) + return nullptr; + std::unique_ptr<QCacheItem> cacheItem = std::make_unique<QCacheItem>(); cacheItem->metaData = metaData; @@ -578,31 +574,27 @@ QString QNetworkDiskCachePrivate::cacheFileName(const QUrl &url) const */ bool QCacheItem::canCompress() const { - bool sizeOk = false; - bool typeOk = false; - const auto headers = metaData.rawHeaders(); - for (const auto &header : headers) { - if (header.first.compare("content-length", Qt::CaseInsensitive) == 0) { - qint64 size = header.second.toLongLong(); - if (size > MAX_COMPRESSION_SIZE) - return false; - else - sizeOk = true; - } + const auto h = metaData.headers(); - if (header.first.compare("content-type", Qt::CaseInsensitive) == 0) { - QByteArray type = header.second; - if (type.startsWith("text/") - || (type.startsWith("application/") - && (type.endsWith("javascript") || type.endsWith("ecmascript")))) - typeOk = true; - else - return false; - } - if (sizeOk && typeOk) - return true; + const auto sizeValue = h.value(QHttpHeaders::WellKnownHeader::ContentLength); + if (sizeValue.empty()) + return false; + + qint64 size = sizeValue.toLongLong(); + if (size > MAX_COMPRESSION_SIZE) + return false; + + const auto type = h.value(QHttpHeaders::WellKnownHeader::ContentType); + if (!type.empty()) + return false; + + if (!type.startsWith("text/") + && !(type.startsWith("application/") + && (type.endsWith("javascript") || type.endsWith("ecmascript")))) { + return false; } - return false; + + return true; } enum @@ -673,7 +665,7 @@ bool QCacheItem::read(QFileDevice *device, bool readData) if (!device->fileName().endsWith(expectedFilename)) return false; - return metaData.isValid() && !metaData.rawHeaders().isEmpty(); + return metaData.isValid() && !metaData.headers().isEmpty(); } QT_END_NAMESPACE diff --git a/src/network/access/qnetworkfile.cpp b/src/network/access/qnetworkfile.cpp index bfedf044de..fb9ce8232d 100644 --- a/src/network/access/qnetworkfile.cpp +++ b/src/network/access/qnetworkfile.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qnetworkfile_p.h" +#include "qnetworkrequest_p.h" #include <QtCore/QDebug> #include <QNetworkReply> @@ -31,8 +32,10 @@ void QNetworkFile::open() "Cannot open %1: Path is a directory").arg(fileName()); emit error(QNetworkReply::ContentOperationNotPermittedError, msg); } else { - emit headerRead(QNetworkRequest::LastModifiedHeader, QVariant::fromValue(fi.lastModified())); - emit headerRead(QNetworkRequest::ContentLengthHeader, QVariant::fromValue(fi.size())); + emit headerRead(QHttpHeaders::WellKnownHeader::LastModified, + QNetworkHeadersPrivate::toHttpDate(fi.lastModified())); + emit headerRead(QHttpHeaders::WellKnownHeader::ContentLength, + QByteArray::number(fi.size())); opened = QFile::open(QIODevice::ReadOnly | QIODevice::Unbuffered); if (!opened) { QString msg = QCoreApplication::translate("QNetworkAccessFileBackend", diff --git a/src/network/access/qnetworkfile_p.h b/src/network/access/qnetworkfile_p.h index df772251b4..9dcb63711e 100644 --- a/src/network/access/qnetworkfile_p.h +++ b/src/network/access/qnetworkfile_p.h @@ -35,7 +35,7 @@ public Q_SLOTS: Q_SIGNALS: void finished(bool ok); - void headerRead(QNetworkRequest::KnownHeaders header, const QVariant &value); + void headerRead(QHttpHeaders::WellKnownHeader, const QByteArray &value); void error(QNetworkReply::NetworkError error, const QString &message); }; diff --git a/src/network/access/qnetworkreply.cpp b/src/network/access/qnetworkreply.cpp index 9334b01de6..3301ae85d2 100644 --- a/src/network/access/qnetworkreply.cpp +++ b/src/network/access/qnetworkreply.cpp @@ -617,7 +617,7 @@ QVariant QNetworkReply::header(QNetworkRequest::KnownHeaders header) const bool QNetworkReply::hasRawHeader(QAnyStringView headerName) const { Q_D(const QNetworkReply); - return d->findRawHeader(headerName) != d->rawHeaders.constEnd(); + return d->headers().contains(headerName); } /*! @@ -633,9 +633,7 @@ bool QNetworkReply::hasRawHeader(QAnyStringView headerName) const QByteArray QNetworkReply::rawHeader(QAnyStringView headerName) const { Q_D(const QNetworkReply); - if (const auto it = d->findRawHeader(headerName); it != d->rawHeaders.constEnd()) - return it->second; - return QByteArray(); + return d->rawHeader(headerName); } /*! \typedef QNetworkReply::RawHeaderPair @@ -650,7 +648,20 @@ QByteArray QNetworkReply::rawHeader(QAnyStringView headerName) const const QList<QNetworkReply::RawHeaderPair>& QNetworkReply::rawHeaderPairs() const { Q_D(const QNetworkReply); - return d->rawHeaders; + return d->allRawHeaders(); +} + +/*! + \since 6.8 + + Returns headers that were sent by the remote server. + + \sa setHeaders(), QNetworkRequest::setAttribute(), QNetworkRequest::Attribute +*/ +QHttpHeaders QNetworkReply::headers() const +{ + Q_D(const QNetworkReply); + return d->headers(); } /*! @@ -888,6 +899,45 @@ void QNetworkReply::setUrl(const QUrl &url) } /*! + \since 6.8 + + Sets \a newHeaders as headers in this network reply, overriding + any previously set headers. + + If some headers correspond to the known headers, they will be + parsed and the corresponding parsed form will also be set. + + \sa headers(), QNetworkRequest::KnownHeaders +*/ +void QNetworkReply::setHeaders(const QHttpHeaders &newHeaders) +{ + Q_D(QNetworkReply); + d->setHeaders(newHeaders); +} + +/*! + \overload + \since 6.8 +*/ +void QNetworkReply::setHeaders(QHttpHeaders &&newHeaders) +{ + Q_D(QNetworkReply); + d->setHeaders(std::move(newHeaders)); +} + +/*! + \since 6.8 + + Sets the header \a name to be of value \a value. If \a + name was previously set, it is overridden. +*/ +void QNetworkReply::setWellKnownHeader(QHttpHeaders::WellKnownHeader name, const QByteArray &value) +{ + Q_D(QNetworkReply); + d->setHeader(name, value); +} + +/*! Sets the known header \a header to be of value \a value. The corresponding raw form of the header will be set as well. diff --git a/src/network/access/qnetworkreply.h b/src/network/access/qnetworkreply.h index 390a6f2f51..48ec0397c6 100644 --- a/src/network/access/qnetworkreply.h +++ b/src/network/access/qnetworkreply.h @@ -109,6 +109,7 @@ public: typedef QPair<QByteArray, QByteArray> RawHeaderPair; const QList<RawHeaderPair>& rawHeaderPairs() const; + QHttpHeaders headers() const; // attributes QVariant attribute(QNetworkRequest::Attribute code) const; @@ -152,6 +153,9 @@ protected: void setUrl(const QUrl &url); void setHeader(QNetworkRequest::KnownHeaders header, const QVariant &value); void setRawHeader(const QByteArray &headerName, const QByteArray &value); + void setHeaders(const QHttpHeaders &newHeaders); + void setHeaders(QHttpHeaders &&newHeaders); + void setWellKnownHeader(QHttpHeaders::WellKnownHeader name, const QByteArray &value); void setAttribute(QNetworkRequest::Attribute code, const QVariant &value); #if QT_CONFIG(ssl) diff --git a/src/network/access/qnetworkreplydataimpl.cpp b/src/network/access/qnetworkreplydataimpl.cpp index 7cb7621bca..006cfd57cb 100644 --- a/src/network/access/qnetworkreplydataimpl.cpp +++ b/src/network/access/qnetworkreplydataimpl.cpp @@ -36,8 +36,11 @@ QNetworkReplyDataImpl::QNetworkReplyDataImpl(QObject *parent, const QNetworkRequ QByteArray payload; if (qDecodeDataUrl(url, mimeType, payload)) { qint64 size = payload.size(); - setHeader(QNetworkRequest::ContentTypeHeader, mimeType); - setHeader(QNetworkRequest::ContentLengthHeader, size); + auto h = headers(); + h.replaceOrAppend(QHttpHeaders::WellKnownHeader::ContentType, mimeType); + h.replaceOrAppend(QHttpHeaders::WellKnownHeader::ContentLength, QByteArray::number(size)); + setHeaders(std::move(h)); + QMetaObject::invokeMethod(this, "metaDataChanged", Qt::QueuedConnection); d->decodedData.setData(payload); diff --git a/src/network/access/qnetworkreplyfileimpl.cpp b/src/network/access/qnetworkreplyfileimpl.cpp index e6208a5c85..bad0bb7b0a 100644 --- a/src/network/access/qnetworkreplyfileimpl.cpp +++ b/src/network/access/qnetworkreplyfileimpl.cpp @@ -85,7 +85,7 @@ QNetworkReplyFileImpl::QNetworkReplyFileImpl(QNetworkAccessManager *manager, con if (req.attribute(QNetworkRequest::BackgroundRequestAttribute).toBool()) { // Asynchronous open auto realFile = new QNetworkFile(fileName); - connect(realFile, &QNetworkFile::headerRead, this, &QNetworkReplyFileImpl::setHeader, + connect(realFile, &QNetworkFile::headerRead, this, &QNetworkReplyFileImpl::setWellKnownHeader, Qt::QueuedConnection); connect(realFile, &QNetworkFile::error, this, &QNetworkReplyFileImpl::setError, Qt::QueuedConnection); @@ -128,8 +128,12 @@ QNetworkReplyFileImpl::QNetworkReplyFileImpl(QNetworkAccessManager *manager, con QMetaObject::invokeMethod(this, "finished", Qt::QueuedConnection); return; } - setHeader(QNetworkRequest::LastModifiedHeader, fi.lastModified()); - setHeader(QNetworkRequest::ContentLengthHeader, fi.size()); + auto h = headers(); + h.replaceOrAppend(QHttpHeaders::WellKnownHeader::LastModified, + QNetworkHeadersPrivate::toHttpDate(fi.lastModified())); + h.replaceOrAppend(QHttpHeaders::WellKnownHeader::ContentLength, + QByteArray::number(fi.size())); + setHeaders(std::move(h)); QMetaObject::invokeMethod(this, "metaDataChanged", Qt::QueuedConnection); QMetaObject::invokeMethod(this, "downloadProgress", Qt::QueuedConnection, @@ -171,9 +175,9 @@ bool QNetworkReplyFileImpl::isSequential () const qint64 QNetworkReplyFileImpl::size() const { - bool ok; - int size = header(QNetworkRequest::ContentLengthHeader).toInt(&ok); - return ok ? size : 0; + const auto totalSizeOpt = QNetworkHeadersPrivate::toInt( + headers().value(QHttpHeaders::WellKnownHeader::ContentLength)); + return totalSizeOpt.value_or(0); } /*! diff --git a/src/network/access/qnetworkreplyhttpimpl.cpp b/src/network/access/qnetworkreplyhttpimpl.cpp index 1eee98f834..89458825e9 100644 --- a/src/network/access/qnetworkreplyhttpimpl.cpp +++ b/src/network/access/qnetworkreplyhttpimpl.cpp @@ -202,7 +202,10 @@ QNetworkReplyHttpImpl::QNetworkReplyHttpImpl(QNetworkAccessManager* const manage if (bufferingDisallowed) { // if a valid content-length header for the request was supplied, we can disable buffering // if not, we will buffer anyway - if (request.header(QNetworkRequest::ContentLengthHeader).isValid()) { + + const auto sizeOpt = QNetworkHeadersPrivate::toInt( + request.headers().value(QHttpHeaders::WellKnownHeader::ContentLength)); + if (sizeOpt) { QMetaObject::invokeMethod(this, "_q_startOperation", Qt::QueuedConnection); // FIXME make direct call? } else { @@ -478,10 +481,12 @@ bool QNetworkReplyHttpImplPrivate::loadFromCacheIfAllowed(QHttpNetworkRequest &h { QNetworkRequest::CacheLoadControl CacheLoadControlAttribute = (QNetworkRequest::CacheLoadControl)request.attribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferNetwork).toInt(); + + auto requestHeaders = request.headers(); if (CacheLoadControlAttribute == QNetworkRequest::AlwaysNetwork) { // If the request does not already specify preferred cache-control // force reload from the network and tell any caching proxy servers to reload too - if (!request.rawHeaderList().contains(cacheControlName())) { + if (!requestHeaders.contains(QHttpHeaders::WellKnownHeader::CacheControl)) { const auto noCache = "no-cache"_ba; httpRequest.setHeaderField(cacheControlName(), noCache); httpRequest.setHeaderField("Pragma"_ba, noCache); @@ -491,7 +496,7 @@ bool QNetworkReplyHttpImplPrivate::loadFromCacheIfAllowed(QHttpNetworkRequest &h // The disk cache API does not currently support partial content retrieval. // That is why we don't use the disk cache for any such requests. - if (request.hasRawHeader(rangeName())) + if (requestHeaders.contains(QHttpHeaders::WellKnownHeader::Range)) return false; QAbstractNetworkCache *nc = managerPrivate->networkCache; @@ -505,28 +510,27 @@ bool QNetworkReplyHttpImplPrivate::loadFromCacheIfAllowed(QHttpNetworkRequest &h if (!metaData.saveToDisk()) return false; - QNetworkHeadersPrivate cacheHeaders; - QNetworkHeadersPrivate::RawHeadersList::ConstIterator it; - cacheHeaders.setAllRawHeaders(metaData.rawHeaders()); + QHttpHeaders cacheHeaders = metaData.headers(); - it = cacheHeaders.findRawHeader("content-length"); - if (it != cacheHeaders.rawHeaders.constEnd()) { + const auto sizeOpt = QNetworkHeadersPrivate::toInt( + cacheHeaders.value(QHttpHeaders::WellKnownHeader::ContentLength)); + if (sizeOpt) { std::unique_ptr<QIODevice> data(nc->data(httpRequest.url())); - if (!data || data->size() < it->second.toLongLong()) + if (!data || data->size() < sizeOpt.value()) return false; // The data is smaller than the content-length specified } - it = cacheHeaders.findRawHeader("etag"); - if (it != cacheHeaders.rawHeaders.constEnd()) - httpRequest.setHeaderField("If-None-Match"_ba, it->second); + auto value = cacheHeaders.value(QHttpHeaders::WellKnownHeader::ETag); + if (!value.empty()) + httpRequest.setHeaderField("If-None-Match"_ba, value.toByteArray()); QDateTime lastModified = metaData.lastModified(); if (lastModified.isValid()) httpRequest.setHeaderField("If-Modified-Since"_ba, QNetworkHeadersPrivate::toHttpDate(lastModified)); - it = cacheHeaders.findRawHeader(cacheControlName()); - if (it != cacheHeaders.rawHeaders.constEnd()) { - QHash<QByteArray, QByteArray> cacheControl = parseHttpOptionHeader(it->second); + value = cacheHeaders.value(QHttpHeaders::WellKnownHeader::CacheControl); + if (!value.empty()) { + QHash<QByteArray, QByteArray> cacheControl = parseHttpOptionHeader(value); if (cacheControl.contains("must-revalidate"_ba)) return false; if (cacheControl.contains("no-cache"_ba)) @@ -553,16 +557,15 @@ bool QNetworkReplyHttpImplPrivate::loadFromCacheIfAllowed(QHttpNetworkRequest &h * now * is the current (local) time */ - qint64 age_value = 0; - it = cacheHeaders.findRawHeader("age"); - if (it != cacheHeaders.rawHeaders.constEnd()) - age_value = it->second.toLongLong(); + const auto ageOpt = QNetworkHeadersPrivate::toInt( + cacheHeaders.value(QHttpHeaders::WellKnownHeader::Age)); + const qint64 age_value = ageOpt.value_or(0); QDateTime dateHeader; qint64 date_value = 0; - it = cacheHeaders.findRawHeader("date"); - if (it != cacheHeaders.rawHeaders.constEnd()) { - dateHeader = QNetworkHeadersPrivate::fromHttpDate(it->second); + value = cacheHeaders.value(QHttpHeaders::WellKnownHeader::Date); + if (!value.empty()) { + dateHeader = QNetworkHeadersPrivate::fromHttpDate(value); date_value = dateHeader.toSecsSinceEpoch(); } @@ -744,18 +747,17 @@ void QNetworkReplyHttpImplPrivate::postRequest(const QNetworkRequest &newHttpReq break; // can't happen } - QList<QByteArray> headers = newHttpRequest.rawHeaderList(); + QHttpHeaders newRequestHeaders = newHttpRequest.headers(); if (resumeOffset != 0) { - const int rangeIndex = headers.indexOf(rangeName()); - if (rangeIndex != -1) { + if (newRequestHeaders.contains(QHttpHeaders::WellKnownHeader::Range)) { // Need to adjust resume offset for user specified range - headers.removeAt(rangeIndex); - // We've already verified that requestRange starts with "bytes=", see canResume. - const auto rangeHeader = newHttpRequest.rawHeader(rangeName()); + const auto rangeHeader = newRequestHeaders.value(QHttpHeaders::WellKnownHeader::Range); const auto requestRange = QByteArrayView(rangeHeader).mid(bytesEqualPrefix().size()); + newRequestHeaders.removeAll(QHttpHeaders::WellKnownHeader::Range); + int index = requestRange.indexOf('-'); quint64 requestStartOffset = requestRange.left(index).toULongLong(); @@ -771,8 +773,11 @@ void QNetworkReplyHttpImplPrivate::postRequest(const QNetworkRequest &newHttpReq } } - for (const QByteArray &header : std::as_const(headers)) - httpRequest.setHeaderField(header, newHttpRequest.rawHeader(header)); + for (int i = 0; i < newRequestHeaders.size(); i++) { + const auto name = newRequestHeaders.nameAt(i); + const auto value = newRequestHeaders.valueAt(i); + httpRequest.setHeaderField(QByteArray(name.data(), name.size()), value.toByteArray()); + } if (newHttpRequest.attribute(QNetworkRequest::HttpPipeliningAllowedAttribute).toBool()) httpRequest.setPipeliningAllowed(true); @@ -804,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: @@ -1151,7 +1163,8 @@ void QNetworkReplyHttpImplPrivate::replyDownloadData(QByteArray d) } lastReadyReadEmittedSize = bytesDownloaded; - QVariant totalSize = cookedHeaders.value(QNetworkRequest::ContentLengthHeader); + const auto totalSizeOpt = QNetworkHeadersPrivate::toInt( + headers().value(QHttpHeaders::WellKnownHeader::ContentLength)); emit q->readyRead(); // emit readyRead before downloadProgress in case this will cause events to be @@ -1159,8 +1172,7 @@ void QNetworkReplyHttpImplPrivate::replyDownloadData(QByteArray d) if (downloadProgressSignalChoke.elapsed() >= progressSignalInterval && (!decompressHelper.isValid() || decompressHelper.isCountingBytes())) { downloadProgressSignalChoke.restart(); - emit q->downloadProgress(bytesDownloaded, - totalSize.isNull() ? Q_INT64_C(-1) : totalSize.toLongLong()); + emit q->downloadProgress(bytesDownloaded, totalSizeOpt.value_or(-1)); } } @@ -1223,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", @@ -1237,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; @@ -1255,6 +1271,7 @@ void QNetworkReplyHttpImplPrivate::onRedirected(const QUrl &redirectUrl, int htt // Clear stale headers, the relevant ones get set again later httpRequest.clearHeaders(); + auto newHeaders = redirectRequest.headers(); if ((operation == QNetworkAccessManager::GetOperation || operation == QNetworkAccessManager::HeadOperation) && !getOperationKeepsBody) { // possibly changed from not-GET/HEAD to GET/HEAD, make sure to get rid of upload device @@ -1269,18 +1286,20 @@ void QNetworkReplyHttpImplPrivate::onRedirected(const QUrl &redirectUrl, int htt outgoingData = nullptr; outgoingDataBuffer.reset(); // We need to explicitly unset these headers so they're not reapplied to the httpRequest - redirectRequest.setHeader(QNetworkRequest::ContentLengthHeader, QVariant()); - redirectRequest.setHeader(QNetworkRequest::ContentTypeHeader, QVariant()); + newHeaders.removeAll(QHttpHeaders::WellKnownHeader::ContentLength); + newHeaders.removeAll(QHttpHeaders::WellKnownHeader::ContentType); } if (const QNetworkCookieJar *const cookieJar = manager->cookieJar()) { auto cookies = cookieJar->cookiesForUrl(url); if (!cookies.empty()) { - redirectRequest.setHeader(QNetworkRequest::KnownHeaders::CookieHeader, - QVariant::fromValue(cookies)); + auto cookieHeader = QNetworkHeadersPrivate::fromCookieList(cookies); + newHeaders.replaceOrAppend(QHttpHeaders::WellKnownHeader::Cookie, cookieHeader); } } + redirectRequest.setHeaders(std::move(newHeaders)); + if (httpRequest.redirectPolicy() != QNetworkRequest::UserVerifiedRedirectPolicy) followRedirect(); @@ -1293,8 +1312,7 @@ void QNetworkReplyHttpImplPrivate::followRedirect() Q_ASSERT(managerPrivate); decompressHelper.clear(); - rawHeaders.clear(); - cookedHeaders.clear(); + clearHeaders(); if (managerPrivate->thread) managerPrivate->thread->disconnect(); @@ -1317,7 +1335,7 @@ void QNetworkReplyHttpImplPrivate::checkForRedirect(const int statusCode) // What do we do about the caching of the HTML note? // The response to a 303 MUST NOT be cached, while the response to // all of the others is cacheable if the headers indicate it to be - QByteArray header = q->rawHeader(locationHeader()); + QByteArrayView header = q->headers().value(locationHeader()); QUrl url = QUrl(QString::fromUtf8(header)); if (!url.isValid()) url = QUrl(QLatin1StringView(header)); @@ -1361,20 +1379,19 @@ void QNetworkReplyHttpImplPrivate::replyDownloadMetaData(const QHttpHeaders &hm, // A user having manually defined which encodings they accept is, for // somwehat unknown (presumed legacy compatibility) reasons treated as // disabling our decompression: - const bool autoDecompress = request.rawHeader("accept-encoding").isEmpty(); + const bool autoDecompress = !request.headers().contains(QHttpHeaders::WellKnownHeader::AcceptEncoding); const bool shouldDecompress = isCompressed && autoDecompress; // reconstruct the HTTP header + auto h = q->headers(); for (qsizetype i = 0; i < hm.size(); ++i) { const auto key = hm.nameAt(i); const auto originValue = hm.valueAt(i); - QByteArray value = q->rawHeader(key); - // Reset any previous "location" header set in the reply. In case of // redirects, we don't want to 'append' multiple location header values, // rather we keep only the latest one - if (key == locationHeader()) - value.clear(); + if (key.compare(locationHeader(), Qt::CaseInsensitive) == 0) + h.removeAll(key); if (shouldDecompress && !decompressHelper.isValid() && key == "content-encoding"_L1) { if (!synchronous) // with synchronous all the data is expected to be handled at once @@ -1390,17 +1407,9 @@ void QNetworkReplyHttpImplPrivate::replyDownloadMetaData(const QHttpHeaders &hm, request.decompressedSafetyCheckThreshold()); } - if (!value.isEmpty()) { - // Why are we appending values for headers which are already - // present? - if (key == "set-cookie"_L1) - value += '\n'; - else - value += ", "; - } - value += originValue; - q->setRawHeader({key.data(), key.size()}, value); + h.append(key, originValue); } + q->setHeaders(std::move(h)); q->setAttribute(QNetworkRequest::HttpStatusCodeAttribute, statusCode); q->setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, reasonPhrase); @@ -1415,13 +1424,10 @@ void QNetworkReplyHttpImplPrivate::replyDownloadMetaData(const QHttpHeaders &hm, QAbstractNetworkCache *nc = managerPrivate->networkCache; if (nc) { QNetworkCacheMetaData metaData = nc->metaData(httpRequest.url()); - QNetworkHeadersPrivate cacheHeaders; - cacheHeaders.setAllRawHeaders(metaData.rawHeaders()); - QNetworkHeadersPrivate::RawHeadersList::ConstIterator it; - it = cacheHeaders.findRawHeader(cacheControlName()); + auto value = metaData.headers().value(QHttpHeaders::WellKnownHeader::CacheControl); bool mustReValidate = false; - if (it != cacheHeaders.rawHeaders.constEnd()) { - QHash<QByteArray, QByteArray> cacheControl = parseHttpOptionHeader(it->second); + if (!value.empty()) { + QHash<QByteArray, QByteArray> cacheControl = parseHttpOptionHeader(value); if (cacheControl.contains("must-revalidate"_ba)) mustReValidate = true; } @@ -1660,16 +1666,21 @@ bool QNetworkReplyHttpImplPrivate::sendCacheContents(const QNetworkCacheMetaData q->setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, attributes.value(QNetworkRequest::HttpReasonPhraseAttribute)); q->setAttribute(QNetworkRequest::SourceIsFromCacheAttribute, true); - QNetworkCacheMetaData::RawHeaderList rawHeaders = metaData.rawHeaders(); - QNetworkCacheMetaData::RawHeaderList::ConstIterator it = rawHeaders.constBegin(), - end = rawHeaders.constEnd(); + QHttpHeaders cachedHeaders = metaData.headers(); + QHttpHeaders h = headers(); QUrl redirectUrl; - for ( ; it != end; ++it) { - if (httpRequest.isFollowRedirects() && - !it->first.compare(locationHeader(), Qt::CaseInsensitive)) - redirectUrl = QUrl::fromEncoded(it->second); - setRawHeader(it->first, it->second); + for (qsizetype i = 0; i < cachedHeaders.size(); ++i) { + const auto name = cachedHeaders.nameAt(i); + const auto value = cachedHeaders.valueAt(i); + + if (httpRequest.isFollowRedirects() + && !name.compare(locationHeader(), Qt::CaseInsensitive)) { + redirectUrl = QUrl::fromEncoded(value); + } + + h.replaceOrAppend(name, value); } + setHeaders(std::move(h)); if (!isHttpRedirectResponse()) checkForRedirect(status); @@ -1729,17 +1740,17 @@ QNetworkCacheMetaData QNetworkReplyHttpImplPrivate::fetchCacheMetaData(const QNe Q_Q(const QNetworkReplyHttpImpl); QNetworkCacheMetaData metaData = oldMetaData; + QHttpHeaders cacheHeaders = metaData.headers(); - QNetworkHeadersPrivate cacheHeaders; - cacheHeaders.setAllRawHeaders(metaData.rawHeaders()); - QNetworkHeadersPrivate::RawHeadersList::ConstIterator it; + const auto newHeaders = q->headers(); + for (qsizetype i = 0; i < newHeaders.size(); ++i) { + const auto name = newHeaders.nameAt(i); + const auto value = newHeaders.valueAt(i); - const QList<QByteArray> newHeaders = q->rawHeaderList(); - for (const QByteArray& header : newHeaders) { - if (isHopByHop(header)) + if (isHopByHop(name)) continue; - if (header.compare("set-cookie", Qt::CaseInsensitive) == 0) + if (name.compare("set-cookie", Qt::CaseInsensitive) == 0) continue; // for 4.6.0, we were planning to not store the date header in the @@ -1752,50 +1763,46 @@ QNetworkCacheMetaData QNetworkReplyHttpImplPrivate::fetchCacheMetaData(const QNe //continue; // Don't store Warning 1xx headers - if (header.compare("warning", Qt::CaseInsensitive) == 0) { - const QByteArray v = q->rawHeader(header); - if (v.size() == 3 - && v[0] == '1' - && isAsciiDigit(v[1]) - && isAsciiDigit(v[2])) + if (name.compare("warning", Qt::CaseInsensitive) == 0) { + if (value.size() == 3 + && value[0] == '1' + && isAsciiDigit(value[1]) + && isAsciiDigit(value[2])) continue; } - it = cacheHeaders.findRawHeader(header); - if (it != cacheHeaders.rawHeaders.constEnd()) { + if (cacheHeaders.contains(name)) { // Match the behavior of Firefox and assume Cache-Control: "no-transform" constexpr QByteArrayView headers[]= {"content-encoding", "content-range", "content-type"}; - if (std::any_of(std::begin(headers), std::end(headers), caseInsensitiveCompare(header))) + if (std::any_of(std::begin(headers), std::end(headers), caseInsensitiveCompare(name))) continue; } // IIS has been known to send "Content-Length: 0" on 304 responses, so // ignore this too - if (statusCode == 304 && header.compare("content-length", Qt::CaseInsensitive) == 0) + if (statusCode == 304 && name.compare("content-length", Qt::CaseInsensitive) == 0) continue; #if defined(QNETWORKACCESSHTTPBACKEND_DEBUG) - QByteArray n = q->rawHeader(header); - QByteArray o; - if (it != cacheHeaders.rawHeaders.constEnd()) - o = (*it).second; - if (n != o && headerheader.compare("date", Qt::CaseInsensitive) != 0) { - qDebug() << "replacing" << header; + QByteArrayView n = newHeaders.value(name); + QByteArrayView o = cacheHeaders.value(name); + if (n != o && name.compare("date", Qt::CaseInsensitive) != 0) { + qDebug() << "replacing" << name; qDebug() << "new" << n; qDebug() << "old" << o; } #endif - cacheHeaders.setRawHeader(header, q->rawHeader(header)); + cacheHeaders.replaceOrAppend(name, value); } - metaData.setRawHeaders(cacheHeaders.rawHeaders); + metaData.setHeaders(cacheHeaders); bool checkExpired = true; QHash<QByteArray, QByteArray> cacheControl; - it = cacheHeaders.findRawHeader(cacheControlName()); - if (it != cacheHeaders.rawHeaders.constEnd()) { - cacheControl = parseHttpOptionHeader(it->second); + auto value = cacheHeaders.value(QHttpHeaders::WellKnownHeader::CacheControl); + if (!value.empty()) { + cacheControl = parseHttpOptionHeader(value); QByteArray maxAge = cacheControl.value("max-age"_ba); if (!maxAge.isEmpty()) { checkExpired = false; @@ -1805,16 +1812,18 @@ QNetworkCacheMetaData QNetworkReplyHttpImplPrivate::fetchCacheMetaData(const QNe } } if (checkExpired) { - it = cacheHeaders.findRawHeader("expires"); - if (it != cacheHeaders.rawHeaders.constEnd()) { - QDateTime expiredDateTime = QNetworkHeadersPrivate::fromHttpDate(it->second); + if (const auto value = cacheHeaders.value( + QHttpHeaders::WellKnownHeader::Expires); !value.isEmpty()) { + QDateTime expiredDateTime = QNetworkHeadersPrivate::fromHttpDate(value); metaData.setExpirationDate(expiredDateTime); } } - it = cacheHeaders.findRawHeader("last-modified"); - if (it != cacheHeaders.rawHeaders.constEnd()) - metaData.setLastModified(QNetworkHeadersPrivate::fromHttpDate(it->second)); + if (const auto value = cacheHeaders.value( + QHttpHeaders::WellKnownHeader::LastModified); !value.isEmpty()) { + metaData.setLastModified(QNetworkHeadersPrivate::fromHttpDate(value)); + } + bool canDiskCache; // only cache GET replies by default, all other replies (POST, PUT, DELETE) @@ -1862,14 +1871,16 @@ bool QNetworkReplyHttpImplPrivate::canResume() const if (operation != QNetworkAccessManager::GetOperation) return false; + const auto h = q->headers(); + // Can only resume if server/resource supports Range header. - constexpr auto acceptRangesheaderName = QByteArrayView("Accept-Ranges"); - if (!q->hasRawHeader(acceptRangesheaderName) || q->rawHeader(acceptRangesheaderName) == "none") + const auto acceptRanges = h.value(QHttpHeaders::WellKnownHeader::AcceptRanges); + if (acceptRanges.empty() || acceptRanges == "none") return false; // We only support resuming for byte ranges. - if (request.hasRawHeader(rangeName())) { - QByteArray range = request.rawHeader(rangeName()); + const auto range = h.value(QHttpHeaders::WellKnownHeader::Range); + if (!range.empty()) { if (!range.startsWith(bytesEqualPrefix())) return false; } @@ -1918,8 +1929,8 @@ void QNetworkReplyHttpImplPrivate::_q_cacheLoadReadyRead() // Needs to be done where sendCacheContents() (?) of HTTP is emitting // metaDataChanged ? - - QVariant totalSize = cookedHeaders.value(QNetworkRequest::ContentLengthHeader); + const auto totalSizeOpt = QNetworkHeadersPrivate::toInt( + headers().value(QHttpHeaders::WellKnownHeader::ContentLength)); // emit readyRead before downloadProgress in case this will cause events to be // processed and we get into a recursive call (as in QProgressDialog). @@ -1930,8 +1941,7 @@ void QNetworkReplyHttpImplPrivate::_q_cacheLoadReadyRead() if (downloadProgressSignalChoke.elapsed() >= progressSignalInterval) { downloadProgressSignalChoke.restart(); - emit q->downloadProgress(bytesDownloaded, - totalSize.isNull() ? Q_INT64_C(-1) : totalSize.toLongLong()); + emit q->downloadProgress(bytesDownloaded, totalSizeOpt.value_or(-1)); } } @@ -2118,11 +2128,13 @@ void QNetworkReplyHttpImplPrivate::finished() if (state == Finished || state == Aborted) return; - QVariant totalSize = cookedHeaders.value(QNetworkRequest::ContentLengthHeader); + const auto totalSizeOpt = QNetworkHeadersPrivate::toInt( + headers().value(QHttpHeaders::WellKnownHeader::ContentLength)); + const qint64 totalSize = totalSizeOpt.value_or(-1); // if we don't know the total size of or we received everything save the cache. // If the data is compressed then this is done in readData() - if ((totalSize.isNull() || totalSize == -1 || bytesDownloaded == totalSize) + if ((totalSize == -1 || bytesDownloaded == totalSize) && !decompressHelper.isValid()) { completeCacheSave(); } @@ -2135,10 +2147,10 @@ void QNetworkReplyHttpImplPrivate::finished() state = Finished; q->setFinished(true); - if (totalSize.isNull() || totalSize == -1) { + if (totalSize == -1) { emit q->downloadProgress(bytesDownloaded, bytesDownloaded); } else { - emit q->downloadProgress(bytesDownloaded, totalSize.toLongLong()); + emit q->downloadProgress(bytesDownloaded, totalSize); } if (bytesUploaded == -1 && (outgoingData || outgoingDataBuffer)) @@ -2182,14 +2194,15 @@ void QNetworkReplyHttpImplPrivate::_q_metaDataChanged() // 1. do we have cookies? // 2. are we allowed to set them? Q_ASSERT(manager); - const auto it = cookedHeaders.constFind(QNetworkRequest::SetCookieHeader); - if (it != cookedHeaders.cend() + + const auto cookiesOpt = QNetworkHeadersPrivate::toSetCookieList( + headers().values(QHttpHeaders::WellKnownHeader::SetCookie)); + const auto cookies = cookiesOpt.value_or(QList<QNetworkCookie>()); + if (!cookies.empty() && request.attribute(QNetworkRequest::CookieSaveControlAttribute, QNetworkRequest::Automatic).toInt() == QNetworkRequest::Automatic) { QNetworkCookieJar *jar = manager->cookieJar(); if (jar) { - QList<QNetworkCookie> cookies = - qvariant_cast<QList<QNetworkCookie> >(it.value()); jar->setCookiesFromUrl(cookies, url); } } diff --git a/src/network/access/qnetworkreplyimpl.cpp b/src/network/access/qnetworkreplyimpl.cpp index 8b2acfdb4e..b90ec1cc4c 100644 --- a/src/network/access/qnetworkreplyimpl.cpp +++ b/src/network/access/qnetworkreplyimpl.cpp @@ -118,15 +118,16 @@ void QNetworkReplyImplPrivate::_q_copyReadyRead() return; } - QVariant totalSize = cookedHeaders.value(QNetworkRequest::ContentLengthHeader); + const auto totalSizeOpt = QNetworkHeadersPrivate::toInt( + headers().value(QHttpHeaders::WellKnownHeader::ContentLength)); + pauseNotificationHandling(); // emit readyRead before downloadProgress in case this will cause events to be // processed and we get into a recursive call (as in QProgressDialog). emit q->readyRead(); if (downloadProgressSignalChoke.elapsed() >= progressSignalInterval) { downloadProgressSignalChoke.restart(); - emit q->downloadProgress(bytesDownloaded, - totalSize.isNull() ? Q_INT64_C(-1) : totalSize.toLongLong()); + emit q->downloadProgress(bytesDownloaded, totalSizeOpt.value_or(-1)); } resumeNotificationHandling(); } @@ -243,7 +244,10 @@ void QNetworkReplyImplPrivate::setup(QNetworkAccessManager::Operation op, const if (bufferingDisallowed) { // if a valid content-length header for the request was supplied, we can disable buffering // if not, we will buffer anyway - if (req.header(QNetworkRequest::ContentLengthHeader).isValid()) { + const auto sizeOpt = QNetworkHeadersPrivate::toInt( + headers().value(QHttpHeaders::WellKnownHeader::ContentLength)); + + if (sizeOpt) { QMetaObject::invokeMethod(q, "_q_startOperation", Qt::QueuedConnection); } else { state = Buffering; @@ -478,7 +482,8 @@ void QNetworkReplyImplPrivate::appendDownstreamDataSignalEmissions() { Q_Q(QNetworkReplyImpl); - QVariant totalSize = cookedHeaders.value(QNetworkRequest::ContentLengthHeader); + const auto totalSizeOpt = QNetworkHeadersPrivate::toInt( + headers().value(QHttpHeaders::WellKnownHeader::ContentLength)); pauseNotificationHandling(); // important: At the point of this readyRead(), the data parameter list must be empty, // else implicit sharing will trigger memcpy when the user is reading data! @@ -487,8 +492,7 @@ void QNetworkReplyImplPrivate::appendDownstreamDataSignalEmissions() // processed and we get into a recursive call (as in QProgressDialog). if (downloadProgressSignalChoke.elapsed() >= progressSignalInterval) { downloadProgressSignalChoke.restart(); - emit q->downloadProgress(bytesDownloaded, - totalSize.isNull() ? Q_INT64_C(-1) : totalSize.toLongLong()); + emit q->downloadProgress(bytesDownloaded, totalSizeOpt.value_or(-1)); } resumeNotificationHandling(); @@ -589,7 +593,9 @@ void QNetworkReplyImplPrivate::finished() return; pauseNotificationHandling(); - QVariant totalSize = cookedHeaders.value(QNetworkRequest::ContentLengthHeader); + const auto totalSizeOpt = QNetworkHeadersPrivate::toInt( + headers().value(QHttpHeaders::WellKnownHeader::ContentLength)); + const auto totalSize = totalSizeOpt.value_or(-1); resumeNotificationHandling(); @@ -599,10 +605,10 @@ void QNetworkReplyImplPrivate::finished() pendingNotifications.clear(); pauseNotificationHandling(); - if (totalSize.isNull() || totalSize == -1) { + if (totalSize == -1) { emit q->downloadProgress(bytesDownloaded, bytesDownloaded); } else { - emit q->downloadProgress(bytesDownloaded, totalSize.toLongLong()); + emit q->downloadProgress(bytesDownloaded, totalSize); } if (bytesUploaded == -1 && (outgoingData || outgoingDataBuffer)) @@ -610,7 +616,7 @@ void QNetworkReplyImplPrivate::finished() resumeNotificationHandling(); // if we don't know the total size of or we received everything save the cache - if (totalSize.isNull() || totalSize == -1 || bytesDownloaded == totalSize) + if (totalSize == -1 || bytesDownloaded == totalSize) completeCacheSave(); // note: might not be a good idea, since users could decide to delete us @@ -646,14 +652,14 @@ void QNetworkReplyImplPrivate::metaDataChanged() // 1. do we have cookies? // 2. are we allowed to set them? if (!manager.isNull()) { - const auto it = cookedHeaders.constFind(QNetworkRequest::SetCookieHeader); - if (it != cookedHeaders.cend() + const auto cookiesOpt = QNetworkHeadersPrivate::toSetCookieList( + headers().values(QHttpHeaders::WellKnownHeader::SetCookie)); + const auto cookies = cookiesOpt.value_or(QList<QNetworkCookie>()); + if (!cookies.empty() && request.attribute(QNetworkRequest::CookieSaveControlAttribute, QNetworkRequest::Automatic).toInt() == QNetworkRequest::Automatic) { QNetworkCookieJar *jar = manager->cookieJar(); if (jar) { - QList<QNetworkCookie> cookies = - qvariant_cast<QList<QNetworkCookie> >(it.value()); jar->setCookiesFromUrl(cookies, url); } } @@ -857,9 +863,10 @@ qint64 QNetworkReplyImpl::readData(char *data, qint64 maxlen) break; } } - QVariant totalSize = d->cookedHeaders.value(QNetworkRequest::ContentLengthHeader); - emit downloadProgress(bytesRead, - totalSize.isNull() ? Q_INT64_C(-1) : totalSize.toLongLong()); + + const auto totalSizeOpt = QNetworkHeadersPrivate::toInt( + headers().value(QHttpHeaders::WellKnownHeader::ContentLength)); + emit downloadProgress(bytesRead, totalSizeOpt.value_or(-1)); return bytesRead; } else if (d->backend && d->backend->bytesAvailable()) { return d->backend->read(data, maxlen); diff --git a/src/network/access/qnetworkreplywasmimpl.cpp b/src/network/access/qnetworkreplywasmimpl.cpp index c02f0b4e61..7d2b6a701e 100644 --- a/src/network/access/qnetworkreplywasmimpl.cpp +++ b/src/network/access/qnetworkreplywasmimpl.cpp @@ -9,6 +9,7 @@ #include <QtCore/qcoreapplication.h> #include <QtCore/qfileinfo.h> #include <QtCore/qthread.h> +#include <QtCore/private/qeventdispatcher_wasm_p.h> #include <QtCore/private/qoffsetstringarray_p.h> #include <QtCore/private/qtools_p.h> @@ -63,11 +64,27 @@ QNetworkReplyWasmImplPrivate::QNetworkReplyWasmImplPrivate() , totalDownloadSize(0) , percentFinished(0) , m_fetch(nullptr) + , m_fetchContext(nullptr) { } QNetworkReplyWasmImplPrivate::~QNetworkReplyWasmImplPrivate() { + + if (m_fetchContext) { // fetch has been initiated + std::unique_lock lock{ m_fetchContext->mutex }; + + if (m_fetchContext->state == FetchContext::State::SCHEDULED + || m_fetchContext->state == FetchContext::State::SENT + || m_fetchContext->state == FetchContext::State::CANCELED) { + m_fetchContext->reply = nullptr; + m_fetchContext->state = FetchContext::State::TO_BE_DESTROYED; + } else if (m_fetchContext->state == FetchContext::State::FINISHED) { + lock.unlock(); + delete m_fetchContext; + } + } + } QNetworkReplyWasmImpl::QNetworkReplyWasmImpl(QObject *parent) @@ -116,7 +133,7 @@ void QNetworkReplyWasmImpl::close() d->state = QNetworkReplyPrivate::Finished; d->setCanceled(); } - + emscripten_fetch_close(d->m_fetch); QNetworkReply::close(); } @@ -134,8 +151,14 @@ void QNetworkReplyWasmImpl::abort() void QNetworkReplyWasmImplPrivate::setCanceled() { Q_Q(QNetworkReplyWasmImpl); - if (m_fetch) - m_fetch->userData = nullptr; + { + if (m_fetchContext) { + std::scoped_lock lock{ m_fetchContext->mutex }; + if (m_fetchContext->state == FetchContext::State::SCHEDULED + || m_fetchContext->state == FetchContext::State::SENT) + m_fetchContext->state = FetchContext::State::CANCELED; + } + } emitReplyError(QNetworkReply::OperationCanceledError, QStringLiteral("Operation canceled")); q->setFinished(true); @@ -227,48 +250,7 @@ void QNetworkReplyWasmImplPrivate::doSendRequest() emscripten_fetch_attr_t attr; emscripten_fetch_attr_init(&attr); - strcpy(attr.requestMethod, q->methodName().constData()); - - QList<QByteArray> headersData = request.rawHeaderList(); - int arrayLength = getArraySize(headersData.count()); - const char *customHeaders[arrayLength]; - QStringList trimmedHeaders; - - if (headersData.count() > 0) { - int i = 0; - for (const auto &headerName : headersData) { - if (isUnsafeHeader(QLatin1StringView(headerName.constData()))) { - trimmedHeaders.push_back(QString::fromLatin1(headerName)); - } else { - customHeaders[i++] = headerName.constData(); - customHeaders[i++] = request.rawHeader(headerName).constData(); - } - } - if (!trimmedHeaders.isEmpty()) { - qWarning() << "Qt has trimmed the following forbidden headers from the request:" - << trimmedHeaders.join(QLatin1StringView(", ")); - } - customHeaders[i] = nullptr; - attr.requestHeaders = customHeaders; - } - - if (outgoingData) { // data from post request - // handle extra data - requestData = outgoingData->readAll(); // is there a size restriction here? - if (!requestData.isEmpty()) { - attr.requestData = requestData.data(); - attr.requestDataSize = requestData.size(); - } - } - - QByteArray userName, password; - // username & password - if (!request.url().userInfo().isEmpty()) { - userName = request.url().userName().toUtf8(); - password = request.url().password().toUtf8(); - attr.userName = userName.constData(); - attr.password = password.constData(); - } + qstrncpy(attr.requestMethod, q->methodName().constData(), 32); // requestMethod is char[32] in emscripten attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY; @@ -293,13 +275,67 @@ void QNetworkReplyWasmImplPrivate::doSendRequest() attr.onprogress = QNetworkReplyWasmImplPrivate::downloadProgress; attr.onreadystatechange = QNetworkReplyWasmImplPrivate::stateChange; attr.timeoutMSecs = request.transferTimeout(); - attr.userData = reinterpret_cast<void *>(this); - QString dPath = "/home/web_user/"_L1 + request.url().fileName(); - QByteArray destinationPath = dPath.toUtf8(); - attr.destinationPath = destinationPath.constData(); + m_fetchContext = new FetchContext(this);; + attr.userData = static_cast<void *>(m_fetchContext); + if (outgoingData) { // data from post request + m_fetchContext->requestData = outgoingData->readAll(); // is there a size restriction here? + if (!m_fetchContext->requestData.isEmpty()) { + attr.requestData = m_fetchContext->requestData.data(); + attr.requestDataSize = m_fetchContext->requestData.size(); + } + } - m_fetch = emscripten_fetch(&attr, request.url().toString().toUtf8().constData()); + QEventDispatcherWasm::runOnMainThread([attr, fetchContext = m_fetchContext]() mutable { + std::unique_lock lock{ fetchContext->mutex }; + if (fetchContext->state == FetchContext::State::CANCELED) { + fetchContext->state = FetchContext::State::FINISHED; + return; + } else if (fetchContext->state == FetchContext::State::TO_BE_DESTROYED) { + lock.unlock(); + delete fetchContext; + return; + } + const auto reply = fetchContext->reply; + const auto &request = reply->request; + + QByteArray userName, password; + if (!request.url().userInfo().isEmpty()) { + userName = request.url().userName().toUtf8(); + password = request.url().password().toUtf8(); + attr.userName = userName.constData(); + attr.password = password.constData(); + } + + QList<QByteArray> headersData = request.rawHeaderList(); + int arrayLength = getArraySize(headersData.count()); + const char *customHeaders[arrayLength]; + QStringList trimmedHeaders; + if (headersData.count() > 0) { + int i = 0; + for (const auto &headerName : headersData) { + if (isUnsafeHeader(QLatin1StringView(headerName.constData()))) { + trimmedHeaders.push_back(QString::fromLatin1(headerName)); + } else { + customHeaders[i++] = headerName.constData(); + customHeaders[i++] = request.rawHeader(headerName).constData(); + } + } + if (!trimmedHeaders.isEmpty()) { + qWarning() << "Qt has trimmed the following forbidden headers from the request:" + << trimmedHeaders.join(QLatin1StringView(", ")); + } + customHeaders[i] = nullptr; + attr.requestHeaders = customHeaders; + } + + auto url = request.url().toString().toUtf8(); + QString dPath = "/home/web_user/"_L1 + request.url().fileName(); + QByteArray destinationPath = dPath.toUtf8(); + attr.destinationPath = destinationPath.constData(); + reply->m_fetch = emscripten_fetch(&attr, url.constData()); + fetchContext->state = FetchContext::State::SENT; + }); state = Working; } @@ -477,8 +513,18 @@ void QNetworkReplyWasmImplPrivate::_q_bufferOutgoingData() void QNetworkReplyWasmImplPrivate::downloadSucceeded(emscripten_fetch_t *fetch) { - auto reply = reinterpret_cast<QNetworkReplyWasmImplPrivate*>(fetch->userData); - if (reply) { + auto fetchContext = static_cast<FetchContext *>(fetch->userData); + std::unique_lock lock{ fetchContext->mutex }; + + if (fetchContext->state == FetchContext::State::TO_BE_DESTROYED) { + lock.unlock(); + delete fetchContext; + return; + } else if (fetchContext->state == FetchContext::State::CANCELED) { + fetchContext->state = FetchContext::State::FINISHED; + return; + } else if (fetchContext->state == FetchContext::State::SENT) { + const auto reply = fetchContext->reply; if (reply->state != QNetworkReplyPrivate::Aborted) { QByteArray buffer(fetch->data, fetch->numBytes); reply->dataReceived(buffer); @@ -487,8 +533,8 @@ void QNetworkReplyWasmImplPrivate::downloadSucceeded(emscripten_fetch_t *fetch) reply->setReplyFinished(); } reply->m_fetch = nullptr; + fetchContext->state = FetchContext::State::FINISHED; } - emscripten_fetch_close(fetch); } void QNetworkReplyWasmImplPrivate::setReplyFinished() @@ -509,7 +555,8 @@ void QNetworkReplyWasmImplPrivate::setStatusCode(int status, const QByteArray &s void QNetworkReplyWasmImplPrivate::stateChange(emscripten_fetch_t *fetch) { - auto reply = reinterpret_cast<QNetworkReplyWasmImplPrivate*>(fetch->userData); + const auto fetchContext = static_cast<FetchContext*>(fetch->userData); + const auto reply = fetchContext->reply; if (reply && reply->state != QNetworkReplyPrivate::Aborted) { if (fetch->readyState == /*HEADERS_RECEIVED*/ 2) { size_t headerLength = emscripten_fetch_get_response_headers_length(fetch); @@ -522,7 +569,8 @@ void QNetworkReplyWasmImplPrivate::stateChange(emscripten_fetch_t *fetch) void QNetworkReplyWasmImplPrivate::downloadProgress(emscripten_fetch_t *fetch) { - auto reply = reinterpret_cast<QNetworkReplyWasmImplPrivate*>(fetch->userData); + const auto fetchContext = static_cast<FetchContext*>(fetch->userData); + const auto reply = fetchContext->reply; if (reply && reply->state != QNetworkReplyPrivate::Aborted) { if (fetch->status < 400) { uint64_t bytes = fetch->dataOffset + fetch->numBytes; @@ -536,8 +584,18 @@ void QNetworkReplyWasmImplPrivate::downloadProgress(emscripten_fetch_t *fetch) void QNetworkReplyWasmImplPrivate::downloadFailed(emscripten_fetch_t *fetch) { - auto reply = reinterpret_cast<QNetworkReplyWasmImplPrivate*>(fetch->userData); - if (reply) { + const auto fetchContext = static_cast<FetchContext*>(fetch->userData); + std::unique_lock lock{ fetchContext->mutex }; + + if (fetchContext->state == FetchContext::State::TO_BE_DESTROYED) { + lock.unlock(); + delete fetchContext; + return; + } else if (fetchContext->state == FetchContext::State::CANCELED) { + fetchContext->state = FetchContext::State::FINISHED; + return; + } else if (fetchContext->state == FetchContext::State::SENT) { + const auto reply = fetchContext->reply; if (reply->state != QNetworkReplyPrivate::Aborted) { QString reasonStr; if (fetch->status > 600) @@ -548,12 +606,13 @@ void QNetworkReplyWasmImplPrivate::downloadFailed(emscripten_fetch_t *fetch) reply->dataReceived(buffer); QByteArray statusText(fetch->statusText); reply->setStatusCode(fetch->status, statusText); - reply->emitReplyError(reply->statusCodeFromHttp(fetch->status, reply->request.url()), reasonStr); + reply->emitReplyError(reply->statusCodeFromHttp(fetch->status, reply->request.url()), + reasonStr); reply->setReplyFinished(); } reply->m_fetch = nullptr; + fetchContext->state = FetchContext::State::FINISHED; } - emscripten_fetch_close(fetch); } //taken from qhttpthreaddelegate.cpp diff --git a/src/network/access/qnetworkreplywasmimpl_p.h b/src/network/access/qnetworkreplywasmimpl_p.h index ae167799d7..4b00bb09ea 100644 --- a/src/network/access/qnetworkreplywasmimpl_p.h +++ b/src/network/access/qnetworkreplywasmimpl_p.h @@ -28,6 +28,7 @@ #include <emscripten/fetch.h> #include <memory> +#include <mutex> QT_BEGIN_NAMESPACE @@ -63,6 +64,29 @@ private: QByteArray methodName() const; }; +class QNetworkReplyWasmImplPrivate; + +/*! + The FetchContext class ensures the requestData object remains valid + while a fetch operation is pending. Since Emscripten fetch is asynchronous, + requestData must persist until one of the final callbacks is invoked. + Additionally, there's a potential race condition between the thread + scheduling the fetch operation and the one executing it. Since fetch must + occur on the main thread due to browser limitations, + a mutex safeguards the FetchContext to ensure atomic state transitions. +*/ +struct FetchContext +{ + enum class State { SCHEDULED, SENT, FINISHED, CANCELED, TO_BE_DESTROYED }; + + FetchContext(QNetworkReplyWasmImplPrivate *networkReply) : reply(networkReply) { } + + QNetworkReplyWasmImplPrivate *reply{ nullptr }; + std::mutex mutex; + QByteArray requestData; + State state{ State::SCHEDULED }; +}; + class QNetworkReplyWasmImplPrivate: public QNetworkReplyPrivate { public: @@ -101,7 +125,6 @@ public: QIODevice *outgoingData; std::shared_ptr<QRingBuffer> outgoingDataBuffer; - QByteArray requestData; static void downloadProgress(emscripten_fetch_t *fetch); static void downloadFailed(emscripten_fetch_t *fetch); @@ -111,6 +134,7 @@ public: static QNetworkReply::NetworkError statusCodeFromHttp(int httpStatusCode, const QUrl &url); emscripten_fetch_t *m_fetch; + FetchContext *m_fetchContext; void setReplyFinished(); void setCanceled(); diff --git a/src/network/access/qnetworkrequest.cpp b/src/network/access/qnetworkrequest.cpp index ad83dd38e3..0e1172b15c 100644 --- a/src/network/access/qnetworkrequest.cpp +++ b/src/network/access/qnetworkrequest.cpp @@ -17,6 +17,7 @@ #include "QtCore/qlocale.h" #include "QtCore/qshareddata.h" #include "QtCore/qtimezone.h" +#include "QtCore/private/qduplicatetracker_p.h" #include "QtCore/private/qtools_p.h" #include <ctype.h> @@ -25,6 +26,7 @@ #endif #include <algorithm> +#include <q20algorithm.h> QT_BEGIN_NAMESPACE @@ -110,6 +112,8 @@ QT_IMPL_METATYPE_EXTERN_TAGGED(QNetworkRequest::RedirectPolicy, QNetworkRequest_ \value ServerHeader The Server header received by HTTP clients. + \omitvalue NumKnownHeaders + \sa header(), setHeader(), rawHeader(), setRawHeader() */ @@ -320,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 @@ -466,7 +480,6 @@ public: { return url == other.url && priority == other.priority && - rawHeaders == other.rawHeaders && attributes == other.attributes && maxRedirectsAllowed == other.maxRedirectsAllowed && peerVerifyName == other.peerVerifyName @@ -672,7 +685,7 @@ void QNetworkRequest::setHeader(KnownHeaders header, const QVariant &value) */ bool QNetworkRequest::hasRawHeader(QAnyStringView headerName) const { - return d->findRawHeader(headerName) != d->rawHeaders.constEnd(); + return d->headers().contains(headerName); } /*! @@ -688,9 +701,7 @@ bool QNetworkRequest::hasRawHeader(QAnyStringView headerName) const */ QByteArray QNetworkRequest::rawHeader(QAnyStringView headerName) const { - if (const auto it = d->findRawHeader(headerName); it != d->rawHeaders.constEnd()) - return it->second; - return QByteArray(); + return d->rawHeader(headerName); } /*! @@ -1077,63 +1088,72 @@ void QNetworkRequest::setTransferTimeout(std::chrono::milliseconds duration) } #endif // QT_CONFIG(http) || defined (Q_OS_WASM) -static QByteArray headerName(QNetworkRequest::KnownHeaders header) -{ - switch (header) { - case QNetworkRequest::ContentTypeHeader: - return "Content-Type"_ba; - - case QNetworkRequest::ContentLengthHeader: - return "Content-Length"_ba; - - case QNetworkRequest::LocationHeader: - return "Location"_ba; - - case QNetworkRequest::LastModifiedHeader: - return "Last-Modified"_ba; - - case QNetworkRequest::IfModifiedSinceHeader: - return "If-Modified-Since"_ba; - - case QNetworkRequest::ETagHeader: - return "ETag"_ba; +namespace { - case QNetworkRequest::IfMatchHeader: - return "If-Match"_ba; - - case QNetworkRequest::IfNoneMatchHeader: - return "If-None-Match"_ba; +struct HeaderPair { + QHttpHeaders::WellKnownHeader wellKnownHeader; + QNetworkRequest::KnownHeaders knownHeader; +}; - case QNetworkRequest::CookieHeader: - return "Cookie"_ba; +constexpr bool operator<(const HeaderPair &lhs, const HeaderPair &rhs) +{ + return lhs.wellKnownHeader < rhs.wellKnownHeader; +} - case QNetworkRequest::SetCookieHeader: - return "Set-Cookie"_ba; +constexpr bool operator<(const HeaderPair &lhs, QHttpHeaders::WellKnownHeader rhs) +{ + return lhs.wellKnownHeader < rhs; +} - case QNetworkRequest::ContentDispositionHeader: - return "Content-Disposition"_ba; +constexpr bool operator<(QHttpHeaders::WellKnownHeader lhs, const HeaderPair &rhs) +{ + return lhs < rhs.wellKnownHeader; +} - case QNetworkRequest::UserAgentHeader: - return "User-Agent"_ba; +} // anonymous namespace - case QNetworkRequest::ServerHeader: - return "Server"_ba; +static constexpr HeaderPair knownHeadersArr[] = { + { QHttpHeaders::WellKnownHeader::ContentDisposition, QNetworkRequest::KnownHeaders::ContentDispositionHeader }, + { QHttpHeaders::WellKnownHeader::ContentLength, QNetworkRequest::KnownHeaders::ContentLengthHeader }, + { QHttpHeaders::WellKnownHeader::ContentType, QNetworkRequest::KnownHeaders::ContentTypeHeader }, + { QHttpHeaders::WellKnownHeader::Cookie, QNetworkRequest::KnownHeaders::CookieHeader }, + { QHttpHeaders::WellKnownHeader::ETag, QNetworkRequest::KnownHeaders::ETagHeader }, + { QHttpHeaders::WellKnownHeader::IfMatch , QNetworkRequest::KnownHeaders::IfMatchHeader }, + { QHttpHeaders::WellKnownHeader::IfModifiedSince, QNetworkRequest::KnownHeaders::IfModifiedSinceHeader }, + { QHttpHeaders::WellKnownHeader::IfNoneMatch, QNetworkRequest::KnownHeaders::IfNoneMatchHeader }, + { QHttpHeaders::WellKnownHeader::LastModified, QNetworkRequest::KnownHeaders::LastModifiedHeader}, + { QHttpHeaders::WellKnownHeader::Location, QNetworkRequest::KnownHeaders::LocationHeader}, + { QHttpHeaders::WellKnownHeader::Server, QNetworkRequest::KnownHeaders::ServerHeader }, + { QHttpHeaders::WellKnownHeader::SetCookie, QNetworkRequest::KnownHeaders::SetCookieHeader }, + { QHttpHeaders::WellKnownHeader::UserAgent, QNetworkRequest::KnownHeaders::UserAgentHeader } +}; - // no default: - // if new values are added, this will generate a compiler warning - } +static_assert(std::size(knownHeadersArr) == size_t(QNetworkRequest::KnownHeaders::NumKnownHeaders)); +static_assert(q20::is_sorted(std::begin(knownHeadersArr), std::end(knownHeadersArr))); - return QByteArray(); +static std::optional<QNetworkRequest::KnownHeaders> toKnownHeader(QHttpHeaders::WellKnownHeader key) +{ + const auto it = std::lower_bound(std::begin(knownHeadersArr), std::end(knownHeadersArr), key); + if (it == std::end(knownHeadersArr) || key < *it) + return std::nullopt; + return it->knownHeader; } -static QByteArray makeCookieHeader(const QVariant &value, QNetworkCookie::RawForm type, QByteArrayView separator) +static std::optional<QHttpHeaders::WellKnownHeader> toWellKnownHeader(QNetworkRequest::KnownHeaders key) { - QList<QNetworkCookie> cookies = qvariant_cast<QList<QNetworkCookie> >(value); - if (cookies.isEmpty() && value.userType() == qMetaTypeId<QNetworkCookie>()) - cookies << qvariant_cast<QNetworkCookie>(value); + auto pred = [key](const HeaderPair &pair) { return pair.knownHeader == key; }; + const auto it = std::find_if(std::begin(knownHeadersArr), std::end(knownHeadersArr), pred); + if (it == std::end(knownHeadersArr)) + return std::nullopt; + return it->wellKnownHeader; +} +static QByteArray makeCookieHeader(const QList<QNetworkCookie> &cookies, + QNetworkCookie::RawForm type, + QByteArrayView separator) +{ QByteArray result; - for (const QNetworkCookie &cookie : std::as_const(cookies)) { + for (const QNetworkCookie &cookie : cookies) { result += cookie.toRawForm(type); result += separator; } @@ -1142,6 +1162,15 @@ static QByteArray makeCookieHeader(const QVariant &value, QNetworkCookie::RawFor return result; } +static QByteArray makeCookieHeader(const QVariant &value, QNetworkCookie::RawForm type, + QByteArrayView separator) +{ + const QList<QNetworkCookie> *cookies = get_if<QList<QNetworkCookie>>(&value); + if (!cookies) + return {}; + return makeCookieHeader(*cookies, type, separator); +} + static QByteArray headerValue(QNetworkRequest::KnownHeaders header, const QVariant &value) { switch (header) { @@ -1182,9 +1211,10 @@ static QByteArray headerValue(QNetworkRequest::KnownHeaders header, const QVaria case QNetworkRequest::SetCookieHeader: return makeCookieHeader(value, QNetworkCookie::Full, ", "); - } - return QByteArray(); + default: + Q_UNREACHABLE_RETURN({}); + } } static int parseHeaderName(QByteArrayView headerName) @@ -1245,7 +1275,7 @@ static int parseHeaderName(QByteArrayView headerName) return -1; // nothing found } -static QVariant parseHttpDate(const QByteArray &raw) +static QVariant parseHttpDate(QByteArrayView raw) { QDateTime dt = QNetworkHeadersPrivate::fromHttpDate(raw); if (dt.isValid()) @@ -1253,18 +1283,18 @@ static QVariant parseHttpDate(const QByteArray &raw) return QVariant(); // transform an invalid QDateTime into a null QVariant } -static QVariant parseCookieHeader(QByteArrayView raw) +static QList<QNetworkCookie> parseCookieHeader(QByteArrayView raw) { QList<QNetworkCookie> result; for (auto cookie : QLatin1StringView(raw).tokenize(';'_L1)) { QList<QNetworkCookie> parsed = QNetworkCookie::parseCookies(cookie.trimmed()); if (parsed.size() != 1) - return QVariant(); // invalid Cookie: header + return {}; // invalid Cookie: header result += parsed; } - return QVariant::fromValue(result); + return result; } static QVariant parseETag(QByteArrayView raw) @@ -1280,7 +1310,7 @@ static QVariant parseETag(QByteArrayView raw) } template<typename T> -static QVariant parseMatchImpl(QByteArrayView raw, T op) +static QStringList parseMatchImpl(QByteArrayView raw, T op) { const QByteArrayView trimmedRaw = raw.trimmed(); if (trimmedRaw == "*") @@ -1295,14 +1325,14 @@ static QVariant parseMatchImpl(QByteArrayView raw, T op) } -static QVariant parseIfMatch(QByteArrayView raw) +static QStringList parseIfMatch(QByteArrayView raw) { return parseMatchImpl(raw, [](QByteArrayView element) { return element.startsWith('"') && element.endsWith('"'); }); } -static QVariant parseIfNoneMatch(QByteArrayView raw) +static QStringList parseIfNoneMatch(QByteArrayView raw) { return parseMatchImpl(raw, [](QByteArrayView element) { return (element.startsWith('"') || element.startsWith(R"(W/")")) && element.endsWith('"'); @@ -1310,7 +1340,7 @@ static QVariant parseIfNoneMatch(QByteArrayView raw) } -static QVariant parseHeaderValue(QNetworkRequest::KnownHeaders header, const QByteArray &value) +static QVariant parseHeaderValue(QNetworkRequest::KnownHeaders header, QByteArrayView value) { // header is always a valid value switch (header) { @@ -1350,40 +1380,127 @@ static QVariant parseHeaderValue(QNetworkRequest::KnownHeaders header, const QBy return parseIfNoneMatch(value); case QNetworkRequest::CookieHeader: - return parseCookieHeader(value); + return QVariant::fromValue(parseCookieHeader(value)); case QNetworkRequest::SetCookieHeader: return QVariant::fromValue(QNetworkCookie::parseCookies(value)); default: - Q_ASSERT(0); + Q_UNREACHABLE_RETURN({}); + } +} + +static QVariant parseHeaderValue(QNetworkRequest::KnownHeaders header, QList<QByteArray> values) +{ + if (values.empty()) + return QVariant(); + + // header is always a valid value + switch (header) { + case QNetworkRequest::IfMatchHeader: { + QStringList res; + for (const auto &val : values) + res << parseIfMatch(val); + return res; + } + case QNetworkRequest::IfNoneMatchHeader: { + QStringList res; + for (const auto &val : values) + res << parseIfNoneMatch(val); + return res; + } + case QNetworkRequest::CookieHeader: { + auto listOpt = QNetworkHeadersPrivate::toCookieList(values); + return listOpt.has_value() ? QVariant::fromValue(listOpt.value()) : QVariant(); + } + case QNetworkRequest::SetCookieHeader: { + QList<QNetworkCookie> res; + for (const auto &val : values) + res << QNetworkCookie::parseCookies(val); + return QVariant::fromValue(res); + } + default: + return parseHeaderValue(header, values.first()); } return QVariant(); } -QNetworkHeadersPrivate::RawHeadersList::ConstIterator -QNetworkHeadersPrivate::findRawHeader(QAnyStringView key) const +static bool isSetCookie(QByteArrayView name) { - auto isKeyEqual = [key](const auto &headerPair) - { - QLatin1StringView name{headerPair.first}; - return QAnyStringView::compare(name, key, Qt::CaseInsensitive) == 0; - }; - return std::find_if(rawHeaders.begin(), rawHeaders.end(), isKeyEqual); + return name.compare(QHttpHeaders::wellKnownHeaderName(QHttpHeaders::WellKnownHeader::SetCookie), + Qt::CaseInsensitive) == 0; } -QNetworkHeadersPrivate::RawHeadersList QNetworkHeadersPrivate::allRawHeaders() const +static bool isSetCookie(QHttpHeaders::WellKnownHeader name) { - return rawHeaders; + return name == QHttpHeaders::WellKnownHeader::SetCookie; +} + +template<class HeaderName> +static void setFromRawHeader(QHttpHeaders &headers, HeaderName header, + QByteArrayView value) +{ + headers.removeAll(header); + + if (value.isNull()) + // only wanted to erase key + return; + + if (isSetCookie(header)) { + for (auto cookie : QLatin1StringView(value).tokenize('\n'_L1)) + headers.append(QHttpHeaders::WellKnownHeader::SetCookie, cookie); + } else { + headers.append(header, value); + } +} + +const QNetworkHeadersPrivate::RawHeadersList &QNetworkHeadersPrivate::allRawHeaders() const +{ + if (rawHeaderCache.isCached) + return rawHeaderCache.headersList; + + rawHeaderCache.headersList = fromHttpToRaw(httpHeaders); + rawHeaderCache.isCached = true; + return rawHeaderCache.headersList; } QList<QByteArray> QNetworkHeadersPrivate::rawHeadersKeys() const { + if (httpHeaders.isEmpty()) + return {}; + QList<QByteArray> result; - result.reserve(rawHeaders.size()); - for (const auto &pair : rawHeaders) - result << pair.first; + result.reserve(httpHeaders.size()); + QDuplicateTracker<QByteArray> seen(httpHeaders.size()); + + for (qsizetype i = 0; i < httpHeaders.size(); i++) { + const auto nameL1 = httpHeaders.nameAt(i); + const auto name = QByteArray(nameL1.data(), nameL1.size()); + if (seen.hasSeen(name)) + continue; + + result << name; + } + + return result; +} + +QByteArray QNetworkHeadersPrivate::rawHeader(QAnyStringView headerName) const +{ + QByteArrayView setCookieStr = QHttpHeaders::wellKnownHeaderName( + QHttpHeaders::WellKnownHeader::SetCookie); + if (QAnyStringView::compare(headerName, setCookieStr, Qt::CaseInsensitive) != 0) + return httpHeaders.combinedValue(headerName); + QByteArray result; + const char* separator = ""; + for (qsizetype i = 0; i < httpHeaders.size(); ++i) { + if (QAnyStringView::compare(httpHeaders.nameAt(i), headerName, Qt::CaseInsensitive) == 0) { + result.append(separator); + result.append(httpHeaders.valueAt(i)); + separator = "\n"; + } + } return result; } @@ -1393,51 +1510,39 @@ void QNetworkHeadersPrivate::setRawHeader(const QByteArray &key, const QByteArra // refuse to accept an empty raw header return; - setRawHeaderInternal(key, value); + setFromRawHeader(httpHeaders, key, value); parseAndSetHeader(key, value); -} - -/*! - \internal - Sets the internal raw headers list to match \a list. The cooked headers - will also be updated. - - If \a list contains duplicates, they will be stored, but only the first one - is usually accessed. -*/ -void QNetworkHeadersPrivate::setAllRawHeaders(const RawHeadersList &list) -{ - cookedHeaders.clear(); - rawHeaders = list; - for (const auto &[key, value] : std::as_const(rawHeaders)) - parseAndSetHeader(key, value); + invalidateHeaderCache(); } void QNetworkHeadersPrivate::setCookedHeader(QNetworkRequest::KnownHeaders header, const QVariant &value) { - QByteArray name = headerName(header); - if (name.isEmpty()) { - // headerName verifies that \a header is a known value + const auto wellKnownOpt = toWellKnownHeader(header); + if (!wellKnownOpt) { + // verifies that \a header is a known value qWarning("QNetworkRequest::setHeader: invalid header value KnownHeader(%d) received", header); return; } if (value.isNull()) { - setRawHeaderInternal(name, QByteArray()); + httpHeaders.removeAll(wellKnownOpt.value()); cookedHeaders.remove(header); } else { QByteArray rawValue = headerValue(header, value); if (rawValue.isEmpty()) { qWarning("QNetworkRequest::setHeader: QVariant of type %s cannot be used with header %s", - value.typeName(), name.constData()); + value.typeName(), + QHttpHeaders::wellKnownHeaderName(wellKnownOpt.value()).constData()); return; } - setRawHeaderInternal(name, rawValue); + setFromRawHeader(httpHeaders, wellKnownOpt.value(), rawValue); cookedHeaders.insert(header, value); } + + invalidateHeaderCache(); } QHttpHeaders QNetworkHeadersPrivate::headers() const @@ -1448,58 +1553,58 @@ QHttpHeaders QNetworkHeadersPrivate::headers() const void QNetworkHeadersPrivate::setHeaders(const QHttpHeaders &newHeaders) { httpHeaders = newHeaders; + setCookedFromHttp(httpHeaders); + invalidateHeaderCache(); } void QNetworkHeadersPrivate::setHeaders(QHttpHeaders &&newHeaders) { httpHeaders = std::move(newHeaders); + setCookedFromHttp(httpHeaders); + invalidateHeaderCache(); } void QNetworkHeadersPrivate::setHeader(QHttpHeaders::WellKnownHeader name, QByteArrayView value) { httpHeaders.replaceOrAppend(name, value); + + // set cooked header + const auto knownHeaderOpt = toKnownHeader(name); + if (knownHeaderOpt) + parseAndSetHeader(knownHeaderOpt.value(), value); + + invalidateHeaderCache(); } void QNetworkHeadersPrivate::clearHeaders() { httpHeaders.clear(); - rawHeaders.clear(); cookedHeaders.clear(); + invalidateHeaderCache(); } -void QNetworkHeadersPrivate::setRawHeaderInternal(const QByteArray &key, const QByteArray &value) -{ - auto firstEqualsKey = [&key](const RawHeaderPair &header) { - return header.first.compare(key, Qt::CaseInsensitive) == 0; - }; - rawHeaders.removeIf(firstEqualsKey); - - if (value.isNull()) - return; // only wanted to erase key - - RawHeaderPair pair; - pair.first = key; - pair.second = value; - rawHeaders.append(pair); -} - -void QNetworkHeadersPrivate::parseAndSetHeader(const QByteArray &key, const QByteArray &value) +void QNetworkHeadersPrivate::parseAndSetHeader(QByteArrayView key, QByteArrayView value) { // is it a known header? const int parsedKeyAsInt = parseHeaderName(key); if (parsedKeyAsInt != -1) { const QNetworkRequest::KnownHeaders parsedKey = static_cast<QNetworkRequest::KnownHeaders>(parsedKeyAsInt); - if (value.isNull()) { - cookedHeaders.remove(parsedKey); - } else if (parsedKey == QNetworkRequest::ContentLengthHeader - && cookedHeaders.contains(QNetworkRequest::ContentLengthHeader)) { - // Only set the cooked header "Content-Length" once. - // See bug QTBUG-15311 - } else { - cookedHeaders.insert(parsedKey, parseHeaderValue(parsedKey, value)); - } + parseAndSetHeader(parsedKey, value); + } +} +void QNetworkHeadersPrivate::parseAndSetHeader(QNetworkRequest::KnownHeaders key, + QByteArrayView value) +{ + if (value.isNull()) { + cookedHeaders.remove(key); + } else if (key == QNetworkRequest::ContentLengthHeader + && cookedHeaders.contains(QNetworkRequest::ContentLengthHeader)) { + // Only set the cooked header "Content-Length" once. + // See bug QTBUG-15311 + } else { + cookedHeaders.insert(key, parseHeaderValue(key, value)); } } @@ -1553,7 +1658,7 @@ static int name_to_month(const char* month_str) return 0; } -QDateTime QNetworkHeadersPrivate::fromHttpDate(const QByteArray &value) +QDateTime QNetworkHeadersPrivate::fromHttpDate(QByteArrayView value) { // HTTP dates have three possible formats: // RFC 1123/822 - ddd, dd MMM yyyy hh:mm:ss "GMT" @@ -1602,6 +1707,137 @@ QByteArray QNetworkHeadersPrivate::toHttpDate(const QDateTime &dt) return QLocale::c().toString(dt.toUTC(), u"ddd, dd MMM yyyy hh:mm:ss 'GMT'").toLatin1(); } +QNetworkHeadersPrivate::RawHeadersList QNetworkHeadersPrivate::fromHttpToRaw( + const QHttpHeaders &headers) +{ + if (headers.isEmpty()) + return {}; + + QNetworkHeadersPrivate::RawHeadersList list; + QHash<QByteArray, qsizetype> nameToIndex; + list.reserve(headers.size()); + nameToIndex.reserve(headers.size()); + + for (qsizetype i = 0; i < headers.size(); ++i) { + const auto nameL1 = headers.nameAt(i); + const auto value = headers.valueAt(i); + + const bool isSetCookie = nameL1 == QHttpHeaders::wellKnownHeaderName( + QHttpHeaders::WellKnownHeader::SetCookie); + + const auto name = QByteArray(nameL1.data(), nameL1.size()); + if (auto it = nameToIndex.find(name); it != nameToIndex.end()) { + list[it.value()].second += isSetCookie ? "\n" : ", "; + list[it.value()].second += value; + } else { + nameToIndex[name] = list.size(); + list.emplaceBack(name, value.toByteArray()); + } + } + + return list; +} + +QHttpHeaders QNetworkHeadersPrivate::fromRawToHttp(const RawHeadersList &raw) +{ + if (raw.empty()) + return {}; + + QHttpHeaders headers; + headers.reserve(raw.size()); + + for (const auto &[key, value] : raw) { + const bool isSetCookie = key.compare(QHttpHeaders::wellKnownHeaderName( + QHttpHeaders::WellKnownHeader::SetCookie), + Qt::CaseInsensitive) == 0; + if (isSetCookie) { + for (auto header : QLatin1StringView(value).tokenize('\n'_L1)) + headers.append(key, header); + } else { + headers.append(key, value); + } + } + + return headers; +} + +std::optional<qint64> QNetworkHeadersPrivate::toInt(QByteArrayView value) +{ + if (value.empty()) + return std::nullopt; + + bool ok; + qint64 res = value.toLongLong(&ok); + if (ok) + return res; + return std::nullopt; +} + +std::optional<QNetworkHeadersPrivate::NetworkCookieList> QNetworkHeadersPrivate::toSetCookieList( + const QList<QByteArray> &values) +{ + if (values.empty()) + return std::nullopt; + + QList<QNetworkCookie> cookies; + for (const auto &s : values) + cookies += QNetworkCookie::parseCookies(s); + + if (cookies.empty()) + return std::nullopt; + return cookies; +} + +QByteArray QNetworkHeadersPrivate::fromCookieList(const QList<QNetworkCookie> &cookies) +{ + return makeCookieHeader(cookies, QNetworkCookie::NameAndValueOnly, "; "); +} + +std::optional<QNetworkHeadersPrivate::NetworkCookieList> QNetworkHeadersPrivate::toCookieList( + const QList<QByteArray> &values) +{ + if (values.empty()) + return std::nullopt; + + QList<QNetworkCookie> cookies; + for (const auto &s : values) + cookies += parseCookieHeader(s); + + if (cookies.empty()) + return std::nullopt; + return cookies; +} + +void QNetworkHeadersPrivate::invalidateHeaderCache() +{ + rawHeaderCache.headersList.clear(); + rawHeaderCache.isCached = false; +} + +void QNetworkHeadersPrivate::setCookedFromHttp(const QHttpHeaders &newHeaders) +{ + cookedHeaders.clear(); + + QMap<QNetworkRequest::KnownHeaders, QList<QByteArray>> multipleHeadersMap; + for (int i = 0; i < newHeaders.size(); ++i) { + const auto name = newHeaders.nameAt(i); + const auto value = newHeaders.valueAt(i); + + const int parsedKeyAsInt = parseHeaderName(name); + if (parsedKeyAsInt == -1) + continue; + + const QNetworkRequest::KnownHeaders parsedKey + = static_cast<QNetworkRequest::KnownHeaders>(parsedKeyAsInt); + + auto &list = multipleHeadersMap[parsedKey]; + list.append(value.toByteArray()); + } + + for (auto i = multipleHeadersMap.cbegin(), end = multipleHeadersMap.cend(); i != end; ++i) + cookedHeaders.insert(i.key(), parseHeaderValue(i.key(), i.value())); +} + QT_END_NAMESPACE #include "moc_qnetworkrequest.cpp" diff --git a/src/network/access/qnetworkrequest.h b/src/network/access/qnetworkrequest.h index 875c787673..368eb99d95 100644 --- a/src/network/access/qnetworkrequest.h +++ b/src/network/access/qnetworkrequest.h @@ -35,7 +35,8 @@ public: IfModifiedSinceHeader, ETagHeader, IfMatchHeader, - IfNoneMatchHeader + IfNoneMatchHeader, + NumKnownHeaders }; Q_ENUM(KnownHeaders) @@ -69,6 +70,7 @@ public: ConnectionCacheExpiryTimeoutSecondsAttribute, Http2CleartextAllowedAttribute, UseCredentialsAttribute, + FullLocalServerNameAttribute, User = 1000, UserMax = 32767 diff --git a/src/network/access/qnetworkrequest_p.h b/src/network/access/qnetworkrequest_p.h index 88fb8cb246..7268e1a4aa 100644 --- a/src/network/access/qnetworkrequest_p.h +++ b/src/network/access/qnetworkrequest_p.h @@ -27,6 +27,8 @@ QT_BEGIN_NAMESPACE +class QNetworkCookie; + // this is the common part between QNetworkRequestPrivate, QNetworkReplyPrivate and QHttpPartPrivate class QNetworkHeadersPrivate { @@ -36,17 +38,20 @@ public: typedef QHash<QNetworkRequest::KnownHeaders, QVariant> CookedHeadersMap; typedef QHash<QNetworkRequest::Attribute, QVariant> AttributesMap; - RawHeadersList rawHeaders; + mutable struct { + RawHeadersList headersList; + bool isCached = false; + } rawHeaderCache; + QHttpHeaders httpHeaders; CookedHeadersMap cookedHeaders; AttributesMap attributes; QPointer<QObject> originatingObject; - RawHeadersList::ConstIterator findRawHeader(QAnyStringView key) const; - RawHeadersList allRawHeaders() const; + const RawHeadersList &allRawHeaders() const; QList<QByteArray> rawHeadersKeys() const; + QByteArray rawHeader(QAnyStringView headerName) const; void setRawHeader(const QByteArray &key, const QByteArray &value); - void setAllRawHeaders(const RawHeadersList &list); void setCookedHeader(QNetworkRequest::KnownHeaders header, const QVariant &value); QHttpHeaders headers() const; @@ -56,12 +61,26 @@ public: void clearHeaders(); - static QDateTime fromHttpDate(const QByteArray &value); + static QDateTime fromHttpDate(QByteArrayView value); static QByteArray toHttpDate(const QDateTime &dt); + static std::optional<qint64> toInt(QByteArrayView value); + + typedef QList<QNetworkCookie> NetworkCookieList; + static QByteArray fromCookieList(const NetworkCookieList &cookies); + static std::optional<NetworkCookieList> toSetCookieList(const QList<QByteArray> &values); + static std::optional<NetworkCookieList> toCookieList(const QList<QByteArray> &values); + + static RawHeadersList fromHttpToRaw(const QHttpHeaders &headers); + static QHttpHeaders fromRawToHttp(const RawHeadersList &raw); + private: - void setRawHeaderInternal(const QByteArray &key, const QByteArray &value); - void parseAndSetHeader(const QByteArray &key, const QByteArray &value); + void invalidateHeaderCache(); + + void setCookedFromHttp(const QHttpHeaders &newHeaders); + void parseAndSetHeader(QByteArrayView key, QByteArrayView value); + void parseAndSetHeader(QNetworkRequest::KnownHeaders key, QByteArrayView value); + }; Q_DECLARE_TYPEINFO(QNetworkHeadersPrivate::RawHeaderPair, Q_RELOCATABLE_TYPE); diff --git a/src/network/access/qnetworkrequestfactory.cpp b/src/network/access/qnetworkrequestfactory.cpp index 87738d9086..2a6938bb7f 100644 --- a/src/network/access/qnetworkrequestfactory.cpp +++ b/src/network/access/qnetworkrequestfactory.cpp @@ -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 @@ -615,17 +613,11 @@ QNetworkRequest QNetworkRequestFactoryPrivate::newRequest(const QUrl &url) const if (!sslConfig.isNull()) request.setSslConfiguration(sslConfig); #endif - // Set the header entries to the request. Combine values as there - // may be multiple values per name. Note: this would not necessarily - // produce right result for 'Set-Cookie' header if it has multiple values, - // but since it is a purely server-side (response) header, not relevant here. - const auto headerNames = headers.toMultiMap().uniqueKeys(); // ### fixme: port QNR to QHH - for (const auto &name : headerNames) - request.setRawHeader(name, headers.combinedValue(name)); - + auto h = headers; constexpr char Bearer[] = "Bearer "; if (!bearerToken.isEmpty()) - request.setRawHeader("Authorization"_ba, Bearer + bearerToken); + h.replaceOrAppend(QHttpHeaders::WellKnownHeader::Authorization, Bearer + bearerToken); + request.setHeaders(std::move(h)); request.setTransferTimeout(transferTimeout); request.setPriority(priority); 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 51af299f5c..2e6c1afb90 100644 --- a/src/network/access/qrestaccessmanager_p.h +++ b/src/network/access/qrestaccessmanager_p.h @@ -61,10 +61,12 @@ public: return warnNoAccessManager(); verifyThreadAffinity(context); QNetworkRequest req(request); - if (!request.header(QNetworkRequest::ContentTypeHeader).isValid()) { - req.setHeader(QNetworkRequest::ContentTypeHeader, - QLatin1StringView{"application/json"}); + auto h = req.headers(); + if (!h.contains(QHttpHeaders::WellKnownHeader::ContentType)) { + h.append(QHttpHeaders::WellKnownHeader::ContentType, + QLatin1StringView{"application/json"}); } + req.setHeaders(std::move(h)); QNetworkReply *reply = requestOperation(qnam, req, jsonDoc.toJson(QJsonDocument::Compact)); return createActiveRequest(reply, context, slot); } diff --git a/src/network/access/qrestreply.cpp b/src/network/access/qrestreply.cpp index 155e5877fd..1ce9100e66 100644 --- a/src/network/access/qrestreply.cpp +++ b/src/network/access/qrestreply.cpp @@ -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. @@ -335,7 +333,7 @@ QDebug operator<<(QDebug debug, const QRestReply &reply) << ", bytesAvailable = " << reply.networkReply()->bytesAvailable() << ", url " << reply.networkReply()->url() << ", operation = " << operationName(reply.networkReply()->operation()) - << ", reply headers = " << reply.networkReply()->rawHeaderPairs() + << ", reply headers = " << reply.networkReply()->headers() << ")"; return debug; } @@ -420,6 +418,54 @@ static constexpr bool is_tchar(char ch) noexcept } } +static constexpr auto parse_comment(QByteArrayView data) noexcept +{ + struct R { + QByteArrayView comment, tail; + constexpr explicit operator bool() const noexcept { return !comment.isEmpty(); } + }; + + const auto invalid = R{{}, data}; // preserves original `data` + + // comment = "(" *( ctext / quoted-pair / comment ) ")" + // ctext = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text + + if (!data.startsWith('(')) + return invalid; + + qsizetype i = 1; + qsizetype level = 1; + while (i < data.size()) { + switch (data[i++]) { + case '(': // nested comment + ++level; + break; + case ')': // end of comment + if (--level == 0) + return R{data.first(i), data.sliced(i)}; + break; + case '\\': // quoted-pair + if (i == data.size()) + return invalid; // premature end + ++i; // eat escaped character + break; + default: + ; // don't validate ctext - accept everything (Postel's Law) + } + } + + return invalid; // premature end / unbalanced nesting levels +} + +static constexpr void eat_CWS(QByteArrayView &data) noexcept +{ + eat_OWS(data); + while (const auto comment = parse_comment(data)) { + data = comment.tail; + eat_OWS(data); + } +} + static constexpr auto parse_token(QByteArrayView data) noexcept { struct R { @@ -452,13 +498,13 @@ static constexpr auto parse_parameter(QByteArrayView data, qxp::function_ref<voi return invalid; data = name.tail; - eat_OWS(data); // not in the grammar, but accepted under Postel's Law + eat_CWS(data); // not in the grammar, but accepted under Postel's Law if (!data.startsWith('=')) return invalid; data = data.sliced(1); - eat_OWS(data); // not in the grammar, but accepted under Postel's Law + eat_CWS(data); // not in the grammar, but accepted under Postel's Law if (Q_UNLIKELY(data.startsWith('"'))) { // value is a quoted-string @@ -488,27 +534,27 @@ static auto parse_content_type(QByteArrayView data) constexpr explicit operator bool() const noexcept { return !type.isEmpty(); } }; - eat_OWS(data); // not in the grammar, but accepted under Postel's Law + eat_CWS(data); // not in the grammar, but accepted under Postel's Law const auto type = parse_token(data); if (!type) return R{}; data = type.tail; - eat_OWS(data); // not in the grammar, but accepted under Postel's Law + eat_CWS(data); // not in the grammar, but accepted under Postel's Law if (!data.startsWith('/')) return R{}; data = data.sliced(1); - eat_OWS(data); // not in the grammar, but accepted under Postel's Law + eat_CWS(data); // not in the grammar, but accepted under Postel's Law const auto subtype = parse_token(data); if (!subtype) return R{}; data = subtype.tail; - eat_OWS(data); + eat_CWS(data); auto r = R{QLatin1StringView{type.token}, QLatin1StringView{subtype.token}, {}}; @@ -516,7 +562,7 @@ static auto parse_content_type(QByteArrayView data) data = data.sliced(1); // eat ';' - eat_OWS(data); + eat_CWS(data); const auto param = parse_parameter(data, [&](char ch) { r.charset.append(1, ch); }); if (param.name.compare("charset"_L1, Qt::CaseInsensitive) == 0) { @@ -530,7 +576,7 @@ static auto parse_content_type(QByteArrayView data) // otherwise, continue (accepting e.g. `;;`) data = param.tail; - eat_OWS(data); + eat_CWS(data); } return r; // no charset found @@ -548,7 +594,7 @@ QByteArray QRestReplyPrivate::contentCharset(const QNetworkReply* reply) // text/plain; charset ="utf-8" const QByteArray contentTypeValue = - reply->header(QNetworkRequest::KnownHeaders::ContentTypeHeader).toByteArray(); + reply->headers().value(QHttpHeaders::WellKnownHeader::ContentType).toByteArray(); const auto r = parse_content_type(contentTypeValue); if (r && !r.charset.empty()) 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); diff --git a/src/network/android/jar/build.gradle b/src/network/android/jar/build.gradle index 68a9381ad2..ea6d06c257 100644 --- a/src/network/android/jar/build.gradle +++ b/src/network/android/jar/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.0.2' + classpath 'com.android.tools.build:gradle:8.4.0' } } @@ -26,7 +26,7 @@ android { compileSdk 34 defaultConfig { - minSdkVersion 23 + minSdkVersion 28 } sourceSets { diff --git a/src/network/compat/removed_api.cpp b/src/network/compat/removed_api.cpp index 86951d9222..ceda117538 100644 --- a/src/network/compat/removed_api.cpp +++ b/src/network/compat/removed_api.cpp @@ -58,6 +58,9 @@ QList<QNetworkCookie> QNetworkCookie::parseCookies(const QByteArray &cookieStrin #if QT_NETWORK_REMOVED_SINCE(6, 8) +#if QT_CONFIG(dnslookup) +# include "qdnslookup.h" // inlined API +#endif #include "qnetworkrequest.h" // inlined API // #include "qotherheader.h" diff --git a/src/network/kernel/qdnslookup.cpp b/src/network/kernel/qdnslookup.cpp index c310c7e28e..d8ac95b3ec 100644 --- a/src/network/kernel/qdnslookup.cpp +++ b/src/network/kernel/qdnslookup.cpp @@ -8,15 +8,23 @@ #include <qapplicationstatic.h> #include <qcoreapplication.h> #include <qdatetime.h> +#include <qendian.h> #include <qloggingcategory.h> #include <qrandom.h> +#include <qspan.h> #include <qurl.h> +#if QT_CONFIG(ssl) +# include <qsslsocket.h> +#endif + #include <algorithm> QT_BEGIN_NAMESPACE -static Q_LOGGING_CATEGORY(lcDnsLookup, "qt.network.dnslookup", QtCriticalMsg) +using namespace Qt::StringLiterals; + +Q_STATIC_LOGGING_CATEGORY(lcDnsLookup, "qt.network.dnslookup", QtCriticalMsg) namespace { struct QDnsLookupThreadPool : QThreadPool @@ -159,6 +167,42 @@ static void qt_qdnsservicerecord_sort(QList<QDnsServiceRecord> &records) \note If you simply want to find the IP address(es) associated with a host name, or the host name associated with an IP address you should use QHostInfo instead. + + \section1 DNS-over-TLS and Authentic Data + + QDnsLookup supports DNS-over-TLS (DoT, as specified by \l{RFC 7858}) on + some platforms. That currently includes all Unix platforms where regular + queries are supported, if \l QSslSocket support is present in Qt. To query + if support is present at runtime, use isProtocolSupported(). + + When using DNS-over-TLS, QDnsLookup only implements the "Opportunistic + Privacy Profile" method of authentication, as described in \l{RFC 7858} + section 4.1. In this mode, QDnsLookup (through \l QSslSocket) only + validates that the server presents a certificate that is valid for the + server being connected to. Clients may use setSslConfiguration() to impose + additional restrictions and sslConfiguration() to obtain information after + the query is complete. + + QDnsLookup will request DNS servers queried over TLS to perform + authentication on the data they return. If they confirm the data is valid, + the \l authenticData property will be set to true. QDnsLookup does not + verify the integrity of the data by itself, so applications should only + trust this property on servers they have confirmed through other means to + be trustworthy. + + \section2 Authentic Data without TLS + + QDnsLookup request Authentic Data for any server set with setNameserver(), + even if TLS encryption is not required. This is useful when querying a + caching nameserver on the same host as the application or on a trusted + network. Though similar to the TLS case, the application is responsible for + determining if the server it chose to use is trustworthy, and if the + unencrypted connection cannot be tampered with. + + QDnsLookup obeys the system configuration to request Authentic Data on the + default nameserver (that is, if setNameserver() is not called). This is + currently only supported on Linux systems using glibc 2.31 or later. On any + other systems, QDnsLookup will ignore the AD bit in the query header. */ /*! @@ -213,10 +257,72 @@ static void qt_qdnsservicerecord_sort(QList<QDnsServiceRecord> &records) \value SRV service records. + \value[since 6.8] TLSA TLS association records. + \value TXT text records. */ /*! + \enum QDnsLookup::Protocol + + Indicates the type of DNS server that is being queried. + + \value Standard + Regular, unencrypted DNS, using UDP and falling back to TCP as necessary + (default port: 53) + + \value DnsOverTls + Encrypted DNS over TLS (DoT, as specified by \l{RFC 7858}), over TCP + (default port: 853) + + \sa isProtocolSupported(), nameserverProtocol, setNameserver() +*/ + +/*! + \since 6.8 + + Returns true if DNS queries using \a protocol are supported with QDnsLookup. + + \sa nameserverProtocol +*/ +bool QDnsLookup::isProtocolSupported(Protocol protocol) +{ +#if QT_CONFIG(libresolv) || defined(Q_OS_WIN) + switch (protocol) { + case QDnsLookup::Standard: + return true; + case QDnsLookup::DnsOverTls: +# if QT_CONFIG(ssl) + if (QSslSocket::supportsSsl()) + return true; +# endif + return false; + } +#else + Q_UNUSED(protocol) +#endif + return false; +} + +/*! + \since 6.8 + + Returns the standard (default) port number for the protocol \a protocol. + + \sa isProtocolSupported() +*/ +quint16 QDnsLookup::defaultPortForProtocol(Protocol protocol) noexcept +{ + switch (protocol) { + case QDnsLookup::Standard: + return DnsPort; + case QDnsLookup::DnsOverTls: + return DnsOverTlsPort; + } + return 0; // will probably fail somewhere +} + +/*! \fn void QDnsLookup::finished() This signal is emitted when the reply has finished processing. @@ -270,7 +376,7 @@ QDnsLookup::QDnsLookup(Type type, const QString &name, QObject *parent) */ QDnsLookup::QDnsLookup(Type type, const QString &name, const QHostAddress &nameserver, QObject *parent) - : QDnsLookup(type, name, nameserver, DnsPort, parent) + : QDnsLookup(type, name, nameserver, 0, parent) { } @@ -285,8 +391,9 @@ QDnsLookup::QDnsLookup(Type type, const QString &name, const QHostAddress &names //! [nameserver-port] \note Setting the port number to any value other than the default (53) can cause the name resolution to fail, depending on the operating system - limitations and firewalls. Notably, the Windows API used by QDnsLookup is - unable to handle alternate port numbers. + limitations and firewalls, if the nameserverProtocol() to be used + QDnsLookup::Standard. Notably, the Windows API used by QDnsLookup is unable + to handle alternate port numbers. //! [nameserver-port] */ QDnsLookup::QDnsLookup(Type type, const QString &name, const QHostAddress &nameserver, quint16 port, QObject *parent) @@ -300,6 +407,30 @@ QDnsLookup::QDnsLookup(Type type, const QString &name, const QHostAddress &names } /*! + \since 6.8 + + Constructs a QDnsLookup object to issue a query for \a name of record type + \a type, using the DNS server \a nameserver running on port \a port, and + sets \a parent as the parent object. + + The query will be sent using \a protocol, if supported. Use + isProtocolSupported() to check if it is supported. + + \include qdnslookup.cpp nameserver-port +*/ +QDnsLookup::QDnsLookup(Type type, const QString &name, Protocol protocol, + const QHostAddress &nameserver, quint16 port, QObject *parent) + : QObject(*new QDnsLookupPrivate, parent) +{ + Q_D(QDnsLookup); + d->name = name; + d->type = type; + d->nameserver = nameserver; + d->port = port; + d->protocol = protocol; +} + +/*! Destroys the QDnsLookup object. It is safe to delete a QDnsLookup object even if it is not finished, you @@ -311,6 +442,28 @@ QDnsLookup::~QDnsLookup() } /*! + \since 6.8 + \property QDnsLookup::authenticData + \brief whether the reply was authenticated by the resolver. + + QDnsLookup does not perform the authentication itself. Instead, it trusts + the name server that was queried to perform the authentication and report + it. The application is responsible for determining if any servers it + configured with setNameserver() are trustworthy; if no server was set, + QDnsLookup obeys system configuration on whether responses should be + trusted. + + This property may be set even if error() indicates a resolver error + occurred. + + \sa setNameserver(), nameserverProtocol() +*/ +bool QDnsLookup::isAuthenticData() const +{ + return d_func()->reply.authenticData; +} + +/*! \property QDnsLookup::error \brief the type of error that occurred if the DNS lookup failed, or NoError. */ @@ -416,6 +569,10 @@ QBindable<QHostAddress> QDnsLookup::bindableNameserver() \property QDnsLookup::nameserverPort \since 6.6 \brief the port number of nameserver to use for DNS lookup. + + The value of 0 indicates that QDnsLookup should use the default port for + the nameserverProtocol(). + \include qdnslookup.cpp nameserver-port */ @@ -437,18 +594,44 @@ QBindable<quint16> QDnsLookup::bindableNameserverPort() } /*! + \property QDnsLookup::nameserverProtocol + \since 6.8 + \brief the protocol to use when sending the DNS query + + \sa isProtocolSupported() +*/ +QDnsLookup::Protocol QDnsLookup::nameserverProtocol() const +{ + return d_func()->protocol; +} + +void QDnsLookup::setNameserverProtocol(Protocol protocol) +{ + d_func()->protocol = protocol; +} + +QBindable<QDnsLookup::Protocol> QDnsLookup::bindableNameserverProtocol() +{ + return &d_func()->protocol; +} + +/*! + \fn void QDnsLookup::setNameserver(const QHostAddress &nameserver, quint16 port) \since 6.6 + Sets the nameserver to \a nameserver and the port to \a port. \include qdnslookup.cpp nameserver-port \sa QDnsLookup::nameserver, QDnsLookup::nameserverPort */ -void QDnsLookup::setNameserver(const QHostAddress &nameserver, quint16 port) + +void QDnsLookup::setNameserver(Protocol protocol, const QHostAddress &nameserver, quint16 port) { Qt::beginPropertyUpdateGroup(); setNameserver(nameserver); setNameserverPort(port); + setNameserverProtocol(protocol); Qt::endPropertyUpdateGroup(); } @@ -524,6 +707,46 @@ QList<QDnsTextRecord> QDnsLookup::textRecords() const } /*! + \since 6.8 + Returns the list of TLS association records associated with this lookup. + + According to the standards relating to DNS-based Authentication of Named + Entities (DANE), this field should be ignored and must not be used for + verifying the authentity of a given server if the authenticity of the DNS + reply cannot itself be confirmed. See isAuthenticData() for more + information. + */ +QList<QDnsTlsAssociationRecord> QDnsLookup::tlsAssociationRecords() const +{ + return d_func()->reply.tlsAssociationRecords; +} + +#if QT_CONFIG(ssl) +/*! + \since 6.8 + Sets the \a sslConfiguration to use for outgoing DNS-over-TLS connections. + + \sa sslConfiguration(), QSslSocket::setSslConfiguration() +*/ +void QDnsLookup::setSslConfiguration(const QSslConfiguration &sslConfiguration) +{ + Q_D(QDnsLookup); + d->sslConfiguration.emplace(sslConfiguration); +} + +/*! + Returns the current SSL configuration. + + \sa setSslConfiguration() +*/ +QSslConfiguration QDnsLookup::sslConfiguration() const +{ + const Q_D(QDnsLookup); + return d->sslConfiguration.value_or(QSslConfiguration::defaultConfiguration()); +} +#endif + +/*! Aborts the DNS lookup operation. If the lookup is already finished, does nothing. @@ -565,6 +788,9 @@ void QDnsLookup::lookup() #ifdef QDNSLOOKUP_DEBUG qDebug("DNS reply for %s: %i (%s)", qPrintable(d->name), reply.error, qPrintable(reply.errorString)); #endif +#if QT_CONFIG(ssl) + d->sslConfiguration = std::move(reply.sslConfiguration); +#endif d->reply = reply; d->runnable = nullptr; d->isFinished = true; @@ -1052,6 +1278,223 @@ QDnsTextRecord &QDnsTextRecord::operator=(const QDnsTextRecord &other) very fast and never fails. */ +/*! + \class QDnsTlsAssociationRecord + \since 6.8 + \brief The QDnsTlsAssociationRecord class stores information about a DNS TLSA record. + + \inmodule QtNetwork + \ingroup network + \ingroup shared + + When performing a text lookup, zero or more records will be returned. Each + record is represented by a QDnsTlsAssociationRecord instance. + + The meaning of the fields is defined in \l{RFC 6698}. + + \sa QDnsLookup +*/ + +QT_DEFINE_QSDP_SPECIALIZATION_DTOR(QDnsTlsAssociationRecordPrivate) + +/*! + \enum QDnsTlsAssociationRecord::CertificateUsage + + This enumeration contains valid values for the certificate usage field of + TLS Association queries. The following list is up-to-date with \l{RFC 6698} + section 2.1.1 and RFC 7218 section 2.1. Please refer to those documents for + authoritative instructions on interpreting this enumeration. + + \value CertificateAuthorityConstrait + Indicates the record includes an association to a specific Certificate + Authority that must be found in the TLS server's certificate chain and + must pass PKIX validation. + + \value ServiceCertificateConstraint + Indicates the record includes an association to a certificate that must + match the end entity certificate provided by the TLS server and must + pass PKIX validation. + + \value TrustAnchorAssertion + Indicates the record includes an association to a certificate that MUST + be used as the ultimate trust anchor to validate the TLS server's + certificate and must pass PKIX validation. + + \value DomainIssuedCertificate + Indicates the record includes an association to a certificate that must + match the end entity certificate provided by the TLS server. PKIX + validation is not tested. + + \value PrivateUse + No standard meaning applied. + + \value PKIX_TA + Alias; mnemonic for Public Key Infrastructure Trust Anchor + + \value PKIX_EE + Alias; mnemonic for Public Key Infrastructure End Entity + + \value DANE_TA + Alias; mnemonic for DNS-based Authentication of Named Entities Trust Anchor + + \value DANE_EE + Alias; mnemonic for DNS-based Authentication of Named Entities End Entity + + \value PrivCert + Alias + + Other values are currently reserved, but may be unreserved by future + standards. This enumeration can be used for those values even if no + enumerator is provided. + + \sa certificateUsage() +*/ + +/*! + \enum QDnsTlsAssociationRecord::Selector + + This enumeration contains valid values for the selector field of TLS + Association queries. The following list is up-to-date with \l{RFC 6698} + section 2.1.2 and RFC 7218 section 2.2. Please refer to those documents for + authoritative instructions on interpreting this enumeration. + + \value FullCertificate + Indicates this record refers to the full certificate in its binary + structure form. + + \value SubjectPublicKeyInfo + Indicates the record refers to the certificate's subject and public + key information, in DER-encoded binary structure form. + + \value PrivateUse + No standard meaning applied. + + \value Cert + Alias + + \value SPKI + Alias + + \value PrivSel + Alias + + Other values are currently reserved, but may be unreserved by future + standards. This enumeration can be used for those values even if no + enumerator is provided. + + \sa selector() +*/ + +/*! + \enum QDnsTlsAssociationRecord::MatchingType + + This enumeration contains valid values for the matching type field of TLS + Association queries. The following list is up-to-date with \l{RFC 6698} + section 2.1.3 and RFC 7218 section 2.3. Please refer to those documents for + authoritative instructions on interpreting this enumeration. + + \value Exact + Indicates this the certificate or SPKI data is stored verbatim in this + record. + + \value Sha256 + Indicates this a SHA-256 checksum of the the certificate or SPKI data + present in this record. + + \value Sha512 + Indicates this a SHA-512 checksum of the the certificate or SPKI data + present in this record. + + \value PrivateUse + No standard meaning applied. + + \value PrivMatch + Alias + + Other values are currently reserved, but may be unreserved by future + standards. This enumeration can be used for those values even if no + enumerator is provided. + + \sa matchingType() +*/ + +/*! + Constructs an empty TLS Association record. + */ +QDnsTlsAssociationRecord::QDnsTlsAssociationRecord() + : d(new QDnsTlsAssociationRecordPrivate) +{ +} + +/*! + Constructs a copy of \a other. + */ +QDnsTlsAssociationRecord::QDnsTlsAssociationRecord(const QDnsTlsAssociationRecord &other) = default; + +/*! + Moves the content of \a other into this object. + */ +QDnsTlsAssociationRecord & +QDnsTlsAssociationRecord::operator=(const QDnsTlsAssociationRecord &other) = default; + +/*! + Destroys this TLS Association record object. + */ +QDnsTlsAssociationRecord::~QDnsTlsAssociationRecord() = default; + +/*! + Returns the name of this record. +*/ +QString QDnsTlsAssociationRecord::name() const +{ + return d->name; +} + +/*! + Returns the duration in seconds for which this record is valid. +*/ +quint32 QDnsTlsAssociationRecord::timeToLive() const +{ + return d->timeToLive; +} + +/*! + Returns the certificate usage field for this record. + */ +QDnsTlsAssociationRecord::CertificateUsage QDnsTlsAssociationRecord::usage() const +{ + return d->usage; +} + +/*! + Returns the selector field for this record. + */ +QDnsTlsAssociationRecord::Selector QDnsTlsAssociationRecord::selector() const +{ + return d->selector; +} + +/*! + Returns the match type field for this record. + */ +QDnsTlsAssociationRecord::MatchingType QDnsTlsAssociationRecord::matchType() const +{ + return d->matchType; +} + +/*! + Returns the binary data field for this record. The interpretation of this + binary data depends on the three numeric fields provided by + certificateUsage(), selector(), and matchType(). + + Do note this is a binary field, even for the checksums, similar to what + QCyrptographicHash::result() returns. + */ +QByteArray QDnsTlsAssociationRecord::value() const +{ + return d->value; +} + static QDnsLookupRunnable::EncodedLabel encodeLabel(const QString &label) { QDnsLookupRunnable::EncodedLabel::value_type rootDomain = u'.'; @@ -1070,8 +1513,14 @@ inline QDnsLookupRunnable::QDnsLookupRunnable(const QDnsLookupPrivate *d) : requestName(encodeLabel(d->name)), nameserver(d->nameserver), requestType(d->type), - port(d->port) + port(d->port), + protocol(d->protocol) { + if (port == 0) + port = QDnsLookup::defaultPortForProtocol(protocol); +#if QT_CONFIG(ssl) + sslConfiguration = d->sslConfiguration; +#endif } void QDnsLookupRunnable::run() @@ -1120,12 +1569,103 @@ inline QDebug operator<<(QDebug &d, QDnsLookupRunnable *r) if (r->requestName.size() > MaxDomainNameLength) d << "... (truncated)"; d << " type " << r->requestType; - if (!r->nameserver.isNull()) + if (!r->nameserver.isNull()) { d << " to nameserver " << qUtf16Printable(r->nameserver.toString()) - << " port " << (r->port ? r->port : DnsPort); + << " port " << (r->port ? r->port : QDnsLookup::defaultPortForProtocol(r->protocol)); + switch (r->protocol) { + case QDnsLookup::Standard: + break; + case QDnsLookup::DnsOverTls: + d << " (TLS)"; + } + } return d; } +#if QT_CONFIG(ssl) +static constexpr std::chrono::milliseconds DnsOverTlsConnectTimeout(15'000); +static constexpr std::chrono::milliseconds DnsOverTlsTimeout(120'000); +static constexpr quint8 DnsAuthenticDataBit = 0x20; + +static int makeReplyErrorFromSocket(QDnsLookupReply *reply, const QAbstractSocket *socket) +{ + QDnsLookup::Error error = [&] { + switch (socket->error()) { + case QAbstractSocket::SocketTimeoutError: + case QAbstractSocket::ProxyConnectionTimeoutError: + return QDnsLookup::TimeoutError; + default: + return QDnsLookup::ResolverError; + } + }(); + reply->setError(error, socket->errorString()); + return false; +} + +bool QDnsLookupRunnable::sendDnsOverTls(QDnsLookupReply *reply, QSpan<unsigned char> query, + ReplyBuffer &response) +{ + QSslSocket socket; + socket.setSslConfiguration(sslConfiguration.value_or(QSslConfiguration::defaultConfiguration())); + +# if QT_CONFIG(networkproxy) + socket.setProtocolTag("domain-s"_L1); +# endif + + // Request the name server attempt to authenticate the reply. + query[3] |= DnsAuthenticDataBit; + + do { + quint16 size = qToBigEndian<quint16>(query.size()); + QDeadlineTimer timeout(DnsOverTlsTimeout); + + socket.connectToHostEncrypted(nameserver.toString(), port); + socket.write(reinterpret_cast<const char *>(&size), sizeof(size)); + socket.write(reinterpret_cast<const char *>(query.data()), query.size()); + if (!socket.waitForEncrypted(DnsOverTlsConnectTimeout.count())) + break; + + reply->sslConfiguration = socket.sslConfiguration(); + + // accumulate reply + auto waitForBytes = [&](void *buffer, int count) { + int remaining = timeout.remainingTime(); + while (remaining >= 0 && socket.bytesAvailable() < count) { + if (!socket.waitForReadyRead(remaining)) + return false; + } + return socket.read(static_cast<char *>(buffer), count) == count; + }; + if (!waitForBytes(&size, sizeof(size))) + break; + + // note: strictly speaking, we're allocating memory based on untrusted data + // but in practice, due to limited range of the data type (16 bits), + // the maximum allocation is small. + size = qFromBigEndian(size); + response.resize(size); + if (waitForBytes(response.data(), size)) { + // check if the AD bit is set; we'll trust it over TLS requests + if (size >= 4) + reply->authenticData = response[3] & DnsAuthenticDataBit; + return true; + } + } while (false); + + // handle errors + return makeReplyErrorFromSocket(reply, &socket); +} +#else +bool QDnsLookupRunnable::sendDnsOverTls(QDnsLookupReply *reply, QSpan<unsigned char> query, + ReplyBuffer &response) +{ + Q_UNUSED(query) + Q_UNUSED(response) + reply->setError(QDnsLookup::ResolverError, QDnsLookup::tr("SSL/TLS support not present")); + return false; +} +#endif + QT_END_NAMESPACE #include "moc_qdnslookup.cpp" diff --git a/src/network/kernel/qdnslookup.h b/src/network/kernel/qdnslookup.h index ae89a0a11f..8d21e99c84 100644 --- a/src/network/kernel/qdnslookup.h +++ b/src/network/kernel/qdnslookup.h @@ -22,6 +22,10 @@ class QDnsHostAddressRecordPrivate; class QDnsMailExchangeRecordPrivate; class QDnsServiceRecordPrivate; class QDnsTextRecordPrivate; +class QDnsTlsAssociationRecordPrivate; +class QSslConfiguration; + +QT_DECLARE_QSDP_SPECIALIZATION_DTOR(QDnsTlsAssociationRecordPrivate) class Q_NETWORK_EXPORT QDnsDomainNameRecord { @@ -137,10 +141,83 @@ private: Q_DECLARE_SHARED(QDnsTextRecord) +class Q_NETWORK_EXPORT QDnsTlsAssociationRecord +{ + Q_GADGET +public: + enum class CertificateUsage : quint8 { + // https://www.iana.org/assignments/dane-parameters/dane-parameters.xhtml#certificate-usages + // RFC 6698 + CertificateAuthorityConstrait = 0, + ServiceCertificateConstraint = 1, + TrustAnchorAssertion = 2, + DomainIssuedCertificate = 3, + PrivateUse = 255, + + // Aliases by RFC 7218 + PKIX_TA = 0, + PKIX_EE = 1, + DANE_TA = 2, + DANE_EE = 3, + PrivCert = 255, + }; + Q_ENUM(CertificateUsage) + + enum class Selector : quint8 { + // https://www.iana.org/assignments/dane-parameters/dane-parameters.xhtml#selectors + // RFC 6698 + FullCertificate = 0, + SubjectPublicKeyInfo = 1, + PrivateUse = 255, + + // Aliases by RFC 7218 + Cert = FullCertificate, + SPKI = SubjectPublicKeyInfo, + PrivSel = PrivateUse, + }; + Q_ENUM(Selector) + + enum class MatchingType : quint8 { + // https://www.iana.org/assignments/dane-parameters/dane-parameters.xhtml#matching-types + // RFC 6698 + Exact = 0, + Sha256 = 1, + Sha512 = 2, + PrivateUse = 255, + PrivMatch = PrivateUse, + }; + Q_ENUM(MatchingType) + + QDnsTlsAssociationRecord(); + QDnsTlsAssociationRecord(const QDnsTlsAssociationRecord &other); + QDnsTlsAssociationRecord(QDnsTlsAssociationRecord &&other) + : d(std::move(other.d)) + {} + QDnsTlsAssociationRecord &operator=(QDnsTlsAssociationRecord &&other) noexcept { swap(other); return *this; } + QDnsTlsAssociationRecord &operator=(const QDnsTlsAssociationRecord &other); + ~QDnsTlsAssociationRecord(); + + void swap(QDnsTlsAssociationRecord &other) noexcept { d.swap(other.d); } + + QString name() const; + quint32 timeToLive() const; + CertificateUsage usage() const; + Selector selector() const; + MatchingType matchType() const; + QByteArray value() const; + +private: + QSharedDataPointer<QDnsTlsAssociationRecordPrivate> d; + friend class QDnsLookupRunnable; +}; + +Q_DECLARE_SHARED(QDnsTlsAssociationRecord) + class Q_NETWORK_EXPORT QDnsLookup : public QObject { Q_OBJECT Q_PROPERTY(Error error READ error NOTIFY finished) + Q_PROPERTY(bool authenticData READ isAuthenticData NOTIFY finished) Q_PROPERTY(QString errorString READ errorString NOTIFY finished) Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged BINDABLE bindableName) Q_PROPERTY(Type type READ type WRITE setType NOTIFY typeChanged BINDABLE bindableType) @@ -148,6 +225,8 @@ class Q_NETWORK_EXPORT QDnsLookup : public QObject BINDABLE bindableNameserver) Q_PROPERTY(quint16 nameserverPort READ nameserverPort WRITE setNameserverPort NOTIFY nameserverPortChanged BINDABLE bindableNameserverPort) + Q_PROPERTY(Protocol nameserverProtocol READ nameserverProtocol WRITE setNameserverProtocol + NOTIFY nameserverProtocolChanged BINDABLE bindableNameserverProtocol) public: enum Error @@ -174,17 +253,27 @@ public: NS = 2, PTR = 12, SRV = 33, + TLSA = 52, TXT = 16 }; Q_ENUM(Type) + enum Protocol : quint8 { + Standard = 0, + DnsOverTls, + }; + Q_ENUM(Protocol) + explicit QDnsLookup(QObject *parent = nullptr); QDnsLookup(Type type, const QString &name, QObject *parent = nullptr); QDnsLookup(Type type, const QString &name, const QHostAddress &nameserver, QObject *parent = nullptr); QDnsLookup(Type type, const QString &name, const QHostAddress &nameserver, quint16 port, QObject *parent = nullptr); + QDnsLookup(Type type, const QString &name, Protocol protocol, const QHostAddress &nameserver, + quint16 port = 0, QObject *parent = nullptr); ~QDnsLookup(); + bool isAuthenticData() const; Error error() const; QString errorString() const; bool isFinished() const; @@ -203,6 +292,11 @@ public: quint16 nameserverPort() const; void setNameserverPort(quint16 port); QBindable<quint16> bindableNameserverPort(); + Protocol nameserverProtocol() const; + void setNameserverProtocol(Protocol protocol); + QBindable<Protocol> bindableNameserverProtocol(); + void setNameserver(Protocol protocol, const QHostAddress &nameserver, quint16 port = 0); + QT_NETWORK_INLINE_SINCE(6, 8) void setNameserver(const QHostAddress &nameserver, quint16 port); QList<QDnsDomainNameRecord> canonicalNameRecords() const; @@ -212,7 +306,15 @@ public: QList<QDnsDomainNameRecord> pointerRecords() const; QList<QDnsServiceRecord> serviceRecords() const; QList<QDnsTextRecord> textRecords() const; + QList<QDnsTlsAssociationRecord> tlsAssociationRecords() const; + +#if QT_CONFIG(ssl) + void setSslConfiguration(const QSslConfiguration &sslConfiguration); + QSslConfiguration sslConfiguration() const; +#endif + static bool isProtocolSupported(Protocol protocol); + static quint16 defaultPortForProtocol(Protocol protocol) noexcept Q_DECL_CONST_FUNCTION; public Q_SLOTS: void abort(); @@ -224,11 +326,19 @@ Q_SIGNALS: void typeChanged(Type type); void nameserverChanged(const QHostAddress &nameserver); void nameserverPortChanged(quint16 port); + void nameserverProtocolChanged(Protocol protocol); private: Q_DECLARE_PRIVATE(QDnsLookup) }; +#if QT_NETWORK_INLINE_IMPL_SINCE(6, 8) +void QDnsLookup::setNameserver(const QHostAddress &nameserver, quint16 port) +{ + setNameserver(Standard, nameserver, port); +} +#endif + QT_END_NAMESPACE #endif // QDNSLOOKUP_H diff --git a/src/network/kernel/qdnslookup_p.h b/src/network/kernel/qdnslookup_p.h index da4721411b..1f32b4ee4f 100644 --- a/src/network/kernel/qdnslookup_p.h +++ b/src/network/kernel/qdnslookup_p.h @@ -27,6 +27,10 @@ #include "private/qobject_p.h" #include "private/qurl_p.h" +#if QT_CONFIG(ssl) +# include "qsslconfiguration.h" +#endif + QT_REQUIRE_CONFIG(dnslookup); QT_BEGIN_NAMESPACE @@ -35,6 +39,7 @@ QT_BEGIN_NAMESPACE constexpr qsizetype MaxDomainNameLength = 255; constexpr quint16 DnsPort = 53; +constexpr quint16 DnsOverTlsPort = 853; class QDnsLookupRunnable; QDebug operator<<(QDebug &, QDnsLookupRunnable *); @@ -43,6 +48,7 @@ class QDnsLookupReply { public: QDnsLookup::Error error = QDnsLookup::NoError; + bool authenticData = false; QString errorString; QList<QDnsDomainNameRecord> canonicalNameRecords; @@ -51,8 +57,13 @@ public: QList<QDnsDomainNameRecord> nameServerRecords; QList<QDnsDomainNameRecord> pointerRecords; QList<QDnsServiceRecord> serviceRecords; + QList<QDnsTlsAssociationRecord> tlsAssociationRecords; QList<QDnsTextRecord> textRecords; +#if QT_CONFIG(ssl) + std::optional<QSslConfiguration> sslConfiguration; +#endif + // helper methods void setError(QDnsLookup::Error err, QString &&msg) { @@ -120,6 +131,7 @@ private: && nameServerRecords.isEmpty() && pointerRecords.isEmpty() && serviceRecords.isEmpty() + && tlsAssociationRecords.isEmpty() && textRecords.isEmpty(); } }; @@ -129,7 +141,8 @@ class QDnsLookupPrivate : public QObjectPrivate public: QDnsLookupPrivate() : type(QDnsLookup::A) - , port(DnsPort) + , port(0) + , protocol(QDnsLookup::Standard) { } void nameChanged() @@ -162,11 +175,22 @@ public: Q_OBJECT_BINDABLE_PROPERTY(QDnsLookupPrivate, quint16, port, &QDnsLookupPrivate::nameserverPortChanged); + void nameserverProtocolChanged() + { + emit q_func()->nameserverProtocolChanged(protocol); + } + + Q_OBJECT_BINDABLE_PROPERTY(QDnsLookupPrivate, QDnsLookup::Protocol, + protocol, &QDnsLookupPrivate::nameserverProtocolChanged); QDnsLookupReply reply; QDnsLookupRunnable *runnable = nullptr; bool isFinished = false; +#if QT_CONFIG(ssl) + std::optional<QSslConfiguration> sslConfiguration; +#endif + Q_DECLARE_PUBLIC(QDnsLookup) }; @@ -180,9 +204,13 @@ public: #else using EncodedLabel = QByteArray; #endif + // minimum IPv6 MTU (1280) minus the IPv6 (40) and UDP headers (8) + static constexpr qsizetype ReplyBufferSize = 1280 - 40 - 8; + using ReplyBuffer = QVarLengthArray<unsigned char, ReplyBufferSize>; QDnsLookupRunnable(const QDnsLookupPrivate *d); void run() override; + bool sendDnsOverTls(QDnsLookupReply *reply, QSpan<unsigned char> query, ReplyBuffer &response); signals: void finished(const QDnsLookupReply &reply); @@ -198,6 +226,11 @@ private: QHostAddress nameserver; QDnsLookup::Type requestType; quint16 port; + QDnsLookup::Protocol protocol; + +#if QT_CONFIG(ssl) + std::optional<QSslConfiguration> sslConfiguration; +#endif friend QDebug operator<<(QDebug &, QDnsLookupRunnable *); }; @@ -265,6 +298,15 @@ public: QList<QByteArray> values; }; +class QDnsTlsAssociationRecordPrivate : public QDnsRecordPrivate +{ +public: + QDnsTlsAssociationRecord::CertificateUsage usage; + QDnsTlsAssociationRecord::Selector selector; + QDnsTlsAssociationRecord::MatchingType matchType; + QByteArray value; +}; + QT_END_NAMESPACE #endif // QDNSLOOKUP_P_H diff --git a/src/network/kernel/qdnslookup_unix.cpp b/src/network/kernel/qdnslookup_unix.cpp index 5696a3ca70..9de073b781 100644 --- a/src/network/kernel/qdnslookup_unix.cpp +++ b/src/network/kernel/qdnslookup_unix.cpp @@ -6,6 +6,7 @@ #include <qendian.h> #include <qscopedpointer.h> +#include <qspan.h> #include <qurl.h> #include <qvarlengtharray.h> #include <private/qnativesocketengine_p.h> // for setSockAddr @@ -32,15 +33,13 @@ QT_REQUIRE_CONFIG(libresolv); QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; - -// minimum IPv6 MTU (1280) minus the IPv6 (40) and UDP headers (8) -static constexpr qsizetype ReplyBufferSize = 1280 - 40 - 8; +using ReplyBuffer = QDnsLookupRunnable::ReplyBuffer; // https://www.rfc-editor.org/rfc/rfc6891 static constexpr unsigned char Edns0Record[] = { 0x00, // root label T_OPT >> 8, T_OPT & 0xff, // type OPT - ReplyBufferSize >> 8, ReplyBufferSize & 0xff, // payload size + ReplyBuffer::PreallocatedSize >> 8, ReplyBuffer::PreallocatedSize & 0xff, // payload size NOERROR, // extended rcode 0, // version 0x00, 0x00, // flags @@ -68,11 +67,9 @@ using Cache = QList<QDnsCachedName>; // QHash or QMap are overkill // https://docs.oracle.com/cd/E86824_01/html/E54774/res-setservers-3resolv.html static bool applyNameServer(res_state state, const QHostAddress &nameserver, quint16 port) { - if (!nameserver.isNull()) { - union res_sockaddr_union u; - setSockaddr(reinterpret_cast<sockaddr *>(&u.sin), nameserver, port); - res_setservers(state, &u, 1); - } + union res_sockaddr_union u; + setSockaddr(reinterpret_cast<sockaddr *>(&u.sin), nameserver, port); + res_setservers(state, &u, 1); return true; } #else @@ -123,9 +120,6 @@ template <typename State> bool setIpv6NameServer(State *, const void *, quint16) static bool applyNameServer(res_state state, const QHostAddress &nameserver, quint16 port) { - if (nameserver.isNull()) - return true; - state->nscount = 1; state->nsaddr_list[0].sin_family = AF_UNSPEC; if (nameserver.protocol() == QAbstractSocket::IPv6Protocol) @@ -153,36 +147,30 @@ prepareQueryBuffer(res_state state, QueryBuffer &buffer, const char *label, ns_r return queryLength + sizeof(Edns0Record); } -void QDnsLookupRunnable::query(QDnsLookupReply *reply) +static int sendStandardDns(QDnsLookupReply *reply, res_state state, QSpan<unsigned char> qbuffer, + ReplyBuffer &buffer, const QHostAddress &nameserver, quint16 port) { - // Initialize state. - std::remove_pointer_t<res_state> state = {}; - if (res_ninit(&state) < 0) { - int error = errno; - qErrnoWarning(error, "QDnsLookup: Resolver initialization failed"); - return reply->makeResolverSystemError(error); - } - auto guard = qScopeGuard([&] { res_nclose(&state); }); + // Check if a nameserver was set. If so, use it. + if (!nameserver.isNull()) { + if (!applyNameServer(state, nameserver, port)) { + reply->setError(QDnsLookup::ResolverError, + QDnsLookup::tr("IPv6 nameservers are currently not supported on this OS")); + return -1; + } - //Check if a nameserver was set. If so, use it - if (!applyNameServer(&state, nameserver, port)) - return reply->setError(QDnsLookup::ResolverError, - QDnsLookup::tr("IPv6 nameservers are currently not supported on this OS")); -#ifdef QDNSLOOKUP_DEBUG - state.options |= RES_DEBUG; -#endif + // Request the name server attempt to authenticate the reply. + reinterpret_cast<HEADER *>(buffer.data())->ad = true; - // Prepare the DNS query. - QueryBuffer qbuffer; - int queryLength = prepareQueryBuffer(&state, qbuffer, requestName.constData(), ns_rcode(requestType)); - if (Q_UNLIKELY(queryLength < 0)) - return reply->makeResolverSystemError(); +#ifdef RES_TRUSTAD + // Need to set this option even though we set the AD bit, otherwise + // glibc turns it off. + state->options |= RES_TRUSTAD; +#endif + } - // Perform DNS query. - QVarLengthArray<unsigned char, ReplyBufferSize> buffer(ReplyBufferSize); auto attemptToSend = [&]() { std::memset(buffer.data(), 0, HFIXEDSZ); // the header is enough - int responseLength = res_nsend(&state, qbuffer.data(), queryLength, buffer.data(), buffer.size()); + int responseLength = res_nsend(state, qbuffer.data(), qbuffer.size(), buffer.data(), buffer.size()); if (responseLength >= 0) return responseLength; // success @@ -202,10 +190,10 @@ void QDnsLookupRunnable::query(QDnsLookupReply *reply) }; // strictly use UDP, we'll deal with truncated replies ourselves - state.options |= RES_IGNTC; + state->options |= RES_IGNTC; int responseLength = attemptToSend(); if (responseLength < 0) - return; + return responseLength; // check if we need to use the virtual circuit (TCP) auto header = reinterpret_cast<HEADER *>(buffer.data()); @@ -216,17 +204,65 @@ void QDnsLookupRunnable::query(QDnsLookupReply *reply) // remove the EDNS record in the query reinterpret_cast<HEADER *>(qbuffer.data())->arcount = 0; - queryLength -= sizeof(Edns0Record); + qbuffer = qbuffer.first(qbuffer.size() - sizeof(Edns0Record)); // send using the virtual circuit - state.options |= RES_USEVC; + state->options |= RES_USEVC; responseLength = attemptToSend(); if (Q_UNLIKELY(responseLength > buffer.size())) { // Ok, we give up. - return reply->setError(QDnsLookup::ResolverError, - QDnsLookup::tr("Reply was too large")); + reply->setError(QDnsLookup::ResolverError, QDnsLookup::tr("Reply was too large")); + return -1; } } + + // We only trust the AD bit in the reply if we're querying a custom name + // server or if we can tell the system administrator configured the resolver + // to trust replies. +#ifndef RES_TRUSTAD + if (nameserver.isNull()) + header->ad = false; +#endif + reply->authenticData = header->ad; + + return responseLength; +} + +void QDnsLookupRunnable::query(QDnsLookupReply *reply) +{ + // Initialize state. + std::remove_pointer_t<res_state> state = {}; + if (res_ninit(&state) < 0) { + int error = errno; + qErrnoWarning(error, "QDnsLookup: Resolver initialization failed"); + return reply->makeResolverSystemError(error); + } + auto guard = qScopeGuard([&] { res_nclose(&state); }); + +#ifdef QDNSLOOKUP_DEBUG + state.options |= RES_DEBUG; +#endif + + // Prepare the DNS query. + QueryBuffer qbuffer; + int queryLength = prepareQueryBuffer(&state, qbuffer, requestName.constData(), ns_rcode(requestType)); + if (Q_UNLIKELY(queryLength < 0)) + return reply->makeResolverSystemError(); + + // Perform DNS query. + ReplyBuffer buffer(ReplyBufferSize); + int responseLength = -1; + switch (protocol) { + case QDnsLookup::Standard: + responseLength = sendStandardDns(reply, &state, qbuffer, buffer, nameserver, port); + break; + case QDnsLookup::DnsOverTls: + if (!sendDnsOverTls(reply, qbuffer, buffer)) + return; + responseLength = buffer.size(); + break; + } + if (responseLength < 0) return; @@ -235,6 +271,7 @@ void QDnsLookupRunnable::query(QDnsLookupReply *reply) return reply->makeInvalidReplyError(); // Parse the reply. + auto header = reinterpret_cast<HEADER *>(buffer.data()); if (header->rcode) return reply->makeDnsRcodeError(header->rcode); @@ -273,7 +310,7 @@ void QDnsLookupRunnable::query(QDnsLookupReply *reply) expandHost(offset); if (status < 0) return; - if (offset + status + 4 >= responseLength) + if (offset + status + 4 > responseLength) header->qdcount = 0xffff; // invalid reply below else offset += status + 4; @@ -356,6 +393,8 @@ void QDnsLookupRunnable::query(QDnsLookupReply *reply) return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid mail exchange record")); reply->mailExchangeRecords.append(record); } else if (type == QDnsLookup::SRV) { + if (size < 7) + return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid service record")); const quint16 priority = qFromBigEndian<quint16>(response + offset); const quint16 weight = qFromBigEndian<quint16>(response + offset + 2); const quint16 port = qFromBigEndian<quint16>(response + offset + 4); @@ -369,6 +408,23 @@ void QDnsLookupRunnable::query(QDnsLookupReply *reply) if (status < 0) return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid service record")); reply->serviceRecords.append(record); + } else if (type == QDnsLookup::TLSA) { + // https://datatracker.ietf.org/doc/html/rfc6698#section-2.1 + if (size < 3) + return reply->makeInvalidReplyError(QDnsLookup::tr("Invalid TLS association record")); + + const quint8 usage = response[offset]; + const quint8 selector = response[offset + 1]; + const quint8 matchType = response[offset + 2]; + + QDnsTlsAssociationRecord record; + record.d->name = name; + record.d->timeToLive = ttl; + record.d->usage = QDnsTlsAssociationRecord::CertificateUsage(usage); + record.d->selector = QDnsTlsAssociationRecord::Selector(selector); + record.d->matchType = QDnsTlsAssociationRecord::MatchingType(matchType); + record.d->value.assign(response + offset + 3, response + offset + size); + reply->tlsAssociationRecords.append(std::move(record)); } else if (type == QDnsLookup::TXT) { QDnsTextRecord record; record.d->name = name; diff --git a/src/network/kernel/qdnslookup_win.cpp b/src/network/kernel/qdnslookup_win.cpp index 72d5ae5c86..1b07776db9 100644 --- a/src/network/kernel/qdnslookup_win.cpp +++ b/src/network/kernel/qdnslookup_win.cpp @@ -5,9 +5,11 @@ #include <winsock2.h> #include "qdnslookup_p.h" -#include <qurl.h> +#include <qendian.h> #include <private/qnativesocketengine_p.h> #include <private/qsystemerror_p.h> +#include <qurl.h> +#include <qspan.h> #include <qt_windows.h> #include <windns.h> @@ -63,6 +65,58 @@ DNS_STATUS WINAPI DnsQueryEx(PDNS_QUERY_REQUEST pQueryRequest, QT_BEGIN_NAMESPACE +static DNS_STATUS sendAlternate(QDnsLookupRunnable *self, QDnsLookupReply *reply, + PDNS_QUERY_REQUEST request, PDNS_QUERY_RESULT results) +{ + // WinDNS wants MTU - IP Header - UDP header for some reason, in spite + // of never needing that much + QVarLengthArray<unsigned char, 1472> query(1472); + + auto dnsBuffer = new (query.data()) DNS_MESSAGE_BUFFER; + DWORD dnsBufferSize = query.size(); + WORD xid = 0; + bool recursionDesired = true; + + SetLastError(ERROR_SUCCESS); + + // MinGW winheaders incorrectly declare the third parameter as LPWSTR + if (!DnsWriteQuestionToBuffer_W(dnsBuffer, &dnsBufferSize, + const_cast<LPWSTR>(request->QueryName), request->QueryType, + xid, recursionDesired)) { + // let's try reallocating + query.resize(dnsBufferSize); + if (!DnsWriteQuestionToBuffer_W(dnsBuffer, &dnsBufferSize, + const_cast<LPWSTR>(request->QueryName), request->QueryType, + xid, recursionDesired)) { + return GetLastError(); + } + } + + // set AD bit: we want to trust this server + dnsBuffer->MessageHead.AuthenticatedData = true; + + QDnsLookupRunnable::ReplyBuffer replyBuffer; + if (!self->sendDnsOverTls(reply, { query.data(), qsizetype(dnsBufferSize) }, replyBuffer)) + return DNS_STATUS(-1); // error set in reply + + // interpret the RCODE in the reply + auto response = reinterpret_cast<PDNS_MESSAGE_BUFFER>(replyBuffer.data()); + DNS_HEADER *header = &response->MessageHead; + if (!header->IsResponse) + return DNS_ERROR_BAD_PACKET; // not a reply + + // Convert the byte order for the 16-bit quantities in the header, so + // DnsExtractRecordsFromMessage can parse the contents. + //header->Xid = qFromBigEndian(header->Xid); + header->QuestionCount = qFromBigEndian(header->QuestionCount); + header->AnswerCount = qFromBigEndian(header->AnswerCount); + header->NameServerCount = qFromBigEndian(header->NameServerCount); + header->AdditionalCount = qFromBigEndian(header->AdditionalCount); + + results->QueryOptions = request->QueryOptions; + return DnsExtractRecordsFromMessage_W(response, replyBuffer.size(), &results->pQueryRecords); +} + void QDnsLookupRunnable::query(QDnsLookupReply *reply) { // Perform DNS query. @@ -73,7 +127,7 @@ void QDnsLookupRunnable::query(QDnsLookupReply *reply) request.QueryType = requestType; request.QueryOptions = DNS_QUERY_STANDARD | DNS_QUERY_TREAT_AS_FQDN; - if (!nameserver.isNull()) { + if (protocol == QDnsLookup::Standard && !nameserver.isNull()) { memset(dnsAddresses, 0, sizeof(dnsAddresses)); request.pDnsServerList = new (dnsAddresses) DNS_ADDR_ARRAY; auto addr = new (request.pDnsServerList->AddrArray) DNS_ADDR[1]; @@ -87,7 +141,18 @@ void QDnsLookupRunnable::query(QDnsLookupReply *reply) DNS_QUERY_RESULT results = {}; results.Version = 1; - const DNS_STATUS status = DnsQueryEx(&request, &results, nullptr); + DNS_STATUS status = ERROR_INVALID_PARAMETER; + switch (protocol) { + case QDnsLookup::Standard: + status = DnsQueryEx(&request, &results, nullptr); + break; + case QDnsLookup::DnsOverTls: + status = sendAlternate(this, reply, &request, &results); + break; + } + + if (status == DNS_STATUS(-1)) + return; // error already set in reply if (status >= DNS_ERROR_RCODE_FORMAT_ERROR && status <= DNS_ERROR_RCODE_LAST) return reply->makeDnsRcodeError(status - DNS_ERROR_RCODE_FORMAT_ERROR + 1); else if (status == ERROR_TIMEOUT) @@ -159,6 +224,25 @@ void QDnsLookupRunnable::query(QDnsLookupReply *reply) record.d->timeToLive = ptr->dwTtl; record.d->weight = ptr->Data.Srv.wWeight; reply->serviceRecords.append(record); + } else if (ptr->wType == QDnsLookup::TLSA) { + // Note: untested, because the DNS_RECORD reply appears to contain + // no records relating to TLSA. Maybe WinDNS filters them out of + // zones without DNSSEC. + QDnsTlsAssociationRecord record; + record.d->name = name; + record.d->timeToLive = ptr->dwTtl; + + const auto &tlsa = ptr->Data.Tlsa; + const quint8 usage = tlsa.bCertUsage; + const quint8 selector = tlsa.bSelector; + const quint8 matchType = tlsa.bMatchingType; + + record.d->usage = QDnsTlsAssociationRecord::CertificateUsage(usage); + record.d->selector = QDnsTlsAssociationRecord::Selector(selector); + record.d->matchType = QDnsTlsAssociationRecord::MatchingType(matchType); + record.d->value.assign(tlsa.bCertificateAssociationData, + tlsa.bCertificateAssociationData + tlsa.bCertificateAssociationDataLength); + reply->tlsAssociationRecords.append(std::move(record)); } else if (ptr->wType == QDnsLookup::TXT) { QDnsTextRecord record; record.d->name = name; diff --git a/src/network/kernel/qnetworkinformation.h b/src/network/kernel/qnetworkinformation.h index 4e70a7faf2..57a49f23c8 100644 --- a/src/network/kernel/qnetworkinformation.h +++ b/src/network/kernel/qnetworkinformation.h @@ -83,7 +83,6 @@ Q_SIGNALS: private: friend struct QNetworkInformationDeleter; - friend class QNetworkInformationPrivate; QNetworkInformation(QNetworkInformationBackend *backend); ~QNetworkInformation() override; diff --git a/src/network/kernel/qnetworkinterface_unix.cpp b/src/network/kernel/qnetworkinterface_unix.cpp index c0a7d9e00d..39ff8dbb92 100644 --- a/src/network/kernel/qnetworkinterface_unix.cpp +++ b/src/network/kernel/qnetworkinterface_unix.cpp @@ -310,12 +310,20 @@ QT_BEGIN_INCLUDE_NAMESPACE QT_END_INCLUDE_NAMESPACE # endif +static int openSocket(int &socket) +{ + if (socket == -1) + socket = qt_safe_socket(AF_INET, SOCK_DGRAM, 0); + return socket; +} + # if defined(Q_OS_LINUX) && __GLIBC__ - 0 >= 2 && __GLIBC_MINOR__ - 0 >= 1 && !defined(QT_LINUXBASE) # include <netpacket/packet.h> static QList<QNetworkInterfacePrivate *> createInterfaces(ifaddrs *rawList) { Q_UNUSED(getMtu); + Q_UNUSED(openSocket); QList<QNetworkInterfacePrivate *> interfaces; QDuplicateTracker<QString> seenInterfaces; QDuplicateTracker<int> seenIndexes; @@ -387,13 +395,6 @@ QT_BEGIN_INCLUDE_NAMESPACE #endif // QT_PLATFORM_UIKIT QT_END_INCLUDE_NAMESPACE -static int openSocket(int &socket) -{ - if (socket == -1) - socket = qt_safe_socket(AF_INET, SOCK_DGRAM, 0); - return socket; -} - static QNetworkInterface::InterfaceType probeIfType(int socket, int iftype, struct ifmediareq *req) { // Determine the interface type. @@ -537,8 +538,8 @@ static void getAddressExtraInfo(QNetworkAddressEntry *entry, struct sockaddr *sa static QList<QNetworkInterfacePrivate *> createInterfaces(ifaddrs *rawList) { - Q_UNUSED(getMtu); QList<QNetworkInterfacePrivate *> interfaces; + int socket = -1; // make sure there's one entry for each interface for (ifaddrs *ptr = rawList; ptr; ptr = ptr->ifa_next) { @@ -559,9 +560,18 @@ static QList<QNetworkInterfacePrivate *> createInterfaces(ifaddrs *rawList) iface->index = ifindex; iface->name = QString::fromLatin1(ptr->ifa_name); iface->flags = convertFlags(ptr->ifa_flags); + + if ((socket = openSocket(socket)) >= 0) { + struct ifreq ifr; + qstrncpy(ifr.ifr_name, ptr->ifa_name, sizeof(ifr.ifr_name)); + iface->mtu = getMtu(socket, &ifr); + } } } + if (socket != -1) + qt_safe_close(socket); + return interfaces; } diff --git a/src/network/kernel/qnetworkproxy.cpp b/src/network/kernel/qnetworkproxy.cpp index 62bba24bce..9b91b11d6b 100644 --- a/src/network/kernel/qnetworkproxy.cpp +++ b/src/network/kernel/qnetworkproxy.cpp @@ -752,6 +752,53 @@ QNetworkProxy QNetworkProxy::applicationProxy() } /*! + \since 6.8 + + Returns headers that are set in this network request. + + If the proxy is not of type HttpProxy or HttpCachingProxy, + default constructed QHttpHeaders is returned. + + \sa setHeaders() +*/ +QHttpHeaders QNetworkProxy::headers() const +{ + if (d->type != HttpProxy && d->type != HttpCachingProxy) + return {}; + return d->headers.headers(); +} + +/*! + \since 6.8 + + Sets \a newHeaders as headers in this network request, overriding + any previously set headers. + + If some headers correspond to the known headers, the values will + be parsed and the corresponding parsed form will also be set. + + If the proxy is not of type HttpProxy or HttpCachingProxy this has no + effect. + + \sa headers(), QNetworkRequest::KnownHeaders +*/ +void QNetworkProxy::setHeaders(QHttpHeaders &&newHeaders) +{ + if (d->type == HttpProxy || d->type == HttpCachingProxy) + d->headers.setHeaders(std::move(newHeaders)); +} + +/*! + \overload + \since 6.8 +*/ +void QNetworkProxy::setHeaders(const QHttpHeaders &newHeaders) +{ + if (d->type == HttpProxy || d->type == HttpCachingProxy) + d->headers.setHeaders(newHeaders); +} + +/*! \since 5.0 Returns the value of the known network header \a header if it is in use for this proxy. If it is not present, returns QVariant() @@ -795,7 +842,7 @@ bool QNetworkProxy::hasRawHeader(const QByteArray &headerName) const { if (d->type != HttpProxy && d->type != HttpCachingProxy) return false; - return d->headers.findRawHeader(headerName) != d->headers.rawHeaders.constEnd(); + return d->headers.headers().contains(headerName); } /*! @@ -814,11 +861,7 @@ QByteArray QNetworkProxy::rawHeader(const QByteArray &headerName) const { if (d->type != HttpProxy && d->type != HttpCachingProxy) return QByteArray(); - QNetworkHeadersPrivate::RawHeadersList::ConstIterator it = - d->headers.findRawHeader(headerName); - if (it != d->headers.rawHeaders.constEnd()) - return it->second; - return QByteArray(); + return d->headers.rawHeader(headerName); } /*! diff --git a/src/network/kernel/qnetworkproxy.h b/src/network/kernel/qnetworkproxy.h index d04bd9ee13..9f92ffeb12 100644 --- a/src/network/kernel/qnetworkproxy.h +++ b/src/network/kernel/qnetworkproxy.h @@ -136,6 +136,10 @@ public: static void setApplicationProxy(const QNetworkProxy &proxy); static QNetworkProxy applicationProxy(); + QHttpHeaders headers() const; + void setHeaders(const QHttpHeaders &newHeaders); + void setHeaders(QHttpHeaders &&newHeaders); + // "cooked" headers QVariant header(QNetworkRequest::KnownHeaders header) const; void setHeader(QNetworkRequest::KnownHeaders header, const QVariant &value); diff --git a/src/network/kernel/qnetworkproxy_libproxy.cpp b/src/network/kernel/qnetworkproxy_libproxy.cpp index 248a8d2456..da1e8fdbd4 100644 --- a/src/network/kernel/qnetworkproxy_libproxy.cpp +++ b/src/network/kernel/qnetworkproxy_libproxy.cpp @@ -166,13 +166,15 @@ QList<QNetworkProxy> QNetworkProxyFactory::systemProxyForQuery(const QNetworkPro break; // fake URLs to get libproxy to tell us the SOCKS proxy case QNetworkProxyQuery::TcpSocket: - queryUrl.setScheme(QStringLiteral("tcp")); + if (queryUrl.scheme().isEmpty()) + queryUrl.setScheme(QStringLiteral("tcp")); queryUrl.setHost(query.peerHostName()); queryUrl.setPort(query.peerPort()); requiredCapabilities |= QNetworkProxy::TunnelingCapability; break; case QNetworkProxyQuery::UdpSocket: - queryUrl.setScheme(QStringLiteral("udp")); + if (queryUrl.scheme().isEmpty()) + queryUrl.setScheme(QStringLiteral("udp")); queryUrl.setHost(query.peerHostName()); queryUrl.setPort(query.peerPort()); requiredCapabilities |= QNetworkProxy::UdpTunnelingCapability; diff --git a/src/network/socket/qhttpsocketengine.cpp b/src/network/socket/qhttpsocketengine.cpp index a700023bd5..725bc21359 100644 --- a/src/network/socket/qhttpsocketengine.cpp +++ b/src/network/socket/qhttpsocketengine.cpp @@ -467,11 +467,14 @@ void QHttpSocketEngine::slotSocketConnected() data += " HTTP/1.1\r\n"; data += "Proxy-Connection: keep-alive\r\n"; data += "Host: " + peerAddress + "\r\n"; - if (!d->proxy.hasRawHeader("User-Agent")) + const auto headers = d->proxy.headers(); + if (!headers.contains(QHttpHeaders::WellKnownHeader::UserAgent)) data += "User-Agent: Mozilla/5.0\r\n"; - const auto headers = d->proxy.rawHeaderList(); - for (const QByteArray &header : headers) - data += header + ": " + d->proxy.rawHeader(header) + "\r\n"; + for (qsizetype i = 0; i < headers.size(); ++i) { + const auto name = headers.nameAt(i); + data += QByteArrayView(name.data(), name.size()) + ": " + + headers.valueAt(i) + "\r\n"; + } QAuthenticatorPrivate *priv = QAuthenticatorPrivate::getPrivate(d->authenticator); //qDebug() << "slotSocketConnected: priv=" << priv << (priv ? (int)priv->method : -1); if (priv && priv->method != QAuthenticatorPrivate::None) { diff --git a/src/network/socket/qsocks5socketengine.cpp b/src/network/socket/qsocks5socketengine.cpp index 0564ad7a33..b0fdc63d66 100644 --- a/src/network/socket/qsocks5socketengine.cpp +++ b/src/network/socket/qsocks5socketengine.cpp @@ -1460,7 +1460,7 @@ qint64 QSocks5SocketEngine::read(char *data, qint64 maxlen) //imitate remote closed close(); setError(QAbstractSocket::RemoteHostClosedError, - "Remote host closed connection###"_L1); + "Remote host closed connection"_L1); setState(QAbstractSocket::UnconnectedState); return -1; } else { |