/*
* 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 "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 {
/**
* Calculates the Alder-32 checksum for the given file.
*/
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);
// Calculate Adler-32 checksum
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); // Cannot add headers after starting download
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); // Cannot set file after starting download
mMemoryWrite = false;
mFileName = filename;
mAdler = adler32;
}
void Download::setUseBuffer()
{
assert(!mThread); // Cannot set write function after starting download
mMemoryWrite = true;
}
bool Download::start()
{
assert(!mThread); // Download already started
Log::info("Starting download: %s", mUrl.c_str());
mThread = SDL_CreateThread(downloadThread, "Download", this);
if (!mThread)
{
Log::info("%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()
{
Log::info("Canceling download: %s", mUrl.c_str());
mCancel = true;
}
std::string_view Download::getBuffer() const
{
assert(mMemoryWrite); // Buffer not used
return std::string_view(mBuffer, mDownloadedBytes);
}
/**
* A libcurl callback for reporting progress.
*/
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;
}
/**
* A libcurl callback for writing to memory.
*/
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;
if (!d->mMemoryWrite)
outFilename = d->mFileName + ".part";
for (int attempts = 0; attempts < 3 && !complete && !d->mCancel; ++attempts)
{
CURL *curl = curl_easy_init();
if (!curl)
break;
Log::info("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(), "w+b");
curl_easy_setopt(curl, CURLOPT_WRITEDATA, file);
}
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, 15);
const CURLcode res = curl_easy_perform(curl);
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)
{
Log::info("curl error %d: %s host: %s",
res, d->mError, d->mUrl.c_str());
if (file)
{
fclose(file);
::remove(outFilename.c_str());
}
break;
}
if (!d->mMemoryWrite)
{
// Check the checksum if available
if (d->mAdler)
{
unsigned long adler = fadler32(file);
if (d->mAdler != adler)
{
if (file)
fclose(file);
// Remove the corrupted file
::remove(outFilename.c_str());
Log::info("Checksum for file %s failed: (%lx/%lx)",
d->mFileName.c_str(),
adler, *d->mAdler);
continue; // Bail out here to avoid the renaming
}
}
if (file)
fclose(file);
// Any existing file with this name is deleted first, otherwise
// the rename will fail on Windows.
::remove(d->mFileName.c_str());
::rename(outFilename.c_str(), d->mFileName.c_str());
// Check if we can open it and no errors were encountered
// during renaming
file = fopen(d->mFileName.c_str(), "rb");
if (file)
{
fclose(file);
file = nullptr;
complete = true;
}
}
else
{
// It's stored in memory, we're done
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;
return 0;
}
} // namespace Net