diff options
Diffstat (limited to 'src/network/access')
19 files changed, 658 insertions, 106 deletions
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/qhttpheaders.h b/src/network/access/qhttpheaders.h index 97dc415e55..260df1421b 100644 --- a/src/network/access/qhttpheaders.h +++ b/src/network/access/qhttpheaders.h @@ -4,7 +4,8 @@ #ifndef QHTTPHEADERS_H #define QHTTPHEADERS_H -#include <QtNetwork/qnetworkrequest.h> +#include <QtNetwork/qtnetworkglobal.h> +#include <QtCore/qmetaobject.h> #include <QtCore/qshareddata.h> #include <QtCore/qcontainerfwd.h> diff --git a/src/network/access/qhttpheadershelper.cpp b/src/network/access/qhttpheadershelper.cpp new file mode 100644 index 0000000000..d3cc9e439f --- /dev/null +++ b/src/network/access/qhttpheadershelper.cpp @@ -0,0 +1,25 @@ +// 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 "qhttpheadershelper_p.h" + +#include <QtNetwork/qhttpheaders.h> + +QT_BEGIN_NAMESPACE + +bool QHttpHeadersHelper::compareStrict(const QHttpHeaders &left, const QHttpHeaders &right) +{ + if (left.size() != right.size()) + return false; + + for (qsizetype i = 0; i < left.size(); ++i) { + if (left.nameAt(i) != right.nameAt(i)) + return false; + if (left.valueAt(i) != right.valueAt(i)) + return false; + } + + return true; +} + +QT_END_NAMESPACE diff --git a/src/network/access/qhttpheadershelper_p.h b/src/network/access/qhttpheadershelper_p.h new file mode 100644 index 0000000000..d1e38a1a8e --- /dev/null +++ b/src/network/access/qhttpheadershelper_p.h @@ -0,0 +1,30 @@ +// 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 QHTTPHEADERSHELPER_H +#define QHTTPHEADERSHELPER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the Network Access API. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +#include <QtNetwork/private/qtnetworkglobal_p.h> + +QT_BEGIN_NAMESPACE + +class QHttpHeaders; + +namespace QHttpHeadersHelper { + Q_NETWORK_EXPORT bool compareStrict(const QHttpHeaders &left, const QHttpHeaders &right); +}; + +QT_END_NAMESPACE + +#endif // QHTTPHEADERSHELPER_H diff --git a/src/network/access/qhttpmultipart.cpp b/src/network/access/qhttpmultipart.cpp index 6d81f1b957..2b5f1163c8 100644 --- a/src/network/access/qhttpmultipart.cpp +++ b/src/network/access/qhttpmultipart.cpp @@ -511,6 +511,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 + << "], raw headers = [" + << part.d->rawHeaders + << "],"; + + 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/qhttpnetworkconnection.cpp b/src/network/access/qhttpnetworkconnection.cpp index 1897380e0e..419491a711 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> @@ -319,7 +320,7 @@ void QHttpNetworkConnectionPrivate::prepareRequest(HttpMessagePair &messagePair) -void QHttpNetworkConnectionPrivate::emitReplyError(QAbstractSocket *socket, +void QHttpNetworkConnectionPrivate::emitReplyError(QIODevice *socket, QHttpNetworkReply *reply, QNetworkReply::NetworkError errorCode) { @@ -384,7 +385,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 +487,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); @@ -740,7 +741,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 +769,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 +863,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; diff --git a/src/network/access/qhttpnetworkconnection_p.h b/src/network/access/qhttpnetworkconnection_p.h index 9cd4d75bbc..c2d062fb16 100644 --- a/src/network/access/qhttpnetworkconnection_p.h +++ b/src/network/access/qhttpnetworkconnection_p.h @@ -177,7 +177,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 +197,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); @@ -212,22 +212,22 @@ public: // The total number of channels we reserved: const int channelCount; QTimer delayedConnectionTimer; - QHttpNetworkConnectionChannel *channels; // parallel connections to the server + QHttpNetworkConnectionChannel * const channels; // parallel connections to the server bool shouldEmitChannelError(QIODevice *socket); qint64 uncompressedBytesAvailable(const QHttpNetworkReply &reply) const; 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..71b880f6f8 100644 --- a/src/network/access/qhttpnetworkconnectionchannel.cpp +++ b/src/network/access/qhttpnetworkconnectionchannel.cpp @@ -91,14 +91,14 @@ void QHttpNetworkConnectionChannel::init() // 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()), + QObject::connect(socket, &QAbstractSocket::connected, + this, &QHttpNetworkConnectionChannel::_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 @@ -108,17 +108,17 @@ void QHttpNetworkConnectionChannel::init() // 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()), + QObject::connect(socket, &QAbstractSocket::disconnected, + this, &QHttpNetworkConnectionChannel::_q_disconnected, Qt::DirectConnection); - QObject::connect(socket, SIGNAL(errorOccurred(QAbstractSocket::SocketError)), - this, SLOT(_q_error(QAbstractSocket::SocketError)), + QObject::connect(socket, &QAbstractSocket::errorOccurred, + this, &QHttpNetworkConnectionChannel::_q_error, Qt::DirectConnection); #ifndef QT_NO_NETWORKPROXY - QObject::connect(socket, SIGNAL(proxyAuthenticationRequired(QNetworkProxy,QAuthenticator*)), - this, SLOT(_q_proxyAuthenticationRequired(QNetworkProxy,QAuthenticator*)), + QObject::connect(socket, &QAbstractSocket::proxyAuthenticationRequired, + this, &QHttpNetworkConnectionChannel::_q_proxyAuthenticationRequired, Qt::DirectConnection); #endif @@ -126,17 +126,17 @@ void QHttpNetworkConnectionChannel::init() 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) 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/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/qnetworkreply.cpp b/src/network/access/qnetworkreply.cpp index 9334b01de6..0613a65c34 100644 --- a/src/network/access/qnetworkreply.cpp +++ b/src/network/access/qnetworkreply.cpp @@ -654,6 +654,19 @@ const QList<QNetworkReply::RawHeaderPair>& QNetworkReply::rawHeaderPairs() const } /*! + \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(); +} + +/*! Returns a list of headers fields that were sent by the remote server, in the order that they were sent. Duplicate headers are merged together and take place of the latter duplicate. @@ -888,6 +901,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/qnetworkrequest.cpp b/src/network/access/qnetworkrequest.cpp index 6f5a7ff19a..a59de42a44 100644 --- a/src/network/access/qnetworkrequest.cpp +++ b/src/network/access/qnetworkrequest.cpp @@ -6,6 +6,7 @@ #include "qplatformdefs.h" #include "qnetworkcookie.h" #include "qsslconfiguration.h" +#include "qhttpheadershelper_p.h" #if QT_CONFIG(http) #include "qhttp1configuration.h" #include "qhttp2configuration.h" @@ -475,6 +476,7 @@ public: && decompressedSafetyCheckThreshold == other.decompressedSafetyCheckThreshold #endif && transferTimeout == other.transferTimeout + && QHttpHeadersHelper::compareStrict(httpHeaders, other.httpHeaders) ; // don't compare cookedHeaders } @@ -601,6 +603,43 @@ void QNetworkRequest::setUrl(const QUrl &url) } /*! + \since 6.8 + + Returns headers that are set in this network request. + + \sa setHeaders() +*/ +QHttpHeaders QNetworkRequest::headers() const +{ + return d->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. + + \sa headers(), KnownHeaders +*/ +void QNetworkRequest::setHeaders(QHttpHeaders &&newHeaders) +{ + d->setHeaders(std::move(newHeaders)); +} + +/*! + \overload + \since 6.8 +*/ +void QNetworkRequest::setHeaders(const QHttpHeaders &newHeaders) +{ + d->setHeaders(newHeaders); +} + +/*! Returns the value of the known network header \a header if it is present in this request. If it is not present, returns QVariant() (i.e., an invalid variant). @@ -1401,6 +1440,33 @@ void QNetworkHeadersPrivate::setCookedHeader(QNetworkRequest::KnownHeaders heade } } +QHttpHeaders QNetworkHeadersPrivate::headers() const +{ + return httpHeaders; +} + +void QNetworkHeadersPrivate::setHeaders(const QHttpHeaders &newHeaders) +{ + httpHeaders = newHeaders; +} + +void QNetworkHeadersPrivate::setHeaders(QHttpHeaders &&newHeaders) +{ + httpHeaders = std::move(newHeaders); +} + +void QNetworkHeadersPrivate::setHeader(QHttpHeaders::WellKnownHeader name, QByteArrayView value) +{ + httpHeaders.replaceOrAppend(name, value); +} + +void QNetworkHeadersPrivate::clearHeaders() +{ + httpHeaders.clear(); + rawHeaders.clear(); + cookedHeaders.clear(); +} + void QNetworkHeadersPrivate::setRawHeaderInternal(const QByteArray &key, const QByteArray &value) { auto firstEqualsKey = [&key](const RawHeaderPair &header) { @@ -1536,6 +1602,59 @@ 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; +} + QT_END_NAMESPACE #include "moc_qnetworkrequest.cpp" diff --git a/src/network/access/qnetworkrequest.h b/src/network/access/qnetworkrequest.h index 3ca61a2ee3..875c787673 100644 --- a/src/network/access/qnetworkrequest.h +++ b/src/network/access/qnetworkrequest.h @@ -5,6 +5,7 @@ #define QNETWORKREQUEST_H #include <QtNetwork/qtnetworkglobal.h> +#include <QtNetwork/qhttpheaders.h> #include <QtCore/QSharedDataPointer> #include <QtCore/QString> #include <QtCore/QUrl> @@ -119,6 +120,10 @@ public: QUrl url() const; void setUrl(const QUrl &url); + QHttpHeaders headers() const; + void setHeaders(const QHttpHeaders &newHeaders); + void setHeaders(QHttpHeaders &&newHeaders); + // "cooked" headers QVariant header(KnownHeaders header) const; void setHeader(KnownHeaders header, const QVariant &value); diff --git a/src/network/access/qnetworkrequest_p.h b/src/network/access/qnetworkrequest_p.h index 48fcdcf1ed..a73d8f2162 100644 --- a/src/network/access/qnetworkrequest_p.h +++ b/src/network/access/qnetworkrequest_p.h @@ -16,6 +16,7 @@ // #include <QtNetwork/private/qtnetworkglobal_p.h> +#include <QtNetwork/qhttpheaders.h> #include "qnetworkrequest.h" #include "QtCore/qbytearray.h" #include "QtCore/qlist.h" @@ -36,6 +37,7 @@ public: typedef QHash<QNetworkRequest::Attribute, QVariant> AttributesMap; RawHeadersList rawHeaders; + QHttpHeaders httpHeaders; CookedHeadersMap cookedHeaders; AttributesMap attributes; QPointer<QObject> originatingObject; @@ -47,9 +49,19 @@ public: void setAllRawHeaders(const RawHeadersList &list); void setCookedHeader(QNetworkRequest::KnownHeaders header, const QVariant &value); + QHttpHeaders headers() const; + void setHeaders(const QHttpHeaders &newHeaders); + void setHeaders(QHttpHeaders &&newHeaders); + void setHeader(QHttpHeaders::WellKnownHeader name, QByteArrayView value); + + void clearHeaders(); + static QDateTime fromHttpDate(const QByteArray &value); static QByteArray toHttpDate(const QDateTime &dt); + 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); diff --git a/src/network/access/qrestreply.cpp b/src/network/access/qrestreply.cpp index 3cc62c7efa..cdd264ba6e 100644 --- a/src/network/access/qrestreply.cpp +++ b/src/network/access/qrestreply.cpp @@ -4,11 +4,17 @@ #include "qrestreply.h" #include "qrestreply_p.h" +#include <QtNetwork/private/qnetworkreply_p.h> + +#include <QtCore/qbytearrayview.h> #include <QtCore/qjsondocument.h> #include <QtCore/qlatin1stringmatcher.h> +#include <QtCore/qlatin1stringview.h> #include <QtCore/qloggingcategory.h> #include <QtCore/qstringconverter.h> +#include <QtCore/qxpfunctional.h> + QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; @@ -335,6 +341,249 @@ QDebug operator<<(QDebug debug, const QRestReply &reply) } #endif // QT_NO_DEBUG_STREAM +static constexpr auto parse_OWS(QByteArrayView data) noexcept +{ + struct R { + QByteArrayView ows, tail; + }; + + constexpr auto is_OWS_char = [](auto ch) { return ch == ' ' || ch == '\t'; }; + + qsizetype i = 0; + while (i < data.size() && is_OWS_char(data[i])) + ++i; + + return R{data.first(i), data.sliced(i)}; +} + +static constexpr void eat_OWS(QByteArrayView &data) noexcept +{ + data = parse_OWS(data).tail; +} + +static constexpr auto parse_quoted_string(QByteArrayView data, qxp::function_ref<void(char) const> yield) +{ + struct R { + QByteArrayView quotedString, tail; + constexpr explicit operator bool() const noexcept { return !quotedString.isEmpty(); } + }; + + if (!data.startsWith('"')) + return R{{}, data}; + + qsizetype i = 1; // one past initial DQUOTE + while (i < data.size()) { + switch (auto ch = data[i++]) { + case '"': // final DQUOTE -> end of string + return R{data.first(i), data.sliced(i)}; + case '\\': // quoted-pair + // https://www.rfc-editor.org/rfc/rfc9110.html#section-5.6.4-3: + // Recipients that process the value of a quoted-string MUST handle a + // quoted-pair as if it were replaced by the octet following the backslash. + if (i == data.size()) + break; // premature end + ch = data[i++]; // eat '\\' + [[fallthrough]]; + default: + // we don't validate quoted-string octets to be only qdtext (Postel's Law) + yield(ch); + } + } + + return R{{}, data}; // premature end +} + +static constexpr bool is_tchar(char ch) noexcept +{ + // ### optimize + switch (ch) { + case '!': + case '#': + case '$': + case '%': + case '&': + case '\'': + case '*': + case '+': + case '-': + case '.': + case '^': + case '_': + case '`': + case '|': + case '~': + return true; + default: + return (ch >= 'a' && ch <= 'z') + || (ch >= '0' && ch <= '9') + || (ch >= 'A' && ch <= 'Z'); + } +} + +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 { + QByteArrayView token, tail; + constexpr explicit operator bool() const noexcept { return !token.isEmpty(); } + }; + + qsizetype i = 0; + while (i < data.size() && is_tchar(data[i])) + ++i; + + return R{data.first(i), data.sliced(i)}; +} + +static constexpr auto parse_parameter(QByteArrayView data, qxp::function_ref<void(char) const> yield) +{ + struct R { + QLatin1StringView name; QByteArrayView value; QByteArrayView tail; + constexpr explicit operator bool() const noexcept { return !name.isEmpty(); } + }; + + const auto invalid = R{{}, {}, data}; // preserves original `data` + + // parameter = parameter-name "=" parameter-value + // parameter-name = token + // parameter-value = ( token / quoted-string ) + + const auto name = parse_token(data); + if (!name) + return invalid; + data = name.tail; + + eat_CWS(data); // not in the grammar, but accepted under Postel's Law + + if (!data.startsWith('=')) + return invalid; + data = data.sliced(1); + + eat_CWS(data); // not in the grammar, but accepted under Postel's Law + + if (Q_UNLIKELY(data.startsWith('"'))) { // value is a quoted-string + + const auto value = parse_quoted_string(data, yield); + if (!value) + return invalid; + data = value.tail; + + return R{QLatin1StringView{name.token}, value.quotedString, data}; + + } else { // value is a token + + const auto value = parse_token(data); + if (!value) + return invalid; + data = value.tail; + + return R{QLatin1StringView{name.token}, value.token, data}; + } +} + +static auto parse_content_type(QByteArrayView data) +{ + struct R { + QLatin1StringView type, subtype; + std::string charset; + constexpr explicit operator bool() const noexcept { return !type.isEmpty(); } + }; + + 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_CWS(data); // not in the grammar, but accepted under Postel's Law + + if (!data.startsWith('/')) + return R{}; + data = data.sliced(1); + + 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_CWS(data); + + auto r = R{QLatin1StringView{type.token}, QLatin1StringView{subtype.token}, {}}; + + while (data.startsWith(';')) { + + data = data.sliced(1); // eat ';' + + 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) { + if (r.charset.empty() && !param.value.startsWith('"')) // wasn't a quoted-string + r.charset.assign(param.value.begin(), param.value.end()); + return r; // charset found + } + r.charset.clear(); // wasn't an actual charset + if (param.tail.size() == data.size()) // no progress was made + break; // returns {type, subtype} + // otherwise, continue (accepting e.g. `;;`) + data = param.tail; + + eat_CWS(data); + } + + return r; // no charset found +} + QByteArray QRestReplyPrivate::contentCharset(const QNetworkReply* reply) { // Content-type consists of mimetype and optional parameters, of which one may be 'charset' @@ -345,28 +594,15 @@ QByteArray QRestReplyPrivate::contentCharset(const QNetworkReply* reply) // text/plain; charset=utf-8;version=1.7 // text/plain; charset = utf-8 // text/plain; charset ="utf-8" - // Default to the most commonly used UTF-8. - QByteArray charset{"UTF-8"}; + const QByteArray contentTypeValue = reply->header(QNetworkRequest::KnownHeaders::ContentTypeHeader).toByteArray(); - QList<QByteArray> parameters = contentTypeValue.split(';'); - if (parameters.size() >= 2) { // Need at least one parameter in addition to the mimetype itself - parameters.removeFirst(); // Exclude the mimetype itself, only interested in parameters - QLatin1StringMatcher matcher("charset="_L1, Qt::CaseSensitivity::CaseInsensitive); - qsizetype matchIndex = -1; - for (auto ¶meter : parameters) { - // Remove whitespaces and parantheses - const QByteArray curated = parameter.replace(" ", "").replace("\"",""); - // Check for match - matchIndex = matcher.indexIn(QLatin1String(curated.constData())); - if (matchIndex >= 0) { - charset = curated.sliced(matchIndex + 8); // 8 is size of "charset=" - break; - } - } - } - return charset; + const auto r = parse_content_type(contentTypeValue); + if (r && !r.charset.empty()) + return QByteArrayView(r.charset).toByteArray(); + else + return "UTF-8"_ba; // Default to the most commonly used UTF-8. } QT_END_NAMESPACE |