/* * The Mana Client * Copyright (C) 2009-2012 The Mana Developers * * This file is part of The Mana Client. * * 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; either version 2 of the License, or * any later version. * * 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 . */ #include "net/download.h" #include "client.h" #include "main.h" #include "configuration.h" #include "log.h" #include "main.h" #include "utils/stringutils.h" #include #include #include constexpr char DOWNLOAD_ERROR_MESSAGE_THREAD[] = "Could not create download thread!"; namespace Net { unsigned long Download::fadler32(FILE *file) { if (!file || fseek(file, 0, SEEK_END) != 0) return 0; const long fileSize = ftell(file); if (fileSize < 0) return 0; rewind(file); void *buffer = malloc(fileSize); const size_t read = fread(buffer, 1, fileSize, file); unsigned long adler = adler32_z(0L, Z_NULL, 0); adler = adler32_z(adler, (Bytef*) buffer, read); free(buffer); return adler; } Download::Download(const std::string &url) : mUrl(url) { mError[0] = 0; } Download::~Download() { mCancel = true; SDL_WaitThread(mThread, nullptr); curl_slist_free_all(mHeaders); free(mBuffer); } void Download::addHeader(const char *header) { assert(!mThread); mHeaders = curl_slist_append(mHeaders, header); } void Download::noCache() { addHeader("pragma: no-cache"); addHeader("Cache-Control: no-cache"); } void Download::setFile(const std::string &filename, std::optional adler32) { assert(!mThread); mMemoryWrite = false; mFileName = filename; mAdler = adler32; } void Download::setUseBuffer() { assert(!mThread); mMemoryWrite = true; } bool Download::start() { assert(!mThread); logger->log("Starting download: %s", mUrl.c_str()); mThread = SDL_CreateThread(downloadThread, "Download", this); if (!mThread) { logger->log("%s", DOWNLOAD_ERROR_MESSAGE_THREAD); strncpy(mError, DOWNLOAD_ERROR_MESSAGE_THREAD, CURL_ERROR_SIZE - 1); mState.lock()->status = DownloadStatus::Error; return false; } return true; } void Download::cancel() { logger->log("Canceling download: %s", mUrl.c_str()); mCancel = true; } std::string_view Download::getBuffer() const { assert(mMemoryWrite); return std::string_view(mBuffer, mDownloadedBytes); } int Download::downloadProgress(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { auto *d = reinterpret_cast(clientp); auto state = d->mState.lock(); state->status = DownloadStatus::InProgress; state->progress = 0.0f; if (dltotal > 0) state->progress = static_cast(dlnow) / dltotal; return d->mCancel; } size_t Download::writeBuffer(char *ptr, size_t size, size_t nmemb, void *stream) { auto *d = reinterpret_cast(stream); const size_t totalMem = size * nmemb; d->mBuffer = (char *) realloc(d->mBuffer, d->mDownloadedBytes + totalMem); if (d->mBuffer) { memcpy(d->mBuffer + d->mDownloadedBytes, ptr, totalMem); d->mDownloadedBytes += totalMem; } return totalMem; } int Download::downloadThread(void *ptr) { auto *d = reinterpret_cast(ptr); bool complete = false; std::string outFilename; curl_off_t resumeOffset = 0; if (!d->mMemoryWrite) outFilename = d->mFileName + ".part"; for (int attempts = 0; attempts < 5 && !complete && !d->mCancel; ++attempts) { if (attempts > 0) { SDL_Delay(2000 * (1 << attempts)); // 2s, 4s, 8s, 16s logger->log("Retry attempt %d for %s at offset %lld", attempts + 1, d->mUrl.c_str(), resumeOffset); } CURL *curl = curl_easy_init(); if (!curl) break; logger->log("Downloading: %s", d->mUrl.c_str()); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, d->mHeaders); FILE *file = nullptr; if (d->mMemoryWrite) { curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &Download::writeBuffer); curl_easy_setopt(curl, CURLOPT_WRITEDATA, ptr); } else { file = fopen(outFilename.c_str(), resumeOffset ? "ab" : "wb"); // Append if resuming if (!file) { logger->log("Failed to open file %s", outFilename.c_str()); curl_easy_cleanup(curl); break; } curl_easy_setopt(curl, CURLOPT_WRITEDATA, file); if (resumeOffset) { curl_easy_setopt(curl, CURLOPT_RANGE, (std::to_string(resumeOffset) + "-").c_str()); } } const std::string appShort = branding.getStringValue("appShort"); const std::string userAgent = strprintf(PACKAGE_EXTENDED_VERSION, appShort.c_str()); curl_easy_setopt(curl, CURLOPT_USERAGENT, userAgent.c_str()); curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, d->mError); curl_easy_setopt(curl, CURLOPT_URL, d->mUrl.c_str()); curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, &Download::downloadProgress); curl_easy_setopt(curl, CURLOPT_XFERINFODATA, ptr); curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 30); // 30s for connection curl_easy_setopt(curl, CURLOPT_TIMEOUT, 2100); // 30 for total download curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, 100); // 100 bytes/s curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, 60); // 60s low speed timeout const CURLcode res = curl_easy_perform(curl); if (!d->mMemoryWrite && file) { // Update resume offset resumeOffset = ftell(file); logger->log("Download attempt %d progress: %lld bytes", attempts + 1, resumeOffset); fclose(file); file = nullptr; } curl_easy_cleanup(curl); if (res == CURLE_ABORTED_BY_CALLBACK) { d->mCancel = true; if (file) { fclose(file); ::remove(outFilename.c_str()); } break; } if (res != CURLE_OK) { logger->log("curl error %d: %s host: %s", res, d->mError, d->mUrl.c_str()); if (file) { fclose(file); file = nullptr; } auto state = d->mState.lock(); state->status = DownloadStatus::Error; snprintf(d->mError, CURL_ERROR_SIZE, "Failed to download %s: %s", d->mUrl.c_str(), curl_easy_strerror(res)); if (res == CURLE_URL_MALFORMAT || res == CURLE_COULDNT_RESOLVE_HOST || res == CURLE_HTTP_RETURNED_ERROR) { extern std::string errorMessage; ::errorMessage = d->mError; Client::instance()->showErrorDialog(::errorMessage, STATE_CHOOSE_SERVER); break; } if (!d->mMemoryWrite && resumeOffset > 0) { logger->log("Resumable progress: %lld bytes", resumeOffset); } else { ::remove(outFilename.c_str()); resumeOffset = 0; // Reset if not resumable } continue; } if (!d->mMemoryWrite) { if (d->mAdler) { file = fopen(outFilename.c_str(), "rb"); unsigned long adler = fadler32(file); if (d->mAdler != adler) { if (file) fclose(file); ::remove(outFilename.c_str()); logger->log("Checksum for file %s failed: (%lx/%lx)", d->mFileName.c_str(), adler, *d->mAdler); resumeOffset = 0; // Reset on checksum failure continue; } } if (file) fclose(file); ::remove(d->mFileName.c_str()); ::rename(outFilename.c_str(), d->mFileName.c_str()); file = fopen(d->mFileName.c_str(), "rb"); if (file) { fclose(file); file = nullptr; complete = true; } } else { complete = true; } if (file) fclose(file); } auto state = d->mState.lock(); if (d->mCancel) state->status = DownloadStatus::Canceled; else if (complete) state->status = DownloadStatus::Complete; else { state->status = DownloadStatus::Error; snprintf(d->mError, CURL_ERROR_SIZE, "Download failed after %d attempts: %s", 5, d->mUrl.c_str()); extern std::string errorMessage; ::errorMessage = d->mError; Client::instance()->showErrorDialog(::errorMessage, STATE_CHOOSE_SERVER); } return 0; } } // namespace Net