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

Provided by KDAB

Privacy Policy
Learn Advanced QML with KDAB
Find out more

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