1 /*
2     This file is part of the KDE project.
3     SPDX-FileCopyrightText: 2009-2012 Dawit Alemayehu <adawit @ kde.org>
4     SPDX-FileCopyrightText: 2008-2009 Urs Wolfer <uwolfer @ kde.org>
5     SPDX-FileCopyrightText: 2007 Trolltech ASA
6 
7     SPDX-License-Identifier: LGPL-2.0-or-later
8 */
9 
10 #include "accessmanager.h"
11 
12 #include "accessmanagerreply_p.h"
13 #include "job.h"
14 #include "kio_widgets_debug.h"
15 #include "scheduler.h"
16 #include <KConfigGroup>
17 #include <KJobWidgets>
18 #include <KLocalizedString>
19 #include <KSharedConfig>
20 #include <kprotocolinfo.h>
21 
22 #include <QDBusInterface>
23 #include <QDBusReply>
24 #include <QNetworkCookie>
25 #include <QNetworkReply>
26 #include <QPointer>
27 #include <QSslCertificate>
28 #include <QSslCipher>
29 #include <QSslConfiguration>
30 #include <QUrl>
31 #include <QWidget>
32 
33 
34 static const QNetworkRequest::Attribute gSynchronousNetworkRequestAttribute = QNetworkRequest::SynchronousRequestAttribute;
35 
sizeFromRequest(const QNetworkRequest & req)36 static qint64 sizeFromRequest(const QNetworkRequest &req)
37 {
38     const QVariant size = req.header(QNetworkRequest::ContentLengthHeader);
39     if (!size.isValid()) {
40         return -1;
41     }
42     bool ok = false;
43     const qlonglong value = size.toLongLong(&ok);
44     return (ok ? value : -1);
45 }
46 
47 namespace KIO
48 {
49 class Q_DECL_HIDDEN AccessManager::AccessManagerPrivate
50 {
51 public:
AccessManagerPrivate()52     AccessManagerPrivate()
53         : externalContentAllowed(true)
54         , emitReadyReadOnMetaDataChange(false)
55         , window(nullptr)
56     {
57     }
58 
59     void setMetaDataForRequest(QNetworkRequest request, KIO::MetaData &metaData);
60 
61     bool externalContentAllowed;
62     bool emitReadyReadOnMetaDataChange;
63     KIO::MetaData requestMetaData;
64     KIO::MetaData sessionMetaData;
65     QPointer<QWidget> window;
66 };
67 
68 namespace Integration
69 {
70 class Q_DECL_HIDDEN CookieJar::CookieJarPrivate
71 {
72 public:
CookieJarPrivate()73     CookieJarPrivate()
74         : windowId((WId)-1)
75         , isEnabled(true)
76         , isStorageDisabled(false)
77     {
78     }
79 
80     WId windowId;
81     bool isEnabled;
82     bool isStorageDisabled;
83 };
84 
85 }
86 
87 }
88 
89 using namespace KIO;
90 
AccessManager(QObject * parent)91 AccessManager::AccessManager(QObject *parent)
92     : QNetworkAccessManager(parent)
93     , d(new AccessManager::AccessManagerPrivate())
94 {
95     // KDE Cookiejar (KCookieJar) integration...
96     setCookieJar(new KIO::Integration::CookieJar);
97 }
98 
~AccessManager()99 AccessManager::~AccessManager()
100 {
101     delete d;
102 }
103 
setExternalContentAllowed(bool allowed)104 void AccessManager::setExternalContentAllowed(bool allowed)
105 {
106     d->externalContentAllowed = allowed;
107 }
108 
isExternalContentAllowed() const109 bool AccessManager::isExternalContentAllowed() const
110 {
111     return d->externalContentAllowed;
112 }
113 
114 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 0)
setCookieJarWindowId(WId id)115 void AccessManager::setCookieJarWindowId(WId id)
116 {
117     QWidget *window = QWidget::find(id);
118     if (!window) {
119         return;
120     }
121 
122     KIO::Integration::CookieJar *jar = qobject_cast<KIO::Integration::CookieJar *>(cookieJar());
123     if (jar) {
124         jar->setWindowId(id);
125     }
126 
127     d->window = window->isWindow() ? window : window->window();
128 }
129 #endif
130 
setWindow(QWidget * widget)131 void AccessManager::setWindow(QWidget *widget)
132 {
133     if (!widget) {
134         return;
135     }
136 
137     d->window = widget->isWindow() ? widget : widget->window();
138 
139     if (!d->window) {
140         return;
141     }
142 
143     KIO::Integration::CookieJar *jar = qobject_cast<KIO::Integration::CookieJar *>(cookieJar());
144     if (jar) {
145         jar->setWindowId(d->window->winId());
146     }
147 }
148 
149 #if KIOWIDGETS_BUILD_DEPRECATED_SINCE(5, 0)
cookieJarWindowid() const150 WId AccessManager::cookieJarWindowid() const
151 {
152     KIO::Integration::CookieJar *jar = qobject_cast<KIO::Integration::CookieJar *>(cookieJar());
153     if (jar) {
154         return jar->windowId();
155     }
156 
157     return 0;
158 }
159 #endif
160 
window() const161 QWidget *AccessManager::window() const
162 {
163     return d->window;
164 }
165 
requestMetaData()166 KIO::MetaData &AccessManager::requestMetaData()
167 {
168     return d->requestMetaData;
169 }
170 
sessionMetaData()171 KIO::MetaData &AccessManager::sessionMetaData()
172 {
173     return d->sessionMetaData;
174 }
175 
putReplyOnHold(QNetworkReply * reply)176 void AccessManager::putReplyOnHold(QNetworkReply *reply)
177 {
178     KDEPrivate::AccessManagerReply *r = qobject_cast<KDEPrivate::AccessManagerReply *>(reply);
179     if (!r) {
180         return;
181     }
182 
183     r->putOnHold();
184 }
185 
setEmitReadyReadOnMetaDataChange(bool enable)186 void AccessManager::setEmitReadyReadOnMetaDataChange(bool enable)
187 {
188     d->emitReadyReadOnMetaDataChange = enable;
189 }
190 
createRequest(Operation op,const QNetworkRequest & req,QIODevice * outgoingData)191 QNetworkReply *AccessManager::createRequest(Operation op, const QNetworkRequest &req, QIODevice *outgoingData)
192 {
193     const QUrl reqUrl(req.url());
194 
195     if (!d->externalContentAllowed && !KDEPrivate::AccessManagerReply::isLocalRequest(reqUrl) && reqUrl.scheme() != QLatin1String("data")) {
196         // qDebug() << "Blocked: " << reqUrl;
197         return new KDEPrivate::AccessManagerReply(op, req, QNetworkReply::ContentAccessDenied, i18n("Blocked request."), this);
198     }
199 
200     // Check if the internal ignore content disposition header is set.
201     const bool ignoreContentDisposition = req.hasRawHeader("x-kdewebkit-ignore-disposition");
202 
203     // Retrieve the KIO meta data...
204     KIO::MetaData metaData;
205     d->setMetaDataForRequest(req, metaData);
206 
207     KIO::SimpleJob *kioJob = nullptr;
208 
209     switch (op) {
210     case HeadOperation: {
211         // qDebug() << "HeadOperation:" << reqUrl;
212         kioJob = KIO::mimetype(reqUrl, KIO::HideProgressInfo);
213         break;
214     }
215     case GetOperation: {
216         // qDebug() << "GetOperation:" << reqUrl;
217         if (!reqUrl.path().isEmpty() || reqUrl.host().isEmpty()) {
218             kioJob = KIO::storedGet(reqUrl, KIO::NoReload, KIO::HideProgressInfo);
219         } else {
220             kioJob = KIO::stat(reqUrl, KIO::HideProgressInfo);
221         }
222 
223         // WORKAROUND: Avoid the brain damaged stuff QtWebKit does when a POST
224         // operation is redirected! See BR# 268694.
225         metaData.remove(QStringLiteral("content-type")); // Remove the content-type from a GET/HEAD request!
226         break;
227     }
228     case PutOperation: {
229         // qDebug() << "PutOperation:" << reqUrl;
230         if (outgoingData) {
231             Q_ASSERT(outgoingData->isReadable());
232             StoredTransferJob *storedJob = KIO::storedPut(outgoingData, reqUrl, -1, KIO::HideProgressInfo);
233             storedJob->setAsyncDataEnabled(outgoingData->isSequential());
234 
235             QVariant len = req.header(QNetworkRequest::ContentLengthHeader);
236             if (len.isValid()) {
237                 storedJob->setTotalSize(len.toInt());
238             }
239 
240             kioJob = storedJob;
241         } else {
242             kioJob = KIO::put(reqUrl, -1, KIO::HideProgressInfo);
243         }
244         break;
245     }
246     case PostOperation: {
247         kioJob = KIO::storedHttpPost(outgoingData, reqUrl, sizeFromRequest(req), KIO::HideProgressInfo);
248         if (!metaData.contains(QLatin1String("content-type"))) {
249             const QVariant header = req.header(QNetworkRequest::ContentTypeHeader);
250             if (header.isValid()) {
251                 metaData.insert(QStringLiteral("content-type"), (QStringLiteral("Content-Type: ") + header.toString()));
252             } else {
253                 metaData.insert(QStringLiteral("content-type"), QStringLiteral("Content-Type: application/x-www-form-urlencoded"));
254             }
255         }
256         break;
257     }
258     case DeleteOperation: {
259         // qDebug() << "DeleteOperation:" << reqUrl;
260         kioJob = KIO::http_delete(reqUrl, KIO::HideProgressInfo);
261         break;
262     }
263     case CustomOperation: {
264         const QByteArray &method = req.attribute(QNetworkRequest::CustomVerbAttribute).toByteArray();
265         // qDebug() << "CustomOperation:" << reqUrl << "method:" << method << "outgoing data:" << outgoingData;
266 
267         if (method.isEmpty()) {
268             return new KDEPrivate::AccessManagerReply(op, req, QNetworkReply::ProtocolUnknownError, i18n("Unknown HTTP verb."), this);
269         }
270 
271         const qint64 size = sizeFromRequest(req);
272         if (size > 0) {
273             kioJob = KIO::http_post(reqUrl, outgoingData, size, KIO::HideProgressInfo);
274         } else {
275             kioJob = KIO::get(reqUrl, KIO::NoReload, KIO::HideProgressInfo);
276         }
277 
278         metaData.insert(QStringLiteral("CustomHTTPMethod"), QString::fromUtf8(method));
279         break;
280     }
281     default: {
282         qCWarning(KIO_WIDGETS) << "Unsupported KIO operation requested! Deferring to QNetworkAccessManager...";
283         return QNetworkAccessManager::createRequest(op, req, outgoingData);
284     }
285     }
286 
287     // Set the job priority
288     switch (req.priority()) {
289     case QNetworkRequest::HighPriority:
290         KIO::Scheduler::setJobPriority(kioJob, -5);
291         break;
292     case QNetworkRequest::LowPriority:
293         KIO::Scheduler::setJobPriority(kioJob, 5);
294         break;
295     default:
296         break;
297     }
298 
299     KDEPrivate::AccessManagerReply *reply;
300 
301     /*
302       NOTE: Here we attempt to handle synchronous XHR requests. Unfortunately,
303       due to the fact that QNAM is both synchronous and multi-thread while KIO
304       is completely the opposite (asynchronous and not thread safe), the code
305       below might cause crashes like the one reported in bug# 287778 (nested
306       event loops are inherently dangerous).
307 
308       Unfortunately, all attempts to address the crash has so far failed due to
309       the many regressions they caused, e.g. bug# 231932 and 297954. Hence, until
310       a solution is found, we have to live with the side effects of creating
311       nested event loops.
312     */
313     if (req.attribute(gSynchronousNetworkRequestAttribute).toBool()) {
314         KJobWidgets::setWindow(kioJob, d->window);
315         kioJob->setRedirectionHandlingEnabled(true);
316         if (kioJob->exec()) {
317             QByteArray data;
318             if (StoredTransferJob *storedJob = qobject_cast<KIO::StoredTransferJob *>(kioJob)) {
319                 data = storedJob->data();
320             }
321             reply = new KDEPrivate::AccessManagerReply(op, req, data, kioJob->url(), kioJob->metaData(), this);
322             // qDebug() << "Synchronous XHR:" << reply << reqUrl;
323         } else {
324             qCWarning(KIO_WIDGETS) << "Failed to create a synchronous XHR for" << reqUrl;
325             qCWarning(KIO_WIDGETS) << "REASON:" << kioJob->errorString();
326             reply = new KDEPrivate::AccessManagerReply(op, req, QNetworkReply::UnknownNetworkError, kioJob->errorText(), this);
327         }
328     } else {
329         // Set the window on the KIO ui delegate
330         if (d->window) {
331             KJobWidgets::setWindow(kioJob, d->window);
332         }
333 
334         // Disable internal automatic redirection handling
335         kioJob->setRedirectionHandlingEnabled(false);
336 
337         // Set the job priority
338         switch (req.priority()) {
339         case QNetworkRequest::HighPriority:
340             KIO::Scheduler::setJobPriority(kioJob, -5);
341             break;
342         case QNetworkRequest::LowPriority:
343             KIO::Scheduler::setJobPriority(kioJob, 5);
344             break;
345         default:
346             break;
347         }
348 
349         // Set the meta data for this job...
350         kioJob->setMetaData(metaData);
351 
352         // Create the reply...
353         reply = new KDEPrivate::AccessManagerReply(op, req, kioJob, d->emitReadyReadOnMetaDataChange, this);
354         // qDebug() << reply << reqUrl;
355     }
356 
357     if (ignoreContentDisposition && reply) {
358         // qDebug() << "Content-Disposition WILL BE IGNORED!";
359         reply->setIgnoreContentDisposition(ignoreContentDisposition);
360     }
361 
362     return reply;
363 }
364 
moveMetaData(KIO::MetaData & metaData,const QString & metaDataKey,QNetworkRequest & request,const QByteArray & requestKey)365 static inline void moveMetaData(KIO::MetaData &metaData, const QString &metaDataKey, QNetworkRequest &request, const QByteArray &requestKey)
366 {
367     if (request.hasRawHeader(requestKey)) {
368         metaData.insert(metaDataKey, QString::fromUtf8(request.rawHeader(requestKey)));
369         request.setRawHeader(requestKey, QByteArray());
370     }
371 }
372 
setMetaDataForRequest(QNetworkRequest request,KIO::MetaData & metaData)373 void AccessManager::AccessManagerPrivate::setMetaDataForRequest(QNetworkRequest request, KIO::MetaData &metaData)
374 {
375     // Add any meta data specified within request...
376     QVariant userMetaData = request.attribute(static_cast<QNetworkRequest::Attribute>(MetaData));
377     if (userMetaData.isValid() && userMetaData.type() == QVariant::Map) {
378         metaData += userMetaData.toMap();
379     }
380 
381     metaData.insert(QStringLiteral("PropagateHttpHeader"), QStringLiteral("true"));
382 
383     moveMetaData(metaData, QStringLiteral("UserAgent"), request, QByteArrayLiteral("User-Agent"));
384     moveMetaData(metaData, QStringLiteral("accept"), request, QByteArrayLiteral("Accept"));
385     moveMetaData(metaData, QStringLiteral("Charsets"), request, QByteArrayLiteral("Accept-Charset"));
386     moveMetaData(metaData, QStringLiteral("Languages"), request, QByteArrayLiteral("Accept-Language"));
387     moveMetaData(metaData, QStringLiteral("referrer"), request, QByteArrayLiteral("Referer")); // Don't try to correct spelling!
388     moveMetaData(metaData, QStringLiteral("content-type"), request, QByteArrayLiteral("Content-Type"));
389 
390     if (request.attribute(QNetworkRequest::AuthenticationReuseAttribute) == QNetworkRequest::Manual) {
391         metaData.insert(QStringLiteral("no-preemptive-auth-reuse"), QStringLiteral("true"));
392     }
393 
394     request.setRawHeader("Content-Length", QByteArray());
395     request.setRawHeader("Connection", QByteArray());
396     request.setRawHeader("If-None-Match", QByteArray());
397     request.setRawHeader("If-Modified-Since", QByteArray());
398     request.setRawHeader("x-kdewebkit-ignore-disposition", QByteArray());
399 
400     QStringList customHeaders;
401     const QList<QByteArray> list = request.rawHeaderList();
402     for (const QByteArray &key : list) {
403         const QByteArray value = request.rawHeader(key);
404         if (value.length()) {
405             customHeaders << (QString::fromUtf8(key) + QLatin1String(": ") + QString::fromUtf8(value));
406         }
407     }
408 
409     if (!customHeaders.isEmpty()) {
410         metaData.insert(QStringLiteral("customHTTPHeader"), customHeaders.join(QLatin1String("\r\n")));
411     }
412 
413     // Append per request meta data, if any...
414     if (!requestMetaData.isEmpty()) {
415         metaData += requestMetaData;
416         // Clear per request meta data...
417         requestMetaData.clear();
418     }
419 
420     // Append per session meta data, if any...
421     if (!sessionMetaData.isEmpty()) {
422         metaData += sessionMetaData;
423     }
424 }
425 
426 using namespace KIO::Integration;
427 
428 // The strings come from qtbase/src/network/ssl (grep for protocolString)
qSslProtocolFromString(const QString & str)429 static QSsl::SslProtocol qSslProtocolFromString(const QString &str)
430 {
431     if (str.compare(QStringLiteral("TLSv1"), Qt::CaseInsensitive) == 0) {
432         return QSsl::TlsV1_0;
433     }
434 
435     if (str.compare(QStringLiteral("TLSv1.1"), Qt::CaseInsensitive) == 0) {
436         return QSsl::TlsV1_1;
437     }
438 
439     if (str.compare(QStringLiteral("TLSv1.2"), Qt::CaseInsensitive) == 0) {
440         return QSsl::TlsV1_2;
441     }
442 
443     if (str.compare(QStringLiteral("TLSv1.3"), Qt::CaseInsensitive) == 0) {
444         return QSsl::TlsV1_3;
445     }
446 
447     return QSsl::AnyProtocol;
448 }
449 
sslConfigFromMetaData(const KIO::MetaData & metadata,QSslConfiguration & sslconfig)450 bool KIO::Integration::sslConfigFromMetaData(const KIO::MetaData &metadata, QSslConfiguration &sslconfig)
451 {
452     bool success = false;
453 
454     if (metadata.value(QStringLiteral("ssl_in_use")) == QLatin1String("TRUE")) {
455         const QSsl::SslProtocol sslProto = qSslProtocolFromString(metadata.value(QStringLiteral("ssl_protocol_version")));
456         QList<QSslCipher> cipherList;
457         cipherList << QSslCipher(metadata.value(QStringLiteral("ssl_cipher_name")), sslProto);
458         sslconfig.setCaCertificates(QSslCertificate::fromData(metadata.value(QStringLiteral("ssl_peer_chain")).toUtf8()));
459         sslconfig.setCiphers(cipherList);
460         sslconfig.setProtocol(sslProto);
461         success = sslconfig.isNull();
462     }
463 
464     return success;
465 }
466 
CookieJar(QObject * parent)467 CookieJar::CookieJar(QObject *parent)
468     : QNetworkCookieJar(parent)
469     , d(new CookieJar::CookieJarPrivate)
470 {
471     reparseConfiguration();
472 }
473 
~CookieJar()474 CookieJar::~CookieJar()
475 {
476     delete d;
477 }
478 
windowId() const479 WId CookieJar::windowId() const
480 {
481     return d->windowId;
482 }
483 
isCookieStorageDisabled() const484 bool CookieJar::isCookieStorageDisabled() const
485 {
486     return d->isStorageDisabled;
487 }
488 
cookiesForUrl(const QUrl & url) const489 QList<QNetworkCookie> CookieJar::cookiesForUrl(const QUrl &url) const
490 {
491     QList<QNetworkCookie> cookieList;
492 
493     if (!d->isEnabled) {
494         return cookieList;
495     }
496     QDBusInterface kcookiejar(QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer"));
497     QDBusReply<QString> reply = kcookiejar.call(QStringLiteral("findDOMCookies"), url.toString(QUrl::RemoveUserInfo), (qlonglong)d->windowId);
498 
499     if (!reply.isValid()) {
500         qCWarning(KIO_WIDGETS) << "Unable to communicate with the cookiejar!";
501         return cookieList;
502     }
503 
504     const QString cookieStr = reply.value();
505     const QStringList cookies = cookieStr.split(QStringLiteral("; "), Qt::SkipEmptyParts);
506     for (const QString &cookie : cookies) {
507         const int index = cookie.indexOf(QLatin1Char('='));
508         const QStringView cookieView(cookie);
509         const auto name = cookieView.left(index);
510         const auto value = cookieView.right(cookie.length() - index - 1);
511         cookieList << QNetworkCookie(name.toUtf8(), value.toUtf8());
512         // qDebug() << "cookie: name=" << name << ", value=" << value;
513     }
514 
515     return cookieList;
516 }
517 
setCookiesFromUrl(const QList<QNetworkCookie> & cookieList,const QUrl & url)518 bool CookieJar::setCookiesFromUrl(const QList<QNetworkCookie> &cookieList, const QUrl &url)
519 {
520     if (!d->isEnabled) {
521         return false;
522     }
523 
524     QDBusInterface kcookiejar(QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer"));
525     for (const QNetworkCookie &cookie : cookieList) {
526         QByteArray cookieHeader("Set-Cookie: ");
527         if (d->isStorageDisabled && !cookie.isSessionCookie()) {
528             QNetworkCookie sessionCookie(cookie);
529             sessionCookie.setExpirationDate(QDateTime());
530             cookieHeader += sessionCookie.toRawForm();
531         } else {
532             cookieHeader += cookie.toRawForm();
533         }
534         kcookiejar.call(QStringLiteral("addCookies"), url.toString(QUrl::RemoveUserInfo), cookieHeader, (qlonglong)d->windowId);
535         // qDebug() << "[" << d->windowId << "]" << cookieHeader << " from " << url;
536     }
537 
538     return !kcookiejar.lastError().isValid();
539 }
540 
setDisableCookieStorage(bool disable)541 void CookieJar::setDisableCookieStorage(bool disable)
542 {
543     d->isStorageDisabled = disable;
544 }
545 
setWindowId(WId id)546 void CookieJar::setWindowId(WId id)
547 {
548     d->windowId = id;
549 }
550 
reparseConfiguration()551 void CookieJar::reparseConfiguration()
552 {
553     KConfigGroup cfg = KSharedConfig::openConfig(QStringLiteral("kcookiejarrc"), KConfig::NoGlobals)->group("Cookie Policy");
554     d->isEnabled = cfg.readEntry("Cookies", true);
555 }
556