1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qnetworkcookie.h"
5#include "qnetworkcookie_p.h"
6
7#include "qnetworkrequest.h"
8#include "qnetworkreply.h"
9#include "QtCore/qbytearray.h"
10#include "QtCore/qdatetime.h"
11#include "QtCore/qdebug.h"
12#include "QtCore/qlist.h"
13#include "QtCore/qlocale.h"
14#include <QtCore/qregularexpression.h>
15#include "QtCore/qstring.h"
16#include "QtCore/qstringlist.h"
17#include "QtCore/qtimezone.h"
18#include "QtCore/qurl.h"
19#include "QtNetwork/qhostaddress.h"
20#include "private/qobject_p.h"
21
22QT_BEGIN_NAMESPACE
23
24using namespace Qt::StringLiterals;
25
26QT_IMPL_METATYPE_EXTERN(QNetworkCookie)
27
28/*!
29 \class QNetworkCookie
30 \since 4.4
31 \ingroup shared
32 \inmodule QtNetwork
33
34 \brief The QNetworkCookie class holds one network cookie.
35
36 Cookies are small bits of information that stateless protocols
37 like HTTP use to maintain some persistent information across
38 requests.
39
40 A cookie is set by a remote server when it replies to a request
41 and it expects the same cookie to be sent back when further
42 requests are sent.
43
44 QNetworkCookie holds one such cookie as received from the
45 network. A cookie has a name and a value, but those are opaque to
46 the application (that is, the information stored in them has no
47 meaning to the application). A cookie has an associated path name
48 and domain, which indicate when the cookie should be sent again to
49 the server.
50
51 A cookie can also have an expiration date, indicating its
52 validity. If the expiration date is not present, the cookie is
53 considered a "session cookie" and should be discarded when the
54 application exits (or when its concept of session is over).
55
56 QNetworkCookie provides a way of parsing a cookie from the HTTP
57 header format using the QNetworkCookie::parseCookies()
58 function. However, when received in a QNetworkReply, the cookie is
59 already parsed.
60
61 This class implements cookies as described by the
62 \l{Netscape Cookie Specification}{initial cookie specification by
63 Netscape}, which is somewhat similar to the \l{http://www.rfc-editor.org/rfc/rfc2109.txt}{RFC 2109} specification,
64 plus the \l{Mitigating Cross-site Scripting With HTTP-only Cookies}
65 {"HttpOnly" extension}. The more recent \l{http://www.rfc-editor.org/rfc/rfc2965.txt}{RFC 2965} specification
66 (which uses the Set-Cookie2 header) is not supported.
67
68 \sa QNetworkCookieJar, QNetworkRequest, QNetworkReply
69*/
70
71/*!
72 Create a new QNetworkCookie object, initializing the cookie name
73 to \a name and its value to \a value.
74
75 A cookie is only valid if it has a name. However, the value is
76 opaque to the application and being empty may have significance to
77 the remote server.
78*/
79QNetworkCookie::QNetworkCookie(const QByteArray &name, const QByteArray &value)
80 : d(new QNetworkCookiePrivate)
81{
82 qRegisterMetaType<QNetworkCookie>();
83 qRegisterMetaType<QList<QNetworkCookie> >();
84
85 d->name = name;
86 d->value = value;
87}
88
89/*!
90 Creates a new QNetworkCookie object by copying the contents of \a
91 other.
92*/
93QNetworkCookie::QNetworkCookie(const QNetworkCookie &other)
94 : d(other.d)
95{
96}
97
98/*!
99 Destroys this QNetworkCookie object.
100*/
101QNetworkCookie::~QNetworkCookie()
102{
103 // QSharedDataPointer auto deletes
104 d = nullptr;
105}
106
107/*!
108 Copies the contents of the QNetworkCookie object \a other to this
109 object.
110*/
111QNetworkCookie &QNetworkCookie::operator=(const QNetworkCookie &other)
112{
113 d = other.d;
114 return *this;
115}
116
117/*!
118 \fn void QNetworkCookie::swap(QNetworkCookie &other)
119 \since 5.0
120 \memberswap{cookie}
121*/
122
123/*!
124 \fn bool QNetworkCookie::operator!=(const QNetworkCookie &other) const
125
126 Returns \c true if this cookie is not equal to \a other.
127
128 \sa operator==()
129*/
130
131/*!
132 \since 5.0
133 Returns \c true if this cookie is equal to \a other. This function
134 only returns \c true if all fields of the cookie are the same.
135
136 However, in some contexts, two cookies of the same name could be
137 considered equal.
138
139 \sa operator!=(), hasSameIdentifier()
140*/
141bool QNetworkCookie::operator==(const QNetworkCookie &other) const
142{
143 if (d == other.d)
144 return true;
145 return d->name == other.d->name &&
146 d->value == other.d->value &&
147 d->expirationDate.toUTC() == other.d->expirationDate.toUTC() &&
148 d->domain == other.d->domain &&
149 d->path == other.d->path &&
150 d->secure == other.d->secure &&
151 d->comment == other.d->comment &&
152 d->sameSite == other.d->sameSite;
153}
154
155/*!
156 Returns \c true if this cookie has the same identifier tuple as \a other.
157 The identifier tuple is composed of the name, domain and path.
158
159 \sa operator==()
160*/
161bool QNetworkCookie::hasSameIdentifier(const QNetworkCookie &other) const
162{
163 return d->name == other.d->name && d->domain == other.d->domain && d->path == other.d->path;
164}
165
166/*!
167 Returns \c true if the "secure" option was specified in the cookie
168 string, false otherwise.
169
170 Secure cookies may contain private information and should not be
171 resent over unencrypted connections.
172
173 \sa setSecure()
174*/
175bool QNetworkCookie::isSecure() const
176{
177 return d->secure;
178}
179
180/*!
181 Sets the secure flag of this cookie to \a enable.
182
183 Secure cookies may contain private information and should not be
184 resent over unencrypted connections.
185
186 \sa isSecure()
187*/
188void QNetworkCookie::setSecure(bool enable)
189{
190 d->secure = enable;
191}
192
193/*!
194 Returns the "SameSite" option if specified in the cookie
195 string, \c SameSite::Default if not present.
196
197 \since 6.1
198 \sa setSameSitePolicy()
199*/
200QNetworkCookie::SameSite QNetworkCookie::sameSitePolicy() const
201{
202 return d->sameSite;
203}
204
205/*!
206 Sets the "SameSite" option of this cookie to \a sameSite.
207
208 \since 6.1
209 \sa sameSitePolicy()
210*/
211void QNetworkCookie::setSameSitePolicy(QNetworkCookie::SameSite sameSite)
212{
213 d->sameSite = sameSite;
214}
215
216/*!
217 \since 4.5
218
219 Returns \c true if the "HttpOnly" flag is enabled for this cookie.
220
221 A cookie that is "HttpOnly" is only set and retrieved by the
222 network requests and replies; i.e., the HTTP protocol. It is not
223 accessible from scripts running on browsers.
224
225 \sa isSecure()
226*/
227bool QNetworkCookie::isHttpOnly() const
228{
229 return d->httpOnly;
230}
231
232/*!
233 \since 4.5
234
235 Sets this cookie's "HttpOnly" flag to \a enable.
236*/
237void QNetworkCookie::setHttpOnly(bool enable)
238{
239 d->httpOnly = enable;
240}
241
242/*!
243 Returns \c true if this cookie is a session cookie. A session cookie
244 is a cookie which has no expiration date, which means it should be
245 discarded when the application's concept of session is over
246 (usually, when the application exits).
247
248 \sa expirationDate(), setExpirationDate()
249*/
250bool QNetworkCookie::isSessionCookie() const
251{
252 return !d->expirationDate.isValid();
253}
254
255/*!
256 Returns the expiration date for this cookie. If this cookie is a
257 session cookie, the QDateTime returned will not be valid. If the
258 date is in the past, this cookie has already expired and should
259 not be sent again back to a remote server.
260
261 The expiration date corresponds to the parameters of the "expires"
262 entry in the cookie string.
263
264 \sa isSessionCookie(), setExpirationDate()
265*/
266QDateTime QNetworkCookie::expirationDate() const
267{
268 return d->expirationDate;
269}
270
271/*!
272 Sets the expiration date of this cookie to \a date. Setting an
273 invalid expiration date to this cookie will mean it's a session
274 cookie.
275
276 \sa isSessionCookie(), expirationDate()
277*/
278void QNetworkCookie::setExpirationDate(const QDateTime &date)
279{
280 d->expirationDate = date;
281}
282
283/*!
284 Returns the domain this cookie is associated with. This
285 corresponds to the "domain" field of the cookie string.
286
287 Note that the domain here may start with a dot, which is not a
288 valid hostname. However, it means this cookie matches all
289 hostnames ending with that domain name.
290
291 \sa setDomain()
292*/
293QString QNetworkCookie::domain() const
294{
295 return d->domain;
296}
297
298/*!
299 Sets the domain associated with this cookie to be \a domain.
300
301 \sa domain()
302*/
303void QNetworkCookie::setDomain(const QString &domain)
304{
305 d->domain = domain;
306}
307
308/*!
309 Returns the path associated with this cookie. This corresponds to
310 the "path" field of the cookie string.
311
312 \sa setPath()
313*/
314QString QNetworkCookie::path() const
315{
316 return d->path;
317}
318
319/*!
320 Sets the path associated with this cookie to be \a path.
321
322 \sa path()
323*/
324void QNetworkCookie::setPath(const QString &path)
325{
326 d->path = path;
327}
328
329/*!
330 Returns the name of this cookie. The only mandatory field of a
331 cookie is its name, without which it is not considered valid.
332
333 \sa setName(), value()
334*/
335QByteArray QNetworkCookie::name() const
336{
337 return d->name;
338}
339
340/*!
341 Sets the name of this cookie to be \a cookieName. Note that
342 setting a cookie name to an empty QByteArray will make this cookie
343 invalid.
344
345 \sa name(), value()
346*/
347void QNetworkCookie::setName(const QByteArray &cookieName)
348{
349 d->name = cookieName;
350}
351
352/*!
353 Returns this cookies value, as specified in the cookie
354 string. Note that a cookie is still valid if its value is empty.
355
356 Cookie name-value pairs are considered opaque to the application:
357 that is, their values don't mean anything.
358
359 \sa setValue(), name()
360*/
361QByteArray QNetworkCookie::value() const
362{
363 return d->value;
364}
365
366/*!
367 Sets the value of this cookie to be \a value.
368
369 \sa value(), name()
370*/
371void QNetworkCookie::setValue(const QByteArray &value)
372{
373 d->value = value;
374}
375
376// ### move this to qnetworkcookie_p.h and share with qnetworkaccesshttpbackend
377static QPair<QByteArray, QByteArray> nextField(QByteArrayView text, int &position, bool isNameValue)
378{
379 // format is one of:
380 // (1) token
381 // (2) token = token
382 // (3) token = quoted-string
383 const int length = text.size();
384 position = nextNonWhitespace(text, from: position);
385
386 int semiColonPosition = text.indexOf(ch: ';', from: position);
387 if (semiColonPosition < 0)
388 semiColonPosition = length; //no ';' means take everything to end of string
389
390 int equalsPosition = text.indexOf(ch: '=', from: position);
391 if (equalsPosition < 0 || equalsPosition > semiColonPosition) {
392 if (isNameValue)
393 return qMakePair(value1: QByteArray(), value2: QByteArray()); //'=' is required for name-value-pair (RFC6265 section 5.2, rule 2)
394 equalsPosition = semiColonPosition; //no '=' means there is an attribute-name but no attribute-value
395 }
396
397 QByteArray first = text.mid(pos: position, n: equalsPosition - position).trimmed().toByteArray();
398 QByteArray second;
399 int secondLength = semiColonPosition - equalsPosition - 1;
400 if (secondLength > 0)
401 second = text.mid(pos: equalsPosition + 1, n: secondLength).trimmed().toByteArray();
402
403 position = semiColonPosition;
404 return qMakePair(value1&: first, value2&: second);
405}
406
407/*!
408 \enum QNetworkCookie::RawForm
409
410 This enum is used with the toRawForm() function to declare which
411 form of a cookie shall be returned.
412
413 \value NameAndValueOnly makes toRawForm() return only the
414 "NAME=VALUE" part of the cookie, as suitable for sending back
415 to a server in a client request's "Cookie:" header. Multiple
416 cookies are separated by a semi-colon in the "Cookie:" header
417 field.
418
419 \value Full makes toRawForm() return the full
420 cookie contents, as suitable for sending to a client in a
421 server's "Set-Cookie:" header.
422
423 Note that only the Full form of the cookie can be parsed back into
424 its original contents.
425
426 \sa toRawForm(), parseCookies()
427*/
428
429/*!
430 \enum QNetworkCookie::SameSite
431 \since 6.1
432
433 \value Default SameSite is not set. Can be interpreted as None or Lax by the browser.
434 \value None Cookies can be sent in all contexts. This used to be default, but
435 recent browsers made Lax default, and will now require the cookie to be both secure and to set SameSite=None.
436 \value Lax Cookies are sent on first party requests and GET requests initiated by third party website.
437 This is the default in modern browsers (since mid 2020).
438 \value Strict Cookies will only be sent in a first-party context.
439
440 \sa setSameSitePolicy(), sameSitePolicy()
441*/
442
443namespace {
444
445constexpr QByteArrayView sameSiteNone() noexcept { return "None"; }
446constexpr QByteArrayView sameSiteLax() noexcept { return "Lax"; }
447constexpr QByteArrayView sameSiteStrict() noexcept { return "Strict"; }
448
449QByteArrayView sameSiteToRawString(QNetworkCookie::SameSite samesite) noexcept
450{
451 switch (samesite) {
452 case QNetworkCookie::SameSite::None:
453 return sameSiteNone();
454 case QNetworkCookie::SameSite::Lax:
455 return sameSiteLax();
456 case QNetworkCookie::SameSite::Strict:
457 return sameSiteStrict();
458 case QNetworkCookie::SameSite::Default:
459 break;
460 }
461 return QByteArrayView();
462}
463
464QNetworkCookie::SameSite sameSiteFromRawString(QByteArrayView str) noexcept
465{
466 if (str.compare(a: sameSiteNone(), cs: Qt::CaseInsensitive) == 0)
467 return QNetworkCookie::SameSite::None;
468 if (str.compare(a: sameSiteLax(), cs: Qt::CaseInsensitive) == 0)
469 return QNetworkCookie::SameSite::Lax;
470 if (str.compare(a: sameSiteStrict(), cs: Qt::CaseInsensitive) == 0)
471 return QNetworkCookie::SameSite::Strict;
472 return QNetworkCookie::SameSite::Default;
473}
474} // namespace
475
476/*!
477 Returns the raw form of this QNetworkCookie. The QByteArray
478 returned by this function is suitable for an HTTP header, either
479 in a server response (the Set-Cookie header) or the client request
480 (the Cookie header). You can choose from one of two formats, using
481 \a form.
482
483 \sa parseCookies()
484*/
485QByteArray QNetworkCookie::toRawForm(RawForm form) const
486{
487 QByteArray result;
488 if (d->name.isEmpty())
489 return result; // not a valid cookie
490
491 result = d->name;
492 result += '=';
493 result += d->value;
494
495 if (form == Full) {
496 // same as above, but encoding everything back
497 if (isSecure())
498 result += "; secure";
499 if (isHttpOnly())
500 result += "; HttpOnly";
501 if (d->sameSite != SameSite::Default) {
502 result += "; SameSite=";
503 result += sameSiteToRawString(samesite: d->sameSite);
504 }
505 if (!isSessionCookie()) {
506 result += "; expires=";
507 result += QLocale::c().toString(dateTime: d->expirationDate.toUTC(),
508 format: "ddd, dd-MMM-yyyy hh:mm:ss 'GMT"_L1).toLatin1();
509 }
510 if (!d->domain.isEmpty()) {
511 result += "; domain=";
512 if (d->domain.startsWith(c: u'.')) {
513 result += '.';
514 result += QUrl::toAce(domain: d->domain.mid(position: 1));
515 } else {
516 QHostAddress hostAddr(d->domain);
517 if (hostAddr.protocol() == QAbstractSocket::IPv6Protocol) {
518 result += '[';
519 result += d->domain.toUtf8();
520 result += ']';
521 } else {
522 result += QUrl::toAce(domain: d->domain);
523 }
524 }
525 }
526 if (!d->path.isEmpty()) {
527 result += "; path=";
528 result += d->path.toUtf8();
529 }
530 }
531 return result;
532}
533
534static const char zones[] =
535 "pst\0" // -8
536 "pdt\0"
537 "mst\0" // -7
538 "mdt\0"
539 "cst\0" // -6
540 "cdt\0"
541 "est\0" // -5
542 "edt\0"
543 "ast\0" // -4
544 "nst\0" // -3
545 "gmt\0" // 0
546 "utc\0"
547 "bst\0"
548 "met\0" // 1
549 "eet\0" // 2
550 "jst\0" // 9
551 "\0";
552static const int zoneOffsets[] = {-8, -8, -7, -7, -6, -6, -5, -5, -4, -3, 0, 0, 0, 1, 2, 9 };
553
554static const char months[] =
555 "jan\0"
556 "feb\0"
557 "mar\0"
558 "apr\0"
559 "may\0"
560 "jun\0"
561 "jul\0"
562 "aug\0"
563 "sep\0"
564 "oct\0"
565 "nov\0"
566 "dec\0"
567 "\0";
568
569static inline bool isNumber(char s)
570{ return s >= '0' && s <= '9'; }
571
572static inline bool isTerminator(char c)
573{ return c == '\n' || c == '\r'; }
574
575static inline bool isValueSeparator(char c)
576{ return isTerminator(c) || c == ';'; }
577
578static inline bool isWhitespace(char c)
579{ return c == ' ' || c == '\t'; }
580
581static bool checkStaticArray(int &val, QByteArrayView dateString, int at, const char *array, int size)
582{
583 if (dateString[at] < 'a' || dateString[at] > 'z')
584 return false;
585 if (val == -1 && dateString.size() >= at + 3) {
586 int j = 0;
587 int i = 0;
588 while (i <= size) {
589 const char *str = array + i;
590 if (str[0] == dateString[at]
591 && str[1] == dateString[at + 1]
592 && str[2] == dateString[at + 2]) {
593 val = j;
594 return true;
595 }
596 i += int(strlen(s: str)) + 1;
597 ++j;
598 }
599 }
600 return false;
601}
602
603//#define PARSEDATESTRINGDEBUG
604
605#define ADAY 1
606#define AMONTH 2
607#define AYEAR 4
608
609/*
610 Parse all the date formats that Firefox can.
611
612 The official format is:
613 expires=ddd(d)?, dd-MMM-yyyy hh:mm:ss GMT
614
615 But browsers have been supporting a very wide range of date
616 strings. To work on many sites we need to support more then
617 just the official date format.
618
619 For reference see Firefox's PR_ParseTimeStringToExplodedTime in
620 prtime.c. The Firefox date parser is coded in a very complex way
621 and is slightly over ~700 lines long. While this implementation
622 will be slightly slower for the non standard dates it is smaller,
623 more readable, and maintainable.
624
625 Or in their own words:
626 "} // else what the hell is this."
627*/
628static QDateTime parseDateString(QByteArrayView dateString)
629{
630 QTime time;
631 // placeholders for values when we are not sure it is a year, month or day
632 int unknown[3] = {-1, -1, -1};
633 int month = -1;
634 int day = -1;
635 int year = -1;
636 int zoneOffset = -1;
637
638 // hour:minute:second.ms pm
639 static const QRegularExpression timeRx(
640 u"(\\d\\d?):(\\d\\d?)(?::(\\d\\d?)(?:\\.(\\d{1,3}))?)?(?:\\s*(am|pm))?"_s);
641
642 int at = 0;
643 while (at < dateString.size()) {
644#ifdef PARSEDATESTRINGDEBUG
645 qDebug() << dateString.mid(at);
646#endif
647 bool isNum = isNumber(s: dateString[at]);
648
649 // Month
650 if (!isNum
651 && checkStaticArray(val&: month, dateString, at, array: months, size: sizeof(months)- 1)) {
652 ++month;
653#ifdef PARSEDATESTRINGDEBUG
654 qDebug() << "Month:" << month;
655#endif
656 at += 3;
657 continue;
658 }
659 // Zone
660 if (!isNum
661 && zoneOffset == -1
662 && checkStaticArray(val&: zoneOffset, dateString, at, array: zones, size: sizeof(zones)- 1)) {
663 int sign = (at >= 0 && dateString[at - 1] == '-') ? -1 : 1;
664 zoneOffset = sign * zoneOffsets[zoneOffset] * 60 * 60;
665#ifdef PARSEDATESTRINGDEBUG
666 qDebug() << "Zone:" << month;
667#endif
668 at += 3;
669 continue;
670 }
671 // Zone offset
672 if (!isNum
673 && (zoneOffset == -1 || zoneOffset == 0) // Can only go after gmt
674 && (dateString[at] == '+' || dateString[at] == '-')
675 && (at == 0
676 || isWhitespace(c: dateString[at - 1])
677 || dateString[at - 1] == ','
678 || (at >= 3
679 && (dateString[at - 3] == 'g')
680 && (dateString[at - 2] == 'm')
681 && (dateString[at - 1] == 't')))) {
682
683 int end = 1;
684 while (end < 5 && dateString.size() > at+end
685 && dateString[at + end] >= '0' && dateString[at + end] <= '9')
686 ++end;
687 int minutes = 0;
688 int hours = 0;
689 switch (end - 1) {
690 case 4:
691 minutes = dateString.mid(pos: at + 3, n: 2).toInt();
692 Q_FALLTHROUGH();
693 case 2:
694 hours = dateString.mid(pos: at + 1, n: 2).toInt();
695 break;
696 case 1:
697 hours = dateString.mid(pos: at + 1, n: 1).toInt();
698 break;
699 default:
700 at += end;
701 continue;
702 }
703 if (end != 1) {
704 int sign = dateString[at] == '-' ? -1 : 1;
705 zoneOffset = sign * ((minutes * 60) + (hours * 60 * 60));
706#ifdef PARSEDATESTRINGDEBUG
707 qDebug() << "Zone offset:" << zoneOffset << hours << minutes;
708#endif
709 at += end;
710 continue;
711 }
712 }
713
714 // Time
715 if (isNum && time.isNull()
716 && dateString.size() >= at + 3
717 && (dateString[at + 2] == ':' || dateString[at + 1] == ':')) {
718 // While the date can be found all over the string the format
719 // for the time is set and a nice regexp can be used.
720 // This string needs to stay for as long as the QRegularExpressionMatch is used,
721 // or else we get use-after-free issues:
722 QString dateToString = QString::fromLatin1(ba: dateString);
723 if (auto match = timeRx.match(subject: dateToString, offset: at); match.hasMatch()) {
724 int h = match.capturedView(nth: 1).toInt();
725 int m = match.capturedView(nth: 2).toInt();
726 int s = match.capturedView(nth: 3).toInt();
727 int ms = match.capturedView(nth: 4).toInt();
728 QStringView ampm = match.capturedView(nth: 5);
729 if (h < 12 && !ampm.isEmpty())
730 if (ampm == "pm"_L1)
731 h += 12;
732 time = QTime(h, m, s, ms);
733#ifdef PARSEDATESTRINGDEBUG
734 qDebug() << "Time:" << match.capturedTexts() << match.capturedLength();
735#endif
736 at += match.capturedLength();
737 continue;
738 }
739 }
740
741 // 4 digit Year
742 if (isNum
743 && year == -1
744 && dateString.size() > at + 3) {
745 if (isNumber(s: dateString[at + 1])
746 && isNumber(s: dateString[at + 2])
747 && isNumber(s: dateString[at + 3])) {
748 year = dateString.mid(pos: at, n: 4).toInt();
749 at += 4;
750#ifdef PARSEDATESTRINGDEBUG
751 qDebug() << "Year:" << year;
752#endif
753 continue;
754 }
755 }
756
757 // a one or two digit number
758 // Could be month, day or year
759 if (isNum) {
760 int length = 1;
761 if (dateString.size() > at + 1
762 && isNumber(s: dateString[at + 1]))
763 ++length;
764 int x = dateString.mid(pos: at, n: length).toInt();
765 if (year == -1 && (x > 31 || x == 0)) {
766 year = x;
767 } else {
768 if (unknown[0] == -1) unknown[0] = x;
769 else if (unknown[1] == -1) unknown[1] = x;
770 else if (unknown[2] == -1) unknown[2] = x;
771 }
772 at += length;
773#ifdef PARSEDATESTRINGDEBUG
774 qDebug() << "Saving" << x;
775#endif
776 continue;
777 }
778
779 // Unknown character, typically a weekday such as 'Mon'
780 ++at;
781 }
782
783 // Once we are done parsing the string take the digits in unknown
784 // and determine which is the unknown year/month/day
785
786 int couldBe[3] = { 0, 0, 0 };
787 int unknownCount = 3;
788 for (int i = 0; i < unknownCount; ++i) {
789 if (unknown[i] == -1) {
790 couldBe[i] = ADAY | AYEAR | AMONTH;
791 unknownCount = i;
792 continue;
793 }
794
795 if (unknown[i] >= 1)
796 couldBe[i] = ADAY;
797
798 if (month == -1 && unknown[i] >= 1 && unknown[i] <= 12)
799 couldBe[i] |= AMONTH;
800
801 if (year == -1)
802 couldBe[i] |= AYEAR;
803 }
804
805 // For any possible day make sure one of the values that could be a month
806 // can contain that day.
807 // For any possible month make sure one of the values that can be a
808 // day that month can have.
809 // Example: 31 11 06
810 // 31 can't be a day because 11 and 6 don't have 31 days
811 for (int i = 0; i < unknownCount; ++i) {
812 int currentValue = unknown[i];
813 bool findMatchingMonth = couldBe[i] & ADAY && currentValue >= 29;
814 bool findMatchingDay = couldBe[i] & AMONTH;
815 if (!findMatchingMonth || !findMatchingDay)
816 continue;
817 for (int j = 0; j < 3; ++j) {
818 if (j == i)
819 continue;
820 for (int k = 0; k < 2; ++k) {
821 if (k == 0 && !(findMatchingMonth && (couldBe[j] & AMONTH)))
822 continue;
823 else if (k == 1 && !(findMatchingDay && (couldBe[j] & ADAY)))
824 continue;
825 int m = currentValue;
826 int d = unknown[j];
827 if (k == 0)
828 qSwap(value1&: m, value2&: d);
829 if (m == -1) m = month;
830 bool found = true;
831 switch(m) {
832 case 2:
833 // When we get 29 and the year ends up having only 28
834 // See date.isValid below
835 // Example: 29 23 Feb
836 if (d <= 29)
837 found = false;
838 break;
839 case 4: case 6: case 9: case 11:
840 if (d <= 30)
841 found = false;
842 break;
843 default:
844 if (d > 0 && d <= 31)
845 found = false;
846 }
847 if (k == 0) findMatchingMonth = found;
848 else if (k == 1) findMatchingDay = found;
849 }
850 }
851 if (findMatchingMonth)
852 couldBe[i] &= ~ADAY;
853 if (findMatchingDay)
854 couldBe[i] &= ~AMONTH;
855 }
856
857 // First set the year/month/day that have been deduced
858 // and reduce the set as we go along to deduce more
859 for (int i = 0; i < unknownCount; ++i) {
860 int unset = 0;
861 for (int j = 0; j < 3; ++j) {
862 if (couldBe[j] == ADAY && day == -1) {
863 day = unknown[j];
864 unset |= ADAY;
865 } else if (couldBe[j] == AMONTH && month == -1) {
866 month = unknown[j];
867 unset |= AMONTH;
868 } else if (couldBe[j] == AYEAR && year == -1) {
869 year = unknown[j];
870 unset |= AYEAR;
871 } else {
872 // common case
873 break;
874 }
875 couldBe[j] &= ~unset;
876 }
877 }
878
879 // Now fallback to a standardized order to fill in the rest with
880 for (int i = 0; i < unknownCount; ++i) {
881 if (couldBe[i] & AMONTH && month == -1) month = unknown[i];
882 else if (couldBe[i] & ADAY && day == -1) day = unknown[i];
883 else if (couldBe[i] & AYEAR && year == -1) year = unknown[i];
884 }
885#ifdef PARSEDATESTRINGDEBUG
886 qDebug() << "Final set" << year << month << day;
887#endif
888
889 if (year == -1 || month == -1 || day == -1) {
890#ifdef PARSEDATESTRINGDEBUG
891 qDebug() << "Parser failure" << year << month << day;
892#endif
893 return QDateTime();
894 }
895
896 // Y2k behavior
897 int y2k = 0;
898 if (year < 70)
899 y2k = 2000;
900 else if (year < 100)
901 y2k = 1900;
902
903 QDate date(year + y2k, month, day);
904
905 // When we were given a bad cookie that when parsed
906 // set the day to 29 and the year to one that doesn't
907 // have the 29th of Feb rather then adding the extra
908 // complicated checking earlier just swap here.
909 // Example: 29 23 Feb
910 if (!date.isValid())
911 date = QDate(day + y2k, month, year);
912
913 QDateTime dateTime(date, time, QTimeZone::UTC);
914
915 if (zoneOffset != -1)
916 dateTime = dateTime.addSecs(secs: zoneOffset);
917
918 if (!dateTime.isValid())
919 return QDateTime();
920 return dateTime;
921}
922
923/*!
924 Parses the cookie string \a cookieString as received from a server
925 response in the "Set-Cookie:" header. If there's a parsing error,
926 this function returns an empty list.
927
928 Since the HTTP header can set more than one cookie at the same
929 time, this function returns a QList<QNetworkCookie>, one for each
930 cookie that is parsed.
931
932 \sa toRawForm()
933 \note In Qt versions prior to 6.7, this function took QByteArray only.
934*/
935QList<QNetworkCookie> QNetworkCookie::parseCookies(QByteArrayView cookieString)
936{
937 // cookieString can be a number of set-cookie header strings joined together
938 // by \n, parse each line separately.
939 QList<QNetworkCookie> cookies;
940 for (auto s : QLatin1StringView(cookieString).tokenize(needle: '\n'_L1))
941 cookies += QNetworkCookiePrivate::parseSetCookieHeaderLine(cookieString: s);
942 return cookies;
943}
944
945QList<QNetworkCookie> QNetworkCookiePrivate::parseSetCookieHeaderLine(QByteArrayView cookieString)
946{
947 // According to http://wp.netscape.com/newsref/std/cookie_spec.html,<
948 // the Set-Cookie response header is of the format:
949 //
950 // Set-Cookie: NAME=VALUE; expires=DATE; path=PATH; domain=DOMAIN_NAME; secure
951 //
952 // where only the NAME=VALUE part is mandatory
953 //
954 // We do not support RFC 2965 Set-Cookie2-style cookies
955
956 QList<QNetworkCookie> result;
957 const QDateTime now = QDateTime::currentDateTimeUtc();
958
959 int position = 0;
960 const int length = cookieString.size();
961 while (position < length) {
962 QNetworkCookie cookie;
963
964 // The first part is always the "NAME=VALUE" part
965 QPair<QByteArray,QByteArray> field = nextField(text: cookieString, position, isNameValue: true);
966 if (field.first.isEmpty())
967 // parsing error
968 break;
969 cookie.setName(field.first);
970 cookie.setValue(field.second);
971
972 position = nextNonWhitespace(text: cookieString, from: position);
973 while (position < length) {
974 switch (cookieString.at(n: position++)) {
975 case ';':
976 // new field in the cookie
977 field = nextField(text: cookieString, position, isNameValue: false);
978
979 if (field.first.compare(a: "expires", cs: Qt::CaseInsensitive) == 0) {
980 position -= field.second.size();
981 int end;
982 for (end = position; end < length; ++end)
983 if (isValueSeparator(c: cookieString.at(n: end)))
984 break;
985
986 QByteArray dateString = cookieString.mid(pos: position, n: end - position).trimmed().toByteArray().toLower();
987 position = end;
988 QDateTime dt = parseDateString(dateString);
989 if (dt.isValid())
990 cookie.setExpirationDate(dt);
991 //if unparsed, ignore the attribute but not the whole cookie (RFC6265 section 5.2.1)
992 } else if (field.first.compare(a: "domain", cs: Qt::CaseInsensitive) == 0) {
993 QByteArrayView rawDomain = field.second;
994 //empty domain should be ignored (RFC6265 section 5.2.3)
995 if (!rawDomain.isEmpty()) {
996 QLatin1StringView maybeLeadingDot;
997 if (rawDomain.startsWith(c: '.')) {
998 maybeLeadingDot = "."_L1;
999 rawDomain = rawDomain.mid(pos: 1);
1000 }
1001
1002 //IDN domains are required by RFC6265, accepting utf8 as well doesn't break any test cases.
1003 QString normalizedDomain = QUrl::fromAce(domain: QUrl::toAce(domain: QString::fromUtf8(utf8: rawDomain)));
1004 if (!normalizedDomain.isEmpty()) {
1005 cookie.setDomain(maybeLeadingDot + normalizedDomain);
1006 } else {
1007 //Normalization fails for malformed domains, e.g. "..example.org", reject the cookie now
1008 //rather than accepting it but never sending it due to domain match failure, as the
1009 //strict reading of RFC6265 would indicate.
1010 return result;
1011 }
1012 }
1013 } else if (field.first.compare(a: "max-age", cs: Qt::CaseInsensitive) == 0) {
1014 bool ok = false;
1015 int secs = field.second.toInt(ok: &ok);
1016 if (ok) {
1017 if (secs <= 0) {
1018 //earliest representable time (RFC6265 section 5.2.2)
1019 cookie.setExpirationDate(QDateTime::fromSecsSinceEpoch(secs: 0));
1020 } else {
1021 cookie.setExpirationDate(now.addSecs(secs));
1022 }
1023 }
1024 //if unparsed, ignore the attribute but not the whole cookie (RFC6265 section 5.2.2)
1025 } else if (field.first.compare(a: "path", cs: Qt::CaseInsensitive) == 0) {
1026 if (field.second.startsWith(c: '/')) {
1027 // ### we should treat cookie paths as an octet sequence internally
1028 // However RFC6265 says we should assume UTF-8 for presentation as a string
1029 cookie.setPath(QString::fromUtf8(ba: field.second));
1030 } else {
1031 // if the path doesn't start with '/' then set the default path (RFC6265 section 5.2.4)
1032 // and also IETF test case path0030 which has valid and empty path in the same cookie
1033 cookie.setPath(QString());
1034 }
1035 } else if (field.first.compare(a: "secure", cs: Qt::CaseInsensitive) == 0) {
1036 cookie.setSecure(true);
1037 } else if (field.first.compare(a: "httponly", cs: Qt::CaseInsensitive) == 0) {
1038 cookie.setHttpOnly(true);
1039 } else if (field.first.compare(a: "samesite", cs: Qt::CaseInsensitive) == 0) {
1040 cookie.setSameSitePolicy(sameSiteFromRawString(str: field.second));
1041 } else {
1042 // ignore unknown fields in the cookie (RFC6265 section 5.2, rule 6)
1043 }
1044
1045 position = nextNonWhitespace(text: cookieString, from: position);
1046 }
1047 }
1048
1049 if (!cookie.name().isEmpty())
1050 result += cookie;
1051 }
1052
1053 return result;
1054}
1055
1056/*!
1057 \since 5.0
1058 This functions normalizes the path and domain of the cookie if they were previously empty.
1059 The \a url parameter is used to determine the correct domain and path.
1060*/
1061void QNetworkCookie::normalize(const QUrl &url)
1062{
1063 // don't do path checking. See QTBUG-5815
1064 if (d->path.isEmpty()) {
1065 QString pathAndFileName = url.path();
1066 QString defaultPath = pathAndFileName.left(n: pathAndFileName.lastIndexOf(c: u'/') + 1);
1067 if (defaultPath.isEmpty())
1068 defaultPath = u'/';
1069 d->path = defaultPath;
1070 }
1071
1072 if (d->domain.isEmpty()) {
1073 d->domain = url.host();
1074 } else {
1075 QHostAddress hostAddress(d->domain);
1076 if (hostAddress.protocol() != QAbstractSocket::IPv4Protocol
1077 && hostAddress.protocol() != QAbstractSocket::IPv6Protocol
1078 && !d->domain.startsWith(c: u'.')) {
1079 // Ensure the domain starts with a dot if its field was not empty
1080 // in the HTTP header. There are some servers that forget the
1081 // leading dot and this is actually forbidden according to RFC 2109,
1082 // but all browsers accept it anyway so we do that as well.
1083 d->domain.prepend(c: u'.');
1084 }
1085 }
1086}
1087
1088#ifndef QT_NO_DEBUG_STREAM
1089QDebug operator<<(QDebug s, const QNetworkCookie &cookie)
1090{
1091 QDebugStateSaver saver(s);
1092 s.resetFormat().nospace();
1093 s << "QNetworkCookie(" << cookie.toRawForm(form: QNetworkCookie::Full) << ')';
1094 return s;
1095}
1096#endif
1097
1098QT_END_NAMESPACE
1099
1100#include "moc_qnetworkcookie.cpp"
1101

Provided by KDAB

Privacy Policy
Start learning QML with our Intro Training
Find out more

source code of qtbase/src/network/access/qnetworkcookie.cpp