File indexing completed on 2024-05-19 15:15:49

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"