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