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