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 | |
22 | QT_BEGIN_NAMESPACE |
23 | |
24 | using namespace Qt::StringLiterals; |
25 | |
26 | QT_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 | */ |
79 | QNetworkCookie::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 | */ |
93 | QNetworkCookie::QNetworkCookie(const QNetworkCookie &other) |
94 | : d(other.d) |
95 | { |
96 | } |
97 | |
98 | /*! |
99 | Destroys this QNetworkCookie object. |
100 | */ |
101 | QNetworkCookie::~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 | */ |
111 | QNetworkCookie &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 | */ |
143 | bool 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 | */ |
163 | bool 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 | */ |
177 | bool 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 | */ |
190 | void 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 | */ |
202 | QNetworkCookie::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 | */ |
213 | void 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 | */ |
229 | bool 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 | */ |
239 | void 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 | */ |
252 | bool 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 | */ |
268 | QDateTime 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 | */ |
280 | void 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 | */ |
295 | QString 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 | */ |
305 | void 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 | */ |
316 | QString 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 | */ |
326 | void 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 | */ |
337 | QByteArray 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 | */ |
349 | void 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 | */ |
363 | QByteArray 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 | */ |
373 | void QNetworkCookie::setValue(const QByteArray &value) |
374 | { |
375 | d->value = value; |
376 | } |
377 | |
378 | // ### move this to qnetworkcookie_p.h and share with qnetworkaccesshttpbackend |
379 | static QPair<QByteArray, QByteArray> nextField(const QByteArray &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(c: ';', from: position); |
389 | if (semiColonPosition < 0) |
390 | semiColonPosition = length; //no ';' means take everything to end of string |
391 | |
392 | int equalsPosition = text.indexOf(c: '=', 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(index: position, len: equalsPosition - position).trimmed(); |
400 | QByteArray second; |
401 | int secondLength = semiColonPosition - equalsPosition - 1; |
402 | if (secondLength > 0) |
403 | second = text.mid(index: equalsPosition + 1, len: secondLength).trimmed(); |
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 | |
445 | namespace { |
446 | QByteArray sameSiteToRawString(QNetworkCookie::SameSite samesite) |
447 | { |
448 | switch (samesite) { |
449 | case QNetworkCookie::SameSite::None: |
450 | return QByteArrayLiteral("None" ); |
451 | case QNetworkCookie::SameSite::Lax: |
452 | return QByteArrayLiteral("Lax" ); |
453 | case QNetworkCookie::SameSite::Strict: |
454 | return QByteArrayLiteral("Strict" ); |
455 | case QNetworkCookie::SameSite::Default: |
456 | break; |
457 | } |
458 | return QByteArray(); |
459 | } |
460 | |
461 | QNetworkCookie::SameSite sameSiteFromRawString(QByteArray str) |
462 | { |
463 | str = str.toLower(); |
464 | if (str == QByteArrayLiteral("none" )) |
465 | return QNetworkCookie::SameSite::None; |
466 | if (str == QByteArrayLiteral("lax" )) |
467 | return QNetworkCookie::SameSite::Lax; |
468 | if (str == QByteArrayLiteral("strict" )) |
469 | return QNetworkCookie::SameSite::Strict; |
470 | return QNetworkCookie::SameSite::Default; |
471 | } |
472 | } // namespace |
473 | |
474 | /*! |
475 | Returns the raw form of this QNetworkCookie. The QByteArray |
476 | returned by this function is suitable for an HTTP header, either |
477 | in a server response (the Set-Cookie header) or the client request |
478 | (the Cookie header). You can choose from one of two formats, using |
479 | \a form. |
480 | |
481 | \sa parseCookies() |
482 | */ |
483 | QByteArray QNetworkCookie::toRawForm(RawForm form) const |
484 | { |
485 | QByteArray result; |
486 | if (d->name.isEmpty()) |
487 | return result; // not a valid cookie |
488 | |
489 | result = d->name; |
490 | result += '='; |
491 | result += d->value; |
492 | |
493 | if (form == Full) { |
494 | // same as above, but encoding everything back |
495 | if (isSecure()) |
496 | result += "; secure" ; |
497 | if (isHttpOnly()) |
498 | result += "; HttpOnly" ; |
499 | if (d->sameSite != SameSite::Default) { |
500 | result += "; SameSite=" ; |
501 | result += sameSiteToRawString(samesite: d->sameSite); |
502 | } |
503 | if (!isSessionCookie()) { |
504 | result += "; expires=" ; |
505 | result += QLocale::c().toString(dateTime: d->expirationDate.toUTC(), |
506 | format: "ddd, dd-MMM-yyyy hh:mm:ss 'GMT"_L1 ).toLatin1(); |
507 | } |
508 | if (!d->domain.isEmpty()) { |
509 | result += "; domain=" ; |
510 | if (d->domain.startsWith(c: u'.')) { |
511 | result += '.'; |
512 | result += QUrl::toAce(domain: d->domain.mid(position: 1)); |
513 | } else { |
514 | QHostAddress hostAddr(d->domain); |
515 | if (hostAddr.protocol() == QAbstractSocket::IPv6Protocol) { |
516 | result += '['; |
517 | result += d->domain.toUtf8(); |
518 | result += ']'; |
519 | } else { |
520 | result += QUrl::toAce(domain: d->domain); |
521 | } |
522 | } |
523 | } |
524 | if (!d->path.isEmpty()) { |
525 | result += "; path=" ; |
526 | result += d->path.toUtf8(); |
527 | } |
528 | } |
529 | return result; |
530 | } |
531 | |
532 | static const char zones[] = |
533 | "pst\0" // -8 |
534 | "pdt\0" |
535 | "mst\0" // -7 |
536 | "mdt\0" |
537 | "cst\0" // -6 |
538 | "cdt\0" |
539 | "est\0" // -5 |
540 | "edt\0" |
541 | "ast\0" // -4 |
542 | "nst\0" // -3 |
543 | "gmt\0" // 0 |
544 | "utc\0" |
545 | "bst\0" |
546 | "met\0" // 1 |
547 | "eet\0" // 2 |
548 | "jst\0" // 9 |
549 | "\0" ; |
550 | static const int zoneOffsets[] = {-8, -8, -7, -7, -6, -6, -5, -5, -4, -3, 0, 0, 0, 1, 2, 9 }; |
551 | |
552 | static const char months[] = |
553 | "jan\0" |
554 | "feb\0" |
555 | "mar\0" |
556 | "apr\0" |
557 | "may\0" |
558 | "jun\0" |
559 | "jul\0" |
560 | "aug\0" |
561 | "sep\0" |
562 | "oct\0" |
563 | "nov\0" |
564 | "dec\0" |
565 | "\0" ; |
566 | |
567 | static inline bool isNumber(char s) |
568 | { return s >= '0' && s <= '9'; } |
569 | |
570 | static inline bool isTerminator(char c) |
571 | { return c == '\n' || c == '\r'; } |
572 | |
573 | static inline bool isValueSeparator(char c) |
574 | { return isTerminator(c) || c == ';'; } |
575 | |
576 | static inline bool isWhitespace(char c) |
577 | { return c == ' ' || c == '\t'; } |
578 | |
579 | static bool checkStaticArray(int &val, const QByteArray &dateString, int at, const char *array, int size) |
580 | { |
581 | if (dateString[at] < 'a' || dateString[at] > 'z') |
582 | return false; |
583 | if (val == -1 && dateString.size() >= at + 3) { |
584 | int j = 0; |
585 | int i = 0; |
586 | while (i <= size) { |
587 | const char *str = array + i; |
588 | if (str[0] == dateString[at] |
589 | && str[1] == dateString[at + 1] |
590 | && str[2] == dateString[at + 2]) { |
591 | val = j; |
592 | return true; |
593 | } |
594 | i += int(strlen(s: str)) + 1; |
595 | ++j; |
596 | } |
597 | } |
598 | return false; |
599 | } |
600 | |
601 | //#define PARSEDATESTRINGDEBUG |
602 | |
603 | #define ADAY 1 |
604 | #define AMONTH 2 |
605 | #define AYEAR 4 |
606 | |
607 | /* |
608 | Parse all the date formats that Firefox can. |
609 | |
610 | The official format is: |
611 | expires=ddd(d)?, dd-MMM-yyyy hh:mm:ss GMT |
612 | |
613 | But browsers have been supporting a very wide range of date |
614 | strings. To work on many sites we need to support more then |
615 | just the official date format. |
616 | |
617 | For reference see Firefox's PR_ParseTimeStringToExplodedTime in |
618 | prtime.c. The Firefox date parser is coded in a very complex way |
619 | and is slightly over ~700 lines long. While this implementation |
620 | will be slightly slower for the non standard dates it is smaller, |
621 | more readable, and maintainable. |
622 | |
623 | Or in their own words: |
624 | "} // else what the hell is this." |
625 | */ |
626 | static QDateTime parseDateString(const QByteArray &dateString) |
627 | { |
628 | QTime time; |
629 | // placeholders for values when we are not sure it is a year, month or day |
630 | int unknown[3] = {-1, -1, -1}; |
631 | int month = -1; |
632 | int day = -1; |
633 | int year = -1; |
634 | int zoneOffset = -1; |
635 | |
636 | // hour:minute:second.ms pm |
637 | static const QRegularExpression timeRx( |
638 | u"(\\d\\d?):(\\d\\d?)(?::(\\d\\d?)(?:\\.(\\d{1,3}))?)?(?:\\s*(am|pm))?"_s ); |
639 | |
640 | int at = 0; |
641 | while (at < dateString.size()) { |
642 | #ifdef PARSEDATESTRINGDEBUG |
643 | qDebug() << dateString.mid(at); |
644 | #endif |
645 | bool isNum = isNumber(s: dateString[at]); |
646 | |
647 | // Month |
648 | if (!isNum |
649 | && checkStaticArray(val&: month, dateString, at, array: months, size: sizeof(months)- 1)) { |
650 | ++month; |
651 | #ifdef PARSEDATESTRINGDEBUG |
652 | qDebug() << "Month:" << month; |
653 | #endif |
654 | at += 3; |
655 | continue; |
656 | } |
657 | // Zone |
658 | if (!isNum |
659 | && zoneOffset == -1 |
660 | && checkStaticArray(val&: zoneOffset, dateString, at, array: zones, size: sizeof(zones)- 1)) { |
661 | int sign = (at >= 0 && dateString[at - 1] == '-') ? -1 : 1; |
662 | zoneOffset = sign * zoneOffsets[zoneOffset] * 60 * 60; |
663 | #ifdef PARSEDATESTRINGDEBUG |
664 | qDebug() << "Zone:" << month; |
665 | #endif |
666 | at += 3; |
667 | continue; |
668 | } |
669 | // Zone offset |
670 | if (!isNum |
671 | && (zoneOffset == -1 || zoneOffset == 0) // Can only go after gmt |
672 | && (dateString[at] == '+' || dateString[at] == '-') |
673 | && (at == 0 |
674 | || isWhitespace(c: dateString[at - 1]) |
675 | || dateString[at - 1] == ',' |
676 | || (at >= 3 |
677 | && (dateString[at - 3] == 'g') |
678 | && (dateString[at - 2] == 'm') |
679 | && (dateString[at - 1] == 't')))) { |
680 | |
681 | int end = 1; |
682 | while (end < 5 && dateString.size() > at+end |
683 | && dateString[at + end] >= '0' && dateString[at + end] <= '9') |
684 | ++end; |
685 | int minutes = 0; |
686 | int hours = 0; |
687 | switch (end - 1) { |
688 | case 4: |
689 | minutes = atoi(nptr: dateString.mid(index: at + 3, len: 2).constData()); |
690 | Q_FALLTHROUGH(); |
691 | case 2: |
692 | hours = atoi(nptr: dateString.mid(index: at + 1, len: 2).constData()); |
693 | break; |
694 | case 1: |
695 | hours = atoi(nptr: dateString.mid(index: at + 1, len: 1).constData()); |
696 | break; |
697 | default: |
698 | at += end; |
699 | continue; |
700 | } |
701 | if (end != 1) { |
702 | int sign = dateString[at] == '-' ? -1 : 1; |
703 | zoneOffset = sign * ((minutes * 60) + (hours * 60 * 60)); |
704 | #ifdef PARSEDATESTRINGDEBUG |
705 | qDebug() << "Zone offset:" << zoneOffset << hours << minutes; |
706 | #endif |
707 | at += end; |
708 | continue; |
709 | } |
710 | } |
711 | |
712 | // Time |
713 | if (isNum && time.isNull() |
714 | && dateString.size() >= at + 3 |
715 | && (dateString[at + 2] == ':' || dateString[at + 1] == ':')) { |
716 | // While the date can be found all over the string the format |
717 | // for the time is set and a nice regexp can be used. |
718 | // This string needs to stay for as long as the QRegularExpressionMatch is used, |
719 | // or else we get use-after-free issues: |
720 | QString dateToString = QString::fromLatin1(ba: dateString); |
721 | if (auto match = timeRx.match(subject: dateToString, offset: at); match.hasMatch()) { |
722 | int h = match.capturedView(nth: 1).toInt(); |
723 | int m = match.capturedView(nth: 2).toInt(); |
724 | int s = match.capturedView(nth: 3).toInt(); |
725 | int ms = match.capturedView(nth: 4).toInt(); |
726 | QStringView ampm = match.capturedView(nth: 5); |
727 | if (h < 12 && !ampm.isEmpty()) |
728 | if (ampm == "pm"_L1 ) |
729 | h += 12; |
730 | time = QTime(h, m, s, ms); |
731 | #ifdef PARSEDATESTRINGDEBUG |
732 | qDebug() << "Time:" << match.capturedTexts() << match.capturedLength(); |
733 | #endif |
734 | at += match.capturedLength(); |
735 | continue; |
736 | } |
737 | } |
738 | |
739 | // 4 digit Year |
740 | if (isNum |
741 | && year == -1 |
742 | && dateString.size() > at + 3) { |
743 | if (isNumber(s: dateString[at + 1]) |
744 | && isNumber(s: dateString[at + 2]) |
745 | && isNumber(s: dateString[at + 3])) { |
746 | year = atoi(nptr: dateString.mid(index: at, len: 4).constData()); |
747 | at += 4; |
748 | #ifdef PARSEDATESTRINGDEBUG |
749 | qDebug() << "Year:" << year; |
750 | #endif |
751 | continue; |
752 | } |
753 | } |
754 | |
755 | // a one or two digit number |
756 | // Could be month, day or year |
757 | if (isNum) { |
758 | int length = 1; |
759 | if (dateString.size() > at + 1 |
760 | && isNumber(s: dateString[at + 1])) |
761 | ++length; |
762 | int x = atoi(nptr: dateString.mid(index: at, len: length).constData()); |
763 | if (year == -1 && (x > 31 || x == 0)) { |
764 | year = x; |
765 | } else { |
766 | if (unknown[0] == -1) unknown[0] = x; |
767 | else if (unknown[1] == -1) unknown[1] = x; |
768 | else if (unknown[2] == -1) unknown[2] = x; |
769 | } |
770 | at += length; |
771 | #ifdef PARSEDATESTRINGDEBUG |
772 | qDebug() << "Saving" << x; |
773 | #endif |
774 | continue; |
775 | } |
776 | |
777 | // Unknown character, typically a weekday such as 'Mon' |
778 | ++at; |
779 | } |
780 | |
781 | // Once we are done parsing the string take the digits in unknown |
782 | // and determine which is the unknown year/month/day |
783 | |
784 | int couldBe[3] = { 0, 0, 0 }; |
785 | int unknownCount = 3; |
786 | for (int i = 0; i < unknownCount; ++i) { |
787 | if (unknown[i] == -1) { |
788 | couldBe[i] = ADAY | AYEAR | AMONTH; |
789 | unknownCount = i; |
790 | continue; |
791 | } |
792 | |
793 | if (unknown[i] >= 1) |
794 | couldBe[i] = ADAY; |
795 | |
796 | if (month == -1 && unknown[i] >= 1 && unknown[i] <= 12) |
797 | couldBe[i] |= AMONTH; |
798 | |
799 | if (year == -1) |
800 | couldBe[i] |= AYEAR; |
801 | } |
802 | |
803 | // For any possible day make sure one of the values that could be a month |
804 | // can contain that day. |
805 | // For any possible month make sure one of the values that can be a |
806 | // day that month can have. |
807 | // Example: 31 11 06 |
808 | // 31 can't be a day because 11 and 6 don't have 31 days |
809 | for (int i = 0; i < unknownCount; ++i) { |
810 | int currentValue = unknown[i]; |
811 | bool findMatchingMonth = couldBe[i] & ADAY && currentValue >= 29; |
812 | bool findMatchingDay = couldBe[i] & AMONTH; |
813 | if (!findMatchingMonth || !findMatchingDay) |
814 | continue; |
815 | for (int j = 0; j < 3; ++j) { |
816 | if (j == i) |
817 | continue; |
818 | for (int k = 0; k < 2; ++k) { |
819 | if (k == 0 && !(findMatchingMonth && (couldBe[j] & AMONTH))) |
820 | continue; |
821 | else if (k == 1 && !(findMatchingDay && (couldBe[j] & ADAY))) |
822 | continue; |
823 | int m = currentValue; |
824 | int d = unknown[j]; |
825 | if (k == 0) |
826 | qSwap(value1&: m, value2&: d); |
827 | if (m == -1) m = month; |
828 | bool found = true; |
829 | switch(m) { |
830 | case 2: |
831 | // When we get 29 and the year ends up having only 28 |
832 | // See date.isValid below |
833 | // Example: 29 23 Feb |
834 | if (d <= 29) |
835 | found = false; |
836 | break; |
837 | case 4: case 6: case 9: case 11: |
838 | if (d <= 30) |
839 | found = false; |
840 | break; |
841 | default: |
842 | if (d > 0 && d <= 31) |
843 | found = false; |
844 | } |
845 | if (k == 0) findMatchingMonth = found; |
846 | else if (k == 1) findMatchingDay = found; |
847 | } |
848 | } |
849 | if (findMatchingMonth) |
850 | couldBe[i] &= ~ADAY; |
851 | if (findMatchingDay) |
852 | couldBe[i] &= ~AMONTH; |
853 | } |
854 | |
855 | // First set the year/month/day that have been deduced |
856 | // and reduce the set as we go along to deduce more |
857 | for (int i = 0; i < unknownCount; ++i) { |
858 | int unset = 0; |
859 | for (int j = 0; j < 3; ++j) { |
860 | if (couldBe[j] == ADAY && day == -1) { |
861 | day = unknown[j]; |
862 | unset |= ADAY; |
863 | } else if (couldBe[j] == AMONTH && month == -1) { |
864 | month = unknown[j]; |
865 | unset |= AMONTH; |
866 | } else if (couldBe[j] == AYEAR && year == -1) { |
867 | year = unknown[j]; |
868 | unset |= AYEAR; |
869 | } else { |
870 | // common case |
871 | break; |
872 | } |
873 | couldBe[j] &= ~unset; |
874 | } |
875 | } |
876 | |
877 | // Now fallback to a standardized order to fill in the rest with |
878 | for (int i = 0; i < unknownCount; ++i) { |
879 | if (couldBe[i] & AMONTH && month == -1) month = unknown[i]; |
880 | else if (couldBe[i] & ADAY && day == -1) day = unknown[i]; |
881 | else if (couldBe[i] & AYEAR && year == -1) year = unknown[i]; |
882 | } |
883 | #ifdef PARSEDATESTRINGDEBUG |
884 | qDebug() << "Final set" << year << month << day; |
885 | #endif |
886 | |
887 | if (year == -1 || month == -1 || day == -1) { |
888 | #ifdef PARSEDATESTRINGDEBUG |
889 | qDebug() << "Parser failure" << year << month << day; |
890 | #endif |
891 | return QDateTime(); |
892 | } |
893 | |
894 | // Y2k behavior |
895 | int y2k = 0; |
896 | if (year < 70) |
897 | y2k = 2000; |
898 | else if (year < 100) |
899 | y2k = 1900; |
900 | |
901 | QDate date(year + y2k, month, day); |
902 | |
903 | // When we were given a bad cookie that when parsed |
904 | // set the day to 29 and the year to one that doesn't |
905 | // have the 29th of Feb rather then adding the extra |
906 | // complicated checking earlier just swap here. |
907 | // Example: 29 23 Feb |
908 | if (!date.isValid()) |
909 | date = QDate(day + y2k, month, year); |
910 | |
911 | QDateTime dateTime(date, time, QTimeZone::UTC); |
912 | |
913 | if (zoneOffset != -1) |
914 | dateTime = dateTime.addSecs(secs: zoneOffset); |
915 | |
916 | if (!dateTime.isValid()) |
917 | return QDateTime(); |
918 | return dateTime; |
919 | } |
920 | |
921 | /*! |
922 | Parses the cookie string \a cookieString as received from a server |
923 | response in the "Set-Cookie:" header. If there's a parsing error, |
924 | this function returns an empty list. |
925 | |
926 | Since the HTTP header can set more than one cookie at the same |
927 | time, this function returns a QList<QNetworkCookie>, one for each |
928 | cookie that is parsed. |
929 | |
930 | \sa toRawForm() |
931 | */ |
932 | QList<QNetworkCookie> QNetworkCookie::parseCookies(const QByteArray &cookieString) |
933 | { |
934 | // cookieString can be a number of set-cookie header strings joined together |
935 | // by \n, parse each line separately. |
936 | QList<QNetworkCookie> cookies; |
937 | QList<QByteArray> list = cookieString.split(sep: '\n'); |
938 | for (int a = 0; a < list.size(); a++) |
939 | cookies += QNetworkCookiePrivate::parseSetCookieHeaderLine(cookieString: list.at(i: a)); |
940 | return cookies; |
941 | } |
942 | |
943 | QList<QNetworkCookie> QNetworkCookiePrivate::(const QByteArray &cookieString) |
944 | { |
945 | // According to http://wp.netscape.com/newsref/std/cookie_spec.html,< |
946 | // the Set-Cookie response header is of the format: |
947 | // |
948 | // Set-Cookie: NAME=VALUE; expires=DATE; path=PATH; domain=DOMAIN_NAME; secure |
949 | // |
950 | // where only the NAME=VALUE part is mandatory |
951 | // |
952 | // We do not support RFC 2965 Set-Cookie2-style cookies |
953 | |
954 | QList<QNetworkCookie> result; |
955 | const QDateTime now = QDateTime::currentDateTimeUtc(); |
956 | |
957 | int position = 0; |
958 | const int length = cookieString.size(); |
959 | while (position < length) { |
960 | QNetworkCookie cookie; |
961 | |
962 | // The first part is always the "NAME=VALUE" part |
963 | QPair<QByteArray,QByteArray> field = nextField(text: cookieString, position, isNameValue: true); |
964 | if (field.first.isEmpty()) |
965 | // parsing error |
966 | break; |
967 | cookie.setName(field.first); |
968 | cookie.setValue(field.second); |
969 | |
970 | position = nextNonWhitespace(text: cookieString, from: position); |
971 | while (position < length) { |
972 | switch (cookieString.at(i: position++)) { |
973 | case ';': |
974 | // new field in the cookie |
975 | field = nextField(text: cookieString, position, isNameValue: false); |
976 | field.first = field.first.toLower(); // everything but the NAME=VALUE is case-insensitive |
977 | |
978 | if (field.first == "expires" ) { |
979 | position -= field.second.size(); |
980 | int end; |
981 | for (end = position; end < length; ++end) |
982 | if (isValueSeparator(c: cookieString.at(i: end))) |
983 | break; |
984 | |
985 | QByteArray dateString = cookieString.mid(index: position, len: end - position).trimmed(); |
986 | position = end; |
987 | QDateTime dt = parseDateString(dateString: dateString.toLower()); |
988 | if (dt.isValid()) |
989 | cookie.setExpirationDate(dt); |
990 | //if unparsed, ignore the attribute but not the whole cookie (RFC6265 section 5.2.1) |
991 | } else if (field.first == "domain" ) { |
992 | QByteArray rawDomain = field.second; |
993 | //empty domain should be ignored (RFC6265 section 5.2.3) |
994 | if (!rawDomain.isEmpty()) { |
995 | QString maybeLeadingDot; |
996 | if (rawDomain.startsWith(c: '.')) { |
997 | maybeLeadingDot = u'.'; |
998 | rawDomain = rawDomain.mid(index: 1); |
999 | } |
1000 | |
1001 | //IDN domains are required by RFC6265, accepting utf8 as well doesn't break any test cases. |
1002 | QString normalizedDomain = QUrl::fromAce(domain: QUrl::toAce(domain: QString::fromUtf8(ba: rawDomain))); |
1003 | if (!normalizedDomain.isEmpty()) { |
1004 | cookie.setDomain(maybeLeadingDot + normalizedDomain); |
1005 | } else { |
1006 | //Normalization fails for malformed domains, e.g. "..example.org", reject the cookie now |
1007 | //rather than accepting it but never sending it due to domain match failure, as the |
1008 | //strict reading of RFC6265 would indicate. |
1009 | return result; |
1010 | } |
1011 | } |
1012 | } else if (field.first == "max-age" ) { |
1013 | bool ok = false; |
1014 | int secs = field.second.toInt(ok: &ok); |
1015 | if (ok) { |
1016 | if (secs <= 0) { |
1017 | //earliest representable time (RFC6265 section 5.2.2) |
1018 | cookie.setExpirationDate(QDateTime::fromSecsSinceEpoch(secs: 0)); |
1019 | } else { |
1020 | cookie.setExpirationDate(now.addSecs(secs)); |
1021 | } |
1022 | } |
1023 | //if unparsed, ignore the attribute but not the whole cookie (RFC6265 section 5.2.2) |
1024 | } else if (field.first == "path" ) { |
1025 | if (field.second.startsWith(c: '/')) { |
1026 | // ### we should treat cookie paths as an octet sequence internally |
1027 | // However RFC6265 says we should assume UTF-8 for presentation as a string |
1028 | cookie.setPath(QString::fromUtf8(ba: field.second)); |
1029 | } else { |
1030 | // if the path doesn't start with '/' then set the default path (RFC6265 section 5.2.4) |
1031 | // and also IETF test case path0030 which has valid and empty path in the same cookie |
1032 | cookie.setPath(QString()); |
1033 | } |
1034 | } else if (field.first == "secure" ) { |
1035 | cookie.setSecure(true); |
1036 | } else if (field.first == "httponly" ) { |
1037 | cookie.setHttpOnly(true); |
1038 | } else if (field.first == "samesite" ) { |
1039 | cookie.setSameSitePolicy(sameSiteFromRawString(str: field.second)); |
1040 | } else { |
1041 | // ignore unknown fields in the cookie (RFC6265 section 5.2, rule 6) |
1042 | } |
1043 | |
1044 | position = nextNonWhitespace(text: cookieString, from: position); |
1045 | } |
1046 | } |
1047 | |
1048 | if (!cookie.name().isEmpty()) |
1049 | result += cookie; |
1050 | } |
1051 | |
1052 | return result; |
1053 | } |
1054 | |
1055 | /*! |
1056 | \since 5.0 |
1057 | This functions normalizes the path and domain of the cookie if they were previously empty. |
1058 | The \a url parameter is used to determine the correct domain and path. |
1059 | */ |
1060 | void QNetworkCookie::normalize(const QUrl &url) |
1061 | { |
1062 | // don't do path checking. See QTBUG-5815 |
1063 | if (d->path.isEmpty()) { |
1064 | QString pathAndFileName = url.path(); |
1065 | QString defaultPath = pathAndFileName.left(n: pathAndFileName.lastIndexOf(c: u'/') + 1); |
1066 | if (defaultPath.isEmpty()) |
1067 | defaultPath = u'/'; |
1068 | d->path = defaultPath; |
1069 | } |
1070 | |
1071 | if (d->domain.isEmpty()) { |
1072 | d->domain = url.host(); |
1073 | } else { |
1074 | QHostAddress hostAddress(d->domain); |
1075 | if (hostAddress.protocol() != QAbstractSocket::IPv4Protocol |
1076 | && hostAddress.protocol() != QAbstractSocket::IPv6Protocol |
1077 | && !d->domain.startsWith(c: u'.')) { |
1078 | // Ensure the domain starts with a dot if its field was not empty |
1079 | // in the HTTP header. There are some servers that forget the |
1080 | // leading dot and this is actually forbidden according to RFC 2109, |
1081 | // but all browsers accept it anyway so we do that as well. |
1082 | d->domain.prepend(c: u'.'); |
1083 | } |
1084 | } |
1085 | } |
1086 | |
1087 | #ifndef QT_NO_DEBUG_STREAM |
1088 | QDebug operator<<(QDebug s, const QNetworkCookie &cookie) |
1089 | { |
1090 | QDebugStateSaver saver(s); |
1091 | s.resetFormat().nospace(); |
1092 | s << "QNetworkCookie(" << cookie.toRawForm(form: QNetworkCookie::Full) << ')'; |
1093 | return s; |
1094 | } |
1095 | #endif |
1096 | |
1097 | QT_END_NAMESPACE |
1098 | |
1099 | #include "moc_qnetworkcookie.cpp" |
1100 | |