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(QStringView path, QStringView 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(n: reference.size()) == u'/') |
129 | return true; |
130 | } |
131 | return false; |
132 | } |
133 | |
134 | static inline bool isParentDomain(QStringView domain, QStringView reference) |
135 | { |
136 | if (!reference.startsWith(c: u'.')) |
137 | return domain == reference; |
138 | |
139 | return domain.endsWith(s: reference) || domain == 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 | const bool isEncrypted = url.scheme() == "https"_L1 ; |
204 | |
205 | // scan our cookies for something that matches |
206 | for (const auto &cookie : std::as_const(t: d->allCookies)) { |
207 | if (!isEncrypted && cookie.isSecure()) |
208 | continue; |
209 | if (!cookie.isSessionCookie() && cookie.expirationDate() < now) |
210 | continue; |
211 | const QString urlHost = url.host(); |
212 | const QString cookieDomain = cookie.domain(); |
213 | if (!isParentDomain(domain: urlHost, reference: cookieDomain)) |
214 | continue; |
215 | if (!isParentPath(path: url.path(), reference: cookie.path())) |
216 | continue; |
217 | |
218 | QStringView domain = cookieDomain; |
219 | if (domain.startsWith(c: u'.')) /// Qt6?: remove when compliant with RFC6265 |
220 | domain = domain.sliced(pos: 1); |
221 | #if QT_CONFIG(topleveldomain) |
222 | if (urlHost != domain && qIsEffectiveTLD(domain)) |
223 | continue; |
224 | #else |
225 | if (!domain.contains(u'.') && urlHost != domain) |
226 | continue; |
227 | #endif // topleveldomain |
228 | |
229 | result += cookie; |
230 | } |
231 | |
232 | auto longerPath = [](const auto &c1, const auto &c2) |
233 | { return c1.path().size() > c2.path().size(); }; |
234 | std::sort(first: result.begin(), last: result.end(), comp: longerPath); |
235 | return result; |
236 | } |
237 | |
238 | /*! |
239 | \since 5.0 |
240 | Adds \a cookie to this cookie jar. |
241 | |
242 | Returns \c true if \a cookie was added, false otherwise. |
243 | |
244 | If a cookie with the same identifier already exists in the |
245 | cookie jar, it will be overridden. |
246 | */ |
247 | bool QNetworkCookieJar::insertCookie(const QNetworkCookie &cookie) |
248 | { |
249 | Q_D(QNetworkCookieJar); |
250 | const QDateTime now = QDateTime::currentDateTimeUtc(); |
251 | bool isDeletion = !cookie.isSessionCookie() && |
252 | cookie.expirationDate() < now; |
253 | |
254 | deleteCookie(cookie); |
255 | |
256 | if (!isDeletion) { |
257 | d->allCookies += cookie; |
258 | return true; |
259 | } |
260 | return false; |
261 | } |
262 | |
263 | /*! |
264 | \since 5.0 |
265 | If a cookie with the same identifier as \a cookie exists in this cookie jar |
266 | it will be updated. This function uses insertCookie(). |
267 | |
268 | Returns \c true if \a cookie was updated, false if no cookie in the jar matches |
269 | the identifier of \a cookie. |
270 | |
271 | \sa QNetworkCookie::hasSameIdentifier() |
272 | */ |
273 | bool QNetworkCookieJar::updateCookie(const QNetworkCookie &cookie) |
274 | { |
275 | if (deleteCookie(cookie)) |
276 | return insertCookie(cookie); |
277 | return false; |
278 | } |
279 | |
280 | /*! |
281 | \since 5.0 |
282 | Deletes from cookie jar the cookie found to have the same identifier as \a cookie. |
283 | |
284 | Returns \c true if a cookie was deleted, false otherwise. |
285 | |
286 | \sa QNetworkCookie::hasSameIdentifier() |
287 | */ |
288 | bool QNetworkCookieJar::deleteCookie(const QNetworkCookie &cookie) |
289 | { |
290 | Q_D(QNetworkCookieJar); |
291 | const auto it = std::find_if(first: d->allCookies.cbegin(), last: d->allCookies.cend(), |
292 | pred: [&cookie](const auto &c) { return c.hasSameIdentifier(cookie); }); |
293 | if (it != d->allCookies.cend()) { |
294 | d->allCookies.erase(pos: it); |
295 | return true; |
296 | } |
297 | return false; |
298 | } |
299 | |
300 | /*! |
301 | \since 5.0 |
302 | Returns \c true if the domain and path of \a cookie are valid, false otherwise. |
303 | The \a url parameter is used to determine if the domain specified in the cookie |
304 | is allowed. |
305 | */ |
306 | bool QNetworkCookieJar::validateCookie(const QNetworkCookie &cookie, const QUrl &url) const |
307 | { |
308 | const QString cookieDomain = cookie.domain(); |
309 | QStringView domain = cookieDomain; |
310 | const QString host = url.host(); |
311 | if (!isParentDomain(domain, reference: host) && !isParentDomain(domain: host, reference: domain)) |
312 | return false; // not accepted |
313 | |
314 | if (domain.startsWith(c: u'.')) |
315 | domain = domain.sliced(pos: 1); |
316 | |
317 | // We shouldn't reject if: |
318 | // "[...] the domain-attribute is identical to the canonicalized request-host" |
319 | // https://tools.ietf.org/html/rfc6265#section-5.3 step 5 |
320 | if (host == domain) |
321 | return true; |
322 | // the check for effective TLDs makes the "embedded dot" rule from RFC 2109 section 4.3.2 |
323 | // redundant; the "leading dot" rule has been relaxed anyway, see QNetworkCookie::normalize() |
324 | // we remove the leading dot for this check if it's present |
325 | // Normally defined in qtldurl_p.h, but uses fall-back in this file when topleveldomain isn't |
326 | // configured: |
327 | return !qIsEffectiveTLD(domain); |
328 | } |
329 | |
330 | QT_END_NAMESPACE |
331 | |
332 | #include "moc_qnetworkcookiejar.cpp" |
333 | |