From b0726e864141a09f2968351dbefa03574d9090d4 Mon Sep 17 00:00:00 2001 From: Jan Murawski Date: Fri, 28 Jul 2017 16:03:01 +0200 Subject: Add HTTP caching headers to KnownHeaders QNetworkRequest is already aware of the Last-Modified header but has been lacking support for the If-Modified-Since, ETag, If-Match and If-None-Match headers. These headers are used with HTTP to signal conditional download requests. See RFC 7232 for more information. Change-Id: I248577b28e875fafd3e4c44fb31e8d712b6c14f1 Reviewed-by: Edward Welbourne Reviewed-by: Anton Kudryavtsev Reviewed-by: Thiago Macieira --- src/network/access/qnetworkrequest.cpp | 114 +++++++++++++++++++++ src/network/access/qnetworkrequest.h | 6 +- .../access/qnetworkrequest/tst_qnetworkrequest.cpp | 96 +++++++++++++++++ 3 files changed, 215 insertions(+), 1 deletion(-) diff --git a/src/network/access/qnetworkrequest.cpp b/src/network/access/qnetworkrequest.cpp index 1d7c5bec51..2d6df9de21 100644 --- a/src/network/access/qnetworkrequest.cpp +++ b/src/network/access/qnetworkrequest.cpp @@ -98,6 +98,25 @@ QT_BEGIN_NAMESPACE header and contains a QDateTime representing the last modification date of the contents. + \value IfModifiedSinceHeader Corresponds to the HTTP If-Modified-Since + header and contains a QDateTime. It is usually added to a + QNetworkRequest. The server shall send a 304 (Not Modified) response + if the resource has not changed since this time. + + \value ETagHeader Corresponds to the HTTP ETag + header and contains a QString representing the last modification + state of the contents. + + \value IfMatchHeader Corresponds to the HTTP If-Match + header and contains a QStringList. It is usually added to a + QNetworkRequest. The server shall send a 412 (Precondition Failed) + response if the resource does not match. + + \value IfNoneMatchHeader Corresponds to the HTTP If-None-Match + header and contains a QStringList. It is usually added to a + QNetworkRequest. The server shall send a 304 (Not Modified) response + if the resource does match. + \value CookieHeader Corresponds to the HTTP Cookie header and contains a QList representing the cookies to be sent back to the server. @@ -788,6 +807,18 @@ static QByteArray headerName(QNetworkRequest::KnownHeaders header) case QNetworkRequest::LastModifiedHeader: return "Last-Modified"; + case QNetworkRequest::IfModifiedSinceHeader: + return "If-Modified-Since"; + + case QNetworkRequest::ETagHeader: + return "ETag"; + + case QNetworkRequest::IfMatchHeader: + return "If-Match"; + + case QNetworkRequest::IfNoneMatchHeader: + return "If-None-Match"; + case QNetworkRequest::CookieHeader: return "Cookie"; @@ -818,6 +849,9 @@ static QByteArray headerValue(QNetworkRequest::KnownHeaders header, const QVaria case QNetworkRequest::ContentDispositionHeader: case QNetworkRequest::UserAgentHeader: case QNetworkRequest::ServerHeader: + case QNetworkRequest::ETagHeader: + case QNetworkRequest::IfMatchHeader: + case QNetworkRequest::IfNoneMatchHeader: return value.toByteArray(); case QNetworkRequest::LocationHeader: @@ -830,6 +864,7 @@ static QByteArray headerValue(QNetworkRequest::KnownHeaders header, const QVaria } case QNetworkRequest::LastModifiedHeader: + case QNetworkRequest::IfModifiedSinceHeader: switch (value.userType()) { case QMetaType::QDate: case QMetaType::QDateTime: @@ -891,6 +926,20 @@ static int parseHeaderName(const QByteArray &headerName) return QNetworkRequest::CookieHeader; break; + case 'e': + if (qstricmp(headerName.constData(), "etag") == 0) + return QNetworkRequest::ETagHeader; + break; + + case 'i': + if (qstricmp(headerName.constData(), "if-modified-since") == 0) + return QNetworkRequest::IfModifiedSinceHeader; + if (qstricmp(headerName.constData(), "if-match") == 0) + return QNetworkRequest::IfMatchHeader; + if (qstricmp(headerName.constData(), "if-none-match") == 0) + return QNetworkRequest::IfNoneMatchHeader; + break; + case 'l': if (qstricmp(headerName.constData(), "location") == 0) return QNetworkRequest::LocationHeader; @@ -937,6 +986,61 @@ static QVariant parseCookieHeader(const QByteArray &raw) return QVariant::fromValue(result); } +static QVariant parseETag(const QByteArray &raw) +{ + const QByteArray trimmed = raw.trimmed(); + if (!trimmed.startsWith('"') && !trimmed.startsWith(R"(W/")")) + return QVariant(); + + if (!trimmed.endsWith('"')) + return QVariant(); + + return QString::fromLatin1(trimmed); +} + +static QVariant parseIfMatch(const QByteArray &raw) +{ + const QByteArray trimmedRaw = raw.trimmed(); + if (trimmedRaw == "*") + return QStringList(QStringLiteral("*")); + + QStringList tags; + const QList split = trimmedRaw.split(','); + for (const QByteArray &element : split) { + const QByteArray trimmed = element.trimmed(); + if (!trimmed.startsWith('"')) + continue; + + if (!trimmed.endsWith('"')) + continue; + + tags += QString::fromLatin1(trimmed); + } + return tags; +} + +static QVariant parseIfNoneMatch(const QByteArray &raw) +{ + const QByteArray trimmedRaw = raw.trimmed(); + if (trimmedRaw == "*") + return QStringList(QStringLiteral("*")); + + QStringList tags; + const QList split = trimmedRaw.split(','); + for (const QByteArray &element : split) { + const QByteArray trimmed = element.trimmed(); + if (!trimmed.startsWith('"') && !trimmed.startsWith(R"(W/")")) + continue; + + if (!trimmed.endsWith('"')) + continue; + + tags += QString::fromLatin1(trimmed); + } + return tags; +} + + static QVariant parseHeaderValue(QNetworkRequest::KnownHeaders header, const QByteArray &value) { // header is always a valid value @@ -963,8 +1067,18 @@ static QVariant parseHeaderValue(QNetworkRequest::KnownHeaders header, const QBy } case QNetworkRequest::LastModifiedHeader: + case QNetworkRequest::IfModifiedSinceHeader: return parseHttpDate(value); + case QNetworkRequest::ETagHeader: + return parseETag(value); + + case QNetworkRequest::IfMatchHeader: + return parseIfMatch(value); + + case QNetworkRequest::IfNoneMatchHeader: + return parseIfNoneMatch(value); + case QNetworkRequest::CookieHeader: return parseCookieHeader(value); diff --git a/src/network/access/qnetworkrequest.h b/src/network/access/qnetworkrequest.h index e104c139d9..8462eae8c8 100644 --- a/src/network/access/qnetworkrequest.h +++ b/src/network/access/qnetworkrequest.h @@ -63,7 +63,11 @@ public: SetCookieHeader, ContentDispositionHeader, // added for QMultipartMessage UserAgentHeader, - ServerHeader + ServerHeader, + IfModifiedSinceHeader, + ETagHeader, + IfMatchHeader, + IfNoneMatchHeader }; enum Attribute { HttpStatusCodeAttribute, diff --git a/tests/auto/network/access/qnetworkrequest/tst_qnetworkrequest.cpp b/tests/auto/network/access/qnetworkrequest/tst_qnetworkrequest.cpp index bc9144e40e..8e063733e9 100644 --- a/tests/auto/network/access/qnetworkrequest/tst_qnetworkrequest.cpp +++ b/tests/auto/network/access/qnetworkrequest/tst_qnetworkrequest.cpp @@ -236,6 +236,46 @@ void tst_QNetworkRequest::setHeader_data() << true << "Last-Modified" << "Thu, 01 Nov 2007 18:08:30 GMT"; + QTest::newRow("If-Modified-Since-Date") << QNetworkRequest::IfModifiedSinceHeader + << QVariant(QDate(2017, 7, 01)) + << true << "If-Modified-Since" + << "Sat, 01 Jul 2017 00:00:00 GMT"; + QTest::newRow("If-Modified-Since-DateTime") << QNetworkRequest::IfModifiedSinceHeader + << QVariant(QDateTime(QDate(2017, 7, 01), + QTime(3, 14, 15), + Qt::UTC)) + << true << "If-Modified-Since" + << "Sat, 01 Jul 2017 03:14:15 GMT"; + + QTest::newRow("Etag-strong") << QNetworkRequest::ETagHeader << QVariant(R"("xyzzy")") + << true << "ETag" << R"("xyzzy")"; + QTest::newRow("Etag-weak") << QNetworkRequest::ETagHeader << QVariant(R"(W/"xyzzy")") + << true << "ETag" << R"(W/"xyzzy")"; + QTest::newRow("Etag-empty") << QNetworkRequest::ETagHeader << QVariant(R"("")") + << true << "ETag" << R"("")"; + + QTest::newRow("If-Match-empty") << QNetworkRequest::IfMatchHeader << QVariant(R"("")") + << true << "If-Match" << R"("")"; + QTest::newRow("If-Match-any") << QNetworkRequest::IfMatchHeader << QVariant(R"("*")") + << true << "If-Match" << R"("*")"; + QTest::newRow("If-Match-single") << QNetworkRequest::IfMatchHeader << QVariant(R"("xyzzy")") + << true << "If-Match" << R"("xyzzy")"; + QTest::newRow("If-Match-multiple") << QNetworkRequest::IfMatchHeader + << QVariant(R"("xyzzy", "r2d2xxxx", "c3piozzzz")") + << true << "If-Match" + << R"("xyzzy", "r2d2xxxx", "c3piozzzz")"; + + QTest::newRow("If-None-Match-empty") << QNetworkRequest::IfNoneMatchHeader << QVariant(R"("")") + << true << "If-None-Match" << R"("")"; + QTest::newRow("If-None-Match-any") << QNetworkRequest::IfNoneMatchHeader << QVariant(R"("*")") + << true << "If-None-Match" << R"("*")"; + QTest::newRow("If-None-Match-single") << QNetworkRequest::IfNoneMatchHeader << QVariant(R"("xyzzy")") + << true << "If-None-Match" << R"("xyzzy")"; + QTest::newRow("If-None-Match-multiple") << QNetworkRequest::IfNoneMatchHeader + << QVariant(R"("xyzzy", W/"r2d2xxxx", "c3piozzzz")") + << true << "If-None-Match" + << R"("xyzzy", W/"r2d2xxxx", "c3piozzzz")"; + QNetworkCookie cookie; cookie.setName("a"); cookie.setValue("b"); @@ -327,6 +367,62 @@ void tst_QNetworkRequest::rawHeaderParsing_data() << true << "Last-Modified" << "Sun Nov 6 08:49:37 1994"; + QTest::newRow("If-Modified-Since-RFC1123") << QNetworkRequest::IfModifiedSinceHeader + << QVariant(QDateTime(QDate(1994, 8, 06), + QTime(8, 49, 37), + Qt::UTC)) + << true << "If-Modified-Since" + << "Sun, 06 Aug 1994 08:49:37 GMT"; + QTest::newRow("If-Modified-Since-RFC850") << QNetworkRequest::IfModifiedSinceHeader + << QVariant(QDateTime(QDate(1994, 8, 06), + QTime(8, 49, 37), + Qt::UTC)) + << true << "If-Modified-Since" + << "Sunday, 06-Aug-94 08:49:37 GMT"; + QTest::newRow("If-Modified-Since-asctime") << QNetworkRequest::IfModifiedSinceHeader + << QVariant(QDateTime(QDate(1994, 8, 06), + QTime(8, 49, 37), + Qt::UTC)) + << true << "If-Modified-Since" + << "Sun Aug 6 08:49:37 1994"; + + QTest::newRow("Etag-strong") << QNetworkRequest::ETagHeader << QVariant(R"("xyzzy")") + << true << "ETag" << R"("xyzzy")"; + QTest::newRow("Etag-weak") << QNetworkRequest::ETagHeader << QVariant(R"(W/"xyzzy")") + << true << "ETag" << R"(W/"xyzzy")"; + QTest::newRow("Etag-empty") << QNetworkRequest::ETagHeader << QVariant(R"("")") + << true << "ETag" << R"("")"; + + QTest::newRow("If-Match-empty") << QNetworkRequest::IfMatchHeader << QVariant(QStringList(R"("")")) + << true << "If-Match" << R"("")"; + QTest::newRow("If-Match-any") << QNetworkRequest::IfMatchHeader << QVariant(QStringList(R"("*")")) + << true << "If-Match" << R"("*")"; + QTest::newRow("If-Match-single") << QNetworkRequest::IfMatchHeader + << QVariant(QStringList(R"("xyzzy")")) + << true << "If-Match" << R"("xyzzy")"; + QTest::newRow("If-Match-multiple") << QNetworkRequest::IfMatchHeader + << QVariant(QStringList({R"("xyzzy")", + R"("r2d2xxxx")", + R"("c3piozzzz")"})) + << true << "If-Match" + << R"("xyzzy", "r2d2xxxx", "c3piozzzz")"; + + QTest::newRow("If-None-Match-empty") << QNetworkRequest::IfNoneMatchHeader + << QVariant(QStringList(R"("")")) + << true << "If-None-Match" << R"("")"; + QTest::newRow("If-None-Match-any") << QNetworkRequest::IfNoneMatchHeader + << QVariant(QStringList(R"("*")")) + << true << "If-None-Match" << R"("*")"; + QTest::newRow("If-None-Match-single") << QNetworkRequest::IfNoneMatchHeader + << QVariant(QStringList(R"("xyzzy")")) + << true << "If-None-Match" << R"("xyzzy")"; + QTest::newRow("If-None-Match-multiple") << QNetworkRequest::IfNoneMatchHeader + << QVariant(QStringList({R"("xyzzy")", + R"(W/"r2d2xxxx")", + R"("c3piozzzz")"})) + << true << "If-None-Match" + << R"("xyzzy", W/"r2d2xxxx", "c3piozzzz")"; + QTest::newRow("Content-Length-invalid1") << QNetworkRequest::ContentLengthHeader << QVariant() << false << "Content-Length" << "1a"; QTest::newRow("Content-Length-invalid2") << QNetworkRequest::ContentLengthHeader << QVariant() -- cgit v1.2.3