Files
PrismLauncher/launcher/net/Request.cpp
T
Octol1ttle e109b6de7e fix build
Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
2026-09-13 18:15:32 +05:00

599 lines
21 KiB
C++

// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2022 flowln <flowlnlnln@gmail.com>
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
* Copyright (C) 2023 TheKodeToad <TheKodeToad@proton.me>
* Copyright (C) 2023 Rachel Powers <508861+Ryex@users.noreply.github.com>
* Copyright (c) 2023 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "Request.h"
#include <QDateTime>
#include <QFileInfo>
#include <QHttpMultiPart>
#include <QIODevice>
#include <QLocale>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QUrl>
#include <cstdint>
#include <memory>
#include <utility>
#include <variant>
#ifdef LAUNCHER_APPLICATION
#include "Application.h"
#include "net/ApiHeaderProxy.h"
#include "net/ChecksumValidator.h"
#include "net/MetaCacheSink.h"
#include "settings/SettingsObject.h"
#else
#include "BuildConfig.h"
#endif
#include "net/ByteArraySink.h"
#include "net/FileSink.h"
#include "net/Logging.h"
#include "tasks/Task.h"
#include "MMCTime.h"
#include "StringUtils.h"
namespace Net {
namespace {
auto logCatForMethod(HttpMethod method) -> Request::LogCatFunc
{
switch (method.value()) {
case HttpMethod::Get:
return taskDownloadLogC;
case HttpMethod::Post:
return taskUploadLogC;
default:
break;
}
return taskNetLogC;
}
} // namespace
Request::Request() : Request(Spec{}) {}
Request::Request(const QUrl& url, Options options, const QString& name)
: Request(Spec{ .method = HttpMethod::Get, .url = url, .data = std::monostate{}, .options = options, .name = name })
{}
Request::Request(const QUrl& url, QByteArray postData, Options options)
: Request(Spec{ .method = HttpMethod::Post, .url = url, .data = std::move(postData), .options = options })
{}
Request::Request(const Spec& spec) : m_options(spec.options), m_url(spec.url), m_httpMethod(spec.method), m_postData(spec.data)
{
connect(&m_retryTimer, &QTimer::timeout, this, &Request::executeTask);
if (spec.name.isEmpty()) {
setObjectName(QString("BYTES:") + m_url.toString());
} else {
setObjectName(spec.name);
}
m_logCat = logCatForMethod(m_httpMethod);
#ifdef LAUNCHER_APPLICATION
if (spec.options.testFlag(Option::AddAPIHeaders)) {
addHeaderProxy(std::make_unique<ApiHeaderProxy>());
}
#endif
}
void Request::addValidator(Validator* v)
{
m_sink->addValidator(v);
}
void Request::executeTask()
{
setStatus(tr("Requesting %1").arg(StringUtils::truncateUrlHumanFriendly(m_url, 80)));
if (m_network == nullptr) {
#ifdef LAUNCHER_APPLICATION
m_network = APPLICATION->network();
#else
qCCritical(m_logCat) << getUid().toString() << "No network manager set for request:" << m_url.toString();
emit failed("No network manager set for request");
emit finished();
return;
#endif
}
if (getState() == Task::State::AbortedByUser) {
qCWarning(m_logCat) << getUid().toString() << "Attempt to start an aborted Request:" << m_url.toString();
emit aborted();
emit finished();
return;
}
QNetworkRequest request(m_url);
auto result = m_sink->init(request);
if (!result) {
m_state = Task::State::Failed;
m_failReason = result.error();
emit failed(m_failReason);
emit finished();
return;
}
switch (*result) {
case Sink::InitType::Ok:
qCDebug(m_logCat) << getUid().toString() << "Running" << m_url.toString();
break;
case Sink::InitType::CacheHit:
m_state = Task::State::Succeeded;
qCDebug(m_logCat) << getUid().toString() << "Request cache hit" << m_url.toString();
emit succeeded();
emit finished();
return;
}
#ifdef LAUNCHER_APPLICATION
auto userAgent = APPLICATION->getUserAgent();
#else
auto userAgent = BuildConfig.USER_AGENT;
#endif
request.setHeader(QNetworkRequest::UserAgentHeader, userAgent.toUtf8());
for (auto& headerProxy : m_headerProxies) {
headerProxy->writeHeaders(request);
}
#ifdef LAUNCHER_APPLICATION
request.setTransferTimeout(APPLICATION->settings()->get("RequestTimeout").toInt() * 1000);
#else
request.setTransferTimeout();
#endif
m_lastProgressTime = std::chrono::steady_clock::now();
m_lastProgressBytes = 0;
auto* rep = getReply(request);
if (rep == nullptr) { // it failed
return;
}
m_reply.reset(rep);
connect(rep, &QNetworkReply::uploadProgress, this, &Request::onProgress);
connect(rep, &QNetworkReply::downloadProgress, this, &Request::onProgress);
connect(rep, &QNetworkReply::finished, this, &Request::downloadFinished);
connect(rep, &QNetworkReply::errorOccurred, this, &Request::downloadError);
connect(rep, &QNetworkReply::sslErrors, this, &Request::sslErrors);
connect(rep, &QNetworkReply::readyRead, this, &Request::downloadReadyRead);
}
void Request::onProgress(qint64 bytesReceived, qint64 bytesTotal)
{
auto now = std::chrono::steady_clock::now();
auto elapsed = now - m_lastProgressTime;
// use milliseconds for speed precision
auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(elapsed);
auto bytesReceivedSince = bytesReceived - m_lastProgressBytes;
auto dlSpeedBps = static_cast<double>(bytesReceivedSince) / static_cast<double>(elapsedMs.count()) * 1000;
auto remainingTimeS = static_cast<double>(bytesTotal - bytesReceived) / dlSpeedBps;
//: Current amount of bytes downloaded, out of the total amount of bytes in the download
QString dlProgress = tr("%1 / %2")
.arg(StringUtils::humanReadableFileSize(static_cast<double>(bytesReceived)))
.arg(StringUtils::humanReadableFileSize(static_cast<double>(bytesTotal)));
QString dlSpeedStr;
if (elapsedMs.count() > 0) {
auto strEta = bytesTotal > 0 ? Time::humanReadableDuration(remainingTimeS) : tr("unknown");
//: Download speed, in bytes per second (remaining download time in parenthesis)
dlSpeedStr = tr("%1 /s (%2)").arg(StringUtils::humanReadableFileSize(dlSpeedBps)).arg(strEta);
} else {
//: Download speed at 0 bytes per second
dlSpeedStr = tr("0 B/s");
}
setDetails(dlProgress + "\n" + dlSpeedStr);
setProgress(bytesReceived, bytesTotal);
}
void Request::downloadError(QNetworkReply::NetworkError error)
{
if (error == QNetworkReply::OperationCanceledError) {
qCCritical(m_logCat) << getUid().toString() << "Aborted" << m_url.toString();
m_state = State::Failed;
} else if (replyStatusCode() == 429 /* HTTP Too Many Requests*/ && m_options.testFlag(Option::AutoRetry)) {
qCDebug(m_logCat) << getUid().toString() << "Rate Limited!";
auto delay = static_cast<int64_t>(10 * std::pow(2, m_retryCount));
if (m_reply->hasRawHeader("Retry-After")) {
auto retryAfter = m_reply->rawHeader("Retry-After");
if (retryAfter.trimmed().endsWith("GMT")) /* HTTP Date format */ {
auto afterTimestamp = QDateTime::fromString(QString::fromUtf8(retryAfter.trimmed()), "ddd, dd MMM yyyy HH:mm:ss 'GMT'");
if (afterTimestamp.isValid()) {
auto seconds = QDateTime::currentDateTime().secsTo(afterTimestamp);
if (seconds > 0) {
delay = seconds;
}
}
} else {
bool ok = false;
auto seconds = retryAfter.toLongLong(&ok);
if (ok && seconds > 0) {
delay = seconds;
}
}
}
handleAutoRetry(delay);
} else {
if (m_options.testFlag(Option::AcceptLocalFiles)) {
if (m_sink->hasLocalData()) {
m_state = State::Succeeded;
return;
}
}
// error happened during download.
qCCritical(m_logCat) << getUid().toString() << "Failed" << m_url.toString() << "with error" << error;
if (m_reply) {
qCCritical(m_logCat) << getUid().toString() << "HTTP status:" << replyStatusCode() << errorString();
}
if (m_errorResponse.size() > 0) {
qCCritical(m_logCat) << getUid().toString() << "Response from server:" << m_errorResponse;
}
m_state = State::Failed;
}
}
void Request::sslErrors(const QList<QSslError>& errors)
{
int i = 1;
for (const auto& error : errors) {
qCCritical(m_logCat).nospace() << getUid().toString() << " Request " << m_url.toString() << " SSL Error #" << i << ": "
<< error.errorString();
auto cert = error.certificate();
qCCritical(m_logCat) << getUid().toString() << "Certificate in question:\n" << cert.toText();
i++;
}
}
auto Request::handleRedirect() -> bool
{
QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl();
if (!redirect.isValid()) {
if (!m_reply->hasRawHeader("Location")) {
// no redirect -> it's fine to continue
return false;
}
// there is a Location header, but it's not correct. we need to apply some workarounds...
QByteArray redirectBA = m_reply->rawHeader("Location");
if (redirectBA.size() == 0) {
// empty, yet present redirect header? WTF?
return false;
}
QString redirectStr = QString::fromUtf8(redirectBA);
if (redirectStr.startsWith("//")) {
/*
* IF the URL begins with //, we need to insert the URL scheme.
* See: https://bugreports.qt.io/browse/QTBUG-41061
* See: http://tools.ietf.org/html/rfc3986#section-4.2
*/
redirectStr = m_reply->url().scheme() + ":" + redirectStr;
} else if (redirectStr.startsWith("/")) {
/*
* IF the URL begins with /, we need to process it as a relative URL
*/
auto url = m_reply->url();
url.setPath(redirectStr, QUrl::TolerantMode);
redirectStr = url.toString();
}
/*
* Next, make sure the URL is parsed in tolerant mode. Qt doesn't parse the location header in tolerant mode, which causes
* issues.
* FIXME: report Qt bug for this
*/
redirect = QUrl(redirectStr, QUrl::TolerantMode);
if (!redirect.isValid()) {
qCWarning(m_logCat) << getUid().toString() << "Failed to parse redirect URL:" << redirectStr;
downloadError(QNetworkReply::ProtocolFailure);
return false;
}
qCDebug(m_logCat) << getUid().toString() << "Fixed location header:" << redirect;
} else {
qCDebug(m_logCat) << getUid().toString() << "Location header:" << redirect;
}
m_url = QUrl(redirect.toString());
qCDebug(m_logCat) << getUid().toString() << "Following redirect to" << m_url.toString();
if (++m_redirectCount > 10) {
qCWarning(m_logCat) << getUid().toString() << "Too many redirects for" << m_url.toString();
m_sink->abort();
emitFailed(tr("Too many redirects"));
return true;
}
executeTask();
return true;
}
void Request::handleAutoRetry(int64_t delay)
{
m_retryCount++;
if (delay > 60 || m_retryCount > 4) {
/* 1 minute is too long to wait for retry, fail for now */
m_state = State::Failed;
auto retryAfter = QDateTime::currentDateTime().addSecs(delay);
emitFailed(tr("Request Rate Limited for %n second(s): Retry After %1", "seconds", static_cast<int>(delay))
.arg(retryAfter.toLocalTime().toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat))));
return;
}
qCDebug(m_logCat) << getUid().toString() << "Retyring Request in" << delay << "seconds";
setStatus(tr("Rate Limited: Waiting %n second(s)", "seconds", static_cast<int>(delay)));
m_retryTimer.setTimerType(Qt::VeryCoarseTimer);
m_retryTimer.setSingleShot(true);
m_retryTimer.setInterval(static_cast<int>(delay) * 1000);
m_retryTimer.start();
}
void Request::downloadFinished()
{
// currently waiting for retry
if (m_retryTimer.isActive()) {
return;
}
// handle HTTP redirection first
if (handleRedirect()) {
qCDebug(m_logCat) << getUid().toString() << "Request redirected:" << m_url.toString();
return;
}
// if the download failed before this point ...
if (m_state == State::Succeeded) // pretend to succeed so we continue processing :)
{
qCDebug(m_logCat) << getUid().toString() << "Request failed but we are allowed to proceed:" << m_url.toString();
m_sink->abort();
emit succeeded();
emit finished();
return;
}
if (m_state == State::Failed) {
qCDebug(m_logCat) << getUid().toString() << "Request failed in previous step:" << m_url.toString();
m_sink->abort();
m_failReason = m_reply->errorString();
emit failed(m_reply->errorString());
emit finished();
return;
}
if (m_state == State::AbortedByUser) {
qCDebug(m_logCat) << getUid().toString() << "Request aborted in previous step:" << m_url.toString();
m_sink->abort();
emit aborted();
emit finished();
return;
}
// make sure we got all the remaining data, if any
auto data = m_reply->readAll();
if (!data.isEmpty()) {
qCDebug(m_logCat) << getUid().toString() << "Writing extra" << data.size() << "bytes";
auto result = m_sink->write(data);
if (!result) {
m_state = Task::State::Failed;
qCDebug(m_logCat) << getUid().toString() << "Request failed to write:" << m_url.toString();
m_sink->abort();
m_failReason = result.error();
emit failed(m_failReason);
emit finished();
return;
}
}
// otherwise, finalize the whole graph
auto result = m_sink->finalize(*m_reply);
if (!result) {
m_state = Task::State::Failed;
qCDebug(m_logCat) << getUid().toString() << "Request failed to finalize:" << m_url.toString();
m_sink->abort();
m_failReason = result.error();
emit failed(m_failReason);
emit finished();
return;
}
qCDebug(m_logCat) << getUid().toString() << "Request succeeded:" << m_url.toString();
emit succeeded();
emit finished();
}
void Request::downloadReadyRead()
{
if (m_state == State::Running) {
auto data = m_reply->readAll();
auto result = m_sink->write(data);
if (replyStatusCode() >= 400) {
m_errorResponse.append(data);
}
if (!result) {
m_state = Task::State::Failed;
m_failReason = result.error();
qCCritical(m_logCat) << getUid().toString() << "Failed to process response chunk:" << m_failReason;
}
// qDebug() << "Request" << m_url.toString() << "gained" << data.size() << "bytes";
} else {
qCCritical(m_logCat) << getUid().toString() << "Cannot write download data! illegal status" << m_status;
}
}
auto Request::abort() -> bool
{
m_retryTimer.stop();
m_state = State::AbortedByUser;
if (m_reply && !m_reply->isFinished()) {
disconnect(m_reply.get(), &QNetworkReply::errorOccurred, nullptr, nullptr);
m_reply->abort();
} else {
emit aborted();
emit finished();
}
return true;
}
int Request::replyStatusCode() const
{
return m_reply ? m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() : -1;
}
QNetworkReply::NetworkError Request::error() const
{
return m_reply ? m_reply->error() : QNetworkReply::NoError;
}
QUrl Request::url() const
{
return m_url;
}
QString Request::errorString() const
{
return m_reply ? m_reply->errorString() : "";
}
void Request::enableAutoRetry(bool enable)
{
if (enable) {
m_options |= Option::AutoRetry;
} else {
m_options &= ~static_cast<std::uint8_t>(Option::AutoRetry);
}
}
QNetworkReply* Request::getReply(QNetworkRequest& request)
{
if (m_httpMethod == HttpMethod::Get) {
Q_ASSERT(std::holds_alternative<std::monostate>(m_postData));
return m_network->get(request);
}
return std::visit(
[this, &request](const auto& data) -> QNetworkReply* {
using T = std::remove_cvref_t<decltype(data)>;
const auto verb = m_httpMethod.toString().toUtf8();
if constexpr (std::is_same_v<T, QByteArray>) {
if (m_httpMethod == HttpMethod::Post && !request.hasRawHeader("Content-Type")) {
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
}
return m_network->sendCustomRequest(request, verb, data);
} else if constexpr (std::is_same_v<T, ByteArrayFactory>) {
if (m_httpMethod == HttpMethod::Post && !request.hasRawHeader("Content-Type")) {
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
}
auto body = data();
if (!body) {
emitFailed(body.error());
return nullptr;
}
return m_network->sendCustomRequest(request, verb, body.value());
} else if constexpr (std::is_same_v<T, std::monostate>) {
return m_network->sendCustomRequest(request, verb);
} else if constexpr (std::is_same_v<T, DeviceFactory>) {
auto payload = data();
if (!payload) {
emitFailed(payload.error());
return nullptr;
}
auto device = payload.value();
if (device != nullptr) {
device->setParent(this);
}
return m_network->sendCustomRequest(request, verb, device);
} else if constexpr (std::is_same_v<T, MultiPartFactory>) {
auto payload = data();
if (!payload) {
emitFailed(payload.error());
return nullptr;
}
auto multiPart = payload.value();
if (multiPart != nullptr) {
multiPart->setParent(this);
}
return m_network->sendCustomRequest(request, verb, multiPart);
}
},
m_postData);
}
#ifdef LAUNCHER_APPLICATION
auto Request::makeCached(const QUrl& url, MetaEntryPtr entry, Options options) -> Ptr
{
auto dl = Ptr(new Request(url, options, (QString("CACHE:") + url.toString())));
auto* md5Node = new ChecksumValidator(QCryptographicHash::Md5);
auto* cachedNode = new MetaCacheSink(std::move(entry), md5Node, options.testFlag(Option::MakeEternal));
dl->m_sink.reset(cachedNode);
return dl;
}
#endif
auto Request::makeByteArray(const QUrl& url, QByteArray postData, Options options) -> std::pair<Ptr, QByteArray*>
{
auto dl = Ptr(new Request(url, std::move(postData), options));
auto sink = std::make_unique<ByteArraySink>();
auto* response = sink->output();
dl->m_sink = std::move(sink);
return { dl, response };
}
auto Request::makeByteArray(const QUrl& url, Options options) -> std::pair<Ptr, QByteArray*>
{
auto dl = Ptr(new Request(url, options));
auto sink = std::make_unique<ByteArraySink>();
auto* response = sink->output();
dl->m_sink = std::move(sink);
return { dl, response };
}
auto Request::makeFile(const QUrl& url, const QString& path, Options options) -> Ptr
{
auto dl = Ptr(new Request(url, options, QString("FILE:") + url.toString()));
dl->m_sink = std::make_unique<FileSink>(path);
return dl;
}
auto Request::makeCustomRequest(const Spec& spec) -> Ptr
{
return Ptr(new Request(spec));
}
} // namespace Net