1 /*
2     This file is part of the KDE project.
3 
4     SPDX-FileCopyrightText: 2018 Stefano Crocco <stefano.crocco@alice.it>
5 
6     SPDX-License-Identifier: LGPL-2.1-or-later
7 */
8 
9 #include "webenginepartcookiejar.h"
10 #include "settings/webenginesettings.h"
11 #include <webenginepart_debug.h>
12 
13 #include <QWebEngineProfile>
14 #include <QStringList>
15 #include <QDBusReply>
16 #include <QDebug>
17 #include <QWidget>
18 #include <QDateTime>
19 #include <QTimeZone>
20 #include <QApplication>
21 #include <kio_version.h>
22 
23 const QVariant WebEnginePartCookieJar::s_findCookieFields = QVariant::fromValue(QList<int>{
24         static_cast<int>(CookieDetails::domain),
25         static_cast<int>(CookieDetails::path),
26         static_cast<int>(CookieDetails::name),
27         static_cast<int>(CookieDetails::host),
28         static_cast<int>(CookieDetails::value),
29         static_cast<int>(CookieDetails::expirationDate),
30         static_cast<int>(CookieDetails::protocolVersion),
31         static_cast<int>(CookieDetails::secure)
32     }
33 );
34 
CookieIdentifier(const QNetworkCookie & cookie)35 WebEnginePartCookieJar::CookieIdentifier::CookieIdentifier(const QNetworkCookie& cookie):
36     name(cookie.name()), domain(cookie.domain()), path(cookie.path())
37 {
38 }
39 
CookieIdentifier(const QString & n,const QString & d,const QString & p)40 WebEnginePartCookieJar::CookieIdentifier::CookieIdentifier(const QString& n, const QString& d, const QString& p):
41     name(n), domain(d), path(p)
42 {
43 }
44 
WebEnginePartCookieJar(QWebEngineProfile * prof,QObject * parent)45 WebEnginePartCookieJar::WebEnginePartCookieJar(QWebEngineProfile *prof, QObject *parent):
46     QObject(parent), m_cookieStore(prof->cookieStore()),
47     m_cookieServer("org.kde.kcookiejar5", "/modules/kcookiejar", "org.kde.KCookieServer")
48 {
49     prof->setPersistentCookiesPolicy(QWebEngineProfile::NoPersistentCookies);
50     connect(qApp, &QApplication::lastWindowClosed, this, &WebEnginePartCookieJar::deleteSessionCookies);
51     connect(m_cookieStore, &QWebEngineCookieStore::cookieAdded, this, &WebEnginePartCookieJar::addCookie);
52     connect(m_cookieStore, &QWebEngineCookieStore::cookieRemoved, this, &WebEnginePartCookieJar::removeCookie);
53     if(!m_cookieServer.isValid()){
54         qCDebug(WEBENGINEPART_LOG) << "Couldn't connect to KCookieServer";
55     }
56 
57     loadKIOCookies();
58 
59     auto filter = [this](const QWebEngineCookieStore::FilterRequest &req){return filterCookie(req);};
60     m_cookieStore->setCookieFilter(filter);
61 }
62 
~WebEnginePartCookieJar()63 WebEnginePartCookieJar::~WebEnginePartCookieJar()
64 {
65 }
66 
filterCookie(const QWebEngineCookieStore::FilterRequest & req)67 bool WebEnginePartCookieJar::filterCookie(const QWebEngineCookieStore::FilterRequest& req)
68 {
69     return WebEngineSettings::self()->acceptCrossDomainCookies() || !req.thirdParty;
70 }
71 
deleteSessionCookies()72 void WebEnginePartCookieJar::deleteSessionCookies()
73 {
74     if (!m_cookieServer.isValid()) {
75         return;
76     }
77     foreach(qlonglong id, m_windowsWithSessionCookies) {
78         m_cookieServer.call(QDBus::NoBlock, "deleteSessionCookies", id);
79     }
80 }
81 
constructUrlForCookie(const QNetworkCookie & cookie) const82 QUrl WebEnginePartCookieJar::constructUrlForCookie(const QNetworkCookie& cookie) const
83 {
84     QUrl url;
85     QString domain = cookie.domain().startsWith(".") ? cookie.domain().mid(1) : cookie.domain();
86     if (!domain.isEmpty()) {
87         url.setScheme("http");
88         url.setHost(domain);
89         url.setPath(cookie.path());
90     } else {
91         qCDebug(WEBENGINEPART_LOG) << "EMPTY COOKIE DOMAIN for" << cookie.name();
92     }
93     return url;
94 }
95 
findWinID()96 qlonglong WebEnginePartCookieJar::findWinID()
97 {
98     QWidget *mainWindow = qApp->activeWindow();
99     if (mainWindow && !mainWindow->windowFlags().testFlag(Qt::Dialog)) {
100         return mainWindow->winId();
101     } else {
102         QWidgetList windows = qApp->topLevelWidgets();
103         foreach(QWidget *w, windows){
104             if (!w->windowFlags().testFlag(Qt::Dialog)) {
105                 return w->winId();
106             }
107         }
108     }
109     return 0;
110 }
111 
removeCookieDomain(QNetworkCookie & cookie)112 void WebEnginePartCookieJar::removeCookieDomain(QNetworkCookie& cookie)
113 {
114     if (!cookie.domain().startsWith('.')) {
115         cookie.setDomain(QString());
116     }
117 }
118 
addCookie(const QNetworkCookie & _cookie)119 void WebEnginePartCookieJar::addCookie(const QNetworkCookie& _cookie)
120 {
121     //If the added cookie is in m_cookiesLoadedFromKCookieServer, it means
122     //we're loading the cookie from KCookieServer (from the call to loadKIOCookies
123     //in the constructor (QWebEngineCookieStore::setCookie is asynchronous, though,
124     //so we're not in the constructor anymore)), so don't attempt to add
125     //the cookie back to KCookieServer; instead, remove it from the list.
126     if (m_cookiesLoadedFromKCookieServer.removeOne(_cookie)) {
127         return;
128     }
129 
130 #ifdef BUILD_TESTING
131         m_testCookies.clear();
132 #endif
133 
134     QNetworkCookie cookie(_cookie);
135     CookieIdentifier id(cookie);
136 
137     if (!m_cookieServer.isValid()) {
138         return;
139     }
140 
141     if (cookie.expirationDate().isValid()) {
142     //There's a bug in KCookieJar which causes the expiration date to be interpreted as local time
143     //instead of GMT as it should. The bug is fixed in KIO 5.50
144     }
145     QUrl url = constructUrlForCookie(cookie);
146     if (url.isEmpty()) {
147         return;
148     }
149     //NOTE: the removal of the domain (when not starting with a dot) must be done *after* creating
150     //the URL, as constructUrlForCookie needs the domain
151     removeCookieDomain(cookie);
152     QByteArray header("Set-Cookie: ");
153     header += cookie.toRawForm();
154     header += "\n";
155     qlonglong winId = findWinID();
156     if (!cookie.expirationDate().isValid()) {
157         m_windowsWithSessionCookies.insert(winId);
158     }
159 //     qCDebug(WEBENGINEPART_LOG) << url;
160     QString advice = askAdvice(url);
161     if (advice == "Reject"){
162         m_pendingRejectedCookies << CookieIdentifier(_cookie);
163         m_cookieStore->deleteCookie(_cookie);
164     } else if (advice == "AcceptForSession" && !cookie.isSessionCookie()) {
165         cookie.setExpirationDate(QDateTime());
166         addCookie(cookie);
167     } else {
168         int oldTimeout = m_cookieServer.timeout();
169         if (advice == "Ask") {
170             //Give the user time (10 minutes = 600 000ms) to analyze the cookie
171             m_cookieServer.setTimeout(10*60*1000);
172         }
173         m_cookieServer.call(QDBus::Block, "addCookies", url.toString(), header, winId);
174         m_cookieServer.setTimeout(oldTimeout);
175         if (m_cookieServer.lastError().isValid()) {
176             qCDebug(WEBENGINEPART_LOG) << m_cookieServer.lastError();
177             return;
178         }
179         if (!advice.startsWith("Accept") && !cookieInKCookieJar(id, url)) {
180             m_pendingRejectedCookies << id;
181             m_cookieStore->deleteCookie(_cookie);
182         }
183     }
184 }
185 
askAdvice(const QUrl & url)186 QString WebEnginePartCookieJar::askAdvice(const QUrl& url)
187 {
188     if (!m_cookieServer.isValid()) {
189         return QString();
190     }
191     QDBusReply<QString> rep = m_cookieServer.call(QDBus::Block, "getDomainAdvice", url.toString());
192     if (rep.isValid()) {
193         return rep.value();
194     } else {
195         qCDebug(WEBENGINEPART_LOG) << rep.error().message();
196         return QString();
197     }
198 }
199 
cookieInKCookieJar(const WebEnginePartCookieJar::CookieIdentifier & id,const QUrl & url)200 bool WebEnginePartCookieJar::cookieInKCookieJar(const WebEnginePartCookieJar::CookieIdentifier& id, const QUrl& url)
201 {
202     if (!m_cookieServer.isValid()) {
203         return false;
204     }
205     QList<int> fields = {
206         static_cast<int>(CookieDetails::name),
207         static_cast<int>(CookieDetails::domain),
208         static_cast<int>(CookieDetails::path)
209     };
210     QDBusReply<QStringList> rep = m_cookieServer.call(QDBus::Block, "findCookies", QVariant::fromValue(fields), id.domain, url.toString(QUrl::FullyEncoded), id.path, id.name);
211     if (!rep.isValid()) {
212         qCDebug(WEBENGINEPART_LOG) << rep.error().message();
213         return false;
214     }
215     QStringList cookies = rep.value();
216     for(int i = 0; i < cookies.length()-2; i+=3){
217         if (CookieIdentifier(cookies.at(i), cookies.at(i+1), cookies.at(i+2)) == id) {
218             return true;
219         }
220     }
221     return false;
222 }
223 
removeCookie(const QNetworkCookie & _cookie)224 void WebEnginePartCookieJar::removeCookie(const QNetworkCookie& _cookie)
225 {
226 
227     int pos = m_pendingRejectedCookies.indexOf(CookieIdentifier(_cookie));
228     //Ignore pending cookies
229     if (pos >= 0) {
230         m_pendingRejectedCookies.takeAt(pos);
231         return;
232     }
233 
234     if (!m_cookieServer.isValid()) {
235         return;
236     }
237 
238     QNetworkCookie cookie(_cookie);
239     QUrl url = constructUrlForCookie(cookie);
240     if(url.isEmpty()){
241         qCDebug(WEBENGINEPART_LOG) << "Can't remove cookie" << cookie.name() << "because its URL isn't known";
242         return;
243     }
244     removeCookieDomain(cookie);
245 
246     QDBusPendingCall pcall = m_cookieServer.asyncCall("deleteCookie", cookie.domain(), url.host(), cookie.path(), QString(cookie.name()));
247     QDBusPendingCallWatcher *w = new QDBusPendingCallWatcher(pcall, this);
248     connect(w, &QDBusPendingCallWatcher::finished, this, &WebEnginePartCookieJar::cookieRemovalFailed);
249 }
250 
cookieRemovalFailed(QDBusPendingCallWatcher * watcher)251 void WebEnginePartCookieJar::cookieRemovalFailed(QDBusPendingCallWatcher *watcher)
252 {
253     QDBusPendingReply<> r = *watcher;
254     if (r.isError()){
255         qCDebug(WEBENGINEPART_LOG) << "DBus error:" << r.error().message();
256     }
257     watcher->deleteLater();
258 }
259 
loadKIOCookies()260 void WebEnginePartCookieJar::loadKIOCookies()
261 {
262     const CookieUrlList cookies = findKIOCookies();
263     for (const CookieWithUrl& cookieWithUrl : cookies){
264         QNetworkCookie cookie = cookieWithUrl.cookie;
265         QDateTime currentTime = QDateTime::currentDateTime();
266         //Don't attempt to add expired cookies
267         if (cookie.expirationDate().isValid() && cookie.expirationDate() < currentTime) {
268             continue;
269         }
270         QNetworkCookie normalizedCookie(cookie);
271         normalizedCookie.normalize(cookieWithUrl.url);
272         m_cookiesLoadedFromKCookieServer << cookie;
273 #ifdef BUILD_TESTING
274         m_testCookies << cookie;
275 #endif
276         m_cookieStore->setCookie(cookie, cookieWithUrl.url);
277     }
278 }
279 
findKIOCookies()280 WebEnginePartCookieJar::CookieUrlList WebEnginePartCookieJar::findKIOCookies()
281 {
282     CookieUrlList res;
283     if (!m_cookieServer.isValid()) {
284         return res;
285     }
286     QDBusReply<QStringList> rep = m_cookieServer.call(QDBus::Block, "findDomains");
287     if(!rep.isValid()){
288         qCDebug(WEBENGINEPART_LOG) << rep.error().message();
289         return res;
290     }
291     QStringList domains = rep.value();
292     uint fieldsCount = 8;
293     foreach( const QString &d, domains){
294     QDBusReply<QStringList> rep = m_cookieServer.call(QDBus::Block, "findCookies", s_findCookieFields, d, "", "", "");
295         if (!rep.isValid()) {
296             qCDebug(WEBENGINEPART_LOG) << rep.error().message();
297             return res;
298         }
299         QStringList data = rep.value();
300         for(int i = 0; i < data.count(); i+=fieldsCount){
301             res << parseKIOCookie(data, i);
302         }
303     }
304     return res;
305 }
306 
307 //This function used to return a normalized cookie. However, doing so doesn't work correctly because QWebEngineCookieStore::setCookie
308 //in turns normalizes the cookie. One could think that calling normalize twice wouldn't be a problem, but that would be wrong. If the cookie
309 //domain is originally empty, after a call to normalize it will contain the cookie origin host. The second call to normalize will see a cookie
310 //whose domain is not empty and doesn't start with a dot and will add a dot to it.
parseKIOCookie(const QStringList & data,int start)311 WebEnginePartCookieJar::CookieWithUrl WebEnginePartCookieJar::parseKIOCookie(const QStringList& data, int start)
312 {
313     QNetworkCookie c;
314     auto extractField = [data, start](CookieDetails field){return data.at(start + static_cast<int>(field));};
315     c.setDomain(extractField(CookieDetails::domain));
316     c.setExpirationDate(QDateTime::fromSecsSinceEpoch(extractField(CookieDetails::expirationDate).toInt()));
317     c.setName(extractField(CookieDetails::name).toUtf8());
318     QString path = extractField(CookieDetails::path);
319     c.setPath(path);
320     c.setSecure(extractField(CookieDetails::secure).toInt()); //1 for true, 0 for false
321     c.setValue(extractField(CookieDetails::value).toUtf8());
322 
323     QString host = extractField(CookieDetails::host);
324     QUrl url;
325     url.setScheme(c.isSecure() ? "https" : "http");
326     url.setHost(host);
327     url.setPath(path);
328     return CookieWithUrl{c, url};
329 }
330 
operator <<(QDebug deb,const WebEnginePartCookieJar::CookieIdentifier & id)331 QDebug operator<<(QDebug deb, const WebEnginePartCookieJar::CookieIdentifier& id)
332 {
333     QDebugStateSaver saver(deb);
334     deb << "(" << id.name << "," << id.domain << "," << id.path << ")";
335     return deb;
336 }
337