diff options
author | Juha Vuolle <juha.vuolle@qt.io> | 2023-09-14 11:09:06 +0300 |
---|---|---|
committer | Juha Vuolle <juha.vuolle@qt.io> | 2023-12-08 15:53:32 +0200 |
commit | 925ce9e9084a1a9e3dbd9954fc3bc3117a038915 (patch) | |
tree | 4c41f86929252202b0c423bf271430cafea07dcf /src/network/access | |
parent | 0919f978115b7b942bd408e0723a34674f02e197 (diff) |
Add QHttpHeaders class
New QHttpHeaders class for use in place of
std::pair<QBA,QBA>, QMap<QBA>, and QMultiMap/Hash<QBA,QBA>
to represent HTTP headers.
[ChangeLog][QtNetwork][QHttpHeaders] New QHttpHeaders class
Task-number: QTBUG-107042
Change-Id: I54766886a491acfc9a813a3414322a75011acb9d
Reviewed-by: Marc Mutz <marc.mutz@qt.io>
Diffstat (limited to 'src/network/access')
-rw-r--r-- | src/network/access/qhttpheaders.cpp | 1104 | ||||
-rw-r--r-- | src/network/access/qhttpheaders.h | 276 |
2 files changed, 1380 insertions, 0 deletions
diff --git a/src/network/access/qhttpheaders.cpp b/src/network/access/qhttpheaders.cpp new file mode 100644 index 0000000000..a66adf8161 --- /dev/null +++ b/src/network/access/qhttpheaders.cpp @@ -0,0 +1,1104 @@ +// Copyright (C) 2023 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 "qhttpheaders.h" + +#include <private/qoffsetstringarray_p.h> + +#include <QtCore/qhash.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/qmap.h> +#include <QtCore/qset.h> +#include <QtCore/qttypetraits.h> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcQHttpHeaders, "qt.network.http.headers"); + +/*! + \class QHttpHeaders + \since 6.7 + \ingroup + \inmodule QtNetwork + + \brief QHttpHeaders is a class for holding HTTP headers. + + The primary use case for the class is to provide an interface type + for Qt networking APIs that use/consume such headers. Historically + various QList, QMultiMap, and QMultiHash constructs have been used. + + \section1 Allowed field name and value characters + + An HTTP header consists of \e name and \e value. + When setting these, QHttpHeaders validates \e name and \e value + to only contain characters allowed by the HTTP RFCs. For detailed + information see + \l {https://datatracker.ietf.org/doc/html/rfc9110#name-field-values} + {RFC 9110 Chapters 5.1 and 5.5}. + + Broadly speaking, this means: + \list + \li \c name must consist of visible ASCII characters, and must not be + empty + \li \c value may consist of arbitrary bytes, as long as header + and use case specific encoding rules are adhered to. \c value + may be empty + \endlist + + Furthermore, \e value may have historically contained leading or + trailing whitespace, which has to be ignored while processing such + values. The setters of this class automatically remove any such + whitespace. + + \section1 Combining values + + Most HTTP header values can be combined with a single comma \c {','}, + and the semantic meaning is preserved. As an example, these two should be + semantically similar: + \badcode + // Values as separate header entries + myheadername: myheadervalue1 + myheadername: myheadervalue2 + // Combined value + myheadername: myheadervalue1,myheadervalue2 + \endcode + + However there is a notable exception to this rule: + \l {https://datatracker.ietf.org/doc/html/rfc9110#name-field-order} + {Set-Cookie}. Due to this, as well as due to the possibility + of custom use cases, QHttpHeaders does not automatically combine + the values. +*/ + +// A clarification on case-sensitivity: +// - Header *names* are case-insensitive; Content-Type and content-type are considered equal +// - Header *values* are case-sensitive +// (In addition, the HTTP/2 and HTTP/3 standards mandate that all headers must be lower-cased when +// encoded into transmission) +struct Header { + QByteArray name; + QByteArray value; + +private: + friend bool operator==(const Header &lhs, const Header &rhs) noexcept + { + return lhs.value == rhs.value && lhs.name == rhs.name; + } +}; + +class QHttpHeadersPrivate : public QSharedData +{ +public: + QHttpHeadersPrivate() = default; + + bool equals(const QHttpHeadersPrivate &other, + QHttpHeaders::CompareOptions options) const noexcept; + + QList<Header> headers; + + Q_ALWAYS_INLINE void verify([[maybe_unused]] qsizetype pos = 0, + [[maybe_unused]] qsizetype n = 1) const + { + Q_ASSERT(pos >= 0); + Q_ASSERT(pos <= headers.size()); + Q_ASSERT(n >= 0); + Q_ASSERT(n <= headers.size() - pos); + } +}; + +QT_DEFINE_QESDP_SPECIALIZATION_DTOR(QHttpHeadersPrivate) + +bool QHttpHeadersPrivate::equals(const QHttpHeadersPrivate &other, + QHttpHeaders::CompareOptions options) const noexcept +{ + if (headers.size() != other.headers.size()) + return false; + + if (options & QHttpHeaders::CompareOption::OrderSensitive) + return headers == other.headers; + else + return std::is_permutation(headers.begin(), headers.end(), other.headers.begin()); +} + +// This list is from IANA HTTP Field Name Registry +// https://www.iana.org/assignments/http-fields +// It contains entries that are either "permanent" +// or "deprecated" as of October 2023. +// Usage relies on enum values keeping in same order. +// ### Qt7 check if some of these headers have been obsoleted, +// and also check if the enums benefit from reordering +static constexpr auto headerNames = qOffsetStringArray( + // IANA Permanent status: + "a-im", + "accept", + "accept-additions", + "accept-ch", + "accept-datetime", + "accept-encoding", + "accept-features", + "accept-language", + "accept-patch", + "accept-post", + "accept-ranges", + "accept-signature", + "access-control-allow-credentials", + "access-control-allow-headers", + "access-control-allow-methods", + "access-control-allow-origin", + "access-control-expose-headers", + "access-control-max-age", + "access-control-request-headers", + "access-control-request-method", + "age", + "allow", + "alpn", + "alt-svc", + "alt-used", + "alternates", + "apply-to-redirect-ref", + "authentication-control", + "authentication-info", + "authorization", + "cache-control", + "cache-status", + "cal-managed-id", + "caldav-timezones", + "capsule-protocol", + "cdn-cache-control", + "cdn-loop", + "cert-not-after", + "cert-not-before", + "clear-site-data", + "client-cert", + "client-cert-chain", + "close", + "connection", + "content-digest", + "content-disposition", + "content-encoding", + "content-id", + "content-language", + "content-length", + "content-location", + "content-range", + "content-security-policy", + "content-security-policy-report-only", + "content-type", + "cookie", + "cross-origin-embedder-policy", + "cross-origin-embedder-policy-report-only", + "cross-origin-opener-policy", + "cross-origin-opener-policy-report-only", + "cross-origin-resource-policy", + "dasl", + "date", + "dav", + "delta-base", + "depth", + "destination", + "differential-id", + "dpop", + "dpop-nonce", + "early-data", + "etag", + "expect", + "expect-ct", + "expires", + "forwarded", + "from", + "hobareg", + "host", + "if", + "if-match", + "if-modified-since", + "if-none-match", + "if-range", + "if-schedule-tag-match", + "if-unmodified-since", + "im", + "include-referred-token-binding-id", + "keep-alive", + "label", + "last-event-id", + "last-modified", + "link", + "location", + "lock-token", + "max-forwards", + "memento-datetime", + "meter", + "mime-version", + "negotiate", + "nel", + "odata-entityid", + "odata-isolation", + "odata-maxversion", + "odata-version", + "optional-www-authenticate", + "ordering-type", + "origin", + "origin-agent-cluster", + "oscore", + "oslc-core-version", + "overwrite", + "ping-from", + "ping-to", + "position", + "prefer", + "preference-applied", + "priority", + "proxy-authenticate", + "proxy-authentication-info", + "proxy-authorization", + "proxy-status", + "public-key-pins", + "public-key-pins-report-only", + "range", + "redirect-ref", + "referer", + "refresh", + "replay-nonce", + "repr-digest", + "retry-after", + "schedule-reply", + "schedule-tag", + "sec-purpose", + "sec-token-binding", + "sec-websocket-accept", + "sec-websocket-extensions", + "sec-websocket-key", + "sec-websocket-protocol", + "sec-websocket-version", + "server", + "server-timing", + "set-cookie", + "signature", + "signature-input", + "slug", + "soapaction", + "status-uri", + "strict-transport-security", + "sunset", + "surrogate-capability", + "surrogate-control", + "tcn", + "te", + "timeout", + "topic", + "traceparent", + "tracestate", + "trailer", + "transfer-encoding", + "ttl", + "upgrade", + "urgency", + "user-agent", + "variant-vary", + "vary", + "via", + "want-content-digest", + "want-repr-digest", + "www-authenticate", + "x-content-type-options", + "x-frame-options", + // IANA Deprecated status: + "accept-charset", + "c-pep-info", + "pragma", + "protocol-info", + "protocol-query" +); + +/*! + \enum QHttpHeaders::WellKnownHeader + + List of well known headers as per + \l {https://www.iana.org/assignments/http-fields}{IANA registry}. + + \value AIM + \value Accept + \value AcceptAdditions + \value AcceptCH + \value AcceptDatetime + \value AcceptEncoding + \value AcceptFeatures + \value AcceptLanguage + \value AcceptPatch + \value AcceptPost + \value AcceptRanges + \value AcceptSignature + \value AccessControlAllowCredentials + \value AccessControlAllowHeaders + \value AccessControlAllowMethods + \value AccessControlAllowOrigin + \value AccessControlExposeHeaders + \value AccessControlMaxAge + \value AccessControlRequestHeaders + \value AccessControlRequestMethod + \value Age + \value Allow + \value ALPN + \value AltSvc + \value AltUsed + \value Alternates + \value ApplyToRedirectRef + \value AuthenticationControl + \value AuthenticationInfo + \value Authorization + \value CacheControl + \value CacheStatus + \value CalManagedID + \value CalDAVTimezones + \value CapsuleProtocol + \value CDNCacheControl + \value CDNLoop + \value CertNotAfter + \value CertNotBefore + \value ClearSiteData + \value ClientCert + \value ClientCertChain + \value Close + \value Connection + \value ContentDigest + \value ContentDisposition + \value ContentEncoding + \value ContentID + \value ContentLanguage + \value ContentLength + \value ContentLocation + \value ContentRange + \value ContentSecurityPolicy + \value ContentSecurityPolicyReportOnly + \value ContentType + \value Cookie + \value CrossOriginEmbedderPolicy + \value CrossOriginEmbedderPolicyReportOnly + \value CrossOriginOpenerPolicy + \value CrossOriginOpenerPolicyReportOnly + \value CrossOriginResourcePolicy + \value DASL + \value Date + \value DAV + \value DeltaBase + \value Depth + \value Destination + \value DifferentialID + \value DPoP + \value DPoPNonce + \value EarlyData + \value ETag + \value Expect + \value ExpectCT + \value Expires + \value Forwarded + \value From + \value Hobareg + \value Host + \value If + \value IfMatch + \value IfModifiedSince + \value IfNoneMatch + \value IfRange + \value IfScheduleTagMatch + \value IfUnmodifiedSince + \value IM + \value IncludeReferredTokenBindingID + \value KeepAlive + \value Label + \value LastEventID + \value LastModified + \value Link + \value Location + \value LockToken + \value MaxForwards + \value MementoDatetime + \value Meter + \value MIMEVersion + \value Negotiate + \value NEL + \value ODataEntityId + \value ODataIsolation + \value ODataMaxVersion + \value ODataVersion + \value OptionalWWWAuthenticate + \value OrderingType + \value Origin + \value OriginAgentCluster + \value OSCORE + \value OSLCCoreVersion + \value Overwrite + \value PingFrom + \value PingTo + \value Position + \value Prefer + \value PreferenceApplied + \value Priority + \value ProxyAuthenticate + \value ProxyAuthenticationInfo + \value ProxyAuthorization + \value ProxyStatus + \value PublicKeyPins + \value PublicKeyPinsReportOnly + \value Range + \value RedirectRef + \value Referer + \value Refresh + \value ReplayNonce + \value ReprDigest + \value RetryAfter + \value ScheduleReply + \value ScheduleTag + \value SecPurpose + \value SecTokenBinding + \value SecWebSocketAccept + \value SecWebSocketExtensions + \value SecWebSocketKey + \value SecWebSocketProtocol + \value SecWebSocketVersion + \value Server + \value ServerTiming + \value SetCookie + \value Signature + \value SignatureInput + \value SLUG + \value SoapAction + \value StatusURI + \value StrictTransportSecurity + \value Sunset + \value SurrogateCapability + \value SurrogateControl + \value TCN + \value TE + \value Timeout + \value Topic + \value Traceparent + \value Tracestate + \value Trailer + \value TransferEncoding + \value TTL + \value Upgrade + \value Urgency + \value UserAgent + \value VariantVary + \value Vary + \value Via + \value WantContentDigest + \value WantReprDigest + \value WWWAuthenticate + \value XContentTypeOptions + \value XFrameOptions + \value AcceptCharset + \value CPEPInfo + \value Pragma + \value ProtocolInfo + \value ProtocolQuery +*/ + +/*! + \enum QHttpHeaders::CompareOption + + This enum type contains the options for comparing two + QHttpHeaders instances. + + \value OrderInsensitive + Specifies that the order of headers is not significant in the comparison. + With this option, two QHttpHeaders instances will be considered equal + if they contain the same headers regardless of their order. This is + true with most HTTP headers and use cases. + + \value OrderSensitive + Specifies that the order of headers is significant in the comparison. + With this option, two QHttpHeaders instances will be considered equal + only if they contain the same headers in the same exact order. +*/ + +/*! + Creates a new QHttpHeaders object. +*/ +QHttpHeaders::QHttpHeaders() : d(new QHttpHeadersPrivate) +{ +} + +/*! + Creates a new QHttpHeaders object that is populated with + \a headers. + + \sa {Allowed field name and value characters} +*/ +QHttpHeaders QHttpHeaders::fromListOfPairs(const QList<std::pair<QByteArray, QByteArray>> &headers) +{ + QHttpHeaders h; + for (const auto &header : headers) + h.append(header.first, header.second); + return h; +} + +/*! + Creates a new QHttpHeaders object that is populated with + \a headers. + + \sa {Allowed field name and value characters} +*/ +QHttpHeaders QHttpHeaders::fromMultiMap(const QMultiMap<QByteArray, QByteArray> &headers) +{ + QHttpHeaders h; + for (const auto &[name,value] : headers.asKeyValueRange()) + h.append(name, value); + return h; +} + +/*! + Creates a new QHttpHeaders object that is populated with + \a headers. + + \sa {Allowed field name and value characters} +*/ +QHttpHeaders QHttpHeaders::fromMultiHash(const QMultiHash<QByteArray, QByteArray> &headers) +{ + QHttpHeaders h; + for (const auto &[name,value] : headers.asKeyValueRange()) + h.append(name, value); + return h; +} + +/*! + Disposes of the headers object. +*/ +QHttpHeaders::~QHttpHeaders() + = default; + +/*! + Creates a copy of \a other. +*/ +QHttpHeaders::QHttpHeaders(const QHttpHeaders &other) + = default; + +/*! + Assigns the contents of \a other and returns a reference to this object. +*/ +QHttpHeaders &QHttpHeaders::operator=(const QHttpHeaders &other) + = default; + +/*! + \fn QHttpHeaders::QHttpHeaders(QHttpHeaders &&other) noexcept + + Move-constructs the object from \a other. + + \note The moved-from object \a other is placed in a + partially-formed state, in which the only valid operations are + destruction and assignment of a new value. +*/ + +/*! + \fn QHttpHeaders &QHttpHeaders::operator=(QHttpHeaders &&other) noexcept + + Move-assigns \a other and returns a reference to this object. + + \note The moved-from object \a other is placed in a + partially-formed state, in which the only valid operations are + destruction and assignment of a new value. +*/ + +/*! + \fn void QHttpHeaders::swap(QHttpHeaders &other) + + Swaps this QHttpHeaders with \a other. This function is very fast and + never fails. +*/ + +#ifndef QT_NO_DEBUG_STREAM +/*! + \fn QDebug QHttpHeaders::operator<<(QDebug debug, + const QHttpHeaders &headers) + + Writes \a headers into \a debug stream. +*/ +QDebug operator<<(QDebug debug, const QHttpHeaders &headers) +{ + const QDebugStateSaver saver(debug); + debug.resetFormat().nospace(); + + debug << "QHttpHeaders(headers = "; + const char *separator = ""; + for (const auto &h : headers.d->headers) { + debug << separator << h.name << ':' << h.value; + separator = " | "; + } + debug << ")"; + return debug; +} +#endif + +// A clarification on string encoding: +// Setters and getters only accept names and values that are Latin-1 representable: +// Either they are directly ASCII/Latin-1, or if they are UTF-X, they only use first 256 +// of the unicode points. For example using a '€' (U+20AC) in value would yield a warning +// and the call is ignored. +// Furthermore the 'name' has more strict rules than the 'value' + +// TODO FIXME REMOVEME once this is merged: +// https://codereview.qt-project.org/c/qt/qtbase/+/508829 +static bool isUtf8Latin1Representable(QUtf8StringView s) noexcept +{ + // L1 encoded in UTF8 has at most the form + // - 0b0XXX'XXXX - US-ASCII + // - 0b1100'00XX 0b10XX'XXXX - at most 8 non-zero LSB bits allowed in L1 + bool inMultibyte = false; + for (unsigned char c : s) { + if (c < 128) { // US-ASCII + if (inMultibyte) + return false; // invalid sequence + } else { + // decode as UTF-8: + if ((c & 0b1110'0000) == 0b1100'0000) { // two-octet UTF-8 leader + if (inMultibyte) + return false; // invalid sequence + inMultibyte = true; + const auto bits_7_to_11 = c & 0b0001'1111; + if (bits_7_to_11 < 0b10) + return false; // invalid sequence (US-ASCII encoded in two octets) + if (bits_7_to_11 > 0b11) // more than the two LSB + return false; // outside L1 + } else if ((c & 0b1100'0000) == 0b1000'0000) { // trailing UTF-8 octet + if (!inMultibyte) + return false; // invalid sequence + inMultibyte = false; // only one continuation allowed + } else { + return false; // invalid sequence or outside of L1 + } + } + } + if (inMultibyte) + return false; // invalid sequence: premature end + return true; +} + +static constexpr auto isValidHttpHeaderNameChar = [](uchar c) noexcept +{ + // RFC 9110 Chapters "5.1 Field Names" and "5.6.2 Tokens" + // field-name = token + // token = 1*tchar + // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / + // "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + // / DIGIT / ALPHA + // ; any VCHAR, except delimiters + // (for explanation on VCHAR see isValidHttpHeaderValueChar) + return (('A' <= c && c <= 'Z') + || ('a' <= c && c <= 'z') + || ('0' <= c && c <= '9') + || ('#' <= c && c <= '\'') + || ('^' <= c && c <= '`') + || c == '|' || c == '~' || c == '!' || c == '*' || c == '+' || c == '-' || c == '.'); +}; + +static bool headerNameValidImpl(QLatin1StringView name) noexcept +{ + return std::all_of(name.begin(), name.end(), isValidHttpHeaderNameChar); +} + +static bool headerNameValidImpl(QUtf8StringView name) noexcept +{ + // Traversing the UTF-8 string char-by-char is fine in this case as + // the isValidHttpHeaderNameChar rejects any value above 0x7E. UTF-8 + // only has bytes <= 0x7F if they truly represent that ASCII character. + return headerNameValidImpl(QLatin1StringView(QByteArrayView(name))); +} + +static bool headerNameValidImpl(QStringView name) noexcept +{ + return std::all_of(name.begin(), name.end(), [](QChar c) { + return isValidHttpHeaderNameChar(c.toLatin1()); + }); +} + +static bool isValidHttpHeaderNameField(QAnyStringView name) noexcept +{ + if (name.isEmpty()) { + qCWarning(lcQHttpHeaders, "HTTP header name cannot be empty"); + return false; + } + const bool valid = name.visit([](auto name){ return headerNameValidImpl(name); }); + if (!valid) + qCWarning(lcQHttpHeaders, "HTTP header name contained illegal character(s)"); + return valid; +} + +static constexpr auto isValidHttpHeaderValueChar = [](uchar c) noexcept +{ + // RFC 9110 Chapter 5.5, Field Values + // field-value = *field-content + // field-content = field-vchar + // [ 1*( SP / HTAB / field-vchar ) field-vchar ] + // field-vchar = VCHAR / obs-text + // obs-text = %x80-FF + // VCHAR is defined as "any visible US-ASCII character", and RFC 5234 B.1. + // defines it as %x21-7E + // Note: The ABNF above states that field-content and thus field-value cannot + // start or end with SP/HTAB. The caller should handle this. + return (c >= 0x80 // obs-text (extended ASCII) + || (0x20 <= c && c <= 0x7E) // SP (0x20) + VCHAR + || (c == 0x09)); // HTAB +}; + +static bool headerValueValidImpl(QLatin1StringView value) noexcept +{ + return std::all_of(value.begin(), value.end(), isValidHttpHeaderValueChar); +} + +static bool headerValueValidImpl(QUtf8StringView value) noexcept +{ + if (!isUtf8Latin1Representable(value)) // TODO FIXME see the function + return false; + return std::all_of(value.begin(), value.end(), isValidHttpHeaderValueChar); +} + +static bool headerValueValidImpl(QStringView value) noexcept +{ + return std::all_of(value.begin(), value.end(), [](QChar c) { + return isValidHttpHeaderValueChar(c.toLatin1()); + }); +} + +static bool isValidHttpHeaderValueField(QAnyStringView value) noexcept +{ + const bool valid = value.visit([](auto value){ return headerValueValidImpl(value); }); + if (!valid) + qCWarning(lcQHttpHeaders, "HTTP header value contained illegal character(s)"); + return valid; +} + +static QByteArray fieldToByteArray(QLatin1StringView s) noexcept +{ + return QByteArray(s.data(), s.size()); +} + +static QByteArray fieldToByteArray(QUtf8StringView s) noexcept +{ + return QByteArray(s.data(), s.size()); +} + +static QByteArray fieldToByteArray(QStringView s) +{ + return s.toLatin1(); +} + +static QByteArray normalizedName(QAnyStringView name) +{ + return name.visit([](auto name){ return fieldToByteArray(name); }).toLower(); +} + +static QByteArray normalizedValue(QAnyStringView value) +{ + // Note on trimming away any leading or trailing whitespace of 'value': + // RFC 9110 (HTTP 1.1, 2022, Chapter 5.5) does not allow leading or trailing whitespace + // RFC 7230 (HTTP 1.1, 2014, Chapter 3.2) allows them optionally, but also mandates that + // they are ignored during processing + // RFC 7540 (HTTP/2) does not seem explicit about it + // => for maximum compatibility, trim away any leading or trailing whitespace + return value.visit([](auto value){ return fieldToByteArray(value); }).trimmed(); +} + +static bool headerNameIs(const Header &header, QAnyStringView name) +{ + return header.name == normalizedName(name); +} + +/*! + Appends a header entry with \a name and \a value and returns \c true + if successful. + + \sa append(QHttpHeaders::WellKnownHeader, QAnyStringView) + \sa {Allowed field name and value characters} +*/ +bool QHttpHeaders::append(QAnyStringView name, QAnyStringView value) +{ + if (!isValidHttpHeaderNameField(name) || !isValidHttpHeaderValueField(value)) + return false; + + d.detach(); + d->headers.push_back({normalizedName(name), normalizedValue(value)}); + return true; +} + +/*! + \overload append(QAnyStringView, QAnyStringView) +*/ +bool QHttpHeaders::append(WellKnownHeader name, QAnyStringView value) +{ + if (!isValidHttpHeaderValueField(value)) + return false; + + d.detach(); + d->headers.push_back({headerNames[qToUnderlying(name)], normalizedValue(value)}); + return true; +} + +/*! + Inserts a header entry at index \a i, with \a name and \a value. The index + must be valid (see \l size()). Returns whether the insert succeeded. + + \sa append(), + insert(qsizetype, QHttpHeaders::WellKnownHeader, QAnyStringView), size() + \sa {Allowed field name and value characters} +*/ +bool QHttpHeaders::insert(qsizetype i, QAnyStringView name, QAnyStringView value) +{ + d->verify(i, 0); + if (!isValidHttpHeaderNameField(name) || !isValidHttpHeaderValueField(value)) + return false; + + d.detach(); + d->headers.insert(i, {normalizedName(name), normalizedValue(value)}); + return true; +} + +/*! + \overload insert(qsizetype, QAnyStringView, QAnyStringView) +*/ +bool QHttpHeaders::insert(qsizetype i, WellKnownHeader name, QAnyStringView value) +{ + d->verify(i, 0); + if (!isValidHttpHeaderValueField(value)) + return false; + + d.detach(); + d->headers.insert(i, {headerNames[qToUnderlying(name)], normalizedValue(value)}); + return true; +} + +/*! + Replaces the header entry at index \a i, with \a name and \a value. + The index must be valid (see \l size()). Returns whether the replace + succeeded. + + \sa append(), + replace(qsizetype, QHttpHeaders::WellKnownHeader, QAnyStringView), size() + \sa {Allowed field name and value characters} +*/ +bool QHttpHeaders::replace(qsizetype i, QAnyStringView name, QAnyStringView value) +{ + d->verify(i); + if (!isValidHttpHeaderNameField(name) || !isValidHttpHeaderValueField(value)) + return false; + + d.detach(); + d->headers.replace(i, {normalizedName(name), normalizedValue(value)}); + return true; +} + +/*! + \overload replace(qsizetype, QAnyStringView, QAnyStringView) +*/ +bool QHttpHeaders::replace(qsizetype i, WellKnownHeader name, QAnyStringView value) +{ + d->verify(i); + if (!isValidHttpHeaderValueField(value)) + return false; + + d.detach(); + d->headers.replace(i, {headerNames[qToUnderlying(name)], normalizedValue(value)}); + return true; +} + +/*! + Returns whether the headers contain header with \a name. + + \sa has(QHttpHeaders::WellKnownHeader) +*/ +bool QHttpHeaders::has(QAnyStringView name) const +{ + return std::any_of(d->headers.cbegin(), d->headers.cend(), + [&name](const Header &header) { return headerNameIs(header, name); }); +} + +/*! + \overload has(QAnyStringView) +*/ +bool QHttpHeaders::has(WellKnownHeader name) const +{ + return has(headerNames[qToUnderlying(name)]); +} + +/*! + Returns a list of unique header names. + Header names are case-insensitive, and the returned + names are lower-cased. +*/ +QList<QByteArray> QHttpHeaders::names() const +{ + QList<QByteArray> names; + for (const Header &header: d->headers) { + if (!names.contains(header.name)) + names.append(header.name); + } + return names; +} + +/*! + Removes the header \a name. + + \sa removeAt(), removeAll(QHttpHeaders::WellKnownHeader) +*/ +void QHttpHeaders::removeAll(QAnyStringView name) +{ + if (has(name)) { + d.detach(); + d->headers.removeIf([&name](const Header &header){ + return headerNameIs(header, name); + }); + } +} + +/*! + \overload removeAll(QAnyStringView) +*/ +void QHttpHeaders::removeAll(WellKnownHeader name) +{ + removeAll(headerNames[qToUnderlying(name)]); +} + +/*! + Removes the header at index \a i. The index \a i must be valid + (see \l size()). + + \sa removeAll(QHttpHeaders::WellKnownHeader), + removeAll(QAnyStringView), size() +*/ +void QHttpHeaders::removeAt(qsizetype i) +{ + d->verify(i); + d.detach(); + d->headers.removeAt(i); +} + +/*! + Returns the values of header \a name in a list. Returns an empty + list if header with \a name doesn't exist. + + \sa values(QHttpHeaders::WellKnownHeader) +*/ +QList<QByteArray> QHttpHeaders::values(QAnyStringView name) const +{ + QList<QByteArray> values; + for (const auto &h : std::as_const(d->headers)) { + if (headerNameIs(h, name)) + values.append(h.value); + } + return values; +} + +/*! + \overload values(QAnyStringView) +*/ +QList<QByteArray> QHttpHeaders::values(WellKnownHeader name) const +{ + return values(headerNames[qToUnderlying(name)]); +} + +/*! + Returns the values of header \a name in a comma-combined string. + Returns a \c null QByteArray if the header with \a name doesn't + exist. + + \note Accessing the value(s) of 'Set-Cookie' header this way may not work + as intended. It is a notable exception in the + \l {https://datatracker.ietf.org/doc/html/rfc9110#name-field-order}{HTTP RFC} + in that its values cannot be combined this way. Prefer \l values() instead. + + \sa values(QAnyStringView) +*/ +QByteArray QHttpHeaders::combinedValue(QAnyStringView name) const +{ + QByteArray result; + + const char* separator = ""; + auto valueList = values(name); + for (const auto &v : valueList) { + result.append(separator); + result.append(v); + separator = ","; + } + return result; +} + +/*! + \overload combinedValue(QAnyStringView) +*/ +QByteArray QHttpHeaders::combinedValue(WellKnownHeader name) const +{ + return combinedValue(headerNames[qToUnderlying(name)]); +} + +/*! + Returns the number of header entries. +*/ +qsizetype QHttpHeaders::size() const noexcept +{ + return d->headers.size(); +} + +/*! + Compares this instance with \a other and returns \c true if they + are considered equal in accordance with the provided \a options. + + The header names are always compared as case-insensitive, and values + as case-sensitive. For example \e Accept and \e ACCEPT header names + are considered equal, while values \e something and \e SOMETHING are not. +*/ +bool QHttpHeaders::equals(const QHttpHeaders &other, CompareOptions options) const noexcept +{ + return d == other.d || d->equals(*other.d, options); +} + +/*! + Returns the header entries as a list of (name, value) pairs. + Header names are case-insensitive, and the returned names are lower-cased. +*/ +QList<std::pair<QByteArray, QByteArray>> QHttpHeaders::toListOfPairs() const +{ + QList<std::pair<QByteArray, QByteArray>> list; + list.reserve(size()); + for (const auto & h : std::as_const(d->headers)) + list.append({h.name, h.value}); + return list; +} + +/*! + Returns the header entries as a map from name to value(s). + Header names are case-insensitive, and the returned names are lower-cased. +*/ +QMultiMap<QByteArray, QByteArray> QHttpHeaders::toMultiMap() const +{ + QMultiMap<QByteArray, QByteArray> map; + for (const auto &h : std::as_const(d->headers)) + map.insert(h.name, h.value); + return map; +} + +/*! + Returns the header entries as a hash from name to value(s). + Header names are case-insensitive, and the returned names are lower-cased. +*/ +QMultiHash<QByteArray, QByteArray> QHttpHeaders::toMultiHash() const +{ + QMultiHash<QByteArray, QByteArray> hash; + hash.reserve(size()); + for (const auto &h : std::as_const(d->headers)) + hash.insert(h.name, h.value); + return hash; +} + +/*! + Clears all header entries. + + \sa size() +*/ +void QHttpHeaders::clear() +{ + if (d->headers.isEmpty()) + return; + d.detach(); + d->headers.clear(); +} + +QT_END_NAMESPACE diff --git a/src/network/access/qhttpheaders.h b/src/network/access/qhttpheaders.h new file mode 100644 index 0000000000..636f535c88 --- /dev/null +++ b/src/network/access/qhttpheaders.h @@ -0,0 +1,276 @@ +// Copyright (C) 2023 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 QHTTPHEADERS_H +#define QHTTPHEADERS_H + +#include <QtNetwork/qnetworkrequest.h> +#include <QtCore/qshareddata.h> +#include <QtCore/qcontainerfwd.h> + +#include <initializer_list> + +QT_BEGIN_NAMESPACE + +class QDebug; + +class QHttpHeadersPrivate; +QT_DECLARE_QESDP_SPECIALIZATION_DTOR_WITH_EXPORT(QHttpHeadersPrivate, Q_NETWORK_EXPORT) +class QHttpHeaders +{ + Q_GADGET_EXPORT(Q_NETWORK_EXPORT) +public: + enum class WellKnownHeader { + // IANA Permanent status: + AIM, + Accept, + AcceptAdditions, + AcceptCH, + AcceptDatetime, + AcceptEncoding, + AcceptFeatures, + AcceptLanguage, + AcceptPatch, + AcceptPost, + AcceptRanges, + AcceptSignature, + AccessControlAllowCredentials, + AccessControlAllowHeaders, + AccessControlAllowMethods, + AccessControlAllowOrigin, + AccessControlExposeHeaders, + AccessControlMaxAge, + AccessControlRequestHeaders, + AccessControlRequestMethod, + Age, + Allow, + ALPN, + AltSvc, + AltUsed, + Alternates, + ApplyToRedirectRef, + AuthenticationControl, + AuthenticationInfo, + Authorization, + CacheControl, + CacheStatus, + CalManagedID, + CalDAVTimezones, + CapsuleProtocol, + CDNCacheControl, + CDNLoop, + CertNotAfter, + CertNotBefore, + ClearSiteData, + ClientCert, + ClientCertChain, + Close, + Connection, + ContentDigest, + ContentDisposition, + ContentEncoding, + ContentID, + ContentLanguage, + ContentLength, + ContentLocation, + ContentRange, + ContentSecurityPolicy, + ContentSecurityPolicyReportOnly, + ContentType, + Cookie, + CrossOriginEmbedderPolicy, + CrossOriginEmbedderPolicyReportOnly, + CrossOriginOpenerPolicy, + CrossOriginOpenerPolicyReportOnly, + CrossOriginResourcePolicy, + DASL, + Date, + DAV, + DeltaBase, + Depth, + Destination, + DifferentialID, + DPoP, + DPoPNonce, + EarlyData, + ETag, + Expect, + ExpectCT, + Expires, + Forwarded, + From, + Hobareg, + Host, + If, + IfMatch, + IfModifiedSince, + IfNoneMatch, + IfRange, + IfScheduleTagMatch, + IfUnmodifiedSince, + IM, + IncludeReferredTokenBindingID, + KeepAlive, + Label, + LastEventID, + LastModified, + Link, + Location, + LockToken, + MaxForwards, + MementoDatetime, + Meter, + MIMEVersion, + Negotiate, + NEL, + ODataEntityId, + ODataIsolation, + ODataMaxVersion, + ODataVersion, + OptionalWWWAuthenticate, + OrderingType, + Origin, + OriginAgentCluster, + OSCORE, + OSLCCoreVersion, + Overwrite, + PingFrom, + PingTo, + Position, + Prefer, + PreferenceApplied, + Priority, + ProxyAuthenticate, + ProxyAuthenticationInfo, + ProxyAuthorization, + ProxyStatus, + PublicKeyPins, + PublicKeyPinsReportOnly, + Range, + RedirectRef, + Referer, + Refresh, + ReplayNonce, + ReprDigest, + RetryAfter, + ScheduleReply, + ScheduleTag, + SecPurpose, + SecTokenBinding, + SecWebSocketAccept, + SecWebSocketExtensions, + SecWebSocketKey, + SecWebSocketProtocol, + SecWebSocketVersion, + Server, + ServerTiming, + SetCookie, + Signature, + SignatureInput, + SLUG, + SoapAction, + StatusURI, + StrictTransportSecurity, + Sunset, + SurrogateCapability, + SurrogateControl, + TCN, + TE, + Timeout, + Topic, + Traceparent, + Tracestate, + Trailer, + TransferEncoding, + TTL, + Upgrade, + Urgency, + UserAgent, + VariantVary, + Vary, + Via, + WantContentDigest, + WantReprDigest, + WWWAuthenticate, + XContentTypeOptions, + XFrameOptions, + // IANA Deprecated status: + AcceptCharset, + CPEPInfo, + Pragma, + ProtocolInfo, + ProtocolQuery, + }; + Q_ENUM(WellKnownHeader) + + enum class CompareOption { + OrderInsensitive = 0x01, + OrderSensitive = 0x02, + }; + Q_DECLARE_FLAGS(CompareOptions, CompareOption) + + Q_NETWORK_EXPORT QHttpHeaders(); + Q_NETWORK_EXPORT ~QHttpHeaders(); + + Q_NETWORK_EXPORT QHttpHeaders(const QHttpHeaders &other); + QHttpHeaders(QHttpHeaders &&other) noexcept = default; + Q_NETWORK_EXPORT QHttpHeaders &operator=(const QHttpHeaders &other); + QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_PURE_SWAP(QHttpHeaders) + void swap(QHttpHeaders &other) noexcept { d.swap(other.d); } + + Q_NETWORK_EXPORT bool append(QAnyStringView name, QAnyStringView value); + Q_NETWORK_EXPORT bool append(WellKnownHeader name, QAnyStringView value); + + Q_NETWORK_EXPORT bool insert(qsizetype i, QAnyStringView name, QAnyStringView value); + Q_NETWORK_EXPORT bool insert(qsizetype i, WellKnownHeader name, QAnyStringView value); + + Q_NETWORK_EXPORT bool replace(qsizetype i, QAnyStringView name, QAnyStringView value); + Q_NETWORK_EXPORT bool replace(qsizetype i, WellKnownHeader name, QAnyStringView value); + + Q_NETWORK_EXPORT bool has(QAnyStringView name) const; + Q_NETWORK_EXPORT bool has(WellKnownHeader name) const; + + Q_NETWORK_EXPORT QList<QByteArray> names() const; + + Q_NETWORK_EXPORT void clear(); + Q_NETWORK_EXPORT void removeAll(QAnyStringView name); + Q_NETWORK_EXPORT void removeAll(WellKnownHeader name); + Q_NETWORK_EXPORT void removeAt(qsizetype i); + + Q_NETWORK_EXPORT QList<QByteArray> values(QAnyStringView name) const; + Q_NETWORK_EXPORT QList<QByteArray> values(WellKnownHeader name) const; + + Q_NETWORK_EXPORT QByteArray combinedValue(QAnyStringView name) const; + Q_NETWORK_EXPORT QByteArray combinedValue(WellKnownHeader name) const; + + Q_NETWORK_EXPORT qsizetype size() const noexcept; + bool isEmpty() const noexcept { return size() == 0; } + + Q_NETWORK_EXPORT bool equals(const QHttpHeaders &other, + CompareOptions options = CompareOption::OrderInsensitive) const noexcept; + + Q_NETWORK_EXPORT static QHttpHeaders + fromListOfPairs(const QList<std::pair<QByteArray, QByteArray>> &headers); + Q_NETWORK_EXPORT static QHttpHeaders + fromMultiMap(const QMultiMap<QByteArray, QByteArray> &headers); + Q_NETWORK_EXPORT static QHttpHeaders + fromMultiHash(const QMultiHash<QByteArray, QByteArray> &headers); + + Q_NETWORK_EXPORT QList<std::pair<QByteArray, QByteArray>> toListOfPairs() const; + Q_NETWORK_EXPORT QMultiMap<QByteArray, QByteArray> toMultiMap() const; + Q_NETWORK_EXPORT QMultiHash<QByteArray, QByteArray> toMultiHash() const; + +private: +#ifndef QT_NO_DEBUG_STREAM + friend Q_NETWORK_EXPORT QDebug operator<<(QDebug debug, const QHttpHeaders &headers); +#endif + QExplicitlySharedDataPointer<QHttpHeadersPrivate> d; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(QHttpHeaders::CompareOptions) + +Q_DECLARE_SHARED(QHttpHeaders) + +QT_END_NAMESPACE + +#endif // QHTTPHEADERS_H |