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