File indexing completed on 2025-03-16 10:02:22
0001 /* 0002 SPDX-FileCopyrightText: 2000-2003 Waldo Bastian <bastian@kde.org> 0003 SPDX-FileCopyrightText: 2000-2002 George Staikos <staikos@kde.org> 0004 SPDX-FileCopyrightText: 2000-2002 Dawit Alemayehu <adawit@kde.org> 0005 SPDX-FileCopyrightText: 2001, 2002 Hamish Rodda <rodda@kde.org> 0006 SPDX-FileCopyrightText: 2007 Nick Shaforostoff <shafff@ukr.net> 0007 SPDX-FileCopyrightText: 2007-2018 Daniel Nicoletti <dantti12@gmail.com> 0008 SPDX-FileCopyrightText: 2008, 2009 Andreas Hartmetz <ahartmetz@gmail.com> 0009 SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org> 0010 0011 SPDX-License-Identifier: LGPL-2.0-or-later 0012 */ 0013 0014 // TODO delete / do not save very big files; "very big" to be defined 0015 0016 #include "http.h" 0017 0018 #include "../utils_p.h" 0019 #include <config-kioworker-http.h> 0020 0021 #include <limits> 0022 #include <qplatformdefs.h> // must be explicitly included for MacOSX 0023 0024 #include <QAuthenticator> 0025 #include <QBuffer> 0026 #include <QCoreApplication> 0027 #include <QDataStream> 0028 #include <QFile> 0029 #include <QIODevice> 0030 #include <QLibraryInfo> 0031 #include <QMimeDatabase> 0032 #include <QNetworkProxy> 0033 #include <QRegularExpression> 0034 #include <QStandardPaths> 0035 #include <QTemporaryFile> 0036 #include <QThread> 0037 #include <QUrl> 0038 #include <qdom.h> 0039 0040 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 0041 #include <QNetworkInformation> 0042 #else 0043 #include <QNetworkConfigurationManager> 0044 #endif 0045 0046 #include <KConfig> 0047 #include <KConfigGroup> 0048 #include <KLocalizedString> 0049 #include <QDebug> 0050 0051 #include <QDBusInterface> 0052 #include <QSslSocket> 0053 #include <kremoteencoding.h> 0054 0055 #include <http_worker_defaults.h> 0056 #include <ioworker_defaults.h> 0057 0058 #include <QDBusReply> 0059 #include <QProcess> 0060 #include <httpfilter.h> 0061 0062 #include <sys/stat.h> 0063 0064 #include "httpauthentication.h" 0065 #include "kioglobal_p.h" 0066 #include "workerbase_p.h" 0067 0068 #include <QLoggingCategory> 0069 Q_DECLARE_LOGGING_CATEGORY(KIO_HTTP) 0070 Q_LOGGING_CATEGORY(KIO_HTTP, "kf.kio.workers.http", QtWarningMsg) // disable debug by default 0071 0072 // HeaderTokenizer declarations 0073 #include "parsinghelpers.h" 0074 // string parsing helpers and HeaderTokenizer implementation 0075 #include "parsinghelpers.cpp" 0076 0077 // Pseudo plugin class to embed meta data 0078 class KIOPluginForMetaData : public QObject 0079 { 0080 Q_OBJECT 0081 Q_PLUGIN_METADATA(IID "org.kde.kio.worker.http" FILE "http.json") 0082 }; 0083 0084 static bool supportedProxyScheme(const QString &scheme) 0085 { 0086 // scheme is supposed to be lowercase 0087 return (scheme.startsWith(QLatin1String("http")) || scheme == QLatin1String("socks")); 0088 } 0089 0090 // see filenameFromUrl(): a sha1 hash is 160 bits 0091 static const int s_hashedUrlBits = 160; // this number should always be divisible by eight 0092 static const int s_hashedUrlNibbles = s_hashedUrlBits / 4; 0093 static const int s_MaxInMemPostBufSize = 256 * 1024; // Write anything over 256 KB to file... 0094 0095 using namespace KIO; 0096 0097 extern "C" Q_DECL_EXPORT int kdemain(int argc, char **argv) 0098 { 0099 QCoreApplication app(argc, argv); // needed for QSocketNotifier 0100 app.setApplicationName(QStringLiteral("kio_http")); 0101 0102 if (argc != 4) { 0103 fprintf(stderr, "Usage: kio_http protocol domain-socket1 domain-socket2\n"); 0104 exit(-1); 0105 } 0106 0107 HTTPProtocol worker(argv[1], argv[2], argv[3]); 0108 worker.dispatchLoop(); 0109 return 0; 0110 } 0111 0112 /*********************************** Generic utility functions ********************/ 0113 0114 static QString toQString(const QByteArray &value) 0115 { 0116 return QString::fromLatin1(value.constData(), value.size()); 0117 } 0118 0119 static bool isCrossDomainRequest(const QString &fqdn, const QString &originURL) 0120 { 0121 // TODO read the RFC 0122 if (originURL == QLatin1String("true")) { // Backwards compatibility 0123 return true; 0124 } 0125 0126 QUrl url(originURL); 0127 0128 // Document Origin domain 0129 QString a = url.host(); 0130 // Current request domain 0131 const QString &b = fqdn; 0132 0133 if (a == b) { 0134 return false; 0135 } 0136 0137 QStringList la = a.split(QLatin1Char('.'), Qt::SkipEmptyParts); 0138 QStringList lb = b.split(QLatin1Char('.'), Qt::SkipEmptyParts); 0139 0140 if (qMin(la.count(), lb.count()) < 2) { 0141 return true; // better safe than sorry... 0142 } 0143 0144 while (la.count() > 2) { 0145 la.pop_front(); 0146 } 0147 while (lb.count() > 2) { 0148 lb.pop_front(); 0149 } 0150 0151 return la != lb; 0152 } 0153 0154 /* 0155 Eliminates any custom header that could potentially alter the request 0156 */ 0157 static QString sanitizeCustomHTTPHeader(const QString &_header) 0158 { 0159 QString sanitizedHeaders; 0160 0161 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 0162 const QVector<QStringView> headers = QStringView(_header).split(QRegularExpression(QStringLiteral("[\r\n]"))); 0163 #else 0164 const QVector<QStringRef> headers = _header.splitRef(QRegularExpression(QStringLiteral("[\r\n]"))); 0165 #endif 0166 for (const auto &header : headers) { 0167 // Do not allow Request line to be specified and ignore 0168 // the other HTTP headers. 0169 if (!header.contains(QLatin1Char(':')) || header.startsWith(QLatin1String("host"), Qt::CaseInsensitive) 0170 || header.startsWith(QLatin1String("proxy-authorization"), Qt::CaseInsensitive) || header.startsWith(QLatin1String("via"), Qt::CaseInsensitive) 0171 || header.startsWith(QLatin1String("depth"), Qt::CaseInsensitive)) { 0172 continue; 0173 } 0174 0175 sanitizedHeaders += header + QLatin1String("\r\n"); 0176 } 0177 sanitizedHeaders.chop(2); 0178 0179 return sanitizedHeaders; 0180 } 0181 0182 static bool isPotentialSpoofingAttack(const HTTPProtocol::HTTPRequest &request, const KConfigGroup *config) 0183 { 0184 qCDebug(KIO_HTTP) << request.url << "response code: " << request.responseCode << "previous response code:" << request.prevResponseCode; 0185 if (config->readEntry("no-spoof-check", false)) { 0186 return false; 0187 } 0188 0189 if (request.url.userName().isEmpty()) { 0190 return false; 0191 } 0192 0193 // We already have cached authentication. 0194 if (config->readEntry(QStringLiteral("cached-www-auth"), false)) { 0195 return false; 0196 } 0197 0198 const QString userName = config->readEntry(QStringLiteral("LastSpoofedUserName"), QString()); 0199 return ((userName.isEmpty() || userName != request.url.userName()) && request.responseCode != 401 && request.prevResponseCode != 401); 0200 } 0201 0202 // for a given response code, conclude if the response is going to/likely to have a response body 0203 static bool canHaveResponseBody(int responseCode, KIO::HTTP_METHOD method) 0204 { 0205 /* RFC 2616 says... 0206 1xx: false 0207 200: method HEAD: false, otherwise:true 0208 201: true 0209 202: true 0210 203: see 200 0211 204: false 0212 205: false 0213 206: true 0214 300: see 200 0215 301: see 200 0216 302: see 200 0217 303: see 200 0218 304: false 0219 305: probably like 300, RFC seems to expect disconnection afterwards... 0220 306: (reserved), for simplicity do it just like 200 0221 307: see 200 0222 4xx: see 200 0223 5xx :see 200 0224 */ 0225 if (responseCode >= 100 && responseCode < 200) { 0226 return false; 0227 } 0228 switch (responseCode) { 0229 case 201: 0230 case 202: 0231 case 206: 0232 // RFC 2616 does not mention HEAD in the description of the above. if the assert turns out 0233 // to be a problem the response code should probably be treated just like 200 and friends. 0234 Q_ASSERT(method != HTTP_HEAD); 0235 return true; 0236 case 204: 0237 case 205: 0238 case 304: 0239 return false; 0240 default: 0241 break; 0242 } 0243 // safe (and for most remaining response codes exactly correct) default 0244 return method != HTTP_HEAD; 0245 } 0246 0247 static bool isEncryptedHttpVariety(const QByteArray &p) 0248 { 0249 return p == "https" || p == "webdavs"; 0250 } 0251 0252 static bool isValidProxy(const QUrl &u) 0253 { 0254 return u.isValid() && !u.host().isEmpty(); 0255 } 0256 0257 static bool isHttpProxy(const QUrl &u) 0258 { 0259 return isValidProxy(u) && u.scheme() == QLatin1String("http"); 0260 } 0261 0262 static QIODevice *createPostBufferDeviceFor(KIO::filesize_t size) 0263 { 0264 QIODevice *device; 0265 if (size > static_cast<KIO::filesize_t>(s_MaxInMemPostBufSize)) { 0266 device = new QTemporaryFile; 0267 } else { 0268 device = new QBuffer; 0269 } 0270 0271 if (!device->open(QIODevice::ReadWrite)) { 0272 return nullptr; 0273 } 0274 0275 return device; 0276 } 0277 0278 QByteArray HTTPProtocol::HTTPRequest::methodString() const 0279 { 0280 if (!methodStringOverride.isEmpty()) { 0281 return (methodStringOverride).toLatin1(); 0282 } 0283 0284 switch (method) { 0285 case HTTP_GET: 0286 return "GET"; 0287 case HTTP_PUT: 0288 return "PUT"; 0289 case HTTP_POST: 0290 return "POST"; 0291 case HTTP_HEAD: 0292 return "HEAD"; 0293 case HTTP_DELETE: 0294 return "DELETE"; 0295 case HTTP_OPTIONS: 0296 return "OPTIONS"; 0297 case DAV_PROPFIND: 0298 return "PROPFIND"; 0299 case DAV_PROPPATCH: 0300 return "PROPPATCH"; 0301 case DAV_MKCOL: 0302 return "MKCOL"; 0303 case DAV_COPY: 0304 return "COPY"; 0305 case DAV_MOVE: 0306 return "MOVE"; 0307 case DAV_LOCK: 0308 return "LOCK"; 0309 case DAV_UNLOCK: 0310 return "UNLOCK"; 0311 case DAV_SEARCH: 0312 return "SEARCH"; 0313 case DAV_SUBSCRIBE: 0314 return "SUBSCRIBE"; 0315 case DAV_UNSUBSCRIBE: 0316 return "UNSUBSCRIBE"; 0317 case DAV_POLL: 0318 return "POLL"; 0319 case DAV_NOTIFY: 0320 return "NOTIFY"; 0321 case DAV_REPORT: 0322 return "REPORT"; 0323 default: 0324 Q_ASSERT(false); 0325 return QByteArray(); 0326 } 0327 } 0328 0329 static QString formatHttpDate(const QDateTime &date) 0330 { 0331 return QLocale::c().toString(date, QStringLiteral("ddd, dd MMM yyyy hh:mm:ss 'GMT'")); 0332 } 0333 0334 static bool isAuthenticationRequired(int responseCode) 0335 { 0336 return (responseCode == 401) || (responseCode == 407); 0337 } 0338 0339 static void changeProtocolToHttp(QUrl *url) 0340 { 0341 const QString protocol(url->scheme()); 0342 if (protocol == QLatin1String("webdavs")) { 0343 url->setScheme(QStringLiteral("https")); 0344 } else if (protocol == QLatin1String("webdav")) { 0345 url->setScheme(QStringLiteral("http")); 0346 } 0347 } 0348 0349 #define NO_SIZE ((KIO::filesize_t)-1) 0350 0351 #if HAVE_STRTOLL 0352 #define STRTOLL strtoll 0353 #else 0354 #define STRTOLL strtol 0355 #endif 0356 0357 /************************************** HTTPProtocol **********************************************/ 0358 0359 HTTPProtocol::HTTPProtocol(const QByteArray &protocol, const QByteArray &pool, const QByteArray &app) 0360 : TCPWorkerBase(protocol, pool, app, isEncryptedHttpVariety(protocol)) 0361 , m_iSize(NO_SIZE) 0362 , m_iPostDataSize(NO_SIZE) 0363 , m_isBusy(false) 0364 , m_POSTbuf(nullptr) 0365 , m_maxCacheAge(DEFAULT_MAX_CACHE_AGE) 0366 , m_maxCacheSize(DEFAULT_MAX_CACHE_SIZE) 0367 , m_protocol(protocol) 0368 , m_wwwAuth(nullptr) 0369 , m_triedWwwCredentials(NoCredentials) 0370 , m_proxyAuth(nullptr) 0371 , m_triedProxyCredentials(NoCredentials) 0372 , m_socketProxyAuth(nullptr) 0373 , m_kioError(0) 0374 , m_isLoadingErrorPage(false) 0375 , m_remoteRespTimeout(DEFAULT_RESPONSE_TIMEOUT) 0376 , m_iEOFRetryCount(0) 0377 { 0378 reparseConfiguration(); 0379 setBlocking(true); 0380 connect(tcpSocket(), &QAbstractSocket::proxyAuthenticationRequired, this, &HTTPProtocol::proxyAuthenticationForSocket); 0381 } 0382 0383 HTTPProtocol::~HTTPProtocol() 0384 { 0385 httpClose(false); 0386 } 0387 0388 void HTTPProtocol::reparseConfiguration() 0389 { 0390 qCDebug(KIO_HTTP); 0391 0392 delete m_proxyAuth; 0393 delete m_wwwAuth; 0394 m_proxyAuth = nullptr; 0395 m_wwwAuth = nullptr; 0396 m_request.proxyUrl.clear(); // TODO revisit 0397 m_request.proxyUrls.clear(); 0398 0399 TCPWorkerBase::reparseConfiguration(); 0400 } 0401 0402 void HTTPProtocol::resetConnectionSettings() 0403 { 0404 m_isEOF = false; 0405 m_kioError = 0; 0406 m_kioErrorString.clear(); 0407 m_isLoadingErrorPage = false; 0408 } 0409 0410 quint16 HTTPProtocol::defaultPort() const 0411 { 0412 return isEncryptedHttpVariety(m_protocol) ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT; 0413 } 0414 0415 void HTTPProtocol::resetResponseParsing() 0416 { 0417 m_isRedirection = false; 0418 m_isChunked = false; 0419 m_iSize = NO_SIZE; 0420 clearUnreadBuffer(); 0421 0422 m_responseHeaders.clear(); 0423 m_contentEncodings.clear(); 0424 m_transferEncodings.clear(); 0425 m_contentMD5.clear(); 0426 m_mimeType.clear(); 0427 0428 setMetaData(QStringLiteral("request-id"), m_request.id); 0429 } 0430 0431 void HTTPProtocol::resetSessionSettings() 0432 { 0433 // Follow HTTP/1.1 spec and enable keep-alive by default 0434 // unless the remote side tells us otherwise or we determine 0435 // the persistent link has been terminated by the remote end. 0436 m_request.isKeepAlive = true; 0437 m_request.keepAliveTimeout = 0; 0438 0439 m_request.redirectUrl = QUrl(); 0440 m_request.useCookieJar = configValue(QStringLiteral("Cookies"), false); 0441 m_request.cacheTag.useCache = configValue(QStringLiteral("UseCache"), true); 0442 m_request.preferErrorPage = configValue(QStringLiteral("errorPage"), true); 0443 const bool noAuth = configValue(QStringLiteral("no-auth"), false); 0444 m_request.doNotWWWAuthenticate = configValue(QStringLiteral("no-www-auth"), noAuth); 0445 m_request.doNotProxyAuthenticate = configValue(QStringLiteral("no-proxy-auth"), noAuth); 0446 m_strCacheDir = config()->readPathEntry(QStringLiteral("CacheDir"), QString()); 0447 m_maxCacheAge = configValue(QStringLiteral("MaxCacheAge"), DEFAULT_MAX_CACHE_AGE); 0448 m_request.windowId = configValue(QStringLiteral("window-id")); 0449 0450 m_request.methodStringOverride = metaData(QStringLiteral("CustomHTTPMethod")); 0451 m_request.sentMethodString.clear(); 0452 0453 qCDebug(KIO_HTTP) << "Window Id =" << m_request.windowId; 0454 qCDebug(KIO_HTTP) << "ssl_was_in_use =" << metaData(QStringLiteral("ssl_was_in_use")); 0455 0456 m_request.referrer.clear(); 0457 // RFC 2616: do not send the referrer if the referrer page was served using SSL and 0458 // the current page does not use SSL. 0459 if (configValue(QStringLiteral("SendReferrer"), true) 0460 && (isEncryptedHttpVariety(m_protocol) || metaData(QStringLiteral("ssl_was_in_use")) != QLatin1String("TRUE"))) { 0461 QUrl refUrl(metaData(QStringLiteral("referrer"))); 0462 if (refUrl.isValid()) { 0463 // Sanitize 0464 QString protocol = refUrl.scheme(); 0465 if (protocol.startsWith(QLatin1String("webdav"))) { 0466 protocol.replace(0, 6, QStringLiteral("http")); 0467 refUrl.setScheme(protocol); 0468 } 0469 0470 if (protocol.startsWith(QLatin1String("http"))) { 0471 m_request.referrer = toQString(refUrl.toEncoded(QUrl::RemoveUserInfo | QUrl::RemoveFragment)); 0472 } 0473 } 0474 } 0475 0476 if (configValue(QStringLiteral("SendLanguageSettings"), true)) { 0477 m_request.charsets = configValue(QStringLiteral("Charsets"), QString::fromLatin1(DEFAULT_PARTIAL_CHARSET_HEADER)); 0478 if (!m_request.charsets.contains(QLatin1String("*;"), Qt::CaseInsensitive)) { 0479 m_request.charsets += QLatin1String(",*;q=0.5"); 0480 } 0481 m_request.languages = configValue(QStringLiteral("Languages"), QString::fromLatin1(DEFAULT_LANGUAGE_HEADER)); 0482 } else { 0483 m_request.charsets.clear(); 0484 m_request.languages.clear(); 0485 } 0486 0487 // Adjust the offset value based on the "range-start" meta-data. 0488 QString resumeOffset = metaData(QStringLiteral("range-start")); 0489 if (resumeOffset.isEmpty()) { 0490 resumeOffset = metaData(QStringLiteral("resume")); // old name 0491 } 0492 if (!resumeOffset.isEmpty()) { 0493 m_request.offset = resumeOffset.toULongLong(); 0494 } else { 0495 m_request.offset = 0; 0496 } 0497 // Same procedure for endoffset. 0498 QString resumeEndOffset = metaData(QStringLiteral("range-end")); 0499 if (resumeEndOffset.isEmpty()) { 0500 resumeEndOffset = metaData(QStringLiteral("resume_until")); // old name 0501 } 0502 if (!resumeEndOffset.isEmpty()) { 0503 m_request.endoffset = resumeEndOffset.toULongLong(); 0504 } else { 0505 m_request.endoffset = 0; 0506 } 0507 0508 m_request.disablePassDialog = configValue(QStringLiteral("DisablePassDlg"), false); 0509 m_request.allowTransferCompression = configValue(QStringLiteral("AllowCompressedPage"), true); 0510 m_request.id = metaData(QStringLiteral("request-id")); 0511 0512 // Store user agent for this host. 0513 if (configValue(QStringLiteral("SendUserAgent"), true)) { 0514 m_request.userAgent = metaData(QStringLiteral("UserAgent")); 0515 } else { 0516 m_request.userAgent.clear(); 0517 } 0518 0519 m_request.cacheTag.etag.clear(); 0520 m_request.cacheTag.servedDate = QDateTime(); 0521 m_request.cacheTag.lastModifiedDate = QDateTime(); 0522 m_request.cacheTag.expireDate = QDateTime(); 0523 m_request.responseCode = 0; 0524 m_request.prevResponseCode = 0; 0525 0526 delete m_wwwAuth; 0527 m_wwwAuth = nullptr; 0528 delete m_socketProxyAuth; 0529 m_socketProxyAuth = nullptr; 0530 m_blacklistedWwwAuthMethods.clear(); 0531 m_triedWwwCredentials = NoCredentials; 0532 m_blacklistedProxyAuthMethods.clear(); 0533 m_triedProxyCredentials = NoCredentials; 0534 0535 // Obtain timeout values 0536 m_remoteRespTimeout = responseTimeout(); 0537 0538 // Bounce back the actual referrer sent 0539 setMetaData(QStringLiteral("referrer"), m_request.referrer); 0540 0541 // Reset the post data size 0542 m_iPostDataSize = NO_SIZE; 0543 0544 // Reset the EOF retry counter 0545 m_iEOFRetryCount = 0; 0546 } 0547 0548 void HTTPProtocol::setHost(const QString &host, quint16 port, const QString &user, const QString &pass) 0549 { 0550 // Reset the webdav-capable flags for this host 0551 if (m_request.url.host() != host) { 0552 m_davHostOk = m_davHostUnsupported = false; 0553 } 0554 0555 m_request.url.setHost(host); 0556 0557 // is it an IPv6 address? 0558 if (host.indexOf(QLatin1Char(':')) == -1) { 0559 m_request.encoded_hostname = toQString(QUrl::toAce(host)); 0560 } else { 0561 int pos = host.indexOf(QLatin1Char('%')); 0562 if (pos == -1) { 0563 m_request.encoded_hostname = QLatin1Char('[') + host + QLatin1Char(']'); 0564 } else { // don't send the scope-id in IPv6 addresses to the server 0565 m_request.encoded_hostname = QLatin1Char('[') + QStringView(host).left(pos) + QLatin1Char(']'); 0566 } 0567 } 0568 m_request.url.setPort((port > 0 && port != defaultPort()) ? port : -1); 0569 m_request.url.setUserName(user); 0570 m_request.url.setPassword(pass); 0571 0572 // On new connection always clear previous proxy information... 0573 m_request.proxyUrl.clear(); 0574 m_request.proxyUrls.clear(); 0575 0576 qCDebug(KIO_HTTP) << "Hostname is now:" << m_request.url.host() << "(" << m_request.encoded_hostname << ")"; 0577 } 0578 0579 KIO::WorkerResult HTTPProtocol::maybeSetRequestUrl(const QUrl &u) 0580 { 0581 qCDebug(KIO_HTTP) << u; 0582 0583 m_request.url = u; 0584 m_request.url.setPort(u.port(defaultPort()) != defaultPort() ? u.port() : -1); 0585 0586 if (u.host().isEmpty()) { 0587 return error(KIO::ERR_UNKNOWN_HOST, i18n("No host specified.")); 0588 } 0589 0590 if (u.path().isEmpty()) { 0591 QUrl newUrl(u); 0592 newUrl.setPath(QStringLiteral("/")); 0593 redirection(newUrl); 0594 return WorkerResult::pass(); 0595 } 0596 0597 return WorkerResult::pass(); 0598 } 0599 0600 KIO::WorkerResult HTTPProtocol::proceedUntilResponseContent(bool dataInternal /* = false */) 0601 { 0602 qCDebug(KIO_HTTP); 0603 0604 WorkerResult result = WorkerResult::fail(); 0605 if (result = proceedUntilResponseHeader(); result.success()) { 0606 result = readBody(dataInternal || m_kioError); 0607 } 0608 0609 // If not an error condition or internal request, close 0610 // the connection based on the keep alive settings... 0611 if (result.success() && !dataInternal) { 0612 httpClose(m_request.isKeepAlive); 0613 } 0614 0615 // if data is required internally or we got error, don't finish, 0616 // it is processed before we finish() 0617 if (!result.success() || dataInternal) { 0618 return result; 0619 } 0620 0621 return sendHttpError(); 0622 } 0623 0624 KIO::WorkerResult HTTPProtocol::proceedUntilResponseHeader() 0625 { 0626 qCDebug(KIO_HTTP); 0627 0628 // Retry the request until it succeeds or an unrecoverable error occurs. 0629 // Recoverable errors are, for example: 0630 // - Proxy or server authentication required: Ask for credentials and try again, 0631 // this time with an authorization header in the request. 0632 // - Server-initiated timeout on keep-alive connection: Reconnect and try again 0633 0634 while (true) { 0635 if (const auto result = sendQuery(); !result.success()) { 0636 return result; 0637 } 0638 if (const auto result = readResponseHeader(); result.success()) { 0639 // Success, finish the request. 0640 break; 0641 } 0642 0643 // If not loading error page and the response code requires us to resend the query, 0644 // then throw away any error message that might have been sent by the server. 0645 if (!m_isLoadingErrorPage && isAuthenticationRequired(m_request.responseCode)) { 0646 // This gets rid of any error page sent with 401 or 407 authentication required response... 0647 (void)readBody(true); 0648 } 0649 0650 // no success, close the cache file so the cache state is reset - that way most other code 0651 // doesn't have to deal with the cache being in various states. 0652 cacheFileClose(); 0653 if (m_kioError || m_isLoadingErrorPage) { 0654 // Unrecoverable error, abort everything. 0655 // Also, if we've just loaded an error page there is nothing more to do. 0656 // In that case we abort to avoid loops; some webservers manage to send 401 and 0657 // no authentication request. Or an auth request we don't understand. 0658 setMetaData(QStringLiteral("responsecode"), QString::number(m_request.responseCode)); 0659 return WorkerResult::fail(m_kioError, m_kioErrorString); 0660 } 0661 0662 if (!m_request.isKeepAlive) { 0663 httpCloseConnection(); 0664 m_request.isKeepAlive = true; 0665 m_request.keepAliveTimeout = 0; 0666 } 0667 } 0668 0669 // Do not save authorization if the current response code is 0670 // 4xx (client error) or 5xx (server error). 0671 qCDebug(KIO_HTTP) << "Previous Response:" << m_request.prevResponseCode; 0672 qCDebug(KIO_HTTP) << "Current Response:" << m_request.responseCode; 0673 0674 setMetaData(QStringLiteral("responsecode"), QString::number(m_request.responseCode)); 0675 setMetaData(QStringLiteral("content-type"), m_mimeType); 0676 0677 // At this point sendBody() should have delivered any POST data. 0678 clearPostDataBuffer(); 0679 0680 return WorkerResult::pass(); 0681 } 0682 0683 KIO::WorkerResult HTTPProtocol::stat(const QUrl &url) 0684 { 0685 qCDebug(KIO_HTTP) << url; 0686 0687 if (const auto result = maybeSetRequestUrl(url); !result.success()) { 0688 return result; 0689 } 0690 resetSessionSettings(); 0691 0692 if (m_protocol != "webdav" && m_protocol != "webdavs") { 0693 QString statSide = metaData(QStringLiteral("statSide")); 0694 if (statSide != QLatin1String("source")) { 0695 // When uploading we assume the file does not exist. 0696 return error(ERR_DOES_NOT_EXIST, url.toDisplayString()); 0697 } 0698 0699 // When downloading we assume it exists 0700 UDSEntry entry; 0701 entry.reserve(3); 0702 entry.fastInsert(KIO::UDSEntry::UDS_NAME, url.fileName()); 0703 entry.fastInsert(KIO::UDSEntry::UDS_FILE_TYPE, S_IFREG); // a file 0704 entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, S_IRUSR | S_IRGRP | S_IROTH); // readable by everybody 0705 0706 statEntry(entry); 0707 return KIO::WorkerResult::pass(); 0708 } 0709 0710 return davStatList(url); 0711 } 0712 0713 KIO::WorkerResult HTTPProtocol::listDir(const QUrl &url) 0714 { 0715 qCDebug(KIO_HTTP) << url; 0716 0717 if (const auto result = maybeSetRequestUrl(url); !result.success()) { 0718 return result; 0719 } 0720 resetSessionSettings(); 0721 0722 return davStatList(url, false); 0723 } 0724 0725 void HTTPProtocol::davSetRequest(const QByteArray &requestXML) 0726 { 0727 // insert the document into the POST buffer, kill trailing zero byte 0728 cachePostData(requestXML); 0729 } 0730 0731 KIO::WorkerResult HTTPProtocol::davStatList(const QUrl &url, bool stat) 0732 { 0733 UDSEntry entry; 0734 0735 // check to make sure this host supports WebDAV 0736 if (const auto result = davHostOk(); !result.success()) { 0737 return result; 0738 } 0739 0740 QMimeDatabase db; 0741 0742 // Maybe it's a disguised SEARCH... 0743 QString query = metaData(QStringLiteral("davSearchQuery")); 0744 if (!query.isEmpty()) { 0745 const QByteArray request = 0746 "<?xml version=\"1.0\"?>\r\n" 0747 "<D:searchrequest xmlns:D=\"DAV:\">\r\n" 0748 + query.toUtf8() + "</D:searchrequest>\r\n"; 0749 0750 davSetRequest(request); 0751 } else { 0752 // We are only after certain features... 0753 QByteArray request = QByteArrayLiteral( 0754 "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" 0755 "<D:propfind xmlns:D=\"DAV:\">"); 0756 0757 // insert additional XML request from the davRequestResponse metadata 0758 if (hasMetaData(QStringLiteral("davRequestResponse"))) { 0759 request += metaData(QStringLiteral("davRequestResponse")).toUtf8(); 0760 } else { 0761 // No special request, ask for default properties 0762 request += 0763 "<D:prop>" 0764 "<D:creationdate/>" 0765 "<D:getcontentlength/>" 0766 "<D:displayname/>" 0767 "<D:source/>" 0768 "<D:getcontentlanguage/>" 0769 "<D:getcontenttype/>" 0770 "<D:getlastmodified/>" 0771 "<D:getetag/>" 0772 "<D:supportedlock/>" 0773 "<D:lockdiscovery/>" 0774 "<D:resourcetype/>" 0775 "<D:quota-available-bytes/>" 0776 "<D:quota-used-bytes/>" 0777 "</D:prop>"; 0778 } 0779 request += "</D:propfind>"; 0780 0781 davSetRequest(request); 0782 } 0783 0784 // WebDAV Stat or List... 0785 m_request.method = query.isEmpty() ? DAV_PROPFIND : DAV_SEARCH; 0786 m_request.url.setQuery(QString()); 0787 m_request.cacheTag.policy = CC_Reload; 0788 m_request.davData.depth = stat ? 0 : 1; 0789 if (!stat) { 0790 Utils::appendSlashToPath(m_request.url); 0791 } 0792 0793 (void)/* handling result via result codes */ proceedUntilResponseContent(true); 0794 infoMessage(QLatin1String("")); 0795 0796 // Has a redirection already been called? If so, we're done. 0797 if (m_isRedirection || m_kioError) { 0798 if (m_isRedirection) { 0799 return davFinished(); 0800 } 0801 return WorkerResult::pass(); 0802 } 0803 0804 QDomDocument multiResponse; 0805 multiResponse.setContent(m_webDavDataBuf, true); 0806 0807 bool hasResponse = false; 0808 0809 qCDebug(KIO_HTTP) << '\n' << multiResponse.toString(2); 0810 0811 for (QDomNode n = multiResponse.documentElement().firstChild(); !n.isNull(); n = n.nextSibling()) { 0812 QDomElement thisResponse = n.toElement(); 0813 if (thisResponse.isNull()) { 0814 continue; 0815 } 0816 0817 hasResponse = true; 0818 0819 QDomElement href = thisResponse.namedItem(QStringLiteral("href")).toElement(); 0820 if (!href.isNull()) { 0821 entry.clear(); 0822 0823 const QUrl thisURL(href.text()); // href.text() is a percent-encoded url. 0824 if (thisURL.isValid()) { 0825 const QUrl adjustedThisURL = thisURL.adjusted(QUrl::StripTrailingSlash); 0826 const QUrl adjustedUrl = url.adjusted(QUrl::StripTrailingSlash); 0827 0828 // base dir of a listDir(): name should be "." 0829 QString name; 0830 if (!stat && adjustedThisURL.path() == adjustedUrl.path()) { 0831 name = QLatin1Char('.'); 0832 } else { 0833 name = adjustedThisURL.fileName(); 0834 } 0835 0836 entry.fastInsert(KIO::UDSEntry::UDS_NAME, name.isEmpty() ? href.text() : name); 0837 } 0838 0839 QDomNodeList propstats = thisResponse.elementsByTagName(QStringLiteral("propstat")); 0840 0841 davParsePropstats(propstats, entry); 0842 0843 // Since a lot of webdav servers seem not to send the content-type information 0844 // for the requested directory listings, we attempt to guess the MIME type from 0845 // the resource name so long as the resource is not a directory. 0846 if (entry.stringValue(KIO::UDSEntry::UDS_MIME_TYPE).isEmpty() && entry.numberValue(KIO::UDSEntry::UDS_FILE_TYPE) != S_IFDIR) { 0847 QMimeType mime = db.mimeTypeForFile(thisURL.path(), QMimeDatabase::MatchExtension); 0848 if (mime.isValid() && !mime.isDefault()) { 0849 qCDebug(KIO_HTTP) << "Setting" << mime.name() << "as guessed MIME type for" << thisURL.path(); 0850 entry.fastInsert(KIO::UDSEntry::UDS_GUESSED_MIME_TYPE, mime.name()); 0851 } 0852 } 0853 0854 if (stat) { 0855 // return an item 0856 statEntry(entry); 0857 return davFinished(); 0858 } 0859 0860 listEntry(entry); 0861 } else { 0862 qCDebug(KIO_HTTP) << "Error: no URL contained in response to PROPFIND on" << url; 0863 } 0864 } 0865 0866 if (stat || !hasResponse) { 0867 return error(ERR_DOES_NOT_EXIST, url.toDisplayString()); 0868 } 0869 0870 return davFinished(); 0871 } 0872 0873 KIO::WorkerResult HTTPProtocol::davGeneric(const QUrl &url, KIO::HTTP_METHOD method, qint64 size) 0874 { 0875 qCDebug(KIO_HTTP) << url; 0876 0877 if (const auto result = maybeSetRequestUrl(url); !result.success()) { 0878 return result; 0879 } 0880 resetSessionSettings(); 0881 0882 // check to make sure this host supports WebDAV 0883 if (const auto result = davHostOk(); !result.success()) { 0884 return result; 0885 } 0886 0887 // WebDAV method 0888 m_request.method = method; 0889 m_request.url.setQuery(QString()); 0890 m_request.cacheTag.policy = CC_Reload; 0891 0892 m_iPostDataSize = (size > -1 ? static_cast<KIO::filesize_t>(size) : NO_SIZE); 0893 return proceedUntilResponseContent(); 0894 } 0895 0896 int HTTPProtocol::codeFromResponse(const QString &response) 0897 { 0898 const int firstSpace = response.indexOf(QLatin1Char(' ')); 0899 const int secondSpace = response.indexOf(QLatin1Char(' '), firstSpace + 1); 0900 0901 // QStringView::toInt() implementation in Qt5 uses toString(), which 0902 // defeats the point of "not-allocating" 0903 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 0904 return QStringView(response).mid(firstSpace + 1, secondSpace - firstSpace - 1).toInt(); 0905 #else 0906 return response.midRef(firstSpace + 1, secondSpace - firstSpace - 1).toInt(); 0907 #endif 0908 } 0909 0910 void HTTPProtocol::davParsePropstats(const QDomNodeList &propstats, UDSEntry &entry) 0911 { 0912 QString mimeType; 0913 bool foundExecutable = false; 0914 bool isDirectory = false; 0915 uint lockCount = 0; 0916 uint supportedLockCount = 0; 0917 qlonglong quotaUsed = -1; 0918 qlonglong quotaAvailable = -1; 0919 0920 for (int i = 0; i < propstats.count(); i++) { 0921 QDomElement propstat = propstats.item(i).toElement(); 0922 0923 QDomElement status = propstat.namedItem(QStringLiteral("status")).toElement(); 0924 if (status.isNull()) { 0925 // error, no status code in this propstat 0926 qCDebug(KIO_HTTP) << "Error, no status code in this propstat"; 0927 return; 0928 } 0929 0930 int code = codeFromResponse(status.text()); 0931 0932 if (code != 200) { 0933 qCDebug(KIO_HTTP) << "Got status code" << code << "(this may mean that some properties are unavailable)"; 0934 continue; 0935 } 0936 0937 QDomElement prop = propstat.namedItem(QStringLiteral("prop")).toElement(); 0938 if (prop.isNull()) { 0939 qCDebug(KIO_HTTP) << "Error: no prop segment in this propstat."; 0940 return; 0941 } 0942 0943 if (hasMetaData(QStringLiteral("davRequestResponse"))) { 0944 QDomDocument doc; 0945 doc.appendChild(prop); 0946 entry.replace(KIO::UDSEntry::UDS_XML_PROPERTIES, doc.toString()); 0947 } 0948 0949 for (QDomNode n = prop.firstChild(); !n.isNull(); n = n.nextSibling()) { 0950 QDomElement property = n.toElement(); 0951 if (property.isNull()) { 0952 continue; 0953 } 0954 0955 if (property.namespaceURI() != QLatin1String("DAV:")) { 0956 // break out - we're only interested in properties from the DAV namespace 0957 continue; 0958 } 0959 0960 if (property.tagName() == QLatin1String("creationdate")) { 0961 // Resource creation date. Should be is ISO 8601 format. 0962 entry.replace(KIO::UDSEntry::UDS_CREATION_TIME, parseDateTime(property.text(), property.attribute(QStringLiteral("dt"))).toSecsSinceEpoch()); 0963 } else if (property.tagName() == QLatin1String("getcontentlength")) { 0964 // Content length (file size) 0965 entry.replace(KIO::UDSEntry::UDS_SIZE, property.text().toULong()); 0966 } else if (property.tagName() == QLatin1String("displayname")) { 0967 // Name suitable for presentation to the user 0968 setMetaData(QStringLiteral("davDisplayName"), property.text()); 0969 } else if (property.tagName() == QLatin1String("source")) { 0970 // Source template location 0971 QDomElement source = property.namedItem(QStringLiteral("link")).toElement().namedItem(QStringLiteral("dst")).toElement(); 0972 if (!source.isNull()) { 0973 setMetaData(QStringLiteral("davSource"), source.text()); 0974 } 0975 } else if (property.tagName() == QLatin1String("getcontentlanguage")) { 0976 // equiv. to Content-Language header on a GET 0977 setMetaData(QStringLiteral("davContentLanguage"), property.text()); 0978 } else if (property.tagName() == QLatin1String("getcontenttype")) { 0979 // Content type (MIME type) 0980 // This may require adjustments for other server-side webdav implementations 0981 // (tested with Apache + mod_dav 1.0.3) 0982 if (property.text() == QLatin1String("httpd/unix-directory")) { 0983 isDirectory = true; 0984 } else if (property.text() != QLatin1String("application/octet-stream")) { 0985 // The server could be lazy and always return application/octet-stream; 0986 // we will guess the MIME type later in that case. 0987 mimeType = property.text(); 0988 } 0989 } else if (property.tagName() == QLatin1String("executable")) { 0990 // File executable status 0991 if (property.text() == QLatin1Char('T')) { 0992 foundExecutable = true; 0993 } 0994 0995 } else if (property.tagName() == QLatin1String("getlastmodified")) { 0996 // Last modification date 0997 entry.replace(KIO::UDSEntry::UDS_MODIFICATION_TIME, 0998 parseDateTime(property.text(), property.attribute(QStringLiteral("dt"))).toSecsSinceEpoch()); 0999 } else if (property.tagName() == QLatin1String("getetag")) { 1000 // Entity tag 1001 setMetaData(QStringLiteral("davEntityTag"), property.text()); 1002 } else if (property.tagName() == QLatin1String("supportedlock")) { 1003 // Supported locking specifications 1004 for (QDomNode n2 = property.firstChild(); !n2.isNull(); n2 = n2.nextSibling()) { 1005 QDomElement lockEntry = n2.toElement(); 1006 if (lockEntry.tagName() == QLatin1String("lockentry")) { 1007 QDomElement lockScope = lockEntry.namedItem(QStringLiteral("lockscope")).toElement(); 1008 QDomElement lockType = lockEntry.namedItem(QStringLiteral("locktype")).toElement(); 1009 if (!lockScope.isNull() && !lockType.isNull()) { 1010 // Lock type was properly specified 1011 supportedLockCount++; 1012 const QString lockCountStr = QString::number(supportedLockCount); 1013 const QString scope = lockScope.firstChild().toElement().tagName(); 1014 const QString type = lockType.firstChild().toElement().tagName(); 1015 1016 setMetaData(QLatin1String("davSupportedLockScope") + lockCountStr, scope); 1017 setMetaData(QLatin1String("davSupportedLockType") + lockCountStr, type); 1018 } 1019 } 1020 } 1021 } else if (property.tagName() == QLatin1String("lockdiscovery")) { 1022 // Lists the available locks 1023 davParseActiveLocks(property.elementsByTagName(QStringLiteral("activelock")), lockCount); 1024 } else if (property.tagName() == QLatin1String("resourcetype")) { 1025 // Resource type. "Specifies the nature of the resource." 1026 if (!property.namedItem(QStringLiteral("collection")).toElement().isNull()) { 1027 // This is a collection (directory) 1028 isDirectory = true; 1029 } 1030 } else if (property.tagName() == QLatin1String("quota-used-bytes")) { 1031 // Quota-used-bytes. "Contains the amount of storage already in use." 1032 quotaUsed = property.text().toLongLong(); 1033 } else if (property.tagName() == QLatin1String("quota-available-bytes")) { 1034 // Quota-available-bytes. "Indicates the maximum amount of additional storage available." 1035 quotaAvailable = property.text().toLongLong(); 1036 } else { 1037 qCDebug(KIO_HTTP) << "Found unknown webdav property:" << property.tagName(); 1038 } 1039 } 1040 } 1041 1042 setMetaData(QStringLiteral("davLockCount"), QString::number(lockCount)); 1043 setMetaData(QStringLiteral("davSupportedLockCount"), QString::number(supportedLockCount)); 1044 1045 entry.replace(KIO::UDSEntry::UDS_FILE_TYPE, isDirectory ? S_IFDIR : S_IFREG); 1046 1047 if (foundExecutable || isDirectory) { 1048 // File was executable, or is a directory. 1049 entry.replace(KIO::UDSEntry::UDS_ACCESS, 0700); 1050 } else { 1051 entry.replace(KIO::UDSEntry::UDS_ACCESS, 0600); 1052 } 1053 1054 if (!isDirectory && !mimeType.isEmpty()) { 1055 entry.replace(KIO::UDSEntry::UDS_MIME_TYPE, mimeType); 1056 } 1057 1058 if (quotaUsed >= 0 && quotaAvailable >= 0) { 1059 // Only used and available storage properties exist, the total storage size has to be calculated. 1060 setMetaData(QStringLiteral("total"), QString::number(quotaUsed + quotaAvailable)); 1061 setMetaData(QStringLiteral("available"), QString::number(quotaAvailable)); 1062 } 1063 } 1064 1065 void HTTPProtocol::davParseActiveLocks(const QDomNodeList &activeLocks, uint &lockCount) 1066 { 1067 for (int i = 0; i < activeLocks.count(); i++) { 1068 const QDomElement activeLock = activeLocks.item(i).toElement(); 1069 1070 lockCount++; 1071 // required 1072 const QDomElement lockScope = activeLock.namedItem(QStringLiteral("lockscope")).toElement(); 1073 const QDomElement lockType = activeLock.namedItem(QStringLiteral("locktype")).toElement(); 1074 const QDomElement lockDepth = activeLock.namedItem(QStringLiteral("depth")).toElement(); 1075 // optional 1076 const QDomElement lockOwner = activeLock.namedItem(QStringLiteral("owner")).toElement(); 1077 const QDomElement lockTimeout = activeLock.namedItem(QStringLiteral("timeout")).toElement(); 1078 const QDomElement lockToken = activeLock.namedItem(QStringLiteral("locktoken")).toElement(); 1079 1080 if (!lockScope.isNull() && !lockType.isNull() && !lockDepth.isNull()) { 1081 // lock was properly specified 1082 lockCount++; 1083 const QString lockCountStr = QString::number(lockCount); 1084 const QString scope = lockScope.firstChild().toElement().tagName(); 1085 const QString type = lockType.firstChild().toElement().tagName(); 1086 const QString depth = lockDepth.text(); 1087 1088 setMetaData(QLatin1String("davLockScope") + lockCountStr, scope); 1089 setMetaData(QLatin1String("davLockType") + lockCountStr, type); 1090 setMetaData(QLatin1String("davLockDepth") + lockCountStr, depth); 1091 1092 if (!lockOwner.isNull()) { 1093 setMetaData(QLatin1String("davLockOwner") + lockCountStr, lockOwner.text()); 1094 } 1095 1096 if (!lockTimeout.isNull()) { 1097 setMetaData(QLatin1String("davLockTimeout") + lockCountStr, lockTimeout.text()); 1098 } 1099 1100 if (!lockToken.isNull()) { 1101 QDomElement tokenVal = lockScope.namedItem(QStringLiteral("href")).toElement(); 1102 if (!tokenVal.isNull()) { 1103 setMetaData(QLatin1String("davLockToken") + lockCountStr, tokenVal.text()); 1104 } 1105 } 1106 } 1107 } 1108 } 1109 1110 QDateTime HTTPProtocol::parseDateTime(const QString &input, const QString &type) 1111 { 1112 if (type == QLatin1String("dateTime.tz")) { 1113 return QDateTime::fromString(input, Qt::ISODate); 1114 } else if (type == QLatin1String("dateTime.rfc1123")) { 1115 return QDateTime::fromString(input, Qt::RFC2822Date); 1116 } 1117 1118 // format not advertised... try to parse anyway 1119 QDateTime time = QDateTime::fromString(input, Qt::RFC2822Date); 1120 if (time.isValid()) { 1121 return time; 1122 } 1123 1124 return QDateTime::fromString(input, Qt::ISODate); 1125 } 1126 1127 QString HTTPProtocol::davProcessLocks() 1128 { 1129 if (hasMetaData(QStringLiteral("davLockCount"))) { 1130 QString response = QStringLiteral("If:"); 1131 int numLocks = metaData(QStringLiteral("davLockCount")).toInt(); 1132 bool bracketsOpen = false; 1133 for (int i = 0; i < numLocks; i++) { 1134 const QString countStr = QString::number(i); 1135 if (hasMetaData(QLatin1String("davLockToken") + countStr)) { 1136 if (hasMetaData(QLatin1String("davLockURL") + countStr)) { 1137 if (bracketsOpen) { 1138 response += QLatin1Char(')'); 1139 bracketsOpen = false; 1140 } 1141 response += QLatin1String(" <") + metaData(QLatin1String("davLockURL") + countStr) + QLatin1Char('>'); 1142 } 1143 1144 if (!bracketsOpen) { 1145 response += QLatin1String(" ("); 1146 bracketsOpen = true; 1147 } else { 1148 response += QLatin1Char(' '); 1149 } 1150 1151 if (hasMetaData(QLatin1String("davLockNot") + countStr)) { 1152 response += QLatin1String("Not "); 1153 } 1154 1155 response += QLatin1Char('<') + metaData(QLatin1String("davLockToken") + countStr) + QLatin1Char('>'); 1156 } 1157 } 1158 1159 if (bracketsOpen) { 1160 response += QLatin1Char(')'); 1161 } 1162 1163 response += QLatin1String("\r\n"); 1164 return response; 1165 } 1166 1167 return QString(); 1168 } 1169 1170 KIO::WorkerResult HTTPProtocol::davHostOk() 1171 { 1172 // FIXME needs to be reworked. Switched off for now. 1173 return WorkerResult::pass(); 1174 1175 // cached? 1176 if (m_davHostOk) { 1177 qCDebug(KIO_HTTP) << "true"; 1178 return WorkerResult::pass(); 1179 } 1180 if (m_davHostUnsupported) { 1181 qCDebug(KIO_HTTP) << " false"; 1182 return davError(-2); 1183 } 1184 1185 m_request.method = HTTP_OPTIONS; 1186 1187 // query the server's capabilities generally, not for a specific URL 1188 m_request.url.setPath(QStringLiteral("*")); 1189 m_request.url.setQuery(QString()); 1190 m_request.cacheTag.policy = CC_Reload; 1191 1192 // clear davVersions variable, which holds the response to the DAV: header 1193 m_davCapabilities.clear(); 1194 1195 (void)/* handling result via dav codes */ proceedUntilResponseHeader(); 1196 1197 if (!m_davCapabilities.isEmpty()) { 1198 for (int i = 0; i < m_davCapabilities.count(); i++) { 1199 bool ok; 1200 uint verNo = m_davCapabilities[i].toUInt(&ok); 1201 if (ok && verNo > 0 && verNo < 3) { 1202 m_davHostOk = true; 1203 qCDebug(KIO_HTTP) << "Server supports DAV version" << verNo; 1204 } 1205 } 1206 1207 if (m_davHostOk) { 1208 return WorkerResult::pass(); 1209 } 1210 } 1211 1212 m_davHostUnsupported = true; 1213 return davError(-2); 1214 } 1215 1216 // This function is for closing proceedUntilResponseHeader(); requests 1217 // Required because there may or may not be further info expected 1218 KIO::WorkerResult HTTPProtocol::davFinished() 1219 { 1220 // TODO: Check with the DAV extension developers 1221 httpClose(m_request.isKeepAlive); 1222 return WorkerResult::pass(); 1223 } 1224 1225 KIO::WorkerResult HTTPProtocol::mkdir(const QUrl &url, int) 1226 { 1227 qCDebug(KIO_HTTP) << url; 1228 1229 if (const auto result = maybeSetRequestUrl(url); !result.success()) { 1230 return result; 1231 } 1232 resetSessionSettings(); 1233 1234 m_request.method = DAV_MKCOL; 1235 m_request.url.setQuery(QString()); 1236 m_request.cacheTag.policy = CC_Reload; 1237 1238 (void)/* handling result via dav codes */ proceedUntilResponseContent(true); 1239 1240 if (m_request.responseCode == 201) { 1241 return davFinished(); 1242 } 1243 return davError(); 1244 } 1245 1246 KIO::WorkerResult HTTPProtocol::get(const QUrl &url) 1247 { 1248 qCDebug(KIO_HTTP) << url; 1249 1250 if (const auto result = maybeSetRequestUrl(url); !result.success()) { 1251 return result; 1252 } 1253 resetSessionSettings(); 1254 1255 m_request.method = HTTP_GET; 1256 1257 QString tmp(metaData(QStringLiteral("cache"))); 1258 if (!tmp.isEmpty()) { 1259 m_request.cacheTag.policy = parseCacheControl(tmp); 1260 } else { 1261 m_request.cacheTag.policy = DEFAULT_CACHE_CONTROL; 1262 } 1263 1264 return proceedUntilResponseContent(); 1265 } 1266 1267 KIO::WorkerResult HTTPProtocol::put(const QUrl &url, int, KIO::JobFlags flags) 1268 { 1269 qCDebug(KIO_HTTP) << url; 1270 1271 if (const auto result = maybeSetRequestUrl(url); !result.success()) { 1272 return result; 1273 } 1274 1275 resetSessionSettings(); 1276 1277 // Webdav hosts are capable of observing overwrite == false 1278 if (m_protocol.startsWith("webdav")) { // krazy:exclude=strings 1279 if (!(flags & KIO::Overwrite)) { 1280 // check to make sure this host supports WebDAV 1281 if (const auto result = davHostOk(); !result.success()) { 1282 return result; 1283 } 1284 1285 // Checks if the destination exists and return an error if it does. 1286 if (davDestinationExists()) { 1287 return error(ERR_FILE_ALREADY_EXIST, url.fileName()); 1288 } 1289 } 1290 } 1291 1292 m_request.method = HTTP_PUT; 1293 m_request.cacheTag.policy = CC_Reload; 1294 1295 return proceedUntilResponseContent(); 1296 } 1297 1298 KIO::WorkerResult HTTPProtocol::copy(const QUrl &src, const QUrl &dest, int, KIO::JobFlags flags) 1299 { 1300 qCDebug(KIO_HTTP) << src << "->" << dest; 1301 const bool isSourceLocal = src.isLocalFile(); 1302 const bool isDestinationLocal = dest.isLocalFile(); 1303 1304 if (isSourceLocal && !isDestinationLocal) { 1305 return copyPut(src, dest, flags); 1306 } 1307 1308 if (const auto result = maybeSetRequestUrl(dest); !result.success()) { 1309 return result; 1310 } 1311 1312 resetSessionSettings(); 1313 1314 if (!(flags & KIO::Overwrite)) { 1315 // check to make sure this host supports WebDAV 1316 if (const auto result = davHostOk(); !result.success()) { 1317 return result; 1318 } 1319 1320 // Checks if the destination exists and return an error if it does. 1321 if (davDestinationExists()) { 1322 return error(ERR_FILE_ALREADY_EXIST, dest.fileName()); 1323 } 1324 } 1325 1326 if (const auto result = maybeSetRequestUrl(src); !result.success()) { 1327 return result; 1328 } 1329 1330 // destination has to be "http(s)://..." 1331 QUrl newDest(dest); 1332 changeProtocolToHttp(&newDest); 1333 1334 m_request.method = DAV_COPY; 1335 m_request.davData.desturl = newDest.toString(QUrl::FullyEncoded); 1336 m_request.davData.overwrite = (flags & KIO::Overwrite); 1337 m_request.url.setQuery(QString()); 1338 m_request.cacheTag.policy = CC_Reload; 1339 1340 (void)/* handling result via dav codes */ proceedUntilResponseContent(); 1341 1342 // The server returns a HTTP/1.1 201 Created or 204 No Content on successful completion 1343 if (m_request.responseCode == 201 || m_request.responseCode == 204) { 1344 return davFinished(); 1345 } 1346 return davError(); 1347 } 1348 1349 KIO::WorkerResult HTTPProtocol::rename(const QUrl &src, const QUrl &dest, KIO::JobFlags flags) 1350 { 1351 qCDebug(KIO_HTTP) << src << "->" << dest; 1352 1353 if (const auto result = maybeSetRequestUrl(dest); !result.success()) { 1354 return result; 1355 } 1356 if (const auto result = maybeSetRequestUrl(src); !result.success()) { 1357 return result; 1358 } 1359 resetSessionSettings(); 1360 1361 // destination has to be "http://..." 1362 QUrl newDest(dest); 1363 changeProtocolToHttp(&newDest); 1364 1365 m_request.method = DAV_MOVE; 1366 m_request.davData.desturl = newDest.toString(QUrl::FullyEncoded); 1367 m_request.davData.overwrite = (flags & KIO::Overwrite); 1368 m_request.url.setQuery(QString()); 1369 m_request.cacheTag.policy = CC_Reload; 1370 1371 (void)/* handling result via dav codes */ proceedUntilResponseHeader(); 1372 1373 // Work around strict Apache-2 WebDAV implementation which refuses to cooperate 1374 // with webdav://host/directory, instead requiring webdav://host/directory/ 1375 // (strangely enough it accepts Destination: without a trailing slash) 1376 // See BR# 209508 and BR#187970 1377 if (m_request.responseCode == 301) { 1378 QUrl redir = m_request.redirectUrl; 1379 1380 resetSessionSettings(); 1381 1382 m_request.url = redir; 1383 m_request.method = DAV_MOVE; 1384 m_request.davData.desturl = newDest.toString(); 1385 m_request.davData.overwrite = (flags & KIO::Overwrite); 1386 m_request.url.setQuery(QString()); 1387 m_request.cacheTag.policy = CC_Reload; 1388 1389 (void)/* handling result via dav codes */ proceedUntilResponseHeader(); 1390 } 1391 1392 // The server returns a HTTP/1.1 201 Created or 204 No Content on successful completion 1393 if (m_request.responseCode == 201 || m_request.responseCode == 204) { 1394 return davFinished(); 1395 } 1396 return davError(); 1397 } 1398 1399 KIO::WorkerResult HTTPProtocol::del(const QUrl &url, bool) 1400 { 1401 qCDebug(KIO_HTTP) << url; 1402 1403 if (const auto result = maybeSetRequestUrl(url); !result.success()) { 1404 return result; 1405 } 1406 1407 resetSessionSettings(); 1408 1409 m_request.method = HTTP_DELETE; 1410 m_request.cacheTag.policy = CC_Reload; 1411 1412 if (m_protocol.startsWith("webdav")) { // krazy:exclude=strings due to QByteArray 1413 m_request.url.setQuery(QString()); 1414 if (const auto result = proceedUntilResponseHeader(); !result.success()) { 1415 return result; 1416 } 1417 1418 // The server returns a HTTP/1.1 200 Ok or HTTP/1.1 204 No Content 1419 // on successful completion. 1420 if (m_request.responseCode == 200 || m_request.responseCode == 204 || m_isRedirection) { 1421 return davFinished(); 1422 } 1423 return davError(); 1424 } 1425 1426 return proceedUntilResponseContent(); 1427 } 1428 1429 KIO::WorkerResult HTTPProtocol::post(const QUrl &url, qint64 size) 1430 { 1431 qCDebug(KIO_HTTP) << url; 1432 1433 if (const auto result = maybeSetRequestUrl(url); !result.success()) { 1434 return result; 1435 } 1436 resetSessionSettings(); 1437 1438 m_request.method = HTTP_POST; 1439 m_request.cacheTag.policy = CC_Reload; 1440 1441 m_iPostDataSize = (size > -1 ? static_cast<KIO::filesize_t>(size) : NO_SIZE); 1442 return proceedUntilResponseContent(); 1443 } 1444 1445 KIO::WorkerResult HTTPProtocol::davLock(const QUrl &url, const QString &scope, const QString &type, const QString &owner) 1446 { 1447 qCDebug(KIO_HTTP) << url; 1448 1449 if (const auto result = maybeSetRequestUrl(url); !result.success()) { 1450 return result; 1451 } 1452 resetSessionSettings(); 1453 1454 m_request.method = DAV_LOCK; 1455 m_request.url.setQuery(QString()); 1456 m_request.cacheTag.policy = CC_Reload; 1457 1458 /* Create appropriate lock XML request. */ 1459 QDomDocument lockReq; 1460 1461 QDomElement lockInfo = lockReq.createElementNS(QStringLiteral("DAV:"), QStringLiteral("lockinfo")); 1462 lockReq.appendChild(lockInfo); 1463 1464 QDomElement lockScope = lockReq.createElement(QStringLiteral("lockscope")); 1465 lockInfo.appendChild(lockScope); 1466 1467 lockScope.appendChild(lockReq.createElement(scope)); 1468 1469 QDomElement lockType = lockReq.createElement(QStringLiteral("locktype")); 1470 lockInfo.appendChild(lockType); 1471 1472 lockType.appendChild(lockReq.createElement(type)); 1473 1474 if (!owner.isNull()) { 1475 QDomElement ownerElement = lockReq.createElement(QStringLiteral("owner")); 1476 lockReq.appendChild(ownerElement); 1477 1478 QDomElement ownerHref = lockReq.createElement(QStringLiteral("href")); 1479 ownerElement.appendChild(ownerHref); 1480 1481 ownerHref.appendChild(lockReq.createTextNode(owner)); 1482 } 1483 1484 // insert the document into the POST buffer 1485 cachePostData(lockReq.toByteArray()); 1486 1487 (void)/* handling result via dav codes */ proceedUntilResponseContent(true); 1488 1489 if (m_request.responseCode == 200) { 1490 // success 1491 QDomDocument multiResponse; 1492 multiResponse.setContent(m_webDavDataBuf, true); 1493 1494 QDomElement prop = multiResponse.documentElement().namedItem(QStringLiteral("prop")).toElement(); 1495 1496 QDomElement lockdiscovery = prop.namedItem(QStringLiteral("lockdiscovery")).toElement(); 1497 1498 uint lockCount = 0; 1499 davParseActiveLocks(lockdiscovery.elementsByTagName(QStringLiteral("activelock")), lockCount); 1500 1501 setMetaData(QStringLiteral("davLockCount"), QString::number(lockCount)); 1502 1503 return WorkerResult::pass(); 1504 } 1505 return davError(); 1506 } 1507 1508 KIO::WorkerResult HTTPProtocol::davUnlock(const QUrl &url) 1509 { 1510 qCDebug(KIO_HTTP) << url; 1511 1512 if (const auto result = maybeSetRequestUrl(url); !result.success()) { 1513 return result; 1514 } 1515 resetSessionSettings(); 1516 1517 m_request.method = DAV_UNLOCK; 1518 m_request.url.setQuery(QString()); 1519 m_request.cacheTag.policy = CC_Reload; 1520 1521 (void)/* handling result via dav codes */ proceedUntilResponseContent(true); 1522 1523 if (m_request.responseCode == 200) { 1524 return WorkerResult::pass(); 1525 } 1526 return davError(); 1527 } 1528 1529 KIO::WorkerResult HTTPProtocol::davError(int code /* = -1 */, const QString &_url) 1530 { 1531 QString discard; 1532 return davError(discard, code, _url); 1533 } 1534 1535 KIO::WorkerResult HTTPProtocol::davError(QString &errorMsg, int code /* = -1 */, const QString &_url) 1536 { 1537 bool callError = false; 1538 if (code == -1) { 1539 code = m_request.responseCode; 1540 callError = true; 1541 } 1542 if (code == -2) { 1543 callError = true; 1544 } 1545 1546 QString url = _url; 1547 if (!url.isNull()) { 1548 url = m_request.url.toDisplayString(); 1549 } 1550 1551 QString action; 1552 QString errorString; 1553 int errorCode = ERR_WORKER_DEFINED; 1554 1555 // for 412 Precondition Failed 1556 QString ow = i18n("Otherwise, the request would have succeeded."); 1557 1558 switch (m_request.method) { 1559 case DAV_PROPFIND: 1560 action = i18nc("request type", "retrieve property values"); 1561 break; 1562 case DAV_PROPPATCH: 1563 action = i18nc("request type", "set property values"); 1564 break; 1565 case DAV_MKCOL: 1566 action = i18nc("request type", "create the requested folder"); 1567 break; 1568 case DAV_COPY: 1569 action = i18nc("request type", "copy the specified file or folder"); 1570 break; 1571 case DAV_MOVE: 1572 action = i18nc("request type", "move the specified file or folder"); 1573 break; 1574 case DAV_SEARCH: 1575 action = i18nc("request type", "search in the specified folder"); 1576 break; 1577 case DAV_LOCK: 1578 action = i18nc("request type", "lock the specified file or folder"); 1579 break; 1580 case DAV_UNLOCK: 1581 action = i18nc("request type", "unlock the specified file or folder"); 1582 break; 1583 case HTTP_DELETE: 1584 action = i18nc("request type", "delete the specified file or folder"); 1585 break; 1586 case HTTP_OPTIONS: 1587 action = i18nc("request type", "query the server's capabilities"); 1588 break; 1589 case HTTP_GET: 1590 action = i18nc("request type", "retrieve the contents of the specified file or folder"); 1591 break; 1592 case DAV_REPORT: 1593 action = i18nc("request type", "run a report in the specified folder"); 1594 break; 1595 case HTTP_PUT: 1596 case HTTP_POST: 1597 case HTTP_HEAD: 1598 default: 1599 // this should not happen, this function is for webdav errors only 1600 Q_ASSERT(0); 1601 } 1602 1603 // default error message if the following code fails 1604 errorString = i18nc("%1: code, %2: request type", 1605 "An unexpected error (%1) occurred " 1606 "while attempting to %2.", 1607 code, 1608 action); 1609 1610 switch (code) { 1611 case -2: 1612 // internal error: OPTIONS request did not specify DAV compliance 1613 // ERR_UNSUPPORTED_PROTOCOL 1614 errorString = i18n("The server does not support the WebDAV protocol."); 1615 break; 1616 case 207: 1617 // 207 Multi-status 1618 { 1619 // our error info is in the returned XML document. 1620 // retrieve the XML document 1621 1622 // there was an error retrieving the XML document. 1623 if (const auto result = readBody(true); !result.success() && m_kioError) { 1624 errorMsg.clear(); 1625 return WorkerResult::fail(); 1626 } 1627 1628 QStringList errors; 1629 QDomDocument multiResponse; 1630 1631 multiResponse.setContent(m_webDavDataBuf, true); 1632 1633 QDomElement multistatus = multiResponse.documentElement().namedItem(QStringLiteral("multistatus")).toElement(); 1634 1635 QDomNodeList responses = multistatus.elementsByTagName(QStringLiteral("response")); 1636 1637 for (int i = 0; i < responses.count(); i++) { 1638 int errCode; 1639 QString errUrl; 1640 1641 QDomElement response = responses.item(i).toElement(); 1642 QDomElement code = response.namedItem(QStringLiteral("status")).toElement(); 1643 1644 if (!code.isNull()) { 1645 errCode = codeFromResponse(code.text()); 1646 QDomElement href = response.namedItem(QStringLiteral("href")).toElement(); 1647 if (!href.isNull()) { 1648 errUrl = href.text(); 1649 } 1650 QString error; 1651 (void)davError(error, errCode, errUrl); 1652 errors << error; 1653 } 1654 } 1655 1656 // kError = ERR_WORKER_DEFINED; 1657 errorString = i18nc("%1: request type, %2: url", 1658 "An error occurred while attempting to %1, %2. A " 1659 "summary of the reasons is below.", 1660 action, 1661 url); 1662 1663 errorString += QLatin1String("<ul>"); 1664 1665 for (const QString &error : std::as_const(errors)) { 1666 errorString += QLatin1String("<li>") + error + QLatin1String("</li>"); 1667 } 1668 1669 errorString += QLatin1String("</ul>"); 1670 } 1671 break; 1672 case 403: 1673 case 500: // hack: Apache mod_dav returns this instead of 403 (!) 1674 // 403 Forbidden 1675 // ERR_ACCESS_DENIED 1676 errorString = i18nc("%1: request type", "Access was denied while attempting to %1.", action); 1677 break; 1678 case 405: 1679 // 405 Method Not Allowed 1680 if (m_request.method == DAV_MKCOL) { 1681 // ERR_DIR_ALREADY_EXIST 1682 errorString = url; 1683 errorCode = ERR_DIR_ALREADY_EXIST; 1684 } 1685 break; 1686 case 409: 1687 // 409 Conflict 1688 // ERR_ACCESS_DENIED 1689 errorString = i18n( 1690 "A resource cannot be created at the destination " 1691 "until one or more intermediate collections (folders) " 1692 "have been created."); 1693 break; 1694 case 412: 1695 // 412 Precondition failed 1696 if (m_request.method == DAV_COPY || m_request.method == DAV_MOVE) { 1697 // ERR_ACCESS_DENIED 1698 errorString = i18n( 1699 "The server was unable to maintain the liveness of the\n" 1700 "properties listed in the propertybehavior XML element\n" 1701 "or you attempted to overwrite a file while requesting\n" 1702 "that files are not overwritten.\n %1", 1703 ow); 1704 1705 } else if (m_request.method == DAV_LOCK) { 1706 // ERR_ACCESS_DENIED 1707 errorString = i18n("The requested lock could not be granted. %1", ow); 1708 } 1709 break; 1710 case 415: 1711 // 415 Unsupported Media Type 1712 // ERR_ACCESS_DENIED 1713 errorString = i18n("The server does not support the request type of the body."); 1714 break; 1715 case 423: 1716 // 423 Locked 1717 // ERR_ACCESS_DENIED 1718 errorString = i18nc("%1: request type", "Unable to %1 because the resource is locked.", action); 1719 break; 1720 case 425: 1721 // 424 Failed Dependency 1722 errorString = i18n("This action was prevented by another error."); 1723 break; 1724 case 502: 1725 // 502 Bad Gateway 1726 if (m_request.method == DAV_COPY || m_request.method == DAV_MOVE) { 1727 // ERR_WRITE_ACCESS_DENIED 1728 errorString = i18nc("%1: request type", 1729 "Unable to %1 because the destination server refuses " 1730 "to accept the file or folder.", 1731 action); 1732 } 1733 break; 1734 case 507: 1735 // 507 Insufficient Storage 1736 // ERR_DISK_FULL 1737 errorString = i18n( 1738 "The destination resource does not have sufficient space " 1739 "to record the state of the resource after the execution " 1740 "of this method."); 1741 break; 1742 default: 1743 break; 1744 } 1745 1746 // if ( kError != ERR_WORKER_DEFINED ) 1747 // errorString += " (" + url + ')'; 1748 1749 errorMsg = errorString; 1750 if (callError) { 1751 return error(errorCode, errorString); 1752 } 1753 return WorkerResult::pass(); 1754 } 1755 1756 // HTTP generic error 1757 static int httpGenericError(const HTTPProtocol::HTTPRequest &request, QString *errorString) 1758 { 1759 Q_ASSERT(errorString); 1760 1761 int errorCode = 0; 1762 errorString->clear(); 1763 1764 if (request.responseCode == 204) { 1765 errorCode = ERR_NO_CONTENT; 1766 } 1767 1768 return errorCode; 1769 } 1770 1771 // HTTP DELETE specific errors 1772 static int httpDelError(const HTTPProtocol::HTTPRequest &request, QString *errorString) 1773 { 1774 Q_ASSERT(errorString); 1775 1776 int errorCode = 0; 1777 const int responseCode = request.responseCode; 1778 errorString->clear(); 1779 1780 switch (responseCode) { 1781 case 204: 1782 errorCode = ERR_NO_CONTENT; 1783 break; 1784 default: 1785 break; 1786 } 1787 1788 if (!errorCode && (responseCode < 200 || responseCode > 400) && responseCode != 404) { 1789 errorCode = ERR_WORKER_DEFINED; 1790 *errorString = i18n("The resource cannot be deleted."); 1791 } 1792 1793 return errorCode; 1794 } 1795 1796 // HTTP PUT specific errors 1797 static int httpPutError(const HTTPProtocol::HTTPRequest &request, QString *errorString) 1798 { 1799 Q_ASSERT(errorString); 1800 1801 int errorCode = 0; 1802 const int responseCode = request.responseCode; 1803 const QString action(i18nc("request type", "upload %1", request.url.toDisplayString())); 1804 1805 switch (responseCode) { 1806 case 403: 1807 case 405: 1808 case 500: // hack: Apache mod_dav returns this instead of 403 (!) 1809 // 403 Forbidden 1810 // 405 Method Not Allowed 1811 // ERR_ACCESS_DENIED 1812 *errorString = i18nc("%1: request type", "Access was denied while attempting to %1.", action); 1813 errorCode = ERR_WORKER_DEFINED; 1814 break; 1815 case 409: 1816 // 409 Conflict 1817 // ERR_ACCESS_DENIED 1818 *errorString = i18n( 1819 "A resource cannot be created at the destination " 1820 "until one or more intermediate collections (folders) " 1821 "have been created."); 1822 errorCode = ERR_WORKER_DEFINED; 1823 break; 1824 case 423: 1825 // 423 Locked 1826 // ERR_ACCESS_DENIED 1827 *errorString = i18nc("%1: request type", "Unable to %1 because the resource is locked.", action); 1828 errorCode = ERR_WORKER_DEFINED; 1829 break; 1830 case 502: 1831 // 502 Bad Gateway 1832 // ERR_WRITE_ACCESS_DENIED; 1833 *errorString = i18nc("%1: request type", 1834 "Unable to %1 because the destination server refuses " 1835 "to accept the file or folder.", 1836 action); 1837 errorCode = ERR_WORKER_DEFINED; 1838 break; 1839 case 507: 1840 // 507 Insufficient Storage 1841 // ERR_DISK_FULL 1842 *errorString = i18n( 1843 "The destination resource does not have sufficient space " 1844 "to record the state of the resource after the execution " 1845 "of this method."); 1846 errorCode = ERR_WORKER_DEFINED; 1847 break; 1848 default: 1849 break; 1850 } 1851 1852 if (!errorCode && (responseCode < 200 || responseCode > 400) && responseCode != 404) { 1853 errorCode = ERR_WORKER_DEFINED; 1854 *errorString = i18nc("%1: response code, %2: request type", "An unexpected error (%1) occurred while attempting to %2.", responseCode, action); 1855 } 1856 1857 return errorCode; 1858 } 1859 1860 KIO::WorkerResult HTTPProtocol::sendHttpError() 1861 { 1862 QString errorString; 1863 int errorCode = 0; 1864 1865 switch (m_request.method) { 1866 case HTTP_GET: 1867 case HTTP_POST: 1868 errorCode = httpGenericError(m_request, &errorString); 1869 break; 1870 case HTTP_PUT: 1871 errorCode = httpPutError(m_request, &errorString); 1872 break; 1873 case HTTP_DELETE: 1874 errorCode = httpDelError(m_request, &errorString); 1875 break; 1876 default: 1877 break; 1878 } 1879 1880 // Force any message previously shown by the client to be cleared. 1881 infoMessage(QLatin1String("")); 1882 1883 if (errorCode) { 1884 return error(errorCode, errorString); 1885 } 1886 1887 return KIO::WorkerResult::pass(); 1888 } 1889 1890 bool HTTPProtocol::sendErrorPageNotification() 1891 { 1892 if (!m_request.preferErrorPage) { 1893 return false; 1894 } 1895 1896 if (m_isLoadingErrorPage) { 1897 qCWarning(KIO_HTTP) << "called twice during one request, something is probably wrong."; 1898 } 1899 1900 m_isLoadingErrorPage = true; 1901 WorkerBase::errorPage(); 1902 return true; 1903 } 1904 1905 bool HTTPProtocol::isOffline() 1906 { 1907 #if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) 1908 if (QNetworkInformation::load(QNetworkInformation::Feature::Reachability)) { 1909 return QNetworkInformation::instance()->reachability() != QNetworkInformation::Reachability::Online; 1910 } else { 1911 qCWarning(KIO_HTTP) << "Couldn't find a working backend for QNetworkInformation"; 1912 return false; 1913 } 1914 #else 1915 if (!m_networkConfig) { 1916 // Silence deprecation warnings as there is no Qt 5 substitute for QNetworkConfigurationManager 1917 QT_WARNING_PUSH 1918 QT_WARNING_DISABLE_CLANG("-Wdeprecated-declarations") 1919 QT_WARNING_DISABLE_GCC("-Wdeprecated-declarations") 1920 m_networkConfig = new QNetworkConfigurationManager(this); 1921 QT_WARNING_POP 1922 } 1923 return !m_networkConfig->isOnline(); 1924 #endif 1925 } 1926 1927 KIO::WorkerResult HTTPProtocol::multiGet(const QByteArray &data) 1928 { 1929 QDataStream stream(data); 1930 quint32 n; 1931 stream >> n; 1932 1933 qCDebug(KIO_HTTP) << n; 1934 1935 HTTPRequest saveRequest; 1936 if (m_isBusy) { 1937 saveRequest = m_request; 1938 } 1939 1940 resetSessionSettings(); 1941 1942 for (unsigned i = 0; i < n; ++i) { 1943 QUrl url; 1944 MetaData incomingMetadata; 1945 stream >> url >> incomingMetadata; 1946 setIncomingMetaData(incomingMetadata); 1947 1948 if (const auto result = maybeSetRequestUrl(url); !result.success()) { 1949 return result; 1950 } 1951 1952 // ### should maybe call resetSessionSettings() if the server/domain is 1953 // different from the last request! 1954 1955 qCDebug(KIO_HTTP) << url; 1956 1957 m_request.method = HTTP_GET; 1958 m_request.isKeepAlive = true; // readResponseHeader clears it if necessary 1959 1960 QString tmp = metaData(QStringLiteral("cache")); 1961 if (!tmp.isEmpty()) { 1962 m_request.cacheTag.policy = parseCacheControl(tmp); 1963 } else { 1964 m_request.cacheTag.policy = DEFAULT_CACHE_CONTROL; 1965 } 1966 1967 m_requestQueue.append(m_request); 1968 } 1969 1970 if (m_isBusy) { 1971 m_request = saveRequest; 1972 } 1973 if (!m_isBusy) { 1974 m_isBusy = true; 1975 QMutableListIterator<HTTPRequest> it(m_requestQueue); 1976 // send the requests 1977 while (it.hasNext()) { 1978 m_request = it.next(); 1979 (void)/* handling result via result codes */ sendQuery(); 1980 // save the request state so we can pick it up again in the collection phase 1981 it.setValue(m_request); 1982 qCDebug(KIO_HTTP) << "check one: isKeepAlive =" << m_request.isKeepAlive; 1983 if (m_request.cacheTag.ioMode != ReadFromCache) { 1984 m_server.initFrom(m_request); 1985 } 1986 } 1987 // collect the responses 1988 // ### for the moment we use a hack: instead of saving and restoring request-id 1989 // we just count up like ParallelGetJobs does. 1990 int requestId = 0; 1991 for (const HTTPRequest &r : std::as_const(m_requestQueue)) { 1992 m_request = r; 1993 qCDebug(KIO_HTTP) << "check two: isKeepAlive =" << m_request.isKeepAlive; 1994 setMetaData(QStringLiteral("request-id"), QString::number(requestId++)); 1995 sendAndKeepMetaData(); 1996 if (const auto result = readResponseHeader(); !result.success()) { 1997 return result; 1998 } 1999 if (const auto result = readBody(); !result.success()) { 2000 return result; 2001 } 2002 // the "next job" signal for ParallelGetJob is data of size zero which 2003 // readBody() sends without our intervention. 2004 qCDebug(KIO_HTTP) << "check three: isKeepAlive =" << m_request.isKeepAlive; 2005 httpClose(m_request.isKeepAlive); // actually keep-alive is mandatory for pipelining 2006 } 2007 2008 m_requestQueue.clear(); 2009 m_isBusy = false; 2010 } 2011 return WorkerResult::pass(); 2012 } 2013 2014 ssize_t HTTPProtocol::write(const void *_buf, size_t nbytes) 2015 { 2016 size_t sent = 0; 2017 const char *buf = static_cast<const char *>(_buf); 2018 while (sent < nbytes) { 2019 int n = TCPWorkerBase::write(buf + sent, nbytes - sent); 2020 2021 if (n < 0) { 2022 // some error occurred 2023 return -1; 2024 } 2025 2026 sent += n; 2027 } 2028 2029 return sent; 2030 } 2031 2032 void HTTPProtocol::clearUnreadBuffer() 2033 { 2034 m_unreadBuf.clear(); 2035 } 2036 2037 // Note: the implementation of unread/readBuffered assumes that unread will only 2038 // be used when there is extra data we don't want to handle, and not to wait for more data. 2039 void HTTPProtocol::unread(char *buf, size_t size) 2040 { 2041 // implement LIFO (stack) semantics 2042 const int newSize = m_unreadBuf.size() + size; 2043 m_unreadBuf.resize(newSize); 2044 for (size_t i = 0; i < size; i++) { 2045 m_unreadBuf.data()[newSize - i - 1] = buf[i]; 2046 } 2047 if (size) { 2048 // hey, we still have data, closed connection or not! 2049 m_isEOF = false; 2050 } 2051 } 2052 2053 size_t HTTPProtocol::readBuffered(char *buf, size_t size, bool unlimited) 2054 { 2055 size_t bytesRead = 0; 2056 if (!m_unreadBuf.isEmpty()) { 2057 const int bufSize = m_unreadBuf.size(); 2058 bytesRead = qMin((int)size, bufSize); 2059 2060 for (size_t i = 0; i < bytesRead; i++) { 2061 buf[i] = m_unreadBuf.constData()[bufSize - i - 1]; 2062 } 2063 m_unreadBuf.chop(bytesRead); 2064 2065 // If we have an unread buffer and the size of the content returned by the 2066 // server is unknown, e.g. chunked transfer, return the bytes read here since 2067 // we may already have enough data to complete the response and don't want to 2068 // wait for more. See BR# 180631. 2069 if (unlimited) { 2070 return bytesRead; 2071 } 2072 } 2073 if (bytesRead < size) { 2074 int rawRead = TCPWorkerBase::read(buf + bytesRead, size - bytesRead); 2075 if (rawRead < 1) { 2076 m_isEOF = true; 2077 return bytesRead; 2078 } 2079 bytesRead += rawRead; 2080 } 2081 return bytesRead; 2082 } 2083 2084 // ### this method will detect an n*(\r\n) sequence if it crosses invocations. 2085 // it will look (n*2 - 1) bytes before start at most and never before buf, naturally. 2086 // supported number of newlines are one and two, in line with HTTP syntax. 2087 // return true if numNewlines newlines were found. 2088 bool HTTPProtocol::readDelimitedText(char *buf, int *idx, int end, int numNewlines) 2089 { 2090 Q_ASSERT(numNewlines >= 1 && numNewlines <= 2); 2091 char mybuf[64]; // somewhere close to the usual line length to avoid unread()ing too much 2092 int pos = *idx; 2093 while (pos < end && !m_isEOF) { 2094 int step = qMin((int)sizeof(mybuf), end - pos); 2095 if (m_isChunked) { 2096 // we might be reading the end of the very last chunk after which there is no data. 2097 // don't try to read any more bytes than there are because it causes stalls 2098 //(yes, it shouldn't stall but it does) 2099 step = 1; 2100 } 2101 size_t bufferFill = readBuffered(mybuf, step); 2102 2103 for (size_t i = 0; i < bufferFill; ++i, ++pos) { 2104 // we copy the data from mybuf to buf immediately and look for the newlines in buf. 2105 // that way we don't miss newlines split over several invocations of this method. 2106 buf[pos] = mybuf[i]; 2107 2108 // did we just copy one or two times the (usually) \r\n delimiter? 2109 // until we find even more broken webservers in the wild let's assume that they either 2110 // send \r\n (RFC compliant) or \n (broken) as delimiter... 2111 if (buf[pos] == '\n') { 2112 bool found = numNewlines == 1; 2113 if (!found) { // looking for two newlines 2114 // Detect \n\n and \n\r\n. The other cases (\r\n\n, \r\n\r\n) are covered by the first two. 2115 found = ((pos >= 1 && buf[pos - 1] == '\n') || (pos >= 2 && buf[pos - 2] == '\n' && buf[pos - 1] == '\r')); 2116 } 2117 if (found) { 2118 i++; // unread bytes *after* CRLF 2119 unread(&mybuf[i], bufferFill - i); 2120 *idx = pos + 1; 2121 return true; 2122 } 2123 } 2124 } 2125 } 2126 *idx = pos; 2127 return false; 2128 } 2129 2130 static bool isCompatibleNextUrl(const QUrl &previous, const QUrl &now) 2131 { 2132 if (previous.host() != now.host() || previous.port() != now.port()) { 2133 return false; 2134 } 2135 if (previous.userName().isEmpty() && previous.password().isEmpty()) { 2136 return true; 2137 } 2138 return previous.userName() == now.userName() && previous.password() == now.password(); 2139 } 2140 2141 bool HTTPProtocol::httpShouldCloseConnection() 2142 { 2143 qCDebug(KIO_HTTP); 2144 2145 if (!isConnected()) { 2146 return false; 2147 } 2148 2149 if (!m_request.proxyUrls.isEmpty() && !isAutoSsl()) { 2150 for (const QString &url : std::as_const(m_request.proxyUrls)) { 2151 if (url != QLatin1String("DIRECT")) { 2152 if (isCompatibleNextUrl(m_server.proxyUrl, QUrl(url))) { 2153 return false; 2154 } 2155 } 2156 } 2157 return true; 2158 } 2159 2160 return !isCompatibleNextUrl(m_server.url, m_request.url); 2161 } 2162 2163 KIO::WorkerResult HTTPProtocol::httpOpenConnection() 2164 { 2165 qCDebug(KIO_HTTP); 2166 m_server.clear(); 2167 2168 // Only save proxy auth information after proxy authentication has 2169 // actually taken place, which will set up exactly this connection. 2170 disconnect(tcpSocket(), &QAbstractSocket::connected, this, &HTTPProtocol::saveProxyAuthenticationForSocket); 2171 2172 clearUnreadBuffer(); 2173 2174 int connectError = 0; 2175 QString errorString; 2176 2177 // Get proxy information... 2178 if (m_request.proxyUrls.isEmpty()) { 2179 m_request.proxyUrls = mapConfig().value(QStringLiteral("ProxyUrls"), QString()).toString().split(QLatin1Char(','), Qt::SkipEmptyParts); 2180 2181 qCDebug(KIO_HTTP) << "Proxy URLs:" << m_request.proxyUrls; 2182 } 2183 2184 if (m_request.proxyUrls.isEmpty()) { 2185 QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); 2186 connectError = connectToHost(m_request.url.host(), m_request.url.port(defaultPort()), &errorString); 2187 } else { 2188 QList<QUrl> badProxyUrls; 2189 for (const QString &proxyUrl : std::as_const(m_request.proxyUrls)) { 2190 if (proxyUrl == QLatin1String("DIRECT")) { 2191 QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); 2192 connectError = connectToHost(m_request.url.host(), m_request.url.port(defaultPort()), &errorString); 2193 if (connectError == 0) { 2194 // qDebug() << "Connected DIRECT: host=" << m_request.url.host() << "port=" << m_request.url.port(defaultPort()); 2195 break; 2196 } else { 2197 continue; 2198 } 2199 } 2200 2201 const QUrl url(proxyUrl); 2202 const QString proxyScheme(url.scheme()); 2203 if (!supportedProxyScheme(proxyScheme)) { 2204 connectError = ERR_CANNOT_CONNECT; 2205 errorString = url.toDisplayString(); 2206 badProxyUrls << url; 2207 continue; 2208 } 2209 2210 QNetworkProxy::ProxyType proxyType = QNetworkProxy::NoProxy; 2211 if (proxyScheme == QLatin1String("socks")) { 2212 proxyType = QNetworkProxy::Socks5Proxy; 2213 } else if (isAutoSsl()) { 2214 proxyType = QNetworkProxy::HttpProxy; 2215 } 2216 2217 qCDebug(KIO_HTTP) << "Connecting to proxy: address=" << proxyUrl << "type=" << proxyType; 2218 2219 if (proxyType == QNetworkProxy::NoProxy) { 2220 QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); 2221 connectError = connectToHost(url.host(), url.port(), &errorString); 2222 if (connectError == 0) { 2223 m_request.proxyUrl = url; 2224 // qDebug() << "Connected to proxy: host=" << url.host() << "port=" << url.port(); 2225 break; 2226 } else { 2227 if (connectError == ERR_UNKNOWN_HOST) { 2228 connectError = ERR_UNKNOWN_PROXY_HOST; 2229 } 2230 // qDebug() << "Failed to connect to proxy:" << proxyUrl; 2231 badProxyUrls << url; 2232 } 2233 } else { 2234 QNetworkProxy proxy(proxyType, url.host(), url.port(), url.userName(), url.password()); 2235 QNetworkProxy::setApplicationProxy(proxy); 2236 connectError = connectToHost(m_request.url.host(), m_request.url.port(defaultPort()), &errorString); 2237 if (connectError == 0) { 2238 qCDebug(KIO_HTTP) << "Tunneling thru proxy: host=" << url.host() << "port=" << url.port(); 2239 break; 2240 } else { 2241 if (connectError == ERR_UNKNOWN_HOST) { 2242 connectError = ERR_UNKNOWN_PROXY_HOST; 2243 } 2244 qCDebug(KIO_HTTP) << "Failed to connect to proxy:" << proxyUrl; 2245 badProxyUrls << url; 2246 QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); 2247 } 2248 } 2249 } 2250 2251 if (!badProxyUrls.isEmpty()) { 2252 // TODO: Notify the client of BAD proxy addresses (needed for PAC setups). 2253 } 2254 } 2255 2256 if (connectError != 0) { 2257 return error(connectError, errorString); 2258 } 2259 2260 // Disable Nagle's algorithm, i.e turn on TCP_NODELAY. 2261 qCDebug(KIO_HTTP) << "TCP_NODELAY:" << tcpSocket()->socketOption(QAbstractSocket::LowDelayOption); 2262 tcpSocket()->setSocketOption(QAbstractSocket::LowDelayOption, 1); 2263 2264 m_server.initFrom(m_request); 2265 return WorkerResult::pass(); 2266 } 2267 2268 bool HTTPProtocol::satisfyRequestFromCache(bool *cacheHasPage, WorkerResult &result) 2269 { 2270 qCDebug(KIO_HTTP); 2271 2272 result = WorkerResult::pass(); 2273 if (m_request.cacheTag.useCache) { 2274 const bool offline = isOffline(); 2275 2276 if (offline && m_request.cacheTag.policy != KIO::CC_Reload) { 2277 m_request.cacheTag.policy = KIO::CC_CacheOnly; 2278 } 2279 2280 const bool isCacheOnly = m_request.cacheTag.policy == KIO::CC_CacheOnly; 2281 const CacheTag::CachePlan plan = m_request.cacheTag.plan(m_maxCacheAge); 2282 2283 bool openForReading = false; 2284 if (plan == CacheTag::UseCached || plan == CacheTag::ValidateCached) { 2285 openForReading = cacheFileOpenRead(); 2286 2287 if (!openForReading && (isCacheOnly || offline)) { 2288 // cache-only or offline -> we give a definite answer and it is "no" 2289 *cacheHasPage = false; 2290 if (isCacheOnly) { 2291 result = WorkerResult::fail(ERR_DOES_NOT_EXIST, m_request.url.toDisplayString()); 2292 } else if (offline) { 2293 result = WorkerResult::fail(ERR_CANNOT_CONNECT, m_request.url.toDisplayString()); 2294 } 2295 return true; 2296 } 2297 } 2298 2299 if (openForReading) { 2300 m_request.cacheTag.ioMode = ReadFromCache; 2301 *cacheHasPage = true; 2302 // return false if validation is required, so a network request will be sent 2303 return m_request.cacheTag.plan(m_maxCacheAge) == CacheTag::UseCached; 2304 } 2305 } 2306 *cacheHasPage = false; 2307 return false; 2308 } 2309 2310 QString HTTPProtocol::formatRequestUri() const 2311 { 2312 // Only specify protocol, host and port when they are not already clear, i.e. when 2313 // we handle HTTP proxying ourself and the proxy server needs to know them. 2314 // Sending protocol/host/port in other cases confuses some servers, and it's not their fault. 2315 if (isHttpProxy(m_request.proxyUrl) && !isAutoSsl()) { 2316 QUrl u; 2317 2318 QString protocol = m_request.url.scheme(); 2319 if (protocol.startsWith(QLatin1String("webdav"))) { 2320 protocol.replace(0, qstrlen("webdav"), QStringLiteral("http")); 2321 } 2322 u.setScheme(protocol); 2323 2324 u.setHost(m_request.url.host()); 2325 // if the URL contained the default port it should have been stripped earlier 2326 Q_ASSERT(m_request.url.port() != defaultPort()); 2327 u.setPort(m_request.url.port()); 2328 u.setPath(m_request.url.path(QUrl::FullyEncoded), QUrl::TolerantMode); 2329 u.setQuery(m_request.url.query(QUrl::FullyEncoded)); 2330 return u.toString(QUrl::FullyEncoded); 2331 } else { 2332 QString result = m_request.url.path(QUrl::FullyEncoded); 2333 if (m_request.url.hasQuery()) { 2334 result += QLatin1Char('?') + m_request.url.query(QUrl::FullyEncoded); 2335 } 2336 return result; 2337 } 2338 } 2339 2340 /** 2341 * This function is responsible for opening up the connection to the remote 2342 * HTTP server and sending the header. If this requires special 2343 * authentication or other such fun stuff, then it will handle it. This 2344 * function will NOT receive anything from the server, however. This is in 2345 * contrast to previous incarnations of 'httpOpen' as this method used to be 2346 * called. 2347 * 2348 * The basic process now is this: 2349 * 2350 * 1) Open up the socket and port 2351 * 2) Format our request/header 2352 * 3) Send the header to the remote server 2353 * 4) Call sendBody() if the HTTP method requires sending body data 2354 */ 2355 KIO::WorkerResult HTTPProtocol::sendQuery() 2356 { 2357 qCDebug(KIO_HTTP); 2358 2359 // Cannot have an https request without autoSsl! This can 2360 // only happen if the current installation does not support SSL... 2361 if (isEncryptedHttpVariety(m_protocol) && !isAutoSsl()) { 2362 return error(ERR_UNSUPPORTED_PROTOCOL, toQString(m_protocol)); 2363 } 2364 2365 // Check the reusability of the current connection. 2366 if (httpShouldCloseConnection()) { 2367 httpCloseConnection(); 2368 } 2369 2370 // Create a new connection to the remote machine if we do 2371 // not already have one... 2372 // NB: the !m_socketProxyAuth condition is a workaround for a proxied Qt socket sometimes 2373 // looking disconnected after receiving the initial 407 response. 2374 // I guess the Qt socket fails to hide the effect of proxy-connection: close after receiving 2375 // the 407 header. 2376 if ((!isConnected() && !m_socketProxyAuth)) { 2377 if (const auto result = httpOpenConnection(); !result.success()) { 2378 qCDebug(KIO_HTTP) << "Couldn't connect, oopsie!"; 2379 return result; 2380 } 2381 } 2382 2383 m_request.cacheTag.ioMode = NoCache; 2384 m_request.cacheTag.servedDate = QDateTime(); 2385 m_request.cacheTag.lastModifiedDate = QDateTime(); 2386 m_request.cacheTag.expireDate = QDateTime(); 2387 QString header; 2388 bool hasBodyData = false; 2389 bool hasDavData = false; 2390 2391 { 2392 m_request.sentMethodString = m_request.methodString(); 2393 header = toQString(m_request.sentMethodString) + QLatin1Char(' '); 2394 2395 QString davHeader; 2396 2397 // Fill in some values depending on the HTTP method to guide further processing 2398 switch (m_request.method) { 2399 case HTTP_GET: { 2400 bool cacheHasPage = false; 2401 WorkerResult result = WorkerResult::pass(); 2402 if (satisfyRequestFromCache(&cacheHasPage, result)) { 2403 qCDebug(KIO_HTTP) << "cacheHasPage =" << cacheHasPage; 2404 return result; 2405 } 2406 if (!cacheHasPage) { 2407 // start a new cache file later if appropriate 2408 m_request.cacheTag.ioMode = WriteToCache; 2409 } 2410 break; 2411 } 2412 case HTTP_HEAD: 2413 break; 2414 case HTTP_PUT: 2415 case HTTP_POST: 2416 hasBodyData = true; 2417 break; 2418 case HTTP_DELETE: 2419 case HTTP_OPTIONS: 2420 break; 2421 case DAV_PROPFIND: 2422 case DAV_REPORT: 2423 hasDavData = true; 2424 davHeader = QStringLiteral("Depth: "); 2425 if (hasMetaData(QStringLiteral("davDepth"))) { 2426 qCDebug(KIO_HTTP) << "Reading DAV depth from metadata:" << metaData(QStringLiteral("davDepth")); 2427 davHeader += metaData(QStringLiteral("davDepth")); 2428 } else { 2429 if (m_request.davData.depth == 2) { 2430 davHeader += QLatin1String("infinity"); 2431 } else { 2432 davHeader += QString::number(m_request.davData.depth); 2433 } 2434 } 2435 davHeader += QLatin1String("\r\n"); 2436 break; 2437 case DAV_PROPPATCH: 2438 hasDavData = true; 2439 break; 2440 case DAV_MKCOL: 2441 break; 2442 case DAV_COPY: 2443 case DAV_MOVE: 2444 davHeader = QLatin1String("Destination: ") + m_request.davData.desturl + 2445 // infinity depth means copy recursively 2446 // (optional for copy -> but is the desired action) 2447 QLatin1String("\r\nDepth: infinity\r\nOverwrite: ") + QLatin1Char(m_request.davData.overwrite ? 'T' : 'F') + QLatin1String("\r\n"); 2448 break; 2449 case DAV_LOCK: 2450 davHeader = QStringLiteral("Timeout: "); 2451 { 2452 uint timeout = 0; 2453 if (hasMetaData(QStringLiteral("davTimeout"))) { 2454 timeout = metaData(QStringLiteral("davTimeout")).toUInt(); 2455 } 2456 if (timeout == 0) { 2457 davHeader += QLatin1String("Infinite"); 2458 } else { 2459 davHeader += QLatin1String("Seconds-") + QString::number(timeout); 2460 } 2461 } 2462 davHeader += QLatin1String("\r\n"); 2463 hasDavData = true; 2464 break; 2465 case DAV_UNLOCK: 2466 davHeader = QLatin1String("Lock-token: ") + metaData(QStringLiteral("davLockToken")) + QLatin1String("\r\n"); 2467 break; 2468 case DAV_SEARCH: 2469 hasDavData = true; 2470 /* fall through */ 2471 case DAV_SUBSCRIBE: 2472 case DAV_UNSUBSCRIBE: 2473 case DAV_POLL: 2474 break; 2475 default: 2476 return error(ERR_UNSUPPORTED_ACTION, QString()); 2477 } 2478 // DAV_POLL; DAV_NOTIFY 2479 2480 header += formatRequestUri() + QLatin1String(" HTTP/1.1\r\n"); /* start header */ 2481 2482 /* support for virtual hosts and required by HTTP 1.1 */ 2483 header += QLatin1String("Host: ") + m_request.encoded_hostname; 2484 if (m_request.url.port(defaultPort()) != defaultPort()) { 2485 header += QLatin1Char(':') + QString::number(m_request.url.port()); 2486 } 2487 header += QLatin1String("\r\n"); 2488 2489 // Support old HTTP/1.0 style keep-alive header for compatibility 2490 // purposes as well as performance improvements while giving end 2491 // users the ability to disable this feature for proxy servers that 2492 // don't support it, e.g. junkbuster proxy server. 2493 if (isHttpProxy(m_request.proxyUrl) && !isAutoSsl()) { 2494 header += QLatin1String("Proxy-Connection: "); 2495 } else { 2496 header += QLatin1String("Connection: "); 2497 } 2498 if (m_request.isKeepAlive) { 2499 header += QLatin1String("keep-alive\r\n"); 2500 } else { 2501 header += QLatin1String("close\r\n"); 2502 } 2503 2504 if (!m_request.userAgent.isEmpty()) { 2505 header += QLatin1String("User-Agent: ") + m_request.userAgent + QLatin1String("\r\n"); 2506 } 2507 2508 if (!m_request.referrer.isEmpty()) { 2509 // Don't try to correct spelling! 2510 header += QLatin1String("Referer: ") + m_request.referrer + QLatin1String("\r\n"); 2511 } 2512 2513 if (m_request.endoffset > m_request.offset) { 2514 header += 2515 QLatin1String("Range: bytes=") + KIO::number(m_request.offset) + QLatin1Char('-') + KIO::number(m_request.endoffset) + QLatin1String("\r\n"); 2516 qCDebug(KIO_HTTP) << "kio_http : Range =" << KIO::number(m_request.offset) << "-" << KIO::number(m_request.endoffset); 2517 } else if (m_request.offset > 0 && m_request.endoffset == 0) { 2518 header += QLatin1String("Range: bytes=") + KIO::number(m_request.offset) + QLatin1String("-\r\n"); 2519 qCDebug(KIO_HTTP) << "kio_http: Range =" << KIO::number(m_request.offset); 2520 } 2521 2522 if (!m_request.cacheTag.useCache || m_request.cacheTag.policy == CC_Reload) { 2523 /* No caching for reload */ 2524 header += QLatin1String("Pragma: no-cache\r\n"); /* for HTTP/1.0 caches */ 2525 header += QLatin1String("Cache-control: no-cache\r\n"); /* for HTTP >=1.1 caches */ 2526 } else if (m_request.cacheTag.plan(m_maxCacheAge) == CacheTag::ValidateCached) { 2527 qCDebug(KIO_HTTP) << "needs validation, performing conditional get."; 2528 /* conditional get */ 2529 if (!m_request.cacheTag.etag.isEmpty()) { 2530 header += QLatin1String("If-None-Match: ") + m_request.cacheTag.etag + QLatin1String("\r\n"); 2531 } 2532 2533 if (m_request.cacheTag.lastModifiedDate.isValid()) { 2534 const QString httpDate = formatHttpDate(m_request.cacheTag.lastModifiedDate); 2535 header += QLatin1String("If-Modified-Since: ") + httpDate + QLatin1String("\r\n"); 2536 setMetaData(QStringLiteral("modified"), httpDate); 2537 } 2538 } 2539 2540 header += QLatin1String("Accept: "); 2541 const QString acceptHeader = metaData(QStringLiteral("accept")); 2542 if (!acceptHeader.isEmpty()) { 2543 header += acceptHeader; 2544 } else { 2545 header += QLatin1String(DEFAULT_ACCEPT_HEADER); 2546 } 2547 header += QLatin1String("\r\n"); 2548 2549 if (m_request.allowTransferCompression) { 2550 header += QLatin1String("Accept-Encoding: gzip, deflate, x-gzip, x-deflate\r\n"); 2551 } 2552 2553 if (!m_request.charsets.isEmpty()) { 2554 header += QLatin1String("Accept-Charset: ") + m_request.charsets + QLatin1String("\r\n"); 2555 } 2556 2557 if (!m_request.languages.isEmpty()) { 2558 header += QLatin1String("Accept-Language: ") + m_request.languages + QLatin1String("\r\n"); 2559 } 2560 2561 QString cookieStr; 2562 const QString cookieMode = metaData(QStringLiteral("cookies")).toLower(); 2563 2564 if (cookieMode == QLatin1String("none")) { 2565 m_request.cookieMode = HTTPRequest::CookiesNone; 2566 } else if (cookieMode == QLatin1String("manual")) { 2567 m_request.cookieMode = HTTPRequest::CookiesManual; 2568 cookieStr = metaData(QStringLiteral("setcookies")); 2569 } else { 2570 m_request.cookieMode = HTTPRequest::CookiesAuto; 2571 if (m_request.useCookieJar) { 2572 cookieStr = findCookies(m_request.url.toString()); 2573 } 2574 } 2575 2576 if (!cookieStr.isEmpty()) { 2577 header += cookieStr + QLatin1String("\r\n"); 2578 } 2579 2580 const QString customHeader = metaData(QStringLiteral("customHTTPHeader")); 2581 if (!customHeader.isEmpty()) { 2582 header += sanitizeCustomHTTPHeader(customHeader) + QLatin1String("\r\n"); 2583 } 2584 2585 const QString contentType = metaData(QStringLiteral("content-type")); 2586 if (!contentType.isEmpty()) { 2587 if (!contentType.startsWith(QLatin1String("content-type"), Qt::CaseInsensitive)) { 2588 header += QLatin1String("Content-Type: "); 2589 } 2590 header += contentType + QLatin1String("\r\n"); 2591 } 2592 2593 // DoNotTrack feature... 2594 if (configValue(QStringLiteral("DoNotTrack"), false)) { 2595 header += QLatin1String("DNT: 1\r\n"); 2596 } 2597 2598 // Remember that at least one failed (with 401 or 407) request/response 2599 // roundtrip is necessary for the server to tell us that it requires 2600 // authentication. However, we proactively add authentication headers if when 2601 // we have cached credentials to avoid the extra roundtrip where possible. 2602 header += authenticationHeader(); 2603 2604 if (hasDavData || m_protocol == "webdav" || m_protocol == "webdavs") { 2605 header += davProcessLocks(); 2606 2607 // add extra webdav headers, if supplied 2608 davHeader += metaData(QStringLiteral("davHeader")); 2609 2610 // Set content type of webdav data 2611 if (hasDavData && !header.contains(QLatin1String("Content-Type: "))) { 2612 davHeader += QStringLiteral("Content-Type: text/xml; charset=utf-8\r\n"); 2613 } 2614 2615 // add extra header elements for WebDAV 2616 header += davHeader; 2617 } 2618 } 2619 2620 qCDebug(KIO_HTTP) << "============ Sending Header:"; 2621 const QStringList list = header.split(QStringLiteral("\r\n"), Qt::SkipEmptyParts); 2622 for (const QString &s : list) { 2623 qCDebug(KIO_HTTP) << s; 2624 } 2625 2626 // End the header iff there is no payload data. If we do have payload data 2627 // sendBody() will add another field to the header, Content-Length. 2628 if (!hasBodyData && !hasDavData) { 2629 header += QStringLiteral("\r\n"); 2630 } 2631 2632 // Now that we have our formatted header, let's send it! 2633 2634 // Clear out per-connection settings... 2635 resetConnectionSettings(); 2636 2637 // Send the data to the remote machine... 2638 const QByteArray headerBytes = header.toLatin1(); 2639 ssize_t written = write(headerBytes.constData(), headerBytes.length()); 2640 bool sendOk = (written == (ssize_t)headerBytes.length()); 2641 if (!sendOk) { 2642 qCDebug(KIO_HTTP) << "Connection broken! (" << m_request.url.host() << ")" 2643 << " -- intended to write" << headerBytes.length() << "bytes but wrote" << (int)written << "."; 2644 2645 // The server might have closed the connection due to a timeout, or maybe 2646 // some transport problem arose while the connection was idle. 2647 if (m_request.isKeepAlive) { 2648 httpCloseConnection(); 2649 return WorkerResult::pass(); // Try again 2650 } 2651 2652 qCDebug(KIO_HTTP) << "sendOk == false. Connection broken !" 2653 << " -- intended to write" << headerBytes.length() << "bytes but wrote" << (int)written << "."; 2654 return error(ERR_CONNECTION_BROKEN, m_request.url.host()); 2655 } 2656 qCDebug(KIO_HTTP) << "sent it!"; 2657 2658 const auto result = (hasBodyData || hasDavData) ? sendBody() : WorkerResult::pass(); 2659 infoMessage(i18n("%1 contacted. Waiting for reply...", m_request.url.host())); 2660 return result; 2661 } 2662 2663 void HTTPProtocol::forwardHttpResponseHeader(bool forwardImmediately) 2664 { 2665 // Send the response header if it was requested... 2666 if (!configValue(QStringLiteral("PropagateHttpHeader"), false)) { 2667 return; 2668 } 2669 2670 setMetaData(QStringLiteral("HTTP-Headers"), m_responseHeaders.join(QLatin1Char('\n'))); 2671 2672 if (forwardImmediately) { 2673 sendMetaData(); 2674 } 2675 } 2676 2677 bool HTTPProtocol::parseHeaderFromCache() 2678 { 2679 qCDebug(KIO_HTTP); 2680 if (!cacheFileReadTextHeader2()) { 2681 return false; 2682 } 2683 2684 const QLatin1String languageToken("content-language:"); 2685 const QLatin1String dispositionToken("content-disposition:"); 2686 for (const QString &str : std::as_const(m_responseHeaders)) { 2687 const QString header = str.trimmed(); 2688 if (header.startsWith(QLatin1String("content-type:"), Qt::CaseInsensitive)) { 2689 int pos = header.indexOf(QLatin1String("charset="), Qt::CaseInsensitive); 2690 if (pos != -1) { 2691 const QString charset = header.mid(pos + 8).toLower(); 2692 m_request.cacheTag.charset = charset; 2693 setMetaData(QStringLiteral("charset"), charset); 2694 } 2695 } else if (header.startsWith(languageToken, Qt::CaseInsensitive)) { 2696 const QString language = header.mid(languageToken.size()).trimmed().toLower(); 2697 setMetaData(languageToken, language); 2698 } else if (header.startsWith(dispositionToken, Qt::CaseInsensitive)) { 2699 parseContentDisposition(header.mid(dispositionToken.size()).toLower()); 2700 } 2701 } 2702 2703 if (m_request.cacheTag.lastModifiedDate.isValid()) { 2704 setMetaData(QStringLiteral("modified"), formatHttpDate(m_request.cacheTag.lastModifiedDate)); 2705 } 2706 2707 // this header comes from the cache, so the response must have been cacheable :) 2708 setCacheabilityMetadata(true); 2709 qCDebug(KIO_HTTP) << "Emitting mimeType" << m_mimeType; 2710 forwardHttpResponseHeader(false); 2711 mimeType(m_mimeType); 2712 // IMPORTANT: Do not remove the call below or the http response headers will 2713 // not be available to the application if this worker is put on hold. 2714 forwardHttpResponseHeader(); 2715 return true; 2716 } 2717 2718 void HTTPProtocol::fixupResponseMimetype() 2719 { 2720 if (m_mimeType.isEmpty()) { 2721 return; 2722 } 2723 2724 qCDebug(KIO_HTTP) << "before fixup" << m_mimeType; 2725 // Convert some common MIME types to standard MIME types 2726 if (m_mimeType == QLatin1String("application/x-targz")) { 2727 m_mimeType = QStringLiteral("application/x-compressed-tar"); 2728 } else if (m_mimeType == QLatin1String("image/x-png")) { 2729 m_mimeType = QStringLiteral("image/png"); 2730 } else if (m_mimeType == QLatin1String("audio/x-mp3") || m_mimeType == QLatin1String("audio/x-mpeg") || m_mimeType == QLatin1String("audio/mp3")) { 2731 m_mimeType = QStringLiteral("audio/mpeg"); 2732 } else if (m_mimeType == QLatin1String("audio/microsoft-wave")) { 2733 m_mimeType = QStringLiteral("audio/x-wav"); 2734 } else if (m_mimeType == QLatin1String("image/x-ms-bmp")) { 2735 m_mimeType = QStringLiteral("image/bmp"); 2736 } 2737 2738 // Crypto ones.... 2739 else if (m_mimeType == QLatin1String("application/pkix-cert") || m_mimeType == QLatin1String("application/binary-certificate")) { 2740 m_mimeType = QStringLiteral("application/x-x509-ca-cert"); 2741 } 2742 2743 // Prefer application/x-compressed-tar or x-gzpostscript over application/x-gzip. 2744 else if (m_mimeType == QLatin1String("application/x-gzip")) { 2745 if ((m_request.url.path().endsWith(QLatin1String(".tar.gz"))) || (m_request.url.path().endsWith(QLatin1String(".tar")))) { 2746 m_mimeType = QStringLiteral("application/x-compressed-tar"); 2747 } 2748 if ((m_request.url.path().endsWith(QLatin1String(".ps.gz")))) { 2749 m_mimeType = QStringLiteral("application/x-gzpostscript"); 2750 } 2751 } 2752 2753 // Prefer application/x-xz-compressed-tar over application/x-xz for LMZA compressed 2754 // tar files. Arch Linux AUR servers notoriously send the wrong MIME type for this. 2755 else if (m_mimeType == QLatin1String("application/x-xz")) { 2756 if (m_request.url.path().endsWith(QLatin1String(".tar.xz")) || m_request.url.path().endsWith(QLatin1String(".txz"))) { 2757 m_mimeType = QStringLiteral("application/x-xz-compressed-tar"); 2758 } 2759 } 2760 2761 // Some webservers say "text/plain" when they mean "application/x-bzip" 2762 else if ((m_mimeType == QLatin1String("text/plain")) || (m_mimeType == QLatin1String("application/octet-stream"))) { 2763 const QString ext = QFileInfo(m_request.url.path()).suffix().toUpper(); 2764 if (ext == QLatin1String("BZ2")) { 2765 m_mimeType = QStringLiteral("application/x-bzip"); 2766 } else if (ext == QLatin1String("PEM")) { 2767 m_mimeType = QStringLiteral("application/x-x509-ca-cert"); 2768 } else if (ext == QLatin1String("SWF")) { 2769 m_mimeType = QStringLiteral("application/x-shockwave-flash"); 2770 } else if (ext == QLatin1String("PLS")) { 2771 m_mimeType = QStringLiteral("audio/x-scpls"); 2772 } else if (ext == QLatin1String("WMV")) { 2773 m_mimeType = QStringLiteral("video/x-ms-wmv"); 2774 } else if (ext == QLatin1String("WEBM")) { 2775 m_mimeType = QStringLiteral("video/webm"); 2776 } else if (ext == QLatin1String("DEB")) { 2777 m_mimeType = QStringLiteral("application/x-deb"); 2778 } 2779 } 2780 qCDebug(KIO_HTTP) << "after fixup" << m_mimeType; 2781 } 2782 2783 void HTTPProtocol::fixupResponseContentEncoding() 2784 { 2785 // WABA: Correct for tgz files with a gzip-encoding. 2786 // They really shouldn't put gzip in the Content-Encoding field! 2787 // Web-servers really shouldn't do this: They let Content-Size refer 2788 // to the size of the tgz file, not to the size of the tar file, 2789 // while the Content-Type refers to "tar" instead of "tgz". 2790 if (!m_contentEncodings.isEmpty() && m_contentEncodings.last() == QLatin1String("gzip")) { 2791 if (m_mimeType == QLatin1String("application/x-tar")) { 2792 m_contentEncodings.removeLast(); 2793 m_mimeType = QStringLiteral("application/x-compressed-tar"); 2794 } else if (m_mimeType == QLatin1String("application/postscript")) { 2795 // LEONB: Adding another exception for psgz files. 2796 // Could we use the mimelnk files instead of hardcoding all this? 2797 m_contentEncodings.removeLast(); 2798 m_mimeType = QStringLiteral("application/x-gzpostscript"); 2799 } else if ((m_request.allowTransferCompression && m_mimeType == QLatin1String("text/html")) 2800 || (m_request.allowTransferCompression && m_mimeType != QLatin1String("application/x-compressed-tar") 2801 && m_mimeType != QLatin1String("application/x-tgz") && // deprecated name 2802 m_mimeType != QLatin1String("application/x-targz") && // deprecated name 2803 m_mimeType != QLatin1String("application/x-gzip"))) { 2804 // Unzip! 2805 } else { 2806 m_contentEncodings.removeLast(); 2807 m_mimeType = QStringLiteral("application/x-gzip"); 2808 } 2809 } 2810 2811 // We can't handle "bzip2" encoding (yet). So if we get something with 2812 // bzip2 encoding, we change the MIME type to "application/x-bzip". 2813 // Note for future changes: some web-servers send both "bzip2" as 2814 // encoding and "application/x-bzip[2]" as MIME type. That is wrong. 2815 // currently that doesn't bother us, because we remove the encoding 2816 // and set the MIME type to x-bzip anyway. 2817 if (!m_contentEncodings.isEmpty() && m_contentEncodings.last() == QLatin1String("bzip2")) { 2818 m_contentEncodings.removeLast(); 2819 m_mimeType = QStringLiteral("application/x-bzip"); 2820 } 2821 } 2822 2823 #ifdef Q_CC_MSVC 2824 // strncasecmp does not exist on windows, have to use _strnicmp 2825 static inline int strncasecmp(const char *c1, const char *c2, size_t max) 2826 { 2827 return _strnicmp(c1, c2, max); 2828 } 2829 #endif 2830 2831 // Return true if the term was found, false otherwise. Advance *pos. 2832 // If (*pos + strlen(term) >= end) just advance *pos to end and return false. 2833 // This means that users should always search for the shortest terms first. 2834 static bool consume(const char input[], int *pos, int end, const char *term) 2835 { 2836 // note: gcc/g++ is quite good at optimizing away redundant strlen()s 2837 int idx = *pos; 2838 if (idx + (int)strlen(term) >= end) { 2839 *pos = end; 2840 return false; 2841 } 2842 if (strncasecmp(&input[idx], term, strlen(term)) == 0) { 2843 *pos = idx + strlen(term); 2844 return true; 2845 } 2846 return false; 2847 } 2848 2849 /** 2850 * This function will read in the return header from the server. It will 2851 * not read in the body of the return message. It will also not transmit 2852 * the header to our client as the client doesn't need to know the gory 2853 * details of HTTP headers. 2854 */ 2855 KIO::WorkerResult HTTPProtocol::readResponseHeader() 2856 { 2857 resetResponseParsing(); 2858 if (m_request.cacheTag.ioMode == ReadFromCache && m_request.cacheTag.plan(m_maxCacheAge) == CacheTag::UseCached) { 2859 // parseHeaderFromCache replaces this method in case of cached content 2860 return parseHeaderFromCache() ? WorkerResult::pass() : WorkerResult::fail(); 2861 } 2862 2863 try_again: 2864 qCDebug(KIO_HTTP); 2865 2866 bool upgradeRequired = false; // Server demands that we upgrade to something 2867 // This is also true if we ask to upgrade and 2868 // the server accepts, since we are now 2869 // committed to doing so 2870 bool noHeadersFound = false; 2871 2872 m_request.cacheTag.charset.clear(); 2873 m_responseHeaders.clear(); 2874 2875 static const int maxHeaderSize = 128 * 1024; 2876 2877 char buffer[maxHeaderSize]; 2878 bool cont = false; 2879 bool bCanResume = false; 2880 2881 if (!isConnected()) { 2882 qCDebug(KIO_HTTP) << "No connection."; 2883 return WorkerResult::fail(ERR_CONNECTION_BROKEN); // Reestablish connection and try again 2884 } 2885 2886 int bufPos = 0; 2887 bool foundDelimiter = readDelimitedText(buffer, &bufPos, maxHeaderSize, 1); 2888 if (!foundDelimiter && bufPos < maxHeaderSize) { 2889 qCDebug(KIO_HTTP) << "EOF while waiting for header start."; 2890 if (m_request.isKeepAlive && m_iEOFRetryCount < 2) { 2891 m_iEOFRetryCount++; 2892 httpCloseConnection(); // Try to reestablish connection. 2893 return WorkerResult::fail(); // Reestablish connection and try again. 2894 } 2895 2896 if (m_request.method == HTTP_HEAD) { 2897 // HACK 2898 // Some web-servers fail to respond properly to a HEAD request. 2899 // We compensate for their failure to properly implement the HTTP standard 2900 // by assuming that they will be sending html. 2901 qCDebug(KIO_HTTP) << "HEAD -> returned MIME type:" << DEFAULT_MIME_TYPE; 2902 mimeType(QString::fromLatin1(DEFAULT_MIME_TYPE)); 2903 return WorkerResult::pass(); 2904 } 2905 2906 qCDebug(KIO_HTTP) << "Connection broken !"; 2907 return error(ERR_CONNECTION_BROKEN, m_request.url.host()); 2908 } 2909 if (!foundDelimiter) { 2910 // ### buffer too small for first line of header(!) 2911 Q_ASSERT(0); 2912 } 2913 2914 qCDebug(KIO_HTTP) << "============ Received Status Response:"; 2915 qCDebug(KIO_HTTP) << QByteArray(buffer, bufPos).trimmed(); 2916 2917 HTTP_REV httpRev = HTTP_None; 2918 int idx = 0; 2919 2920 if (idx != bufPos && buffer[idx] == '<') { 2921 qCDebug(KIO_HTTP) << "No valid HTTP header found! Document starts with XML/HTML tag"; 2922 // document starts with a tag, assume HTML instead of text/plain 2923 m_mimeType = QStringLiteral("text/html"); 2924 m_request.responseCode = 200; // Fake it 2925 httpRev = HTTP_Unknown; 2926 m_request.isKeepAlive = false; 2927 noHeadersFound = true; 2928 // put string back 2929 unread(buffer, bufPos); 2930 goto endParsing; 2931 } 2932 2933 // "HTTP/1.1" or similar 2934 if (consume(buffer, &idx, bufPos, "ICY ")) { 2935 httpRev = SHOUTCAST; 2936 m_request.isKeepAlive = false; 2937 } else if (consume(buffer, &idx, bufPos, "HTTP/")) { 2938 if (consume(buffer, &idx, bufPos, "1.0")) { 2939 httpRev = HTTP_10; 2940 m_request.isKeepAlive = false; 2941 } else if (consume(buffer, &idx, bufPos, "1.1")) { 2942 httpRev = HTTP_11; 2943 } 2944 } 2945 2946 if (httpRev == HTTP_None && bufPos != 0) { 2947 // Remote server does not seem to speak HTTP at all 2948 // Put the crap back into the buffer and hope for the best 2949 qCDebug(KIO_HTTP) << "DO NOT WANT." << bufPos; 2950 unread(buffer, bufPos); 2951 if (m_request.responseCode) { 2952 m_request.prevResponseCode = m_request.responseCode; 2953 } 2954 m_request.responseCode = 200; // Fake it 2955 httpRev = HTTP_Unknown; 2956 m_request.isKeepAlive = false; 2957 noHeadersFound = true; 2958 goto endParsing; 2959 } 2960 2961 // response code //### maybe wrong if we need several iterations for this response... 2962 // ### also, do multiple iterations (cf. try_again) to parse one header work w/ pipelining? 2963 if (m_request.responseCode) { 2964 m_request.prevResponseCode = m_request.responseCode; 2965 } 2966 skipSpace(buffer, &idx, bufPos); 2967 // TODO saner handling of invalid response code strings 2968 if (idx != bufPos) { 2969 m_request.responseCode = atoi(&buffer[idx]); 2970 } else { 2971 m_request.responseCode = 200; 2972 } 2973 // move idx to start of (yet to be fetched) next line, skipping the "OK" 2974 idx = bufPos; 2975 // (don't bother parsing the "OK", what do we do if it isn't there anyway?) 2976 2977 // immediately act on most response codes... 2978 2979 // Protect users against bogus username intended to fool them into visiting 2980 // sites they had no intention of visiting. 2981 if (isPotentialSpoofingAttack(m_request, config())) { 2982 qCDebug(KIO_HTTP) << "**** POTENTIAL ADDRESS SPOOFING:" << m_request.url; 2983 const int result = messageBox(WarningTwoActions, 2984 i18nc("@info Security check on url being accessed", 2985 "<p>You are about to log in to the site \"%1\" " 2986 "with the username \"%2\", but the website " 2987 "does not require authentication. " 2988 "This may be an attempt to trick you.</p>" 2989 "<p>Is \"%1\" the site you want to visit?</p>", 2990 m_request.url.host(), 2991 m_request.url.userName()), 2992 i18nc("@title:window", "Confirm Website Access")); 2993 if (result == WorkerBase::SecondaryAction) { 2994 return error(ERR_USER_CANCELED, m_request.url.toDisplayString()); 2995 } 2996 setMetaData(QStringLiteral("{internal~currenthost}LastSpoofedUserName"), m_request.url.userName()); 2997 } 2998 2999 if (m_request.responseCode != 200 && m_request.responseCode != 304) { 3000 m_request.cacheTag.ioMode = NoCache; 3001 3002 if (m_request.responseCode >= 500 && m_request.responseCode <= 599) { 3003 // Server side errors 3004 if (m_request.method == HTTP_HEAD) { 3005 ; // Ignore error 3006 } else { 3007 if (!sendErrorPageNotification()) { 3008 return WorkerResult::fail(ERR_INTERNAL_SERVER, m_request.url.toDisplayString()); 3009 } 3010 } 3011 } else if (m_request.responseCode == 416) { 3012 // Range not supported 3013 m_request.offset = 0; 3014 return WorkerResult::fail(); // Try again. 3015 } else if (m_request.responseCode == 426) { 3016 // Upgrade Required 3017 upgradeRequired = true; 3018 } else if (m_request.responseCode >= 400 && m_request.responseCode <= 499 && !isAuthenticationRequired(m_request.responseCode)) { 3019 // Any other client errors 3020 // Tell that we will only get an error page here. 3021 if (!sendErrorPageNotification()) { 3022 // Error handling is a tad awkward here. We still need to continue parsing after errors here, so 3023 // we cannot return, but also the control flow is mighty complicated. Instead we implicitly rely on 3024 // error() setting m_kioError and our caller excavating that value. It's all a bit unfortunate but 3025 // thankfully backed by a test so we'll know if this should ever break. 3026 if (m_request.responseCode == 403) { 3027 (void)error(ERR_ACCESS_DENIED, m_request.url.toDisplayString()); 3028 } else { 3029 (void)error(ERR_DOES_NOT_EXIST, m_request.url.toDisplayString()); 3030 } 3031 } 3032 } else if (m_request.responseCode >= 301 && m_request.responseCode <= 308) { 3033 // NOTE: According to RFC 2616 (section 10.3.[2-4,8]), 301 and 302 3034 // redirects for a POST operation should not be converted to a GET 3035 // request. That should only be done for a 303 response. However, 3036 // because almost all other client implementations do exactly that 3037 // in violation of the spec, many servers have simply adapted to 3038 // this way of doing things! Thus, we are forced to do the same 3039 // thing here. Otherwise, we loose compatibility and might not be 3040 // able to correctly retrieve sites that redirect. 3041 switch (m_request.responseCode) { 3042 case 301: // Moved Permanently 3043 setMetaData(QStringLiteral("permanent-redirect"), QStringLiteral("true")); 3044 // fall through 3045 case 302: // Found 3046 if (m_request.sentMethodString == "POST") { 3047 m_request.method = HTTP_GET; // FORCE a GET 3048 setMetaData(QStringLiteral("redirect-to-get"), QStringLiteral("true")); 3049 } 3050 break; 3051 case 303: // See Other 3052 if (m_request.method != HTTP_HEAD) { 3053 m_request.method = HTTP_GET; // FORCE a GET 3054 setMetaData(QStringLiteral("redirect-to-get"), QStringLiteral("true")); 3055 } 3056 break; 3057 case 308: // Permanent Redirect 3058 setMetaData(QStringLiteral("permanent-redirect"), QStringLiteral("true")); 3059 break; 3060 default: 3061 break; 3062 } 3063 } else if (m_request.responseCode == 204) { 3064 // No content 3065 3066 // error(ERR_NO_CONTENT, i18n("Data have been successfully sent.")); 3067 // Short circuit and do nothing! 3068 3069 // The original handling here was wrong, this is not an error: eg. in the 3070 // example of a 204 No Content response to a PUT completing. 3071 // return false; 3072 } else if (m_request.responseCode == 206) { 3073 if (m_request.offset) { 3074 bCanResume = true; 3075 } 3076 } else if (m_request.responseCode == 102) { 3077 // Processing (for WebDAV) 3078 /*** 3079 * This status code is given when the server expects the 3080 * command to take significant time to complete. So, inform 3081 * the user. 3082 */ 3083 infoMessage(i18n("Server processing request, please wait...")); 3084 cont = true; 3085 } else if (m_request.responseCode == 100) { 3086 // We got 'Continue' - ignore it 3087 cont = true; 3088 } 3089 } // (m_request.responseCode != 200 && m_request.responseCode != 304) 3090 3091 endParsing: 3092 bool authRequiresAnotherRoundtrip = false; 3093 3094 // Skip the whole header parsing if we got no HTTP headers at all 3095 if (!noHeadersFound) { 3096 // Auth handling 3097 const bool wasAuthError = isAuthenticationRequired(m_request.prevResponseCode); 3098 const bool isAuthError = isAuthenticationRequired(m_request.responseCode); 3099 const bool sameAuthError = (m_request.responseCode == m_request.prevResponseCode); 3100 qCDebug(KIO_HTTP) << "wasAuthError=" << wasAuthError << "isAuthError=" << isAuthError << "sameAuthError=" << sameAuthError; 3101 // Not the same authorization error as before and no generic error? 3102 // -> save the successful credentials. 3103 if (wasAuthError && (m_request.responseCode < 400 || (isAuthError && !sameAuthError))) { 3104 saveAuthenticationData(); 3105 } 3106 3107 // done with the first line; now tokenize the other lines 3108 3109 // TODO review use of STRTOLL vs. QByteArray::toInt() 3110 3111 foundDelimiter = readDelimitedText(buffer, &bufPos, maxHeaderSize, 2); 3112 qCDebug(KIO_HTTP) << " -- full response:\n" << QByteArray(buffer, bufPos).trimmed(); 3113 // Use this to see newlines: 3114 // qCDebug(KIO_HTTP) << " -- full response:" << endl << QByteArray(buffer, bufPos).replace("\r", "\\r").replace("\n", "\\n\n"); 3115 Q_ASSERT(foundDelimiter); 3116 3117 // NOTE because tokenizer will overwrite newlines in case of line continuations in the header 3118 // unread(buffer, bufSize) will not generally work anymore. we don't need it either. 3119 // either we have a http response line -> try to parse the header, fail if it doesn't work 3120 // or we have garbage -> fail. 3121 HeaderTokenizer tokenizer(buffer); 3122 tokenizer.tokenize(idx, sizeof(buffer)); 3123 3124 // Note that not receiving "accept-ranges" means that all bets are off 3125 // wrt the server supporting ranges. 3126 TokenIterator tIt = tokenizer.iterator("accept-ranges"); 3127 if (tIt.hasNext() && tIt.next().toLower().startsWith("none")) { // krazy:exclude=strings 3128 bCanResume = false; 3129 } 3130 3131 tIt = tokenizer.iterator("keep-alive"); 3132 while (tIt.hasNext()) { 3133 QByteArray ka = tIt.next().trimmed().toLower(); 3134 if (ka.startsWith("timeout=")) { // krazy:exclude=strings 3135 int ka_timeout = ka.mid(qstrlen("timeout=")).trimmed().toInt(); 3136 if (ka_timeout > 0) { 3137 m_request.keepAliveTimeout = ka_timeout; 3138 } 3139 if (httpRev == HTTP_10) { 3140 m_request.isKeepAlive = true; 3141 } 3142 3143 break; // we want to fetch ka timeout only 3144 } 3145 } 3146 3147 // get the size of our data 3148 tIt = tokenizer.iterator("content-length"); 3149 if (tIt.hasNext()) { 3150 m_iSize = STRTOLL(tIt.next().constData(), nullptr, 10); 3151 } 3152 3153 tIt = tokenizer.iterator("content-location"); 3154 if (tIt.hasNext()) { 3155 setMetaData(QStringLiteral("content-location"), toQString(tIt.next().trimmed())); 3156 } 3157 3158 // which type of data do we have? 3159 QString mediaValue; 3160 QString mediaAttribute; 3161 tIt = tokenizer.iterator("content-type"); 3162 if (tIt.hasNext()) { 3163 QList<QByteArray> l = tIt.next().split(';'); 3164 if (!l.isEmpty()) { 3165 // Assign the MIME type. 3166 m_mimeType = toQString(l.first().trimmed().toLower()); 3167 if (m_mimeType.startsWith(QLatin1Char('"'))) { 3168 m_mimeType.remove(0, 1); 3169 } 3170 if (m_mimeType.endsWith(QLatin1Char('"'))) { 3171 m_mimeType.chop(1); 3172 } 3173 qCDebug(KIO_HTTP) << "Content-type:" << m_mimeType; 3174 l.removeFirst(); 3175 } 3176 3177 // If we still have text, then it means we have a MIME type with a 3178 // parameter (eg: charset=iso-8851) ; so let's get that... 3179 for (const QByteArray &statement : std::as_const(l)) { 3180 const int index = statement.indexOf('='); 3181 if (index <= 0) { 3182 mediaAttribute = toQString(statement.mid(0, index)); 3183 } else { 3184 mediaAttribute = toQString(statement.mid(0, index)); 3185 mediaValue = toQString(statement.mid(index + 1)); 3186 } 3187 mediaAttribute = mediaAttribute.trimmed(); 3188 mediaValue = mediaValue.trimmed(); 3189 3190 bool quoted = false; 3191 if (mediaValue.startsWith(QLatin1Char('"'))) { 3192 quoted = true; 3193 mediaValue.remove(0, 1); 3194 } 3195 3196 if (mediaValue.endsWith(QLatin1Char('"'))) { 3197 mediaValue.chop(1); 3198 } 3199 3200 qCDebug(KIO_HTTP) << "Encoding-type:" << mediaAttribute << "=" << mediaValue; 3201 3202 if (mediaAttribute == QLatin1String("charset")) { 3203 mediaValue = mediaValue.toLower(); 3204 m_request.cacheTag.charset = mediaValue; 3205 setMetaData(QStringLiteral("charset"), mediaValue); 3206 } else { 3207 setMetaData(QLatin1String("media-") + mediaAttribute, mediaValue); 3208 if (quoted) { 3209 setMetaData(QLatin1String("media-") + mediaAttribute + QLatin1String("-kio-quoted"), QStringLiteral("true")); 3210 } 3211 } 3212 } 3213 } 3214 3215 // content? 3216 tIt = tokenizer.iterator("content-encoding"); 3217 while (tIt.hasNext()) { 3218 // This is so wrong !! No wonder kio_http is stripping the 3219 // gzip encoding from downloaded files. This solves multiple 3220 // bug reports and caitoo's problem with downloads when such a 3221 // header is encountered... 3222 3223 // A quote from RFC 2616: 3224 // " When present, its (Content-Encoding) value indicates what additional 3225 // content have been applied to the entity body, and thus what decoding 3226 // mechanism must be applied to obtain the media-type referenced by the 3227 // Content-Type header field. Content-Encoding is primarily used to allow 3228 // a document to be compressed without losing the identity of its underlying 3229 // media type. Simply put if it is specified, this is the actual MIME type 3230 // we should use when we pull the resource !!! 3231 addEncoding(toQString(tIt.next()), m_contentEncodings); 3232 } 3233 // Refer to RFC 2616 sec 15.5/19.5.1 and RFC 2183 3234 tIt = tokenizer.iterator("content-disposition"); 3235 if (tIt.hasNext()) { 3236 parseContentDisposition(toQString(tIt.next())); 3237 } 3238 tIt = tokenizer.iterator("content-language"); 3239 if (tIt.hasNext()) { 3240 QString language = toQString(tIt.next().trimmed()); 3241 if (!language.isEmpty()) { 3242 setMetaData(QStringLiteral("content-language"), language); 3243 } 3244 } 3245 3246 tIt = tokenizer.iterator("proxy-connection"); 3247 if (tIt.hasNext() && isHttpProxy(m_request.proxyUrl) && !isAutoSsl()) { 3248 QByteArray pc = tIt.next().toLower(); 3249 if (pc.startsWith("close")) { // krazy:exclude=strings 3250 m_request.isKeepAlive = false; 3251 } else if (pc.startsWith("keep-alive")) { // krazy:exclude=strings 3252 m_request.isKeepAlive = true; 3253 } 3254 } 3255 3256 tIt = tokenizer.iterator("link"); 3257 if (tIt.hasNext()) { 3258 // We only support Link: <url>; rel="type" so far 3259 QStringList link = toQString(tIt.next()).split(QLatin1Char(';'), Qt::SkipEmptyParts); 3260 if (link.count() == 2) { 3261 QString rel = link[1].trimmed(); 3262 const QLatin1String relToken("rel=\""); 3263 if (rel.startsWith(relToken)) { 3264 rel = rel.mid(relToken.size(), rel.length() - 6); 3265 if (rel.toLower() == QLatin1String("pageservices")) { 3266 // ### the remove() part looks fishy! 3267 QString url = link[0].remove(QRegularExpression(QStringLiteral("[<>]"))).trimmed(); 3268 setMetaData(QStringLiteral("PageServices"), url); 3269 } 3270 } 3271 } 3272 } 3273 3274 tIt = tokenizer.iterator("p3p"); 3275 if (tIt.hasNext()) { 3276 // P3P privacy policy information 3277 QStringList policyrefs; 3278 QStringList compact; 3279 while (tIt.hasNext()) { 3280 QStringList policy = toQString(tIt.next().simplified()).split(QLatin1Char('='), Qt::SkipEmptyParts); 3281 if (policy.count() == 2) { 3282 if (policy[0].toLower() == QLatin1String("policyref")) { 3283 policyrefs << policy[1].remove(QRegularExpression(QStringLiteral("[\")\']"))).trimmed(); 3284 } else if (policy[0].toLower() == QLatin1String("cp")) { 3285 // We convert to cp\ncp\ncp\n[...]\ncp to be consistent with 3286 // other metadata sent in strings. This could be a bit more 3287 // efficient but I'm going for correctness right now. 3288 const QString s = policy[1].remove(QRegularExpression(QStringLiteral("[\")\']"))); 3289 const QStringList cps = s.split(QLatin1Char(' '), Qt::SkipEmptyParts); 3290 compact << cps; 3291 } 3292 } 3293 } 3294 if (!policyrefs.isEmpty()) { 3295 setMetaData(QStringLiteral("PrivacyPolicy"), policyrefs.join(QLatin1Char('\n'))); 3296 } 3297 if (!compact.isEmpty()) { 3298 setMetaData(QStringLiteral("PrivacyCompactPolicy"), compact.join(QLatin1Char('\n'))); 3299 } 3300 } 3301 3302 // continue only if we know that we're at least HTTP/1.0 3303 if (httpRev == HTTP_11 || httpRev == HTTP_10) { 3304 // let them tell us if we should stay alive or not 3305 tIt = tokenizer.iterator("connection"); 3306 while (tIt.hasNext()) { 3307 QByteArray connection = tIt.next().toLower(); 3308 if (!(isHttpProxy(m_request.proxyUrl) && !isAutoSsl())) { 3309 if (connection.startsWith("close")) { // krazy:exclude=strings 3310 m_request.isKeepAlive = false; 3311 } else if (connection.startsWith("keep-alive")) { // krazy:exclude=strings 3312 m_request.isKeepAlive = true; 3313 } 3314 } 3315 if (connection.startsWith("upgrade")) { // krazy:exclude=strings 3316 if (m_request.responseCode == 101) { 3317 // Ok, an upgrade was accepted, now we must do it 3318 upgradeRequired = true; 3319 } else if (upgradeRequired) { // 426 3320 // Nothing to do since we did it above already 3321 } 3322 } 3323 } 3324 // what kind of encoding do we have? transfer? 3325 tIt = tokenizer.iterator("transfer-encoding"); 3326 while (tIt.hasNext()) { 3327 // If multiple encodings have been applied to an entity, the 3328 // transfer-codings MUST be listed in the order in which they 3329 // were applied. 3330 addEncoding(toQString(tIt.next().trimmed()), m_transferEncodings); 3331 } 3332 3333 // md5 signature 3334 tIt = tokenizer.iterator("content-md5"); 3335 if (tIt.hasNext()) { 3336 m_contentMD5 = toQString(tIt.next().trimmed()); 3337 } 3338 3339 // *** Responses to the HTTP OPTIONS method follow 3340 // WebDAV capabilities 3341 tIt = tokenizer.iterator("dav"); 3342 while (tIt.hasNext()) { 3343 m_davCapabilities << toQString(tIt.next()); 3344 } 3345 // *** Responses to the HTTP OPTIONS method finished 3346 } 3347 3348 // Now process the HTTP/1.1 upgrade 3349 QStringList upgradeOffers; 3350 tIt = tokenizer.iterator("upgrade"); 3351 if (tIt.hasNext()) { 3352 // Now we have to check to see what is offered for the upgrade 3353 QString offered = toQString(tIt.next()); 3354 upgradeOffers = offered.split(QRegularExpression(QStringLiteral("[ \n,\r\t]")), Qt::SkipEmptyParts); 3355 } 3356 for (const QString &opt : std::as_const(upgradeOffers)) { 3357 if (opt == QLatin1String("TLS/1.0")) { 3358 if (!startSsl() && upgradeRequired) { 3359 return WorkerResult::fail(ERR_UPGRADE_REQUIRED, opt); 3360 } 3361 } else if (opt == QLatin1String("HTTP/1.1")) { 3362 httpRev = HTTP_11; 3363 } else if (upgradeRequired) { 3364 // we are told to do an upgrade we don't understand 3365 return WorkerResult::fail(ERR_UPGRADE_REQUIRED, opt); 3366 } 3367 } 3368 3369 // Harvest cookies (mmm, cookie fields!) 3370 QByteArray cookieStr; // In case we get a cookie. 3371 tIt = tokenizer.iterator("set-cookie"); 3372 while (tIt.hasNext()) { 3373 cookieStr += "Set-Cookie: " + tIt.next() + '\n'; 3374 } 3375 if (!cookieStr.isEmpty()) { 3376 if ((m_request.cookieMode == HTTPRequest::CookiesAuto) && m_request.useCookieJar) { 3377 // Give cookies to the cookiejar. 3378 const QString domain = configValue(QStringLiteral("cross-domain")); 3379 if (!domain.isEmpty() && isCrossDomainRequest(m_request.url.host(), domain)) { 3380 cookieStr = "Cross-Domain\n" + cookieStr; 3381 } 3382 addCookies(m_request.url.toString(), cookieStr); 3383 } else if (m_request.cookieMode == HTTPRequest::CookiesManual) { 3384 // Pass cookie to application 3385 setMetaData(QStringLiteral("setcookies"), QString::fromUtf8(cookieStr)); // ## is encoding ok? 3386 } 3387 } 3388 3389 // We need to reread the header if we got a '100 Continue' or '102 Processing' 3390 // This may be a non keepalive connection so we handle this kind of loop internally 3391 if (cont) { 3392 qCDebug(KIO_HTTP) << "cont; returning to mark try_again"; 3393 goto try_again; 3394 } 3395 3396 if (!m_isChunked && (m_iSize == NO_SIZE) && m_request.isKeepAlive && canHaveResponseBody(m_request.responseCode, m_request.method)) { 3397 qCDebug(KIO_HTTP) << "Ignoring keep-alive: otherwise unable to determine response body length."; 3398 m_request.isKeepAlive = false; 3399 } 3400 3401 // TODO cache the proxy auth data (not doing this means a small performance regression for now) 3402 3403 // we may need to send (Proxy or WWW) authorization data 3404 if ((!m_request.doNotWWWAuthenticate && m_request.responseCode == 401) || (!m_request.doNotProxyAuthenticate && m_request.responseCode == 407)) { 3405 authRequiresAnotherRoundtrip = handleAuthenticationHeader(&tokenizer); 3406 if (m_kioError) { 3407 // If error is set, then handleAuthenticationHeader failed. 3408 return WorkerResult::fail(); 3409 } 3410 } else { 3411 authRequiresAnotherRoundtrip = false; 3412 } 3413 3414 QString locationStr; 3415 // In fact we should do redirection only if we have a redirection response code (300 range) 3416 tIt = tokenizer.iterator("location"); 3417 if (tIt.hasNext() && m_request.responseCode > 299 && m_request.responseCode < 400) { 3418 locationStr = QString::fromUtf8(tIt.next().trimmed()); 3419 } 3420 // We need to do a redirect 3421 if (!locationStr.isEmpty()) { 3422 QUrl u = m_request.url.resolved(QUrl(locationStr)); 3423 if (!u.isValid()) { 3424 return error(ERR_MALFORMED_URL, u.toDisplayString()); 3425 } 3426 3427 // preserve #ref: (bug 124654) 3428 // if we were at http://host/resource1#ref, we sent a GET for "/resource1" 3429 // if we got redirected to http://host/resource2, then we have to re-add 3430 // the fragment: 3431 // http to https redirection included 3432 if (m_request.url.hasFragment() && !u.hasFragment() && (m_request.url.host() == u.host()) 3433 && (m_request.url.scheme() == u.scheme() || (m_request.url.scheme() == QLatin1String("http") && u.scheme() == QLatin1String("https")))) { 3434 u.setFragment(m_request.url.fragment()); 3435 } 3436 3437 m_isRedirection = true; 3438 3439 if (!m_request.id.isEmpty()) { 3440 sendMetaData(); 3441 } 3442 3443 // If we're redirected to a http:// url, remember that we're doing webdav... 3444 if (m_protocol == "webdav" || m_protocol == "webdavs") { 3445 if (u.scheme() == QLatin1String("http")) { 3446 u.setScheme(QStringLiteral("webdav")); 3447 } else if (u.scheme() == QLatin1String("https")) { 3448 u.setScheme(QStringLiteral("webdavs")); 3449 } 3450 3451 m_request.redirectUrl = u; 3452 } 3453 3454 qCDebug(KIO_HTTP) << "Re-directing from" << m_request.url << "to" << u; 3455 3456 redirection(u); 3457 3458 // It would be hard to cache the redirection response correctly. The possible benefit 3459 // is small (if at all, assuming fast disk and slow network), so don't do it. 3460 cacheFileClose(); 3461 setCacheabilityMetadata(false); 3462 } 3463 3464 // Inform the job that we can indeed resume... 3465 if (bCanResume && m_request.offset) { 3466 // TODO turn off caching??? 3467 canResume(); 3468 } else { 3469 m_request.offset = 0; 3470 } 3471 3472 // Correct a few common wrong content encodings 3473 fixupResponseContentEncoding(); 3474 3475 // Correct some common incorrect pseudo MIME types 3476 fixupResponseMimetype(); 3477 3478 // parse everything related to expire and other dates, and cache directives; also switch 3479 // between cache reading and writing depending on cache validation result. 3480 cacheParseResponseHeader(tokenizer); 3481 } 3482 3483 if (m_request.cacheTag.ioMode == ReadFromCache) { 3484 if (m_request.cacheTag.policy == CC_Verify && m_request.cacheTag.plan(m_maxCacheAge) != CacheTag::UseCached) { 3485 qCDebug(KIO_HTTP) << "Reading resource from cache even though the cache plan is not " 3486 "UseCached; the server is probably sending wrong expiry information."; 3487 } 3488 // parseHeaderFromCache replaces this method in case of cached content 3489 return parseHeaderFromCache() ? WorkerResult::pass() : WorkerResult::fail(); 3490 } 3491 3492 if (configValue(QStringLiteral("PropagateHttpHeader"), false) || m_request.cacheTag.ioMode == WriteToCache) { 3493 // store header lines if they will be used; note that the tokenizer removing 3494 // line continuation special cases is probably more good than bad. 3495 int nextLinePos = 0; 3496 int prevLinePos = 0; 3497 bool haveMore = true; 3498 while (haveMore) { 3499 haveMore = nextLine(buffer, &nextLinePos, bufPos); 3500 int prevLineEnd = nextLinePos; 3501 while (buffer[prevLineEnd - 1] == '\r' || buffer[prevLineEnd - 1] == '\n') { 3502 prevLineEnd--; 3503 } 3504 3505 m_responseHeaders.append(QString::fromLatin1(&buffer[prevLinePos], prevLineEnd - prevLinePos)); 3506 prevLinePos = nextLinePos; 3507 } 3508 3509 // IMPORTANT: Do not remove this line because forwardHttpResponseHeader 3510 // is called below. This line is here to ensure the response headers are 3511 // available to the client before it receives MIME type information. 3512 // The support for putting KIO workers on hold in the KIO-QNAM integration 3513 // will break if this line is removed. 3514 setMetaData(QStringLiteral("HTTP-Headers"), m_responseHeaders.join(QLatin1Char('\n'))); 3515 } 3516 3517 // Let the app know about the MIME type iff this is not a redirection and 3518 // the mime-type string is not empty. 3519 if (!m_isRedirection && m_request.responseCode != 204 && (!m_mimeType.isEmpty() || m_request.method == HTTP_HEAD) && !m_kioError 3520 && (m_isLoadingErrorPage || !authRequiresAnotherRoundtrip)) { 3521 qCDebug(KIO_HTTP) << "Emitting mimetype " << m_mimeType; 3522 mimeType(m_mimeType); 3523 } 3524 3525 // IMPORTANT: Do not move the function call below before doing any 3526 // redirection. Otherwise it might mess up some sites, see BR# 150904. 3527 forwardHttpResponseHeader(); 3528 3529 if (m_request.method == HTTP_HEAD) { 3530 return WorkerResult::pass(); 3531 } 3532 3533 return !authRequiresAnotherRoundtrip ? WorkerResult::pass() : WorkerResult::fail(); // return true if no more credentials need to be sent 3534 } 3535 3536 void HTTPProtocol::parseContentDisposition(const QString &disposition) 3537 { 3538 const QMap<QString, QString> parameters = contentDispositionParser(disposition); 3539 3540 auto i = parameters.constBegin(); 3541 while (i != parameters.constEnd()) { 3542 setMetaData(QLatin1String("content-disposition-") + i.key(), i.value()); 3543 qCDebug(KIO_HTTP) << "Content-Disposition:" << i.key() << "=" << i.value(); 3544 ++i; 3545 } 3546 } 3547 3548 void HTTPProtocol::addEncoding(const QString &_encoding, QStringList &encs) 3549 { 3550 QString encoding = _encoding.trimmed().toLower(); 3551 // Identity is the same as no encoding 3552 if (encoding == QLatin1String("identity")) { 3553 return; 3554 } else if (encoding == QLatin1String("8bit")) { 3555 // Strange encoding returned by http://linac.ikp.physik.tu-darmstadt.de 3556 return; 3557 } else if (encoding == QLatin1String("chunked")) { 3558 m_isChunked = true; 3559 // Anyone know of a better way to handle unknown sizes possibly/ideally with unsigned ints? 3560 // if ( m_cmd != CMD_COPY ) 3561 m_iSize = NO_SIZE; 3562 } else if ((encoding == QLatin1String("x-gzip")) || (encoding == QLatin1String("gzip"))) { 3563 encs.append(QStringLiteral("gzip")); 3564 } else if ((encoding == QLatin1String("x-bzip2")) || (encoding == QLatin1String("bzip2"))) { 3565 encs.append(QStringLiteral("bzip2")); // Not yet supported! 3566 } else if ((encoding == QLatin1String("x-deflate")) || (encoding == QLatin1String("deflate"))) { 3567 encs.append(QStringLiteral("deflate")); 3568 } else { 3569 qCDebug(KIO_HTTP) << "Unknown encoding encountered. " 3570 << "Please write code. Encoding =" << encoding; 3571 } 3572 } 3573 3574 void HTTPProtocol::cacheParseResponseHeader(const HeaderTokenizer &tokenizer) 3575 { 3576 if (!m_request.cacheTag.useCache) { 3577 return; 3578 } 3579 3580 // might have to add more response codes 3581 if (m_request.responseCode != 200 && m_request.responseCode != 304) { 3582 return; 3583 } 3584 3585 m_request.cacheTag.servedDate = QDateTime(); 3586 m_request.cacheTag.lastModifiedDate = QDateTime(); 3587 m_request.cacheTag.expireDate = QDateTime(); 3588 3589 const QDateTime currentDate = QDateTime::currentDateTime(); 3590 bool mayCache = m_request.cacheTag.ioMode != NoCache; 3591 3592 TokenIterator tIt = tokenizer.iterator("last-modified"); 3593 if (tIt.hasNext()) { 3594 m_request.cacheTag.lastModifiedDate = QDateTime::fromString(toQString(tIt.next()), Qt::RFC2822Date); 3595 3596 // ### might be good to canonicalize the date by using QDateTime::toString() 3597 if (m_request.cacheTag.lastModifiedDate.isValid()) { 3598 setMetaData(QStringLiteral("modified"), toQString(tIt.current())); 3599 } 3600 } 3601 3602 // determine from available information when the response was served by the origin server 3603 { 3604 QDateTime dateHeader; 3605 tIt = tokenizer.iterator("date"); 3606 if (tIt.hasNext()) { 3607 dateHeader = QDateTime::fromString(toQString(tIt.next()), Qt::RFC2822Date); 3608 // -1 on error 3609 } 3610 3611 qint64 ageHeader = 0; 3612 tIt = tokenizer.iterator("age"); 3613 if (tIt.hasNext()) { 3614 ageHeader = tIt.next().toLongLong(); 3615 // 0 on error 3616 } 3617 3618 if (dateHeader.isValid()) { 3619 m_request.cacheTag.servedDate = dateHeader; 3620 } else if (ageHeader) { 3621 m_request.cacheTag.servedDate = currentDate.addSecs(-ageHeader); 3622 } else { 3623 m_request.cacheTag.servedDate = currentDate; 3624 } 3625 } 3626 3627 bool hasCacheDirective = false; 3628 // determine when the response "expires", i.e. becomes stale and needs revalidation 3629 { 3630 // (we also parse other cache directives here) 3631 qint64 maxAgeHeader = 0; 3632 tIt = tokenizer.iterator("cache-control"); 3633 while (tIt.hasNext()) { 3634 QByteArray cacheStr = tIt.next().toLower(); 3635 if (cacheStr.startsWith("no-cache") || cacheStr.startsWith("no-store")) { // krazy:exclude=strings 3636 // Don't put in cache 3637 mayCache = false; 3638 hasCacheDirective = true; 3639 } else if (cacheStr.startsWith("max-age=")) { // krazy:exclude=strings 3640 QByteArray ba = cacheStr.mid(qstrlen("max-age=")).trimmed(); 3641 bool ok = false; 3642 maxAgeHeader = ba.toLongLong(&ok); 3643 if (ok) { 3644 hasCacheDirective = true; 3645 } 3646 } 3647 } 3648 3649 QDateTime expiresHeader; 3650 tIt = tokenizer.iterator("expires"); 3651 if (tIt.hasNext()) { 3652 expiresHeader = QDateTime::fromString(toQString(tIt.next()), Qt::RFC2822Date); 3653 qCDebug(KIO_HTTP) << "parsed expire date from 'expires' header:" << tIt.current(); 3654 } 3655 3656 if (maxAgeHeader) { 3657 m_request.cacheTag.expireDate = m_request.cacheTag.servedDate.addSecs(maxAgeHeader); 3658 } else if (expiresHeader.isValid()) { 3659 m_request.cacheTag.expireDate = expiresHeader; 3660 } else { 3661 // heuristic expiration date 3662 if (m_request.cacheTag.lastModifiedDate.isValid()) { 3663 // expAge is following the RFC 2616 suggestion for heuristic expiration 3664 qint64 expAge = (m_request.cacheTag.lastModifiedDate.secsTo(m_request.cacheTag.servedDate)) / 10; 3665 // not in the RFC: make sure not to have a huge heuristic cache lifetime 3666 expAge = qMin(expAge, qint64(3600 * 24)); 3667 m_request.cacheTag.expireDate = m_request.cacheTag.servedDate.addSecs(expAge); 3668 } else { 3669 m_request.cacheTag.expireDate = m_request.cacheTag.servedDate.addSecs(DEFAULT_CACHE_EXPIRE); 3670 } 3671 } 3672 // make sure that no future clock monkey business causes the cache entry to un-expire 3673 if (m_request.cacheTag.expireDate < currentDate) { 3674 m_request.cacheTag.expireDate.setMSecsSinceEpoch(0); // January 1, 1970 :) 3675 } 3676 } 3677 3678 tIt = tokenizer.iterator("etag"); 3679 if (tIt.hasNext()) { 3680 QString prevEtag = m_request.cacheTag.etag; 3681 m_request.cacheTag.etag = toQString(tIt.next()); 3682 if (m_request.cacheTag.etag != prevEtag && m_request.responseCode == 304) { 3683 qCDebug(KIO_HTTP) << "304 Not Modified but new entity tag - I don't think this is legal HTTP."; 3684 } 3685 } 3686 3687 // whoops.. we received a warning 3688 tIt = tokenizer.iterator("warning"); 3689 if (tIt.hasNext()) { 3690 // Don't use warning() here, no need to bother the user. 3691 // Those warnings are mostly about caches. 3692 infoMessage(toQString(tIt.next())); 3693 } 3694 3695 // Cache management (HTTP 1.0) 3696 tIt = tokenizer.iterator("pragma"); 3697 while (tIt.hasNext()) { 3698 if (tIt.next().toLower().startsWith("no-cache")) { // krazy:exclude=strings 3699 mayCache = false; 3700 hasCacheDirective = true; 3701 } 3702 } 3703 3704 // The deprecated Refresh Response 3705 tIt = tokenizer.iterator("refresh"); 3706 if (tIt.hasNext()) { 3707 mayCache = false; 3708 setMetaData(QStringLiteral("http-refresh"), toQString(tIt.next().trimmed())); 3709 } 3710 3711 // We don't cache certain text objects 3712 if (m_mimeType.startsWith(QLatin1String("text/")) && (m_mimeType != QLatin1String("text/css")) && (m_mimeType != QLatin1String("text/x-javascript")) 3713 && !hasCacheDirective) { 3714 // Do not cache secure pages or pages 3715 // originating from password protected sites 3716 // unless the webserver explicitly allows it. 3717 if (isUsingSsl() || m_wwwAuth) { 3718 mayCache = false; 3719 } 3720 } 3721 3722 // note that we've updated cacheTag, so the plan() is with current data 3723 if (m_request.cacheTag.plan(m_maxCacheAge) == CacheTag::ValidateCached) { 3724 qCDebug(KIO_HTTP) << "Cache needs validation"; 3725 if (m_request.responseCode == 304) { 3726 qCDebug(KIO_HTTP) << "...was revalidated by response code but not by updated expire times. " 3727 "We're going to set the expire date to 60 seconds in the future..."; 3728 m_request.cacheTag.expireDate = currentDate.addSecs(60); 3729 if (m_request.cacheTag.policy == CC_Verify && m_request.cacheTag.plan(m_maxCacheAge) != CacheTag::UseCached) { 3730 // "apparently" because we /could/ have made an error ourselves, but the errors I 3731 // witnessed were all the server's fault. 3732 qCDebug(KIO_HTTP) << "this proxy or server apparently sends bogus expiry information."; 3733 } 3734 } 3735 } 3736 3737 // validation handling 3738 if (mayCache && m_request.responseCode == 200 && !m_mimeType.isEmpty()) { 3739 qCDebug(KIO_HTTP) << "Cache, adding" << m_request.url; 3740 // ioMode can still be ReadFromCache here if we're performing a conditional get 3741 // aka validation 3742 m_request.cacheTag.ioMode = WriteToCache; 3743 if (!cacheFileOpenWrite()) { 3744 qCDebug(KIO_HTTP) << "Error creating cache entry for" << m_request.url << "!"; 3745 } 3746 m_maxCacheSize = configValue(QStringLiteral("MaxCacheSize"), DEFAULT_MAX_CACHE_SIZE); 3747 } else if (m_request.responseCode == 304 && m_request.cacheTag.file) { 3748 if (!mayCache) { 3749 qCDebug(KIO_HTTP) << "This webserver is confused about the cacheability of the data it sends."; 3750 } 3751 // the cache file should still be open for reading, see satisfyRequestFromCache(). 3752 Q_ASSERT(m_request.cacheTag.file->openMode() == QIODevice::ReadOnly); 3753 Q_ASSERT(m_request.cacheTag.ioMode == ReadFromCache); 3754 } else { 3755 cacheFileClose(); 3756 } 3757 3758 setCacheabilityMetadata(mayCache); 3759 } 3760 3761 void HTTPProtocol::setCacheabilityMetadata(bool cachingAllowed) 3762 { 3763 if (!cachingAllowed) { 3764 setMetaData(QStringLiteral("no-cache"), QStringLiteral("true")); 3765 setMetaData(QStringLiteral("expire-date"), QStringLiteral("1")); // Expired 3766 } else { 3767 QString tmp; 3768 tmp.setNum(m_request.cacheTag.expireDate.toSecsSinceEpoch()); 3769 setMetaData(QStringLiteral("expire-date"), tmp); 3770 // slightly changed semantics from old creationDate, probably more correct now 3771 tmp.setNum(m_request.cacheTag.servedDate.toSecsSinceEpoch()); 3772 setMetaData(QStringLiteral("cache-creation-date"), tmp); 3773 } 3774 } 3775 3776 KIO::WorkerResult HTTPProtocol::sendCachedBody() 3777 { 3778 infoMessage(i18n("Sending data to %1", m_request.url.host())); 3779 3780 const qint64 size = m_POSTbuf->size(); 3781 const QByteArray cLength = "Content-Length: " + QByteArray::number(size) + "\r\n\r\n"; 3782 3783 // qDebug() << "sending cached data (size=" << size << ")"; 3784 3785 // Send the content length... 3786 bool sendOk = (write(cLength.data(), cLength.size()) == (ssize_t)cLength.size()); 3787 if (!sendOk) { 3788 qCDebug(KIO_HTTP) << "Connection broken when sending " 3789 << "content length: (" << m_request.url.host() << ")"; 3790 return error(ERR_CONNECTION_BROKEN, m_request.url.host()); 3791 } 3792 3793 totalSize(size); 3794 // Make sure the read head is at the beginning... 3795 m_POSTbuf->reset(); 3796 KIO::filesize_t totalBytesSent = 0; 3797 3798 // Send the data... 3799 while (!m_POSTbuf->atEnd()) { 3800 const QByteArray buffer = m_POSTbuf->read(65536); 3801 const ssize_t bytesSent = write(buffer.data(), buffer.size()); 3802 if (bytesSent != static_cast<ssize_t>(buffer.size())) { 3803 qCDebug(KIO_HTTP) << "Connection broken when sending message body: (" << m_request.url.host() << ")"; 3804 return error(ERR_CONNECTION_BROKEN, m_request.url.host()); 3805 } 3806 3807 totalBytesSent += bytesSent; 3808 processedSize(totalBytesSent); 3809 } 3810 3811 return KIO::WorkerResult::pass(); 3812 } 3813 3814 KIO::WorkerResult HTTPProtocol::sendBody() 3815 { 3816 // If we have cached data, the it is either a repost or a DAV request so send 3817 // the cached data... 3818 if (m_POSTbuf) { 3819 return sendCachedBody(); 3820 } 3821 3822 if (m_iPostDataSize == NO_SIZE) { 3823 // Try the old approach of retrieving content data from the job 3824 // before giving up. 3825 if (const auto result = retrieveAllData(); !result.success()) { 3826 return result; 3827 } 3828 return sendCachedBody(); 3829 } 3830 3831 qCDebug(KIO_HTTP) << "sending data (size=" << m_iPostDataSize << ")"; 3832 3833 infoMessage(i18n("Sending data to %1", m_request.url.host())); 3834 3835 const QByteArray cLength = "Content-Length: " + QByteArray::number(m_iPostDataSize) + "\r\n\r\n"; 3836 3837 qCDebug(KIO_HTTP) << cLength.trimmed(); 3838 3839 // Send the content length... 3840 bool sendOk = (write(cLength.data(), cLength.size()) == (ssize_t)cLength.size()); 3841 if (!sendOk) { 3842 // The server might have closed the connection due to a timeout, or maybe 3843 // some transport problem arose while the connection was idle. 3844 if (m_request.isKeepAlive) { 3845 httpCloseConnection(); 3846 return WorkerResult::pass(); // Try again 3847 } 3848 3849 qCDebug(KIO_HTTP) << "Connection broken while sending POST content size to" << m_request.url.host(); 3850 return WorkerResult::fail(ERR_CONNECTION_BROKEN, m_request.url.host()); 3851 } 3852 3853 // Send the amount 3854 totalSize(m_iPostDataSize); 3855 3856 // If content-length is 0, then do nothing but simply return true. 3857 if (m_iPostDataSize == 0) { 3858 return WorkerResult::pass(); 3859 } 3860 3861 sendOk = true; 3862 KIO::filesize_t bytesSent = 0; 3863 3864 while (true) { 3865 dataReq(); 3866 3867 QByteArray buffer; 3868 const int bytesRead = readData(buffer); 3869 3870 // On done... 3871 if (bytesRead == 0) { 3872 sendOk = (bytesSent == m_iPostDataSize); 3873 break; 3874 } 3875 3876 // On error return false... 3877 if (bytesRead < 0) { 3878 return error(ERR_ABORTED, m_request.url.host()); 3879 } 3880 3881 // Cache the POST data in case of a repost request. 3882 cachePostData(buffer); 3883 3884 // This will only happen if transmitting the data fails, so we will simply 3885 // cache the content locally for the potential re-transmit... 3886 if (!sendOk) { 3887 continue; 3888 } 3889 3890 if (write(buffer.data(), bytesRead) == static_cast<ssize_t>(bytesRead)) { 3891 bytesSent += bytesRead; 3892 processedSize(bytesSent); // Send update status... 3893 continue; 3894 } 3895 3896 qCDebug(KIO_HTTP) << "Connection broken while sending POST content to" << m_request.url.host(); 3897 return error(ERR_CONNECTION_BROKEN, m_request.url.host()); 3898 } 3899 3900 return sendOk ? WorkerResult::pass() : WorkerResult::fail(); 3901 } 3902 3903 void HTTPProtocol::httpClose(bool keepAlive) 3904 { 3905 qCDebug(KIO_HTTP) << "keepAlive =" << keepAlive; 3906 3907 cacheFileClose(); 3908 3909 // Only allow persistent connections for GET requests. 3910 // NOTE: we might even want to narrow this down to non-form 3911 // based submit requests which will require a meta-data from 3912 // khtml. 3913 if (keepAlive) { 3914 if (!m_request.keepAliveTimeout) { 3915 m_request.keepAliveTimeout = DEFAULT_KEEP_ALIVE_TIMEOUT; 3916 } else if (m_request.keepAliveTimeout > 2 * DEFAULT_KEEP_ALIVE_TIMEOUT) { 3917 m_request.keepAliveTimeout = 2 * DEFAULT_KEEP_ALIVE_TIMEOUT; 3918 } 3919 3920 qCDebug(KIO_HTTP) << "keep alive (" << m_request.keepAliveTimeout << ")"; 3921 QByteArray data; 3922 QDataStream stream(&data, QIODevice::WriteOnly); 3923 stream << int(99); // special: Close connection 3924 setTimeoutSpecialCommand(m_request.keepAliveTimeout, data); 3925 3926 return; 3927 } 3928 3929 httpCloseConnection(); 3930 } 3931 3932 void HTTPProtocol::closeConnection() 3933 { 3934 qCDebug(KIO_HTTP); 3935 httpCloseConnection(); 3936 } 3937 3938 void HTTPProtocol::httpCloseConnection() 3939 { 3940 qCDebug(KIO_HTTP); 3941 m_server.clear(); 3942 disconnectFromHost(); 3943 clearUnreadBuffer(); 3944 setTimeoutSpecialCommand(-1); // Cancel any connection timeout 3945 } 3946 3947 void HTTPProtocol::worker_status() 3948 { 3949 qCDebug(KIO_HTTP); 3950 3951 if (!isConnected()) { 3952 httpCloseConnection(); 3953 } 3954 3955 workerStatus(m_server.url.host(), isConnected()); 3956 } 3957 3958 KIO::WorkerResult HTTPProtocol::mimetype(const QUrl &url) 3959 { 3960 qCDebug(KIO_HTTP) << url; 3961 3962 if (const auto result = maybeSetRequestUrl(url); !result.success()) { 3963 return result; 3964 } 3965 resetSessionSettings(); 3966 3967 m_request.method = HTTP_HEAD; 3968 m_request.cacheTag.policy = CC_Cache; 3969 3970 const auto result = proceedUntilResponseHeader(); 3971 if (result.success()) { 3972 httpClose(m_request.isKeepAlive); 3973 } 3974 qCDebug(KIO_HTTP) << m_mimeType; 3975 return result; 3976 } 3977 3978 KIO::WorkerResult HTTPProtocol::special(const QByteArray &data) 3979 { 3980 qCDebug(KIO_HTTP); 3981 3982 int tmp; 3983 QDataStream stream(data); 3984 3985 stream >> tmp; 3986 switch (tmp) { 3987 case 1: { // HTTP POST 3988 QUrl url; 3989 qint64 size; 3990 stream >> url >> size; 3991 return post(url, size); 3992 } 3993 case 2: { // cache_update 3994 QUrl url; 3995 bool no_cache; 3996 qint64 expireDate; 3997 stream >> url >> no_cache >> expireDate; 3998 if (no_cache) { 3999 QString filename = cacheFilePathFromUrl(url); 4000 // there is a tiny risk of deleting the wrong file due to hash collisions here. 4001 // this is an unimportant performance issue. 4002 // FIXME on Windows we may be unable to delete the file if open 4003 QFile::remove(filename); 4004 return WorkerResult::pass(); 4005 } 4006 // let's be paranoid and inefficient here... 4007 HTTPRequest savedRequest = m_request; 4008 4009 m_request.url = url; 4010 if (cacheFileOpenRead()) { 4011 m_request.cacheTag.expireDate.setSecsSinceEpoch(expireDate); 4012 cacheFileClose(); // this sends an update command to the cache cleaner process 4013 } 4014 4015 m_request = savedRequest; 4016 return WorkerResult::pass(); 4017 } 4018 case 5: { // WebDAV lock 4019 QUrl url; 4020 QString scope; 4021 QString type; 4022 QString owner; 4023 stream >> url >> scope >> type >> owner; 4024 return davLock(url, scope, type, owner); 4025 } 4026 case 6: { // WebDAV unlock 4027 QUrl url; 4028 stream >> url; 4029 return davUnlock(url); 4030 } 4031 case 7: { // Generic WebDAV 4032 QUrl url; 4033 int method; 4034 qint64 size; 4035 stream >> url >> method >> size; 4036 return davGeneric(url, (KIO::HTTP_METHOD)method, size); 4037 } 4038 case 99: { // Close Connection 4039 httpCloseConnection(); 4040 return WorkerResult::pass(); 4041 } 4042 default: 4043 // Some command we don't understand. 4044 // Just ignore it, it may come from some future version. 4045 break; 4046 } 4047 return WorkerResult::pass(); 4048 } 4049 4050 /** 4051 * Read a chunk from the data stream. 4052 */ 4053 int HTTPProtocol::readChunked() 4054 { 4055 if ((m_iBytesLeft == 0) || (m_iBytesLeft == NO_SIZE)) { 4056 // discard CRLF from previous chunk, if any, and read size of next chunk 4057 4058 int bufPos = 0; 4059 m_receiveBuf.resize(4096); 4060 4061 bool foundCrLf = readDelimitedText(m_receiveBuf.data(), &bufPos, m_receiveBuf.size(), 1); 4062 4063 if (foundCrLf && bufPos == 2) { 4064 // The previous read gave us the CRLF from the previous chunk. As bufPos includes 4065 // the trailing CRLF it has to be > 2 to possibly include the next chunksize. 4066 bufPos = 0; 4067 foundCrLf = readDelimitedText(m_receiveBuf.data(), &bufPos, m_receiveBuf.size(), 1); 4068 } 4069 if (!foundCrLf) { 4070 qCDebug(KIO_HTTP) << "Failed to read chunk header."; 4071 return -1; 4072 } 4073 Q_ASSERT(bufPos > 2); 4074 4075 long long nextChunkSize = STRTOLL(m_receiveBuf.data(), nullptr, 16); 4076 if (nextChunkSize < 0) { 4077 qCDebug(KIO_HTTP) << "Negative chunk size"; 4078 return -1; 4079 } 4080 m_iBytesLeft = nextChunkSize; 4081 4082 qCDebug(KIO_HTTP) << "Chunk size =" << m_iBytesLeft << "bytes"; 4083 4084 if (m_iBytesLeft == 0) { 4085 // Last chunk; read and discard chunk trailer. 4086 // The last trailer line ends with CRLF and is followed by another CRLF 4087 // so we have CRLFCRLF like at the end of a standard HTTP header. 4088 // Do not miss a CRLFCRLF spread over two of our 4K blocks: keep three previous bytes. 4089 // NOTE the CRLF after the chunksize also counts if there is no trailer. Copy it over. 4090 char trash[4096]; 4091 trash[0] = m_receiveBuf.constData()[bufPos - 2]; 4092 trash[1] = m_receiveBuf.constData()[bufPos - 1]; 4093 int trashBufPos = 2; 4094 bool done = false; 4095 while (!done && !m_isEOF) { 4096 if (trashBufPos > 3) { 4097 // shift everything but the last three bytes out of the buffer 4098 for (int i = 0; i < 3; i++) { 4099 trash[i] = trash[trashBufPos - 3 + i]; 4100 } 4101 trashBufPos = 3; 4102 } 4103 done = readDelimitedText(trash, &trashBufPos, 4096, 2); 4104 } 4105 if (m_isEOF && !done) { 4106 qCDebug(KIO_HTTP) << "Failed to read chunk trailer."; 4107 return -1; 4108 } 4109 4110 return 0; 4111 } 4112 } 4113 4114 int bytesReceived = readLimited(); 4115 if (!m_iBytesLeft) { 4116 m_iBytesLeft = NO_SIZE; // Don't stop, continue with next chunk 4117 } 4118 return bytesReceived; 4119 } 4120 4121 int HTTPProtocol::readLimited() 4122 { 4123 if (!m_iBytesLeft) { 4124 return 0; 4125 } 4126 4127 m_receiveBuf.resize(4096); 4128 4129 int bytesToReceive; 4130 if (m_iBytesLeft > KIO::filesize_t(m_receiveBuf.size())) { 4131 bytesToReceive = m_receiveBuf.size(); 4132 } else { 4133 bytesToReceive = m_iBytesLeft; 4134 } 4135 4136 const int bytesReceived = readBuffered(m_receiveBuf.data(), bytesToReceive, false); 4137 4138 if (bytesReceived <= 0) { 4139 return -1; // Error: connection lost 4140 } 4141 4142 m_iBytesLeft -= bytesReceived; 4143 return bytesReceived; 4144 } 4145 4146 int HTTPProtocol::readUnlimited() 4147 { 4148 if (m_request.isKeepAlive) { 4149 qCDebug(KIO_HTTP) << "Unbounded datastream on a Keep-alive connection!"; 4150 m_request.isKeepAlive = false; 4151 } 4152 4153 m_receiveBuf.resize(4096); 4154 4155 int result = readBuffered(m_receiveBuf.data(), m_receiveBuf.size()); 4156 if (result > 0) { 4157 return result; 4158 } 4159 4160 m_isEOF = true; 4161 m_iBytesLeft = 0; 4162 return 0; 4163 } 4164 4165 void HTTPProtocol::slotData(const QByteArray &_d) 4166 { 4167 if (!_d.size()) { 4168 m_isEOD = true; 4169 return; 4170 } 4171 4172 if (m_iContentLeft != NO_SIZE) { 4173 if (m_iContentLeft >= KIO::filesize_t(_d.size())) { 4174 m_iContentLeft -= _d.size(); 4175 } else { 4176 m_iContentLeft = NO_SIZE; 4177 } 4178 } 4179 4180 QByteArray d = _d; 4181 if (!m_dataInternal) { 4182 // If a broken server does not send the mime-type, 4183 // we try to id it from the content before dealing 4184 // with the content itself. 4185 if (m_mimeType.isEmpty() && !m_isRedirection && !(m_request.responseCode >= 300 && m_request.responseCode <= 399)) { 4186 qCDebug(KIO_HTTP) << "Determining mime-type from content..."; 4187 int old_size = m_mimeTypeBuffer.size(); 4188 m_mimeTypeBuffer.resize(old_size + d.size()); 4189 memcpy(m_mimeTypeBuffer.data() + old_size, d.data(), d.size()); 4190 if ((m_iBytesLeft != NO_SIZE) && (m_iBytesLeft > 0) && (m_mimeTypeBuffer.size() < 1024)) { 4191 m_cpMimeBuffer = true; 4192 return; // Do not send up the data since we do not yet know its MIME type! 4193 } 4194 4195 qCDebug(KIO_HTTP) << "Mimetype buffer size:" << m_mimeTypeBuffer.size(); 4196 4197 QMimeDatabase db; 4198 QMimeType mime = db.mimeTypeForFileNameAndData(m_request.url.adjusted(QUrl::StripTrailingSlash).path(), m_mimeTypeBuffer); 4199 if (mime.isValid() && !mime.isDefault()) { 4200 m_mimeType = mime.name(); 4201 qCDebug(KIO_HTTP) << "MIME type from content:" << m_mimeType; 4202 } 4203 4204 if (m_mimeType.isEmpty()) { 4205 m_mimeType = QString::fromLatin1(DEFAULT_MIME_TYPE); 4206 qCDebug(KIO_HTTP) << "Using default MIME type:" << m_mimeType; 4207 } 4208 4209 // ### we could also open the cache file here 4210 4211 if (m_cpMimeBuffer) { 4212 d.resize(0); 4213 d.resize(m_mimeTypeBuffer.size()); 4214 memcpy(d.data(), m_mimeTypeBuffer.data(), d.size()); 4215 } 4216 mimeType(m_mimeType); 4217 m_mimeTypeBuffer.resize(0); 4218 } 4219 4220 // qDebug() << "Sending data of size" << d.size(); 4221 data(d); 4222 if (m_request.cacheTag.ioMode == WriteToCache) { 4223 cacheFileWritePayload(d); 4224 } 4225 } else { 4226 uint old_size = m_webDavDataBuf.size(); 4227 m_webDavDataBuf.resize(old_size + d.size()); 4228 memcpy(m_webDavDataBuf.data() + old_size, d.data(), d.size()); 4229 } 4230 } 4231 4232 /** 4233 * This function is our "receive" function. It is responsible for 4234 * downloading the message (not the header) from the HTTP server. It 4235 * is called either as a response to a client's KIOJob::dataEnd() 4236 * (meaning that the client is done sending data) or by 'sendQuery()' 4237 * (if we are in the process of a PUT/POST request). It can also be 4238 * called by a webDAV function, to receive stat/list/property/etc. 4239 * data; in this case the data is stored in m_webDavDataBuf. 4240 */ 4241 KIO::WorkerResult HTTPProtocol::readBody(bool dataInternal /* = false */) 4242 { 4243 // special case for reading cached body since we also do it in this function. oh well. 4244 if (!canHaveResponseBody(m_request.responseCode, m_request.method) 4245 && !(m_request.cacheTag.ioMode == ReadFromCache && m_request.responseCode == 304 && m_request.method != HTTP_HEAD)) { 4246 return WorkerResult::pass(); 4247 } 4248 4249 m_isEOD = false; 4250 // Note that when dataInternal is true, we are going to: 4251 // 1) save the body data to a member variable, m_webDavDataBuf 4252 // 2) _not_ advertise the data, speed, size, etc., through the 4253 // corresponding functions. 4254 // This is used for returning data to WebDAV. 4255 m_dataInternal = dataInternal; 4256 if (dataInternal) { 4257 m_webDavDataBuf.clear(); 4258 } 4259 4260 // Check if we need to decode the data. 4261 // If we are in copy mode, then use only transfer decoding. 4262 bool useMD5 = !m_contentMD5.isEmpty(); 4263 4264 // Deal with the size of the file. 4265 KIO::filesize_t sz = m_request.offset; 4266 if (sz) { 4267 m_iSize += sz; 4268 } 4269 4270 if (!m_isRedirection) { 4271 // Update the application with total size except when 4272 // it is compressed, or when the data is to be handled 4273 // internally (webDAV). If compressed we have to wait 4274 // until we uncompress to find out the actual data size 4275 if (!dataInternal) { 4276 if ((m_iSize > 0) && (m_iSize != NO_SIZE)) { 4277 totalSize(m_iSize); 4278 infoMessage(i18n("Retrieving %1 from %2...", KIO::convertSize(m_iSize), m_request.url.host())); 4279 } else { 4280 totalSize(0); 4281 } 4282 } 4283 4284 if (m_request.cacheTag.ioMode == ReadFromCache) { 4285 qCDebug(KIO_HTTP) << "reading data from cache..."; 4286 4287 m_iContentLeft = NO_SIZE; 4288 4289 QByteArray d; 4290 while (true) { 4291 d = cacheFileReadPayload(MAX_IPC_SIZE); 4292 if (d.isEmpty()) { 4293 break; 4294 } 4295 slotData(d); 4296 sz += d.size(); 4297 if (!dataInternal) { 4298 processedSize(sz); 4299 } 4300 } 4301 4302 m_receiveBuf.resize(0); 4303 4304 if (!dataInternal) { 4305 data(QByteArray()); 4306 } 4307 4308 return WorkerResult::pass(); 4309 } 4310 } 4311 4312 if (m_iSize != NO_SIZE) { 4313 m_iBytesLeft = m_iSize - sz; 4314 } else { 4315 m_iBytesLeft = NO_SIZE; 4316 } 4317 4318 m_iContentLeft = m_iBytesLeft; 4319 4320 if (m_isChunked) { 4321 m_iBytesLeft = NO_SIZE; 4322 } 4323 4324 qCDebug(KIO_HTTP) << KIO::number(m_iBytesLeft) << "bytes left."; 4325 4326 // Main incoming loop... Gather everything while we can... 4327 m_cpMimeBuffer = false; 4328 m_mimeTypeBuffer.resize(0); 4329 4330 HTTPFilterChain chain; 4331 4332 // redirection ignores the body 4333 if (!m_isRedirection) { 4334 QObject::connect(&chain, &HTTPFilterBase::output, this, &HTTPProtocol::slotData); 4335 } 4336 QObject::connect(&chain, &HTTPFilterBase::error, this, &HTTPProtocol::slotFilterError); 4337 4338 // decode all of the transfer encodings 4339 while (!m_transferEncodings.isEmpty()) { 4340 QString enc = m_transferEncodings.takeLast(); 4341 if (enc == QLatin1String("gzip")) { 4342 chain.addFilter(new HTTPFilterGZip); 4343 } else if (enc == QLatin1String("deflate")) { 4344 chain.addFilter(new HTTPFilterDeflate); 4345 } 4346 } 4347 4348 // From HTTP 1.1 Draft 6: 4349 // The MD5 digest is computed based on the content of the entity-body, 4350 // including any content-coding that has been applied, but not including 4351 // any transfer-encoding applied to the message-body. If the message is 4352 // received with a transfer-encoding, that encoding MUST be removed 4353 // prior to checking the Content-MD5 value against the received entity. 4354 HTTPFilterMD5 *md5Filter = nullptr; 4355 if (useMD5) { 4356 md5Filter = new HTTPFilterMD5; 4357 chain.addFilter(md5Filter); 4358 } 4359 4360 // now decode all of the content encodings 4361 // -- Why ?? We are not 4362 // -- a proxy server, be a client side implementation!! The applications 4363 // -- are capable of determining how to extract the encoded implementation. 4364 // WB: That's a misunderstanding. We are free to remove the encoding. 4365 // WB: Some braindead www-servers however, give .tgz files an encoding 4366 // WB: of "gzip" (or even "x-gzip") and a content-type of "applications/tar" 4367 // WB: They shouldn't do that. We can work around that though... 4368 while (!m_contentEncodings.isEmpty()) { 4369 QString enc = m_contentEncodings.takeLast(); 4370 if (enc == QLatin1String("gzip")) { 4371 chain.addFilter(new HTTPFilterGZip); 4372 } else if (enc == QLatin1String("deflate")) { 4373 chain.addFilter(new HTTPFilterDeflate); 4374 } 4375 } 4376 4377 while (!m_isEOF) { 4378 int bytesReceived; 4379 4380 if (m_isChunked) { 4381 bytesReceived = readChunked(); 4382 } else if (m_iSize != NO_SIZE) { 4383 bytesReceived = readLimited(); 4384 } else { 4385 bytesReceived = readUnlimited(); 4386 } 4387 4388 // make sure that this wasn't an error, first 4389 qCDebug(KIO_HTTP) << "bytesReceived:" << bytesReceived << " m_iSize:" << (int)m_iSize << " Chunked:" << m_isChunked 4390 << " BytesLeft:" << (int)m_iBytesLeft; 4391 if (bytesReceived == -1) { 4392 if (m_iContentLeft == 0) { 4393 // gzip'ed data sometimes reports a too long content-length. 4394 // (The length of the unzipped data) 4395 m_iBytesLeft = 0; 4396 break; 4397 } 4398 // Oh well... log an error and bug out 4399 qCDebug(KIO_HTTP) << "bytesReceived==-1 sz=" << (int)sz << " Connection broken !"; 4400 return error(ERR_CONNECTION_BROKEN, m_request.url.host()); 4401 } 4402 4403 // I guess that nbytes == 0 isn't an error.. but we certainly 4404 // won't work with it! 4405 if (bytesReceived > 0) { 4406 // Important: truncate the buffer to the actual size received! 4407 // Otherwise garbage will be passed to the app 4408 m_receiveBuf.truncate(bytesReceived); 4409 4410 chain.slotInput(m_receiveBuf); 4411 4412 if (m_kioError) { 4413 return WorkerResult::fail(m_kioError, m_kioErrorString); 4414 } 4415 4416 sz += bytesReceived; 4417 if (!dataInternal) { 4418 processedSize(sz); 4419 } 4420 } 4421 m_receiveBuf.resize(0); // res 4422 4423 if (m_iBytesLeft && m_isEOD && !m_isChunked) { 4424 // gzip'ed data sometimes reports a too long content-length. 4425 // (The length of the unzipped data) 4426 m_iBytesLeft = 0; 4427 } 4428 4429 if (m_iBytesLeft == 0) { 4430 qCDebug(KIO_HTTP) << "EOD received! Left =" << KIO::number(m_iBytesLeft); 4431 break; 4432 } 4433 } 4434 chain.slotInput(QByteArray()); // Flush chain. 4435 4436 if (useMD5) { 4437 QString calculatedMD5 = md5Filter->md5(); 4438 4439 if (m_contentMD5 != calculatedMD5) { 4440 qCWarning(KIO_HTTP) << "MD5 checksum MISMATCH! Expected:" << calculatedMD5 << ", Got:" << m_contentMD5; 4441 } 4442 } 4443 4444 // Close cache entry 4445 if (m_iBytesLeft == 0) { 4446 cacheFileClose(); // no-op if not necessary 4447 } 4448 4449 if (!dataInternal && sz <= 1) { 4450 if (m_request.responseCode >= 500 && m_request.responseCode <= 599) { 4451 return error(ERR_INTERNAL_SERVER, m_request.url.host()); 4452 } 4453 if (m_request.responseCode >= 400 && m_request.responseCode <= 499 && !isAuthenticationRequired(m_request.responseCode)) { 4454 return error(ERR_DOES_NOT_EXIST, m_request.url.host()); 4455 } 4456 } 4457 4458 if (!dataInternal && !m_isRedirection) { 4459 data(QByteArray()); 4460 } 4461 4462 return WorkerResult::pass(); 4463 } 4464 4465 KIO::WorkerResult HTTPProtocol::slotFilterError(const QString &text) 4466 { 4467 return error(KIO::ERR_WORKER_DEFINED, text); 4468 } 4469 4470 KIO::WorkerResult HTTPProtocol::error(int _err, const QString &_text) 4471 { 4472 // Close the connection only on connection errors. Otherwise, honor the 4473 // keep alive flag. 4474 if (_err == ERR_CONNECTION_BROKEN || _err == ERR_CANNOT_CONNECT) { 4475 httpClose(false); 4476 } else { 4477 httpClose(m_request.isKeepAlive); 4478 } 4479 4480 if (!m_request.id.isEmpty()) { 4481 forwardHttpResponseHeader(); 4482 sendMetaData(); 4483 } 4484 4485 // It's over, we don't need it anymore 4486 clearPostDataBuffer(); 4487 4488 m_kioError = _err; 4489 m_kioErrorString = _text; 4490 return WorkerResult::fail(_err, _text); 4491 } 4492 4493 void HTTPProtocol::addCookies(const QString &url, const QByteArray &cookieHeader) 4494 { 4495 qlonglong windowId = m_request.windowId.toLongLong(); 4496 QDBusInterface kcookiejar(QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer")); 4497 (void)kcookiejar.call(QDBus::NoBlock, QStringLiteral("addCookies"), url, cookieHeader, windowId); 4498 } 4499 4500 QString HTTPProtocol::findCookies(const QString &url) 4501 { 4502 qlonglong windowId = m_request.windowId.toLongLong(); 4503 QDBusInterface kcookiejar(QStringLiteral("org.kde.kcookiejar5"), QStringLiteral("/modules/kcookiejar"), QStringLiteral("org.kde.KCookieServer")); 4504 QDBusReply<QString> reply = kcookiejar.call(QStringLiteral("findCookies"), url, windowId); 4505 4506 if (!reply.isValid()) { 4507 qCWarning(KIO_HTTP) << "Can't communicate with kded_kcookiejar!"; 4508 return QString(); 4509 } 4510 return reply; 4511 } 4512 4513 /******************************* CACHING CODE ****************************/ 4514 4515 HTTPProtocol::CacheTag::CachePlan HTTPProtocol::CacheTag::plan(int maxCacheAge) const 4516 { 4517 // notable omission: we're not checking cache file presence or integrity 4518 switch (policy) { 4519 case KIO::CC_Refresh: 4520 // Conditional GET requires the presence of either an ETag or 4521 // last modified date. 4522 if (lastModifiedDate.isValid() || !etag.isEmpty()) { 4523 return ValidateCached; 4524 } 4525 break; 4526 case KIO::CC_Reload: 4527 return IgnoreCached; 4528 case KIO::CC_CacheOnly: 4529 case KIO::CC_Cache: 4530 return UseCached; 4531 default: 4532 break; 4533 } 4534 4535 Q_ASSERT((policy == CC_Verify || policy == CC_Refresh)); 4536 QDateTime currentDate = QDateTime::currentDateTime(); 4537 if ((servedDate.isValid() && (currentDate > servedDate.addSecs(maxCacheAge))) || (expireDate.isValid() && (currentDate > expireDate))) { 4538 return ValidateCached; 4539 } 4540 return UseCached; 4541 } 4542 4543 // !START SYNC! 4544 // The following code should be kept in sync 4545 // with the code in http_cache_cleaner.cpp 4546 4547 // we use QDataStream; this is just an illustration 4548 struct BinaryCacheFileHeader { 4549 quint8 version[2]; 4550 quint8 compression; // for now fixed to 0 4551 quint8 reserved; // for now; also alignment 4552 qint32 useCount; 4553 qint64 servedDate; 4554 qint64 lastModifiedDate; 4555 qint64 expireDate; 4556 qint32 bytesCached; 4557 // packed size should be 36 bytes; we explicitly set it here to make sure that no compiler 4558 // padding ruins it. We write the fields to disk without any padding. 4559 static const int size = 36; 4560 }; 4561 4562 enum CacheCleanerCommandCode { 4563 InvalidCommand = 0, 4564 CreateFileNotificationCommand, 4565 UpdateFileCommand, 4566 }; 4567 4568 // illustration for cache cleaner update "commands" 4569 struct CacheCleanerCommand { 4570 BinaryCacheFileHeader header; 4571 quint32 commandCode; 4572 // filename in ASCII, binary isn't worth the coding and decoding 4573 quint8 filename[s_hashedUrlNibbles]; 4574 }; 4575 4576 QByteArray HTTPProtocol::CacheTag::serialize() const 4577 { 4578 QByteArray ret; 4579 QDataStream stream(&ret, QIODevice::WriteOnly); 4580 stream << quint8('A'); 4581 stream << quint8('\n'); 4582 stream << quint8(0); 4583 stream << quint8(0); 4584 4585 stream << fileUseCount; 4586 4587 stream << servedDate.toMSecsSinceEpoch() / 1000; 4588 stream << lastModifiedDate.toMSecsSinceEpoch() / 1000; 4589 stream << expireDate.toMSecsSinceEpoch() / 1000; 4590 4591 stream << bytesCached; 4592 Q_ASSERT(ret.size() == BinaryCacheFileHeader::size); 4593 return ret; 4594 } 4595 4596 static bool compareByte(QDataStream *stream, quint8 value) 4597 { 4598 quint8 byte; 4599 *stream >> byte; 4600 return byte == value; 4601 } 4602 4603 // If starting a new file cacheFileWriteVariableSizeHeader() must have been called *before* 4604 // calling this! This is to fill in the headerEnd field. 4605 // If the file is not new headerEnd has already been read from the file and in fact the variable 4606 // size header *may* not be rewritten because a size change would mess up the file layout. 4607 bool HTTPProtocol::CacheTag::deserialize(const QByteArray &d) 4608 { 4609 if (d.size() != BinaryCacheFileHeader::size) { 4610 return false; 4611 } 4612 QDataStream stream(d); 4613 stream.setVersion(QDataStream::Qt_4_5); 4614 4615 bool ok = true; 4616 ok = ok && compareByte(&stream, 'A'); 4617 ok = ok && compareByte(&stream, '\n'); 4618 ok = ok && compareByte(&stream, 0); 4619 ok = ok && compareByte(&stream, 0); 4620 if (!ok) { 4621 return false; 4622 } 4623 4624 stream >> fileUseCount; 4625 4626 qint64 servedDateMs; 4627 stream >> servedDateMs; 4628 servedDate = QDateTime::fromSecsSinceEpoch(servedDateMs); 4629 4630 qint64 lastModifiedDateMs; 4631 stream >> lastModifiedDateMs; 4632 lastModifiedDate = QDateTime::fromSecsSinceEpoch(lastModifiedDateMs); 4633 4634 qint64 expireDateMs; 4635 stream >> expireDateMs; 4636 expireDate = QDateTime::fromSecsSinceEpoch(expireDateMs); 4637 4638 stream >> bytesCached; 4639 4640 return true; 4641 } 4642 4643 /* Text part of the header, directly following the binary first part: 4644 URL\n 4645 etag\n 4646 mimetype\n 4647 header line\n 4648 header line\n 4649 ... 4650 \n 4651 */ 4652 4653 static QUrl storableUrl(const QUrl &url) 4654 { 4655 QUrl ret(url); 4656 ret.setPassword(QString()); 4657 ret.setFragment(QString()); 4658 return ret; 4659 } 4660 4661 static void writeLine(QIODevice *dev, const QByteArray &line) 4662 { 4663 static const char linefeed = '\n'; 4664 dev->write(line); 4665 dev->write(&linefeed, 1); 4666 } 4667 4668 void HTTPProtocol::cacheFileWriteTextHeader() 4669 { 4670 QFile *&file = m_request.cacheTag.file; 4671 Q_ASSERT(file); 4672 Q_ASSERT(file->openMode() & QIODevice::WriteOnly); 4673 4674 file->seek(BinaryCacheFileHeader::size); 4675 writeLine(file, storableUrl(m_request.url).toEncoded()); 4676 writeLine(file, m_request.cacheTag.etag.toLatin1()); 4677 writeLine(file, m_mimeType.toLatin1()); 4678 writeLine(file, m_responseHeaders.join(QLatin1Char('\n')).toLatin1()); 4679 // join("\n") adds no \n to the end, but writeLine() does. 4680 // Add another newline to mark the end of text. 4681 writeLine(file, QByteArray()); 4682 } 4683 4684 static bool readLineChecked(QIODevice *dev, QByteArray *line) 4685 { 4686 *line = dev->readLine(MAX_IPC_SIZE); 4687 // if nothing read or the line didn't fit into 8192 bytes(!) 4688 if (line->isEmpty() || !line->endsWith('\n')) { 4689 return false; 4690 } 4691 // we don't actually want the newline! 4692 line->chop(1); 4693 return true; 4694 } 4695 4696 bool HTTPProtocol::cacheFileReadTextHeader1(const QUrl &desiredUrl) 4697 { 4698 QFile *&file = m_request.cacheTag.file; 4699 Q_ASSERT(file); 4700 Q_ASSERT(file->openMode() == QIODevice::ReadOnly); 4701 4702 QByteArray readBuf; 4703 bool ok = readLineChecked(file, &readBuf); 4704 if (storableUrl(desiredUrl).toEncoded() != readBuf) { 4705 qCDebug(KIO_HTTP) << "You have witnessed a very improbable hash collision!"; 4706 return false; 4707 } 4708 4709 ok = ok && readLineChecked(file, &readBuf); 4710 m_request.cacheTag.etag = toQString(readBuf); 4711 4712 return ok; 4713 } 4714 4715 bool HTTPProtocol::cacheFileReadTextHeader2() 4716 { 4717 QFile *&file = m_request.cacheTag.file; 4718 Q_ASSERT(file); 4719 Q_ASSERT(file->openMode() == QIODevice::ReadOnly); 4720 4721 bool ok = true; 4722 QByteArray readBuf; 4723 #ifndef NDEBUG 4724 // we assume that the URL and etag have already been read 4725 qint64 oldPos = file->pos(); 4726 file->seek(BinaryCacheFileHeader::size); 4727 ok = ok && readLineChecked(file, &readBuf); 4728 ok = ok && readLineChecked(file, &readBuf); 4729 Q_ASSERT(file->pos() == oldPos); 4730 #endif 4731 ok = ok && readLineChecked(file, &readBuf); 4732 m_mimeType = toQString(readBuf); 4733 4734 m_responseHeaders.clear(); 4735 // read as long as no error and no empty line found 4736 while (true) { 4737 ok = ok && readLineChecked(file, &readBuf); 4738 if (ok && !readBuf.isEmpty()) { 4739 m_responseHeaders.append(toQString(readBuf)); 4740 } else { 4741 break; 4742 } 4743 } 4744 return ok; // it may still be false ;) 4745 } 4746 4747 static QString filenameFromUrl(const QUrl &url) 4748 { 4749 QCryptographicHash hash(QCryptographicHash::Sha1); 4750 hash.addData(storableUrl(url).toEncoded()); 4751 return toQString(hash.result().toHex()); 4752 } 4753 4754 QString HTTPProtocol::cacheFilePathFromUrl(const QUrl &url) const 4755 { 4756 QString filePath = Utils::slashAppended(m_strCacheDir); 4757 filePath += filenameFromUrl(url); 4758 return filePath; 4759 } 4760 4761 bool HTTPProtocol::cacheFileOpenRead() 4762 { 4763 qCDebug(KIO_HTTP); 4764 QString filename = cacheFilePathFromUrl(m_request.url); 4765 4766 QFile *&file = m_request.cacheTag.file; 4767 if (file) { 4768 qCDebug(KIO_HTTP) << "File unexpectedly open; old file is" << file->fileName() << "new name is" << filename; 4769 Q_ASSERT(file->fileName() == filename); 4770 } 4771 Q_ASSERT(!file); 4772 file = new QFile(filename); 4773 if (file->open(QIODevice::ReadOnly)) { 4774 QByteArray header = file->read(BinaryCacheFileHeader::size); 4775 if (!m_request.cacheTag.deserialize(header)) { 4776 qCDebug(KIO_HTTP) << "Cache file header is invalid."; 4777 4778 file->close(); 4779 } 4780 } 4781 4782 if (file->isOpen() && !cacheFileReadTextHeader1(m_request.url)) { 4783 file->close(); 4784 } 4785 4786 if (!file->isOpen()) { 4787 cacheFileClose(); 4788 return false; 4789 } 4790 return true; 4791 } 4792 4793 bool HTTPProtocol::cacheFileOpenWrite() 4794 { 4795 qCDebug(KIO_HTTP); 4796 QString filename = cacheFilePathFromUrl(m_request.url); 4797 4798 // if we open a cache file for writing while we have a file open for reading we must have 4799 // found out that the old cached content is obsolete, so delete the file. 4800 QFile *&file = m_request.cacheTag.file; 4801 if (file) { 4802 // ensure that the file is in a known state - either open for reading or null 4803 Q_ASSERT(!qobject_cast<QTemporaryFile *>(file)); 4804 Q_ASSERT((file->openMode() & QIODevice::WriteOnly) == 0); 4805 Q_ASSERT(file->fileName() == filename); 4806 qCDebug(KIO_HTTP) << "deleting expired cache entry and recreating."; 4807 file->remove(); 4808 delete file; 4809 file = nullptr; 4810 } 4811 4812 // note that QTemporaryFile will automatically append random chars to filename 4813 file = new QTemporaryFile(filename); 4814 file->open(QIODevice::WriteOnly); 4815 4816 // if we have started a new file we have not initialized some variables from disk data. 4817 m_request.cacheTag.fileUseCount = 0; // the file has not been *read* yet 4818 m_request.cacheTag.bytesCached = 0; 4819 4820 if ((file->openMode() & QIODevice::WriteOnly) == 0) { 4821 qCDebug(KIO_HTTP) << "Could not open file for writing: QTemporaryFile(" << filename << ")" 4822 << "due to error" << file->error(); 4823 cacheFileClose(); 4824 return false; 4825 } 4826 return true; 4827 } 4828 4829 static QByteArray makeCacheCleanerCommand(const HTTPProtocol::CacheTag &cacheTag, CacheCleanerCommandCode cmd) 4830 { 4831 QByteArray ret = cacheTag.serialize(); 4832 QDataStream stream(&ret, QIODevice::ReadWrite); 4833 stream.setVersion(QDataStream::Qt_4_5); 4834 4835 stream.skipRawData(BinaryCacheFileHeader::size); 4836 // append the command code 4837 stream << quint32(cmd); 4838 // append the filename 4839 const QString fileName = cacheTag.file->fileName(); 4840 const int basenameStart = fileName.lastIndexOf(QLatin1Char('/')) + 1; 4841 const QByteArray baseName = QStringView(fileName).mid(basenameStart, s_hashedUrlNibbles).toLatin1(); 4842 stream.writeRawData(baseName.constData(), baseName.size()); 4843 4844 Q_ASSERT(ret.size() == BinaryCacheFileHeader::size + sizeof(quint32) + s_hashedUrlNibbles); 4845 return ret; 4846 } 4847 4848 // ### not yet 100% sure when and when not to call this 4849 void HTTPProtocol::cacheFileClose() 4850 { 4851 qCDebug(KIO_HTTP); 4852 4853 QFile *&file = m_request.cacheTag.file; 4854 if (!file) { 4855 return; 4856 } 4857 4858 m_request.cacheTag.ioMode = NoCache; 4859 4860 QByteArray ccCommand; 4861 QTemporaryFile *tempFile = qobject_cast<QTemporaryFile *>(file); 4862 4863 if (file->openMode() & QIODevice::WriteOnly) { 4864 Q_ASSERT(tempFile); 4865 4866 if (m_request.cacheTag.bytesCached && !m_kioError) { 4867 QByteArray header = m_request.cacheTag.serialize(); 4868 tempFile->seek(0); 4869 tempFile->write(header); 4870 4871 ccCommand = makeCacheCleanerCommand(m_request.cacheTag, CreateFileNotificationCommand); 4872 4873 QString oldName = tempFile->fileName(); 4874 QString newName = oldName; 4875 int basenameStart = newName.lastIndexOf(QLatin1Char('/')) + 1; 4876 // remove the randomized name part added by QTemporaryFile 4877 newName.chop(newName.length() - basenameStart - s_hashedUrlNibbles); 4878 qCDebug(KIO_HTTP) << "Renaming temporary file" << oldName << "to" << newName; 4879 4880 // on windows open files can't be renamed 4881 tempFile->setAutoRemove(false); 4882 delete tempFile; 4883 file = nullptr; 4884 4885 if (!QFile::rename(oldName, newName)) { 4886 // ### currently this hides a minor bug when force-reloading a resource. We 4887 // should not even open a new file for writing in that case. 4888 qCDebug(KIO_HTTP) << "Renaming temporary file failed, deleting it instead."; 4889 QFile::remove(oldName); 4890 ccCommand.clear(); // we have nothing of value to tell the cache cleaner 4891 } 4892 } else { 4893 // oh, we've never written payload data to the cache file. 4894 // the temporary file is closed and removed and no proper cache entry is created. 4895 } 4896 } else if (file->openMode() == QIODevice::ReadOnly) { 4897 Q_ASSERT(!tempFile); 4898 ccCommand = makeCacheCleanerCommand(m_request.cacheTag, UpdateFileCommand); 4899 } 4900 delete file; 4901 file = nullptr; 4902 4903 if (!ccCommand.isEmpty()) { 4904 sendCacheCleanerCommand(ccCommand); 4905 } 4906 } 4907 4908 void HTTPProtocol::sendCacheCleanerCommand(const QByteArray &command) 4909 { 4910 qCDebug(KIO_HTTP); 4911 if (!qEnvironmentVariableIsEmpty("KIO_DISABLE_CACHE_CLEANER")) { // for autotests 4912 return; 4913 } 4914 Q_ASSERT(command.size() == BinaryCacheFileHeader::size + s_hashedUrlNibbles + sizeof(quint32)); 4915 if (m_cacheCleanerConnection.state() != QLocalSocket::ConnectedState) { 4916 QString socketFileName = QStandardPaths::writableLocation(QStandardPaths::RuntimeLocation) + QLatin1Char('/') + QLatin1String("kio_http_cache_cleaner"); 4917 m_cacheCleanerConnection.connectToServer(socketFileName, QIODevice::WriteOnly); 4918 4919 if (m_cacheCleanerConnection.state() == QLocalSocket::UnconnectedState) { 4920 // An error happened. 4921 // Most likely the cache cleaner is not running, let's start it. 4922 4923 // search paths 4924 const QStringList searchPaths = QStringList() 4925 << QCoreApplication::applicationDirPath() // then look where our application binary is located 4926 << QLibraryInfo::location(QLibraryInfo::LibraryExecutablesPath) // look where libexec path is (can be set in qt.conf) 4927 << QFile::decodeName(KDE_INSTALL_FULL_LIBEXECDIR_KF); // look at our installation location 4928 const QString exe = QStandardPaths::findExecutable(QStringLiteral("kio_http_cache_cleaner"), searchPaths); 4929 if (exe.isEmpty()) { 4930 qCWarning(KIO_HTTP) << "kio_http_cache_cleaner not found in" << searchPaths; 4931 return; 4932 } 4933 qCDebug(KIO_HTTP) << "starting" << exe; 4934 QProcess::startDetached(exe, QStringList()); 4935 4936 for (int i = 0; i < 30 && m_cacheCleanerConnection.state() == QLocalSocket::UnconnectedState; ++i) { 4937 // Server is not listening yet; let's hope it does so under 3 seconds 4938 QThread::msleep(100); 4939 m_cacheCleanerConnection.connectToServer(socketFileName, QIODevice::WriteOnly); 4940 if (m_cacheCleanerConnection.state() != QLocalSocket::UnconnectedState) { 4941 break; // connecting or connected, sounds good 4942 } 4943 } 4944 } 4945 4946 if (!m_cacheCleanerConnection.waitForConnected(1500)) { 4947 // updating the stats is not vital, so we just give up. 4948 qCDebug(KIO_HTTP) << "Could not connect to cache cleaner, not updating stats of this cache file."; 4949 return; 4950 } 4951 qCDebug(KIO_HTTP) << "Successfully connected to cache cleaner."; 4952 } 4953 4954 Q_ASSERT(m_cacheCleanerConnection.state() == QLocalSocket::ConnectedState); 4955 m_cacheCleanerConnection.write(command); 4956 m_cacheCleanerConnection.flush(); 4957 } 4958 4959 QByteArray HTTPProtocol::cacheFileReadPayload(int maxLength) 4960 { 4961 Q_ASSERT(m_request.cacheTag.file); 4962 Q_ASSERT(m_request.cacheTag.ioMode == ReadFromCache); 4963 Q_ASSERT(m_request.cacheTag.file->openMode() == QIODevice::ReadOnly); 4964 QByteArray ret = m_request.cacheTag.file->read(maxLength); 4965 if (ret.isEmpty()) { 4966 cacheFileClose(); 4967 } 4968 return ret; 4969 } 4970 4971 void HTTPProtocol::cacheFileWritePayload(const QByteArray &d) 4972 { 4973 if (!m_request.cacheTag.file) { 4974 return; 4975 } 4976 4977 // If the file being downloaded is so big that it exceeds the max cache size, 4978 // do not cache it! See BR# 244215. NOTE: this can be improved upon in the 4979 // future... 4980 if (m_iSize >= KIO::filesize_t(m_maxCacheSize * 1024)) { 4981 qCDebug(KIO_HTTP) << "Caching disabled because content size is too big."; 4982 cacheFileClose(); 4983 return; 4984 } 4985 4986 Q_ASSERT(m_request.cacheTag.ioMode == WriteToCache); 4987 Q_ASSERT(m_request.cacheTag.file->openMode() & QIODevice::WriteOnly); 4988 4989 if (d.isEmpty()) { 4990 cacheFileClose(); 4991 } 4992 4993 // TODO: abort if file grows too big! 4994 4995 // write the variable length text header as soon as we start writing to the file 4996 if (!m_request.cacheTag.bytesCached) { 4997 cacheFileWriteTextHeader(); 4998 } 4999 m_request.cacheTag.bytesCached += d.size(); 5000 m_request.cacheTag.file->write(d); 5001 } 5002 5003 void HTTPProtocol::cachePostData(const QByteArray &data) 5004 { 5005 if (!m_POSTbuf) { 5006 m_POSTbuf = createPostBufferDeviceFor(qMax(m_iPostDataSize, static_cast<KIO::filesize_t>(data.size()))); 5007 if (!m_POSTbuf) { 5008 return; 5009 } 5010 } 5011 5012 m_POSTbuf->write(data.constData(), data.size()); 5013 } 5014 5015 void HTTPProtocol::clearPostDataBuffer() 5016 { 5017 if (!m_POSTbuf) { 5018 return; 5019 } 5020 5021 delete m_POSTbuf; 5022 m_POSTbuf = nullptr; 5023 } 5024 5025 KIO::WorkerResult HTTPProtocol::retrieveAllData() 5026 { 5027 if (!m_POSTbuf) { 5028 m_POSTbuf = createPostBufferDeviceFor(s_MaxInMemPostBufSize + 1); 5029 } 5030 5031 if (!m_POSTbuf) { 5032 return error(ERR_OUT_OF_MEMORY, m_request.url.host()); 5033 } 5034 5035 while (true) { 5036 dataReq(); 5037 QByteArray buffer; 5038 const int bytesRead = readData(buffer); 5039 5040 if (bytesRead < 0) { 5041 return error(ERR_ABORTED, m_request.url.host()); 5042 } 5043 5044 if (bytesRead == 0) { 5045 break; 5046 } 5047 5048 m_POSTbuf->write(buffer.constData(), buffer.size()); 5049 } 5050 5051 return WorkerResult::pass(); 5052 } 5053 5054 // The above code should be kept in sync 5055 // with the code in http_cache_cleaner.cpp 5056 // !END SYNC! 5057 5058 //************************** AUTHENTICATION CODE ********************/ 5059 5060 QString HTTPProtocol::authenticationHeader() 5061 { 5062 QByteArray ret; 5063 5064 // If the internal meta-data "cached-www-auth" is set, then check for cached 5065 // authentication data and preemptively send the authentication header if a 5066 // matching one is found. 5067 if (!m_wwwAuth && configValue(QStringLiteral("cached-www-auth"), false)) { 5068 KIO::AuthInfo authinfo; 5069 authinfo.url = m_request.url; 5070 authinfo.realmValue = configValue(QStringLiteral("www-auth-realm"), QString()); 5071 // If no realm metadata, then make sure path matching is turned on. 5072 authinfo.verifyPath = (authinfo.realmValue.isEmpty()); 5073 5074 const bool useCachedAuth = (m_request.responseCode == 401 || !configValue(QStringLiteral("no-preemptive-auth-reuse"), false)); 5075 5076 if (useCachedAuth && checkCachedAuthentication(authinfo)) { 5077 const QByteArray cachedChallenge = mapConfig().value(QStringLiteral("www-auth-challenge"), QByteArray()).toByteArray(); 5078 if (!cachedChallenge.isEmpty()) { 5079 m_wwwAuth = KAbstractHttpAuthentication::newAuth(cachedChallenge, config()); 5080 if (m_wwwAuth) { 5081 qCDebug(KIO_HTTP) << "creating www authentication header from cached info"; 5082 m_wwwAuth->setChallenge(cachedChallenge, m_request.url, m_request.sentMethodString); 5083 m_wwwAuth->generateResponse(authinfo.username, authinfo.password); 5084 } 5085 } 5086 } 5087 } 5088 5089 // If the internal meta-data "cached-proxy-auth" is set, then check for cached 5090 // authentication data and preemptively send the authentication header if a 5091 // matching one is found. 5092 if (!m_proxyAuth && configValue(QStringLiteral("cached-proxy-auth"), false)) { 5093 KIO::AuthInfo authinfo; 5094 authinfo.url = m_request.proxyUrl; 5095 authinfo.realmValue = configValue(QStringLiteral("proxy-auth-realm"), QString()); 5096 // If no realm metadata, then make sure path matching is turned on. 5097 authinfo.verifyPath = (authinfo.realmValue.isEmpty()); 5098 5099 if (checkCachedAuthentication(authinfo)) { 5100 const QByteArray cachedChallenge = mapConfig().value(QStringLiteral("proxy-auth-challenge"), QByteArray()).toByteArray(); 5101 if (!cachedChallenge.isEmpty()) { 5102 m_proxyAuth = KAbstractHttpAuthentication::newAuth(cachedChallenge, config()); 5103 if (m_proxyAuth) { 5104 qCDebug(KIO_HTTP) << "creating proxy authentication header from cached info"; 5105 m_proxyAuth->setChallenge(cachedChallenge, m_request.proxyUrl, m_request.sentMethodString); 5106 m_proxyAuth->generateResponse(authinfo.username, authinfo.password); 5107 } 5108 } 5109 } 5110 } 5111 5112 // the authentication classes don't know if they are for proxy or webserver authentication... 5113 if (m_wwwAuth && !m_wwwAuth->isError()) { 5114 ret += "Authorization: " + m_wwwAuth->headerFragment(); 5115 } 5116 5117 if (m_proxyAuth && !m_proxyAuth->isError()) { 5118 ret += "Proxy-Authorization: " + m_proxyAuth->headerFragment(); 5119 } 5120 5121 return toQString(ret); // ## encoding ok? 5122 } 5123 5124 static QString protocolForProxyType(QNetworkProxy::ProxyType type) 5125 { 5126 switch (type) { 5127 case QNetworkProxy::DefaultProxy: 5128 break; 5129 case QNetworkProxy::Socks5Proxy: 5130 return QStringLiteral("socks"); 5131 case QNetworkProxy::NoProxy: 5132 break; 5133 case QNetworkProxy::HttpProxy: 5134 case QNetworkProxy::HttpCachingProxy: 5135 case QNetworkProxy::FtpCachingProxy: 5136 break; 5137 } 5138 5139 return QStringLiteral("http"); 5140 } 5141 5142 KIO::WorkerResult HTTPProtocol::proxyAuthenticationForSocket(const QNetworkProxy &proxy, QAuthenticator *authenticator) 5143 { 5144 qCDebug(KIO_HTTP) << "realm:" << authenticator->realm() << "user:" << authenticator->user(); 5145 5146 // Set the proxy URL... 5147 m_request.proxyUrl.setScheme(protocolForProxyType(proxy.type())); 5148 m_request.proxyUrl.setUserName(proxy.user()); 5149 m_request.proxyUrl.setHost(proxy.hostName()); 5150 m_request.proxyUrl.setPort(proxy.port()); 5151 5152 AuthInfo info; 5153 info.url = m_request.proxyUrl; 5154 info.realmValue = authenticator->realm(); 5155 info.username = authenticator->user(); 5156 info.verifyPath = info.realmValue.isEmpty(); 5157 5158 const bool haveCachedCredentials = checkCachedAuthentication(info); 5159 const bool retryAuth = (m_socketProxyAuth != nullptr); 5160 5161 // if m_socketProxyAuth is a valid pointer then authentication has been attempted before, 5162 // and it was not successful. see below and saveProxyAuthenticationForSocket(). 5163 if (!haveCachedCredentials || retryAuth) { 5164 // Save authentication info if the connection succeeds. We need to disconnect 5165 // this after saving the auth data (or an error) so we won't save garbage afterwards! 5166 connect(tcpSocket(), &QAbstractSocket::connected, this, &HTTPProtocol::saveProxyAuthenticationForSocket); 5167 // ### fillPromptInfo(&info); 5168 info.prompt = i18n( 5169 "You need to supply a username and a password for " 5170 "the proxy server listed below before you are allowed " 5171 "to access any sites."); 5172 info.keepPassword = true; 5173 info.commentLabel = i18n("Proxy:"); 5174 info.comment = i18n("<b>%1</b> at <b>%2</b>", info.realmValue.toHtmlEscaped(), m_request.proxyUrl.host()); 5175 5176 const QString errMsg((retryAuth ? i18n("Proxy Authentication Failed.") : QString())); 5177 5178 const int errorCode = openPasswordDialog(info, errMsg); 5179 if (errorCode) { 5180 qCDebug(KIO_HTTP) << "proxy auth cancelled by user, or communication error"; 5181 const auto result = error(errorCode, QString()); 5182 delete m_proxyAuth; 5183 m_proxyAuth = nullptr; 5184 return result; 5185 } 5186 } 5187 authenticator->setUser(info.username); 5188 authenticator->setPassword(info.password); 5189 authenticator->setOption(QStringLiteral("keepalive"), info.keepPassword); 5190 5191 if (m_socketProxyAuth) { 5192 *m_socketProxyAuth = *authenticator; 5193 } else { 5194 m_socketProxyAuth = new QAuthenticator(*authenticator); 5195 } 5196 5197 if (!m_request.proxyUrl.userName().isEmpty()) { 5198 m_request.proxyUrl.setUserName(info.username); 5199 } 5200 return WorkerResult::fail(); 5201 } 5202 5203 void HTTPProtocol::saveProxyAuthenticationForSocket() 5204 { 5205 qCDebug(KIO_HTTP) << "Saving authenticator"; 5206 disconnect(tcpSocket(), &QAbstractSocket::connected, this, &HTTPProtocol::saveProxyAuthenticationForSocket); 5207 Q_ASSERT(m_socketProxyAuth); 5208 if (m_socketProxyAuth) { 5209 qCDebug(KIO_HTTP) << "realm:" << m_socketProxyAuth->realm() << "user:" << m_socketProxyAuth->user(); 5210 KIO::AuthInfo a; 5211 a.verifyPath = true; 5212 a.url = m_request.proxyUrl; 5213 a.realmValue = m_socketProxyAuth->realm(); 5214 a.username = m_socketProxyAuth->user(); 5215 a.password = m_socketProxyAuth->password(); 5216 a.keepPassword = m_socketProxyAuth->option(QStringLiteral("keepalive")).toBool(); 5217 cacheAuthentication(a); 5218 } 5219 delete m_socketProxyAuth; 5220 m_socketProxyAuth = nullptr; 5221 } 5222 5223 void HTTPProtocol::saveAuthenticationData() 5224 { 5225 KIO::AuthInfo authinfo; 5226 bool alreadyCached = false; 5227 KAbstractHttpAuthentication *auth = nullptr; 5228 switch (m_request.prevResponseCode) { 5229 case 401: 5230 auth = m_wwwAuth; 5231 alreadyCached = configValue(QStringLiteral("cached-www-auth"), false); 5232 break; 5233 case 407: 5234 auth = m_proxyAuth; 5235 alreadyCached = configValue(QStringLiteral("cached-proxy-auth"), false); 5236 break; 5237 default: 5238 Q_ASSERT(false); // should never happen! 5239 } 5240 5241 // Prevent recaching of the same credentials over and over again. 5242 if (auth && (!auth->realm().isEmpty() || !alreadyCached)) { 5243 auth->fillKioAuthInfo(&authinfo); 5244 if (auth == m_wwwAuth) { 5245 setMetaData(QStringLiteral("{internal~currenthost}cached-www-auth"), QStringLiteral("true")); 5246 if (!authinfo.realmValue.isEmpty()) { 5247 setMetaData(QStringLiteral("{internal~currenthost}www-auth-realm"), authinfo.realmValue); 5248 } 5249 if (!authinfo.digestInfo.isEmpty()) { 5250 setMetaData(QStringLiteral("{internal~currenthost}www-auth-challenge"), authinfo.digestInfo); 5251 } 5252 } else { 5253 setMetaData(QStringLiteral("{internal~allhosts}cached-proxy-auth"), QStringLiteral("true")); 5254 if (!authinfo.realmValue.isEmpty()) { 5255 setMetaData(QStringLiteral("{internal~allhosts}proxy-auth-realm"), authinfo.realmValue); 5256 } 5257 if (!authinfo.digestInfo.isEmpty()) { 5258 setMetaData(QStringLiteral("{internal~allhosts}proxy-auth-challenge"), authinfo.digestInfo); 5259 } 5260 } 5261 5262 qCDebug(KIO_HTTP) << "Cache authentication info ?" << authinfo.keepPassword; 5263 5264 if (authinfo.keepPassword) { 5265 cacheAuthentication(authinfo); 5266 qCDebug(KIO_HTTP) << "Cached authentication for" << m_request.url; 5267 } 5268 } 5269 // Update our server connection state which includes www and proxy username and password. 5270 m_server.updateCredentials(m_request); 5271 } 5272 5273 bool HTTPProtocol::handleAuthenticationHeader(const HeaderTokenizer *tokenizer) 5274 { 5275 KIO::AuthInfo authinfo; 5276 QList<QByteArray> authTokens; 5277 KAbstractHttpAuthentication **auth; 5278 QList<QByteArray> *blacklistedAuthTokens; 5279 TriedCredentials *triedCredentials; 5280 5281 if (m_request.responseCode == 401) { 5282 auth = &m_wwwAuth; 5283 blacklistedAuthTokens = &m_blacklistedWwwAuthMethods; 5284 triedCredentials = &m_triedWwwCredentials; 5285 authTokens = tokenizer->iterator("www-authenticate").all(); 5286 authinfo.url = m_request.url; 5287 authinfo.username = m_server.url.userName(); 5288 authinfo.prompt = i18n( 5289 "You need to supply a username and a " 5290 "password to access this site."); 5291 authinfo.commentLabel = i18n("Site:"); 5292 } else { 5293 // make sure that the 407 header hasn't escaped a lower layer when it shouldn't. 5294 // this may break proxy chains which were never tested anyway, and AFAIK they are 5295 // rare to nonexistent in the wild. 5296 Q_ASSERT(QNetworkProxy::applicationProxy().type() == QNetworkProxy::NoProxy); 5297 auth = &m_proxyAuth; 5298 blacklistedAuthTokens = &m_blacklistedProxyAuthMethods; 5299 triedCredentials = &m_triedProxyCredentials; 5300 authTokens = tokenizer->iterator("proxy-authenticate").all(); 5301 authinfo.url = m_request.proxyUrl; 5302 authinfo.username = m_request.proxyUrl.userName(); 5303 authinfo.prompt = i18n( 5304 "You need to supply a username and a password for " 5305 "the proxy server listed below before you are allowed " 5306 "to access any sites."); 5307 authinfo.commentLabel = i18n("Proxy:"); 5308 } 5309 5310 bool authRequiresAnotherRoundtrip = false; 5311 5312 // Workaround brain dead server responses that violate the spec and 5313 // incorrectly return a 401/407 without the required WWW/Proxy-Authenticate 5314 // header fields. See bug 215736... 5315 if (!authTokens.isEmpty()) { 5316 QString errorMsg; 5317 authRequiresAnotherRoundtrip = true; 5318 5319 if (m_request.responseCode == m_request.prevResponseCode && *auth) { 5320 // Authentication attempt failed. Retry... 5321 if ((*auth)->wasFinalStage()) { 5322 errorMsg = (m_request.responseCode == 401 ? i18n("Authentication Failed.") : i18n("Proxy Authentication Failed.")); 5323 // The authentication failed in its final stage. If the chosen method didn't use a password or 5324 // if it failed with both the supplied and prompted password then blacklist this method and try 5325 // again with another one if possible. 5326 if (!(*auth)->needCredentials() || *triedCredentials > JobCredentials) { 5327 QByteArray scheme((*auth)->scheme().trimmed()); 5328 qCDebug(KIO_HTTP) << "Blacklisting auth" << scheme; 5329 blacklistedAuthTokens->append(scheme); 5330 } 5331 delete *auth; 5332 *auth = nullptr; 5333 } else { // Create authentication header 5334 // WORKAROUND: The following piece of code prevents brain dead IIS 5335 // servers that send back multiple "WWW-Authenticate" headers from 5336 // screwing up our authentication logic during the challenge 5337 // phase (Type 2) of NTLM authentication. 5338 QMutableListIterator<QByteArray> it(authTokens); 5339 const QByteArray authScheme((*auth)->scheme().trimmed()); 5340 while (it.hasNext()) { 5341 if (qstrnicmp(authScheme.constData(), it.next().constData(), authScheme.length()) != 0) { 5342 it.remove(); 5343 } 5344 } 5345 } 5346 } 5347 5348 QList<QByteArray>::iterator it = authTokens.begin(); 5349 while (it != authTokens.end()) { 5350 QByteArray scheme = *it; 5351 // Separate the method name from any additional parameters (for ex. nonce or realm). 5352 int index = it->indexOf(' '); 5353 if (index > 0) { 5354 scheme.truncate(index); 5355 } 5356 5357 if (blacklistedAuthTokens->contains(scheme)) { 5358 it = authTokens.erase(it); 5359 } else { 5360 it++; 5361 } 5362 } 5363 5364 try_next_auth_scheme: 5365 QByteArray bestOffer = KAbstractHttpAuthentication::bestOffer(authTokens); 5366 if (*auth) { 5367 const QByteArray authScheme((*auth)->scheme().trimmed()); 5368 if (qstrnicmp(authScheme.constData(), bestOffer.constData(), authScheme.length()) != 0) { 5369 // huh, the strongest authentication scheme offered has changed. 5370 delete *auth; 5371 *auth = nullptr; 5372 } 5373 } 5374 5375 if (!(*auth)) { 5376 *auth = KAbstractHttpAuthentication::newAuth(bestOffer, config()); 5377 } 5378 5379 if (*auth) { 5380 qCDebug(KIO_HTTP) << "Trying authentication scheme:" << (*auth)->scheme(); 5381 5382 // remove trailing space from the method string, or digest auth will fail 5383 (*auth)->setChallenge(bestOffer, authinfo.url, m_request.sentMethodString); 5384 5385 QString username; 5386 QString password; 5387 bool generateAuthHeader = true; 5388 if ((*auth)->needCredentials()) { 5389 // use credentials supplied by the application if available 5390 if (!m_request.url.userName().isEmpty() && !m_request.url.password().isEmpty() && *triedCredentials == NoCredentials) { 5391 username = m_request.url.userName(); 5392 password = m_request.url.password(); 5393 // don't try this password any more 5394 *triedCredentials = JobCredentials; 5395 } else { 5396 // try to get credentials from kpasswdserver's cache, then try asking the user. 5397 authinfo.verifyPath = false; // we have realm, no path based checking please! 5398 authinfo.realmValue = (*auth)->realm(); 5399 if (authinfo.realmValue.isEmpty() && !(*auth)->supportsPathMatching()) { 5400 authinfo.realmValue = QLatin1String((*auth)->scheme()); 5401 } 5402 5403 // Save the current authinfo url because it can be modified by the call to 5404 // checkCachedAuthentication. That way we can restore it if the call 5405 // modified it. 5406 const QUrl reqUrl = authinfo.url; 5407 if (!errorMsg.isEmpty() || !checkCachedAuthentication(authinfo)) { 5408 // Reset url to the saved url... 5409 authinfo.url = reqUrl; 5410 authinfo.keepPassword = true; 5411 authinfo.comment = i18n("<b>%1</b> at <b>%2</b>", authinfo.realmValue.toHtmlEscaped(), authinfo.url.host()); 5412 5413 const int errorCode = openPasswordDialog(authinfo, errorMsg); 5414 if (errorCode) { 5415 generateAuthHeader = false; 5416 authRequiresAnotherRoundtrip = false; 5417 if (!sendErrorPageNotification()) { 5418 (void)/* continue processing the header*/ error(ERR_ACCESS_DENIED, reqUrl.host()); 5419 } 5420 qCDebug(KIO_HTTP) << "looks like the user canceled the authentication dialog"; 5421 delete *auth; 5422 *auth = nullptr; 5423 } 5424 *triedCredentials = UserInputCredentials; 5425 } else { 5426 *triedCredentials = CachedCredentials; 5427 } 5428 username = authinfo.username; 5429 password = authinfo.password; 5430 } 5431 } 5432 5433 if (generateAuthHeader) { 5434 (*auth)->generateResponse(username, password); 5435 (*auth)->setCachePasswordEnabled(authinfo.keepPassword); 5436 5437 qCDebug(KIO_HTTP) << "isError=" << (*auth)->isError() << "needCredentials=" << (*auth)->needCredentials() 5438 << "forceKeepAlive=" << (*auth)->forceKeepAlive() << "forceDisconnect=" << (*auth)->forceDisconnect(); 5439 5440 if ((*auth)->isError()) { 5441 QByteArray scheme((*auth)->scheme().trimmed()); 5442 qCDebug(KIO_HTTP) << "Blacklisting auth" << scheme; 5443 authTokens.removeOne(scheme); 5444 blacklistedAuthTokens->append(scheme); 5445 if (!authTokens.isEmpty()) { 5446 goto try_next_auth_scheme; 5447 } else { 5448 if (!sendErrorPageNotification()) { 5449 (void)/* continue processing the header*/ error(ERR_UNSUPPORTED_ACTION, i18n("Authorization failed.")); 5450 } 5451 authRequiresAnotherRoundtrip = false; 5452 } 5453 // ### return false; ? 5454 } else if ((*auth)->forceKeepAlive()) { 5455 // ### think this through for proxied / not proxied 5456 m_request.isKeepAlive = true; 5457 } else if ((*auth)->forceDisconnect()) { 5458 // ### think this through for proxied / not proxied 5459 m_request.isKeepAlive = false; 5460 httpCloseConnection(); 5461 } 5462 } 5463 } else { 5464 authRequiresAnotherRoundtrip = false; 5465 if (!sendErrorPageNotification()) { 5466 (void)/* continue processing the header*/ error(ERR_UNSUPPORTED_ACTION, i18n("Unknown Authorization method.")); 5467 } 5468 } 5469 } 5470 5471 return authRequiresAnotherRoundtrip; 5472 } 5473 5474 KIO::WorkerResult HTTPProtocol::copyPut(const QUrl &src, const QUrl &dest, JobFlags flags) 5475 { 5476 qCDebug(KIO_HTTP) << src << "->" << dest; 5477 5478 if (const auto result = maybeSetRequestUrl(dest); !result.success()) { 5479 return result; 5480 } 5481 5482 resetSessionSettings(); 5483 5484 if (!(flags & KIO::Overwrite)) { 5485 // check to make sure this host supports WebDAV 5486 if (const auto result = davHostOk(); !result.success()) { 5487 return result; 5488 } 5489 5490 // Checks if the destination exists and return an error if it does. 5491 if (davDestinationExists()) { 5492 return WorkerResult::fail(ERR_FILE_ALREADY_EXIST, dest.fileName()); 5493 } 5494 } 5495 5496 m_POSTbuf = new QFile(src.toLocalFile()); 5497 if (!m_POSTbuf->open(QFile::ReadOnly)) { 5498 return error(KIO::ERR_CANNOT_OPEN_FOR_READING, src.fileName()); 5499 } 5500 5501 m_request.method = HTTP_PUT; 5502 m_request.cacheTag.policy = CC_Reload; 5503 5504 return proceedUntilResponseContent(); 5505 } 5506 5507 bool HTTPProtocol::davDestinationExists() 5508 { 5509 const QByteArray request( 5510 "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" 5511 "<D:propfind xmlns:D=\"DAV:\"><D:prop>" 5512 "<D:creationdate/>" 5513 "<D:getcontentlength/>" 5514 "<D:displayname/>" 5515 "<D:resourcetype/>" 5516 "</D:prop></D:propfind>"); 5517 davSetRequest(request); 5518 5519 // WebDAV Stat or List... 5520 m_request.method = DAV_PROPFIND; 5521 m_request.url.setQuery(QString()); 5522 m_request.cacheTag.policy = CC_Reload; 5523 m_request.davData.depth = 0; 5524 5525 (void)/* handling result via response codes */ proceedUntilResponseContent(true); 5526 5527 if (!m_request.isKeepAlive) { 5528 httpCloseConnection(); // close connection if server requested it. 5529 m_request.isKeepAlive = true; // reset the keep alive flag. 5530 } 5531 5532 if (m_request.responseCode >= 200 && m_request.responseCode < 300) { 5533 // 2XX means the file exists. This includes 207 (multi-status response). 5534 qCDebug(KIO_HTTP) << "davDestinationExists: file exists. code:" << m_request.responseCode; 5535 return true; 5536 } else { 5537 qCDebug(KIO_HTTP) << "davDestinationExists: file does not exist. code:" << m_request.responseCode; 5538 } 5539 5540 // force re-authentication... 5541 delete m_wwwAuth; 5542 m_wwwAuth = nullptr; 5543 5544 return false; 5545 } 5546 5547 KIO::WorkerResult HTTPProtocol::fileSystemFreeSpace(const QUrl &url) 5548 { 5549 qCDebug(KIO_HTTP) << url; 5550 5551 if (const auto result = maybeSetRequestUrl(url); !result.success()) { 5552 return result; 5553 } 5554 resetSessionSettings(); 5555 5556 return davStatList(url); 5557 } 5558 5559 // needed for JSON file embedding 5560 #include "http.moc" 5561 5562 #include "moc_http.cpp"