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