1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qnetworkcookiejar.h" |
5 | #include "qnetworkcookiejar_p.h" |
6 | |
7 | #include "QtNetwork/qnetworkcookie.h" |
8 | #include "QtCore/qurl.h" |
9 | #include "QtCore/qdatetime.h" |
10 | #if QT_CONFIG(topleveldomain) |
11 | #include "private/qtldurl_p.h" |
12 | #else |
13 | QT_BEGIN_NAMESPACE |
14 | static bool qIsEffectiveTLD(QStringView domain) |
15 | { |
16 | // provide minimal checking by not accepting cookies on real TLDs |
17 | return !domain.contains(u'.'); |
18 | } |
19 | QT_END_NAMESPACE |
20 | #endif |
21 | |
22 | QT_BEGIN_NAMESPACE |
23 | |
24 | using namespace Qt::StringLiterals; |
25 | |
26 | /*! |
27 | \class QNetworkCookieJar |
28 | \since 4.4 |
29 | \inmodule QtNetwork |
30 | |
31 | \brief The QNetworkCookieJar class implements a simple jar of QNetworkCookie objects. |
32 | |
33 | Cookies are small bits of information that stateless protocols |
34 | like HTTP use to maintain some persistent information across |
35 | requests. |
36 | |
37 | A cookie is set by a remote server when it replies to a request |
38 | and it expects the same cookie to be sent back when further |
39 | requests are sent. |
40 | |
41 | The cookie jar is the object that holds all cookies set in |
42 | previous requests. Web browsers save their cookie jars to disk in |
43 | order to conserve permanent cookies across invocations of the |
44 | application. |
45 | |
46 | QNetworkCookieJar does not implement permanent storage: it only |
47 | keeps the cookies in memory. Once the QNetworkCookieJar object is |
48 | deleted, all cookies it held will be discarded as well. If you |
49 | want to save the cookies, you should derive from this class and |
50 | implement the saving to disk to your own storage format. |
51 | |
52 | This class implements only the basic security recommended by the |
53 | cookie specifications and does not implement any cookie acceptance |
54 | policy (it accepts all cookies set by any requests). In order to |
55 | override those rules, you should reimplement the |
56 | cookiesForUrl() and setCookiesFromUrl() virtual |
57 | functions. They are called by QNetworkReply and |
58 | QNetworkAccessManager when they detect new cookies and when they |
59 | require cookies. |
60 | |
61 | \sa QNetworkCookie, QNetworkAccessManager, QNetworkReply, |
62 | QNetworkRequest, QNetworkAccessManager::setCookieJar() |
63 | */ |
64 | |
65 | /*! |
66 | Creates a QNetworkCookieJar object and sets the parent object to |
67 | be \a parent. |
68 | |
69 | The cookie jar is initialized to empty. |
70 | */ |
71 | QNetworkCookieJar::QNetworkCookieJar(QObject *parent) |
72 | : QObject(*new QNetworkCookieJarPrivate, parent) |
73 | { |
74 | } |
75 | |
76 | /*! |
77 | Destroys this cookie jar object and discards all cookies stored in |
78 | it. Cookies are not saved to disk in the QNetworkCookieJar default |
79 | implementation. |
80 | |
81 | If you need to save the cookies to disk, you have to derive from |
82 | QNetworkCookieJar and save the cookies to disk yourself. |
83 | */ |
84 | QNetworkCookieJar::~QNetworkCookieJar() |
85 | { |
86 | } |
87 | |
88 | /*! |
89 | Returns all cookies stored in this cookie jar. This function is |
90 | suitable for derived classes to save cookies to disk, as well as |
91 | to implement cookie expiration and other policies. |
92 | |
93 | \sa setAllCookies(), cookiesForUrl() |
94 | */ |
95 | QList<QNetworkCookie> QNetworkCookieJar::allCookies() const |
96 | { |
97 | return d_func()->allCookies; |
98 | } |
99 | |
100 | /*! |
101 | Sets the internal list of cookies held by this cookie jar to be \a |
102 | cookieList. This function is suitable for derived classes to |
103 | implement loading cookies from permanent storage, or their own |
104 | cookie acceptance policies by reimplementing |
105 | setCookiesFromUrl(). |
106 | |
107 | \sa allCookies(), setCookiesFromUrl() |
108 | */ |
109 | void QNetworkCookieJar::setAllCookies(const QList<QNetworkCookie> &cookieList) |
110 | { |
111 | Q_D(QNetworkCookieJar); |
112 | d->allCookies = cookieList; |
113 | } |
114 | |
115 | static inline bool isParentPath(const QString &path, const QString &reference) |
116 | { |
117 | if ((path.isEmpty() && reference == "/"_L1 ) || path.startsWith(s: reference)) { |
118 | //The cookie-path and the request-path are identical. |
119 | if (path.size() == reference.size()) |
120 | return true; |
121 | //The cookie-path is a prefix of the request-path, and the last |
122 | //character of the cookie-path is %x2F ("/"). |
123 | if (reference.endsWith(c: u'/')) |
124 | return true; |
125 | //The cookie-path is a prefix of the request-path, and the first |
126 | //character of the request-path that is not included in the cookie- |
127 | //path is a %x2F ("/") character. |
128 | if (path.at(i: reference.size()) == u'/') |
129 | return true; |
130 | } |
131 | return false; |
132 | } |
133 | |
134 | static inline bool isParentDomain(const QString &domain, const QString &reference) |
135 | { |
136 | if (!reference.startsWith(c: u'.')) |
137 | return domain == reference; |
138 | |
139 | return domain.endsWith(s: reference) || domain == QStringView{reference}.mid(pos: 1); |
140 | } |
141 | |
142 | /*! |
143 | Adds the cookies in the list \a cookieList to this cookie |
144 | jar. Before being inserted cookies are normalized. |
145 | |
146 | Returns \c true if one or more cookies are set for \a url, |
147 | otherwise false. |
148 | |
149 | If a cookie already exists in the cookie jar, it will be |
150 | overridden by those in \a cookieList. |
151 | |
152 | The default QNetworkCookieJar class implements only a very basic |
153 | security policy (it makes sure that the cookies' domain and path |
154 | match the reply's). To enhance the security policy with your own |
155 | algorithms, override setCookiesFromUrl(). |
156 | |
157 | Also, QNetworkCookieJar does not have a maximum cookie jar |
158 | size. Reimplement this function to discard older cookies to create |
159 | room for new ones. |
160 | |
161 | \sa cookiesForUrl(), QNetworkAccessManager::setCookieJar(), QNetworkCookie::normalize() |
162 | */ |
163 | bool QNetworkCookieJar::setCookiesFromUrl(const QList<QNetworkCookie> &cookieList, |
164 | const QUrl &url) |
165 | { |
166 | bool added = false; |
167 | for (QNetworkCookie cookie : cookieList) { |
168 | cookie.normalize(url); |
169 | if (validateCookie(cookie, url) && insertCookie(cookie)) |
170 | added = true; |
171 | } |
172 | return added; |
173 | } |
174 | |
175 | /*! |
176 | Returns the cookies to be added to when a request is sent to |
177 | \a url. This function is called by the default |
178 | QNetworkAccessManager::createRequest(), which adds the |
179 | cookies returned by this function to the request being sent. |
180 | |
181 | If more than one cookie with the same name is found, but with |
182 | differing paths, the one with longer path is returned before the |
183 | one with shorter path. In other words, this function returns |
184 | cookies sorted decreasingly by path length. |
185 | |
186 | The default QNetworkCookieJar class implements only a very basic |
187 | security policy (it makes sure that the cookies' domain and path |
188 | match the reply's). To enhance the security policy with your own |
189 | algorithms, override cookiesForUrl(). |
190 | |
191 | \sa setCookiesFromUrl(), QNetworkAccessManager::setCookieJar() |
192 | */ |
193 | QList<QNetworkCookie> QNetworkCookieJar::cookiesForUrl(const QUrl &url) const |
194 | { |
195 | // \b Warning! This is only a dumb implementation! |
196 | // It does NOT follow all of the recommendations from |
197 | // http://wp.netscape.com/newsref/std/cookie_spec.html |
198 | // It does not implement a very good cross-domain verification yet. |
199 | |
200 | Q_D(const QNetworkCookieJar); |
201 | const QDateTime now = QDateTime::currentDateTimeUtc(); |
202 | QList<QNetworkCookie> result; |
203 | bool isEncrypted = url.scheme() == "https"_L1 ; |
204 | |
205 | // scan our cookies for something that matches |
206 | QList<QNetworkCookie>::ConstIterator it = d->allCookies.constBegin(), |
207 | end = d->allCookies.constEnd(); |
208 | for ( ; it != end; ++it) { |
209 | if (!isParentDomain(domain: url.host(), reference: it->domain())) |
210 | continue; |
211 | if (!isParentPath(path: url.path(), reference: it->path())) |
212 | continue; |
213 | if (!(*it).isSessionCookie() && (*it).expirationDate() < now) |
214 | continue; |
215 | if ((*it).isSecure() && !isEncrypted) |
216 | continue; |
217 | |
218 | QString domain = it->domain(); |
219 | if (domain.startsWith(c: u'.')) /// Qt6?: remove when compliant with RFC6265 |
220 | domain = domain.mid(position: 1); |
221 | #if QT_CONFIG(topleveldomain) |
222 | if (qIsEffectiveTLD(domain) && url.host() != domain) |
223 | continue; |
224 | #else |
225 | if (!domain.contains(u'.') && url.host() != domain) |
226 | continue; |
227 | #endif // topleveldomain |
228 | |
229 | // insert this cookie into result, sorted by path |
230 | QList<QNetworkCookie>::Iterator insertIt = result.begin(); |
231 | while (insertIt != result.end()) { |
232 | if (insertIt->path().size() < it->path().size()) { |
233 | // insert here |
234 | insertIt = result.insert(before: insertIt, t: *it); |
235 | break; |
236 | } else { |
237 | ++insertIt; |
238 | } |
239 | } |
240 | |
241 | // this is the shortest path yet, just append |
242 | if (insertIt == result.end()) |
243 | result += *it; |
244 | } |
245 | |
246 | return result; |
247 | } |
248 | |
249 | /*! |
250 | \since 5.0 |
251 | Adds \a cookie to this cookie jar. |
252 | |
253 | Returns \c true if \a cookie was added, false otherwise. |
254 | |
255 | If a cookie with the same identifier already exists in the |
256 | cookie jar, it will be overridden. |
257 | */ |
258 | bool QNetworkCookieJar::insertCookie(const QNetworkCookie &cookie) |
259 | { |
260 | Q_D(QNetworkCookieJar); |
261 | const QDateTime now = QDateTime::currentDateTimeUtc(); |
262 | bool isDeletion = !cookie.isSessionCookie() && |
263 | cookie.expirationDate() < now; |
264 | |
265 | deleteCookie(cookie); |
266 | |
267 | if (!isDeletion) { |
268 | d->allCookies += cookie; |
269 | return true; |
270 | } |
271 | return false; |
272 | } |
273 | |
274 | /*! |
275 | \since 5.0 |
276 | If a cookie with the same identifier as \a cookie exists in this cookie jar |
277 | it will be updated. This function uses insertCookie(). |
278 | |
279 | Returns \c true if \a cookie was updated, false if no cookie in the jar matches |
280 | the identifier of \a cookie. |
281 | |
282 | \sa QNetworkCookie::hasSameIdentifier() |
283 | */ |
284 | bool QNetworkCookieJar::updateCookie(const QNetworkCookie &cookie) |
285 | { |
286 | if (deleteCookie(cookie)) |
287 | return insertCookie(cookie); |
288 | return false; |
289 | } |
290 | |
291 | /*! |
292 | \since 5.0 |
293 | Deletes from cookie jar the cookie found to have the same identifier as \a cookie. |
294 | |
295 | Returns \c true if a cookie was deleted, false otherwise. |
296 | |
297 | \sa QNetworkCookie::hasSameIdentifier() |
298 | */ |
299 | bool QNetworkCookieJar::deleteCookie(const QNetworkCookie &cookie) |
300 | { |
301 | Q_D(QNetworkCookieJar); |
302 | QList<QNetworkCookie>::Iterator it; |
303 | for (it = d->allCookies.begin(); it != d->allCookies.end(); ++it) { |
304 | if (it->hasSameIdentifier(other: cookie)) { |
305 | d->allCookies.erase(pos: it); |
306 | return true; |
307 | } |
308 | } |
309 | return false; |
310 | } |
311 | |
312 | /*! |
313 | \since 5.0 |
314 | Returns \c true if the domain and path of \a cookie are valid, false otherwise. |
315 | The \a url parameter is used to determine if the domain specified in the cookie |
316 | is allowed. |
317 | */ |
318 | bool QNetworkCookieJar::validateCookie(const QNetworkCookie &cookie, const QUrl &url) const |
319 | { |
320 | QString domain = cookie.domain(); |
321 | const QString host = url.host(); |
322 | if (!isParentDomain(domain, reference: host) && !isParentDomain(domain: host, reference: domain)) |
323 | return false; // not accepted |
324 | |
325 | if (domain.startsWith(c: u'.')) |
326 | domain = domain.mid(position: 1); |
327 | |
328 | // We shouldn't reject if: |
329 | // "[...] the domain-attribute is identical to the canonicalized request-host" |
330 | // https://tools.ietf.org/html/rfc6265#section-5.3 step 5 |
331 | if (host == domain) |
332 | return true; |
333 | // the check for effective TLDs makes the "embedded dot" rule from RFC 2109 section 4.3.2 |
334 | // redundant; the "leading dot" rule has been relaxed anyway, see QNetworkCookie::normalize() |
335 | // we remove the leading dot for this check if it's present |
336 | // Normally defined in qtldurl_p.h, but uses fall-back in this file when topleveldomain isn't |
337 | // configured: |
338 | return !qIsEffectiveTLD(domain); |
339 | } |
340 | |
341 | QT_END_NAMESPACE |
342 | |
343 | #include "moc_qnetworkcookiejar.cpp" |
344 | |