File indexing completed on 2024-04-28 03:56:24

0001 /*
0002     SPDX-FileCopyrightText: 2016 Dan Leinir Turthra Jensen <admin@leinir.dk>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-or-later
0005 */
0006 
0007 #include "httpworker.h"
0008 
0009 #include "knewstuff_version.h"
0010 #include "knewstuffcore_debug.h"
0011 
0012 #include <QCoreApplication>
0013 #include <QFile>
0014 #include <QMutex>
0015 #include <QMutexLocker>
0016 #include <QNetworkAccessManager>
0017 #include <QNetworkDiskCache>
0018 #include <QNetworkRequest>
0019 #include <QStandardPaths>
0020 #include <QStorageInfo>
0021 #include <QThread>
0022 
0023 class HTTPWorkerNAM
0024 {
0025 public:
0026     HTTPWorkerNAM()
0027     {
0028         QMutexLocker locker(&mutex);
0029         const QString cacheLocation = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QStringLiteral("/knewstuff");
0030         cache.setCacheDirectory(cacheLocation);
0031         QStorageInfo storageInfo(cacheLocation);
0032         cache.setMaximumCacheSize(qMin(50 * 1024 * 1024, (int)(storageInfo.bytesTotal() / 1000)));
0033         nam.setCache(&cache);
0034     }
0035     QNetworkAccessManager nam;
0036     QMutex mutex;
0037 
0038     QNetworkReply *get(const QNetworkRequest &request)
0039     {
0040         QMutexLocker locker(&mutex);
0041         return nam.get(request);
0042     }
0043 
0044     QNetworkDiskCache cache;
0045 };
0046 
0047 Q_GLOBAL_STATIC(HTTPWorkerNAM, s_httpWorkerNAM)
0048 
0049 using namespace KNSCore;
0050 
0051 class KNSCore::HTTPWorkerPrivate
0052 {
0053 public:
0054     HTTPWorkerPrivate()
0055         : jobType(HTTPWorker::GetJob)
0056         , reply(nullptr)
0057     {
0058     }
0059     HTTPWorker::JobType jobType;
0060     QUrl source;
0061     QUrl destination;
0062     QNetworkReply *reply;
0063     QUrl redirectUrl;
0064 
0065     QFile dataFile;
0066 };
0067 
0068 HTTPWorker::HTTPWorker(const QUrl &url, JobType jobType, QObject *parent)
0069     : QObject(parent)
0070     , d(new HTTPWorkerPrivate)
0071 {
0072     d->jobType = jobType;
0073     d->source = url;
0074 }
0075 
0076 HTTPWorker::HTTPWorker(const QUrl &source, const QUrl &destination, KNSCore::HTTPWorker::JobType jobType, QObject *parent)
0077     : QObject(parent)
0078     , d(new HTTPWorkerPrivate)
0079 {
0080     d->jobType = jobType;
0081     d->source = source;
0082     d->destination = destination;
0083 }
0084 
0085 HTTPWorker::~HTTPWorker() = default;
0086 
0087 void HTTPWorker::setUrl(const QUrl &url)
0088 {
0089     d->source = url;
0090 }
0091 
0092 static void addUserAgent(QNetworkRequest &request)
0093 {
0094     QString agentHeader = QStringLiteral("KNewStuff/%1").arg(QLatin1String(KNEWSTUFF_VERSION_STRING));
0095     if (QCoreApplication::instance()) {
0096         agentHeader += QStringLiteral("-%1/%2").arg(QCoreApplication::instance()->applicationName(), QCoreApplication::instance()->applicationVersion());
0097     }
0098     request.setHeader(QNetworkRequest::UserAgentHeader, agentHeader);
0099     // If the remote supports HTTP/2, then we should definitely be using that
0100     request.setAttribute(QNetworkRequest::Http2AllowedAttribute, true);
0101 
0102     // Assume that no cache expiration time will be longer than a week, but otherwise prefer the cache
0103     // This is mildly hacky, but if we don't do this, we end up with infinite cache expirations in some
0104     // cases, which of course isn't really acceptable... See ed62ee20 for a situation where that happened.
0105     QNetworkCacheMetaData cacheMeta{s_httpWorkerNAM->cache.metaData(request.url())};
0106     if (cacheMeta.isValid()) {
0107         const QDateTime nextWeek{QDateTime::currentDateTime().addDays(7)};
0108         if (cacheMeta.expirationDate().isValid() && cacheMeta.expirationDate() < nextWeek) {
0109             request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache);
0110         }
0111     }
0112 }
0113 
0114 void HTTPWorker::startRequest()
0115 {
0116     if (d->reply) {
0117         // only run one request at a time...
0118         return;
0119     }
0120 
0121     QNetworkRequest request(d->source);
0122     addUserAgent(request);
0123     d->reply = s_httpWorkerNAM->get(request);
0124     connect(d->reply, &QNetworkReply::readyRead, this, &HTTPWorker::handleReadyRead);
0125     connect(d->reply, &QNetworkReply::finished, this, &HTTPWorker::handleFinished);
0126     if (d->jobType == DownloadJob) {
0127         d->dataFile.setFileName(d->destination.toLocalFile());
0128         connect(this, &HTTPWorker::data, this, &HTTPWorker::handleData);
0129     }
0130 }
0131 
0132 void HTTPWorker::handleReadyRead()
0133 {
0134     QMutexLocker locker(&s_httpWorkerNAM->mutex);
0135     if (d->reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isNull()) {
0136         do {
0137             Q_EMIT data(d->reply->read(32768));
0138         } while (!d->reply->atEnd());
0139     }
0140 }
0141 
0142 void HTTPWorker::handleFinished()
0143 {
0144     qCDebug(KNEWSTUFFCORE) << Q_FUNC_INFO << d->reply->url();
0145     if (d->reply->error() != QNetworkReply::NoError) {
0146         qCWarning(KNEWSTUFFCORE) << d->reply->errorString();
0147         if (d->reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() > 100) {
0148             // In this case, we're being asked to wait a bit...
0149             Q_EMIT httpError(d->reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(), d->reply->rawHeaderPairs());
0150         }
0151         Q_EMIT error(d->reply->errorString());
0152     }
0153 
0154     // Check if the data was obtained from cache or not
0155     QString fromCache = d->reply->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool() ? QStringLiteral("(cached)") : QStringLiteral("(NOT cached)");
0156 
0157     // Handle redirections
0158     const QUrl possibleRedirectUrl = d->reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
0159     if (!possibleRedirectUrl.isEmpty() && possibleRedirectUrl != d->redirectUrl) {
0160         d->redirectUrl = d->reply->url().resolved(possibleRedirectUrl);
0161         if (d->redirectUrl.scheme().startsWith(QLatin1String("http"))) {
0162             qCDebug(KNEWSTUFFCORE) << d->reply->url().toDisplayString() << "was redirected to" << d->redirectUrl.toDisplayString() << fromCache
0163                                    << d->reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
0164             d->reply->deleteLater();
0165             QNetworkRequest request(d->redirectUrl);
0166             addUserAgent(request);
0167             d->reply = s_httpWorkerNAM->get(request);
0168             connect(d->reply, &QNetworkReply::readyRead, this, &HTTPWorker::handleReadyRead);
0169             connect(d->reply, &QNetworkReply::finished, this, &HTTPWorker::handleFinished);
0170             return;
0171         } else {
0172             qCWarning(KNEWSTUFFCORE) << "Redirection to" << d->redirectUrl.toDisplayString() << "forbidden.";
0173         }
0174     } else {
0175         qCDebug(KNEWSTUFFCORE) << "Data for" << d->reply->url().toDisplayString() << "was fetched" << fromCache;
0176     }
0177 
0178     if (d->dataFile.isOpen()) {
0179         d->dataFile.close();
0180     }
0181 
0182     d->redirectUrl.clear();
0183     Q_EMIT completed();
0184 }
0185 
0186 void HTTPWorker::handleData(const QByteArray &data)
0187 {
0188     // It turns out that opening a file and then leaving it hanging without writing to it immediately will, at times
0189     // leave you with a file that suddenly (seemingly magically) no longer exists. Thanks for that.
0190     if (!d->dataFile.isOpen()) {
0191         if (d->dataFile.open(QIODevice::WriteOnly)) {
0192             qCDebug(KNEWSTUFFCORE) << "Opened file" << d->dataFile.fileName() << "for writing.";
0193         } else {
0194             qCWarning(KNEWSTUFFCORE) << "Failed to open file for writing!";
0195             Q_EMIT error(QStringLiteral("Failed to open file %1 for writing!").arg(d->destination.toLocalFile()));
0196         }
0197     }
0198     qCDebug(KNEWSTUFFCORE) << "Writing" << data.length() << "bytes of data to" << d->dataFile.fileName();
0199     quint64 written = d->dataFile.write(data);
0200     if (d->dataFile.error()) {
0201         qCDebug(KNEWSTUFFCORE) << "File has error" << d->dataFile.errorString();
0202     }
0203     qCDebug(KNEWSTUFFCORE) << "Wrote" << written << "bytes. File is now size" << d->dataFile.size();
0204 }
0205 
0206 #include "moc_httpworker.cpp"