mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-15 15:30:29 +02:00
The libretro core is the frontend, and the headless tools are the test and debugging hosts. The Windows, UWP, Android, iOS, macOS and SDL apps, the UI framework and imgui go, along with what only they used: the Win32 debugger hooks (g_frameMutex, the shutdown lock), JNI thread attachment, the Android text drawer, RAIntegration, Discord, recent files, game installs, tilt input, the audio resamplers, the control mapper, memstick moving and the remote ISO and upload server modes. The web server keeps the debugger. The MSVC, Gradle and ndk-build projects go too.
642 lines
17 KiB
C++
642 lines
17 KiB
C++
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <memory>
|
|
#include <utility>
|
|
|
|
#include "Common/Net/HTTPClient.h"
|
|
|
|
#include "Common/TimeUtil.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Common/System/OSD.h"
|
|
#include "Common/System/System.h"
|
|
|
|
#include "Common/Net/SocketCompat.h"
|
|
#include "Common/Net/Resolve.h"
|
|
#include "Common/Net/URL.h"
|
|
|
|
#include "Common/File/FileDescriptor.h"
|
|
#include "Common/SysError.h"
|
|
#include "Common/Thread/ThreadUtil.h"
|
|
#include "Common/Data/Encoding/Compression.h"
|
|
#include "Common/Net/NetBuffer.h"
|
|
#include "Common/Log.h"
|
|
|
|
namespace net {
|
|
|
|
Connection::~Connection() {
|
|
Disconnect();
|
|
if (resolved_ != nullptr)
|
|
DNSResolveFree(resolved_);
|
|
}
|
|
|
|
// For whatever crazy reason, htons isn't available on android x86 on the build server. so here we go.
|
|
|
|
// TODO: Fix for big-endian
|
|
inline unsigned short myhtons(unsigned short x) {
|
|
return (x >> 8) | (x << 8);
|
|
}
|
|
|
|
const char *DNSTypeAsString(DNSType type) {
|
|
switch (type) {
|
|
case DNSType::IPV4:
|
|
return "IPV4";
|
|
case DNSType::IPV6:
|
|
return "IPV6";
|
|
case DNSType::ANY:
|
|
return "ANY";
|
|
default:
|
|
return "N/A";
|
|
}
|
|
}
|
|
|
|
std::string Connection::GetLocalIpAsString() const {
|
|
return fd_util::GetLocalIP(this->sock());
|
|
}
|
|
|
|
bool Connection::Resolve(const char *host, int port, DNSType type) {
|
|
if ((intptr_t)sock_ != -1) {
|
|
ERROR_LOG(Log::Net, "Resolve: Already have a socket");
|
|
return false;
|
|
}
|
|
if (!host || port < 1 || port > 65535) {
|
|
ERROR_LOG(Log::Net, "Resolve: Invalid host or port (%d)", port);
|
|
return false;
|
|
}
|
|
|
|
host_ = host;
|
|
port_ = port;
|
|
|
|
char port_str[16];
|
|
snprintf(port_str, sizeof(port_str), "%d", port);
|
|
|
|
std::string processedHostname(host);
|
|
|
|
if (customResolve_) {
|
|
processedHostname = customResolve_(host);
|
|
}
|
|
|
|
std::string err;
|
|
if (!net::DNSResolve(processedHostname, port_str, &resolved_, err, type)) {
|
|
WARN_LOG(Log::Net, "Failed to resolve host '%s': '%s' (%s)", host, err.c_str(), DNSTypeAsString(type));
|
|
// Zero port so that future calls fail.
|
|
port_ = 0;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void FormatAddr(char *addrbuf, size_t bufsize, const addrinfo *info) {
|
|
switch (info->ai_family) {
|
|
case AF_INET: {
|
|
auto sock_addr = (sockaddr_in*)info->ai_addr;
|
|
inet_ntop(info->ai_family, &(sock_addr)->sin_addr, addrbuf, bufsize);
|
|
break;
|
|
}
|
|
case AF_INET6: {
|
|
auto sock_addr = (sockaddr_in6*)info->ai_addr;
|
|
// There's also 'sin6_flowinfo' before 'sin6_addr', so we can't combine these cases into one.
|
|
inet_ntop(info->ai_family, &(sock_addr)->sin6_addr, addrbuf, bufsize);
|
|
break;
|
|
}
|
|
default:
|
|
snprintf(addrbuf, bufsize, "(Unknown AF %d)", info->ai_family);
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool Connection::Connect(int maxTries, double timeout, const net::CancelToken *cancel) {
|
|
if (port_ <= 0) {
|
|
ERROR_LOG(Log::Net, "Bad port");
|
|
return false;
|
|
}
|
|
sock_ = -1;
|
|
|
|
for (int tries = maxTries; tries > 0; --tries) {
|
|
std::vector<uintptr_t> sockets;
|
|
for (addrinfo *possible = resolved_; possible != nullptr; possible = possible->ai_next) {
|
|
if (possible->ai_family != AF_INET && possible->ai_family != AF_INET6)
|
|
continue;
|
|
|
|
int sock = socket(possible->ai_family, SOCK_STREAM, IPPROTO_TCP);
|
|
if ((intptr_t)sock == -1) {
|
|
ERROR_LOG(Log::Net, "Bad socket");
|
|
continue;
|
|
}
|
|
// Windows sockets aren't limited by socket number, just by count, so checking FD_SETSIZE there is wrong.
|
|
#if !PPSSPP_PLATFORM(WINDOWS)
|
|
if (sock >= FD_SETSIZE) {
|
|
ERROR_LOG(Log::Net, "Socket doesn't fit in FD_SET: %d We probably have a leak.", sock);
|
|
closesocket(sock);
|
|
continue;
|
|
}
|
|
#endif
|
|
fd_util::SetNonBlocking(sock, true);
|
|
|
|
// Start trying to connect (async with timeout.)
|
|
errno = 0;
|
|
if (connect(sock, possible->ai_addr, (int)possible->ai_addrlen) < 0) {
|
|
int errorCode = socket_errno;
|
|
std::string errorString = GetStringErrorMsg(errorCode);
|
|
bool unreachable = errorCode == ENETUNREACH;
|
|
bool inProgress = errorCode == EINPROGRESS || errorCode == EWOULDBLOCK;
|
|
if (!inProgress) {
|
|
char addrStr[128]{};
|
|
FormatAddr(addrStr, sizeof(addrStr), possible);
|
|
if (!unreachable) {
|
|
ERROR_LOG(Log::HTTP, "connect(%d) call to %s failed (%d: %s)", sock, addrStr, errorCode, errorString.c_str());
|
|
} else {
|
|
VERBOSE_LOG(Log::HTTP, "connect(%d): Ignoring unreachable resolved address %s", sock, addrStr);
|
|
}
|
|
closesocket(sock);
|
|
continue;
|
|
}
|
|
}
|
|
sockets.push_back(sock);
|
|
}
|
|
|
|
if (cancel && cancel->IsCancelled()) {
|
|
for (uintptr_t sock : sockets)
|
|
closesocket(sock);
|
|
WARN_LOG(Log::Net, "connect: cancelled: %s:%d", host_.c_str(), port_);
|
|
break;
|
|
}
|
|
if (sockets.empty()) {
|
|
// No plausible address this time round; try the next one straight away.
|
|
continue;
|
|
}
|
|
|
|
// Wait for the first to connect, the timeout, or a cancel - no slicing, a cancel wakes the wait.
|
|
std::unique_ptr<bool[]> ready(new bool[sockets.size()]());
|
|
const net::WaitResult wait = net::WaitSockets(sockets.data(), ready.get(), (int)sockets.size(), true, timeout, cancel);
|
|
if (wait == net::WaitResult::READY) {
|
|
// Something connected. Pick the first one that did (if multiple.)
|
|
for (size_t i = 0; i < sockets.size(); i++) {
|
|
if ((intptr_t)sock_ == -1 && ready[i]) {
|
|
sock_ = sockets[i];
|
|
} else {
|
|
closesocket(sockets[i]);
|
|
}
|
|
}
|
|
|
|
// Great, now we're good to go.
|
|
return true;
|
|
}
|
|
|
|
// Fail. Close all the sockets.
|
|
for (uintptr_t sock : sockets) {
|
|
closesocket(sock);
|
|
}
|
|
if (wait == net::WaitResult::CANCELLED) {
|
|
WARN_LOG(Log::HTTP, "connect: cancelled: %s:%d", host_.c_str(), port_);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Nothing connected, unfortunately.
|
|
return false;
|
|
}
|
|
|
|
void Connection::Disconnect() {
|
|
if ((intptr_t)sock_ != -1) {
|
|
closesocket(sock_);
|
|
sock_ = -1;
|
|
}
|
|
}
|
|
|
|
} // net
|
|
|
|
namespace http {
|
|
|
|
// TODO: do something sane here
|
|
constexpr const char *DEFAULT_USERAGENT = "PPSSPP";
|
|
constexpr const char *HTTP_VERSION = "1.1";
|
|
|
|
Client::Client(net::ResolveFunc func) : Connection(std::move(func)) {
|
|
userAgent_ = DEFAULT_USERAGENT;
|
|
httpVersion_ = HTTP_VERSION;
|
|
}
|
|
|
|
Client::~Client() {
|
|
Disconnect();
|
|
}
|
|
|
|
bool GetHeaderValue(const std::vector<std::string> &responseHeaders, std::string_view header, std::string *value) {
|
|
std::string search(header);
|
|
search.push_back(':');
|
|
|
|
bool found = false;
|
|
|
|
value->clear();
|
|
for (const std::string &line : responseHeaders) {
|
|
auto stripped = StripSpaces(line);
|
|
if (startsWithNoCase(stripped, search)) {
|
|
size_t value_pos = search.length();
|
|
size_t after_white = stripped.find_first_not_of(" \t", value_pos);
|
|
if (after_white != stripped.npos)
|
|
value_pos = after_white;
|
|
|
|
if (!found)
|
|
*value = stripped.substr(value_pos);
|
|
else
|
|
*value += "," + std::string(stripped.substr(value_pos));
|
|
found = true;
|
|
}
|
|
}
|
|
|
|
return found;
|
|
}
|
|
|
|
static bool DeChunk(Buffer *inbuffer, Buffer *outbuffer, int contentLength) {
|
|
_dbg_assert_(outbuffer->empty());
|
|
int dechunkedBytes = 0;
|
|
while (true) {
|
|
std::string line;
|
|
inbuffer->TakeLineCRLF(&line);
|
|
if (line.empty())
|
|
return false;
|
|
unsigned int chunkSize = 0;
|
|
if (sscanf(line.c_str(), "%x", &chunkSize) != 1) {
|
|
return false;
|
|
}
|
|
if (chunkSize) {
|
|
std::string data;
|
|
inbuffer->Take(chunkSize, &data);
|
|
outbuffer->Append(data);
|
|
} else {
|
|
// a zero size chunk should mean the end.
|
|
inbuffer->clear();
|
|
return true;
|
|
}
|
|
dechunkedBytes += chunkSize;
|
|
inbuffer->Skip(2);
|
|
}
|
|
// Unreachable
|
|
return true;
|
|
}
|
|
|
|
int Client::GET(const RequestParams &req, Buffer *output, std::vector<std::string> &responseHeaders, net::RequestProgress *progress) {
|
|
const char *otherHeaders =
|
|
"Accept-Encoding: gzip\r\n";
|
|
int err = SendRequest("GET", req, otherHeaders, progress);
|
|
if (err < 0) {
|
|
return err;
|
|
}
|
|
|
|
net::Buffer readbuf;
|
|
int code = ReadResponseHeaders(&readbuf, responseHeaders, progress);
|
|
if (code < 0) {
|
|
return code;
|
|
}
|
|
|
|
err = ReadResponseEntity(&readbuf, responseHeaders, output, progress);
|
|
if (err < 0) {
|
|
return err;
|
|
}
|
|
return code;
|
|
}
|
|
|
|
int Client::GET(const RequestParams &req, Buffer *output, net::RequestProgress *progress) {
|
|
std::vector<std::string> responseHeaders;
|
|
int code = GET(req, output, responseHeaders, progress);
|
|
return code;
|
|
}
|
|
|
|
int Client::POST(const RequestParams &req, std::string_view data, std::string_view mime, Buffer *output, net::RequestProgress *progress) {
|
|
char otherHeaders[2048];
|
|
if (mime.empty()) {
|
|
snprintf(otherHeaders, sizeof(otherHeaders), "Content-Length: %lld\r\n", (long long)data.size());
|
|
} else {
|
|
snprintf(otherHeaders, sizeof(otherHeaders), "Content-Length: %lld\r\nContent-Type: %.*s\r\n", (long long)data.size(), STR_VIEW(mime));
|
|
}
|
|
|
|
int err = SendRequestWithData("POST", req, data, otherHeaders, progress);
|
|
if (err < 0) {
|
|
return err;
|
|
}
|
|
|
|
net::Buffer readbuf;
|
|
std::vector<std::string> responseHeaders;
|
|
int code = ReadResponseHeaders(&readbuf, responseHeaders, progress);
|
|
if (code < 0) {
|
|
return code;
|
|
}
|
|
|
|
err = ReadResponseEntity(&readbuf, responseHeaders, output, progress);
|
|
if (err < 0) {
|
|
return err;
|
|
}
|
|
return code;
|
|
}
|
|
|
|
int Client::POST(const RequestParams &req, std::string_view data, Buffer *output, net::RequestProgress *progress) {
|
|
return POST(req, data, "", output, progress);
|
|
}
|
|
|
|
int Client::SendRequest(const char *method, const RequestParams &req, const char *otherHeaders, net::RequestProgress *progress) {
|
|
return SendRequestWithData(method, req, "", otherHeaders, progress);
|
|
}
|
|
|
|
int Client::SendRequestWithData(const char *method, const RequestParams &req, std::string_view data, const char *otherHeaders, net::RequestProgress *progress) {
|
|
if (progress) {
|
|
progress->Update(0, 0, false);
|
|
}
|
|
|
|
net::Buffer buffer;
|
|
const char *tpl =
|
|
"%s %s HTTP/%s\r\n"
|
|
"Host: %s\r\n"
|
|
"User-Agent: %s\r\n"
|
|
"Accept: %s\r\n"
|
|
"Connection: close\r\n"
|
|
"%s"
|
|
"\r\n";
|
|
|
|
buffer.Printf(tpl,
|
|
method, req.resource.c_str(), HTTP_VERSION,
|
|
host_.c_str(),
|
|
userAgent_.c_str(),
|
|
req.acceptMime,
|
|
otherHeaders ? otherHeaders : "");
|
|
buffer.Append(data);
|
|
bool flushed = buffer.FlushSocket(sock(), headerTimeout_, progress ? progress->cancel : nullptr);
|
|
if (!flushed) {
|
|
return -1; // TODO error code.
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int Client::ReadResponseHeaders(net::Buffer *readbuf, std::vector<std::string> &responseHeaders, net::RequestProgress *progress, std::string *statusLine) {
|
|
// Snarf all the data we can into RAM. A little unsafe but hey.
|
|
const net::WaitResult wait = net::WaitSocket(sock(), false, headerTimeout_, progress ? progress->cancel : nullptr);
|
|
if (wait == net::WaitResult::TIMEOUT) {
|
|
ERROR_LOG(Log::HTTP, "HTTP headers timed out");
|
|
return -1;
|
|
} else if (wait != net::WaitResult::READY) {
|
|
return -1;
|
|
}
|
|
// Let's hope all the headers are available in a single packet...
|
|
if (readbuf->Read(sock(), 4096) < 0) {
|
|
ERROR_LOG(Log::HTTP, "Failed to read HTTP headers :(");
|
|
return -1;
|
|
}
|
|
|
|
if (readbuf->empty()) {
|
|
ERROR_LOG(Log::HTTP, "Empty HTTP header read buffer :(");
|
|
return -1;
|
|
}
|
|
|
|
// Grab the first header line that contains the http code.
|
|
|
|
std::string line;
|
|
readbuf->TakeLineCRLF(&line);
|
|
|
|
int code;
|
|
size_t code_pos = line.find(' ');
|
|
if (code_pos != line.npos) {
|
|
code_pos = line.find_first_not_of(' ', code_pos);
|
|
}
|
|
|
|
if (code_pos != line.npos) {
|
|
code = atoi(&line[code_pos]);
|
|
} else {
|
|
ERROR_LOG(Log::HTTP, "Could not parse HTTP status code: '%s'", line.c_str());
|
|
return -1;
|
|
}
|
|
|
|
if (statusLine) {
|
|
*statusLine = std::move(line);
|
|
}
|
|
|
|
while (true) {
|
|
int sz = readbuf->TakeLineCRLF(&line);
|
|
if (!sz || sz < 0)
|
|
break;
|
|
VERBOSE_LOG(Log::HTTP, "Header line: %s", line.c_str());
|
|
responseHeaders.push_back(std::move(line));
|
|
}
|
|
|
|
if (responseHeaders.empty()) {
|
|
ERROR_LOG(Log::HTTP, "No HTTP response headers");
|
|
return -1;
|
|
}
|
|
|
|
return code;
|
|
}
|
|
|
|
int Client::ReadResponseEntity(net::Buffer *readbuf, const std::vector<std::string> &responseHeaders, Buffer *output, net::RequestProgress *progress) {
|
|
|
|
bool gzip = false;
|
|
bool chunked = false;
|
|
int contentLength = 0;
|
|
for (std::string line : responseHeaders) {
|
|
if (startsWithNoCase(line, "Content-Length:")) {
|
|
size_t size_pos = line.find_first_of(' ');
|
|
if (size_pos != line.npos) {
|
|
size_pos = line.find_first_not_of(' ', size_pos);
|
|
}
|
|
if (size_pos != line.npos) {
|
|
contentLength = atoi(&line[size_pos]);
|
|
chunked = false;
|
|
}
|
|
} else if (startsWithNoCase(line, "Content-Encoding:")) {
|
|
// TODO: Case folding...
|
|
if (line.find("gzip") != std::string::npos) {
|
|
gzip = true;
|
|
}
|
|
} else if (startsWithNoCase(line, "Transfer-Encoding:")) {
|
|
// TODO: Case folding...
|
|
if (line.find("chunked") != std::string::npos) {
|
|
chunked = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (contentLength < 0) {
|
|
WARN_LOG(Log::HTTP, "Negative content length %d", contentLength);
|
|
// Just sanity checking...
|
|
contentLength = 0;
|
|
}
|
|
|
|
if (!readbuf->ReadAllWithProgress(sock(), contentLength, progress))
|
|
return -1;
|
|
|
|
// output now contains the rest of the reply. Dechunk it.
|
|
if (!output->IsVoid()) {
|
|
if (chunked) {
|
|
if (!DeChunk(readbuf, output, contentLength)) {
|
|
ERROR_LOG(Log::HTTP, "Bad chunked data, couldn't read chunk size");
|
|
if (progress) {
|
|
progress->Update(0, 0, true);
|
|
}
|
|
return -1;
|
|
}
|
|
} else {
|
|
output->Append(*readbuf);
|
|
}
|
|
|
|
// If it's gzipped, we decompress it and put it back in the buffer.
|
|
if (gzip) {
|
|
std::string compressed, decompressed;
|
|
output->TakeAll(&compressed);
|
|
bool result = decompress_string(compressed, &decompressed);
|
|
if (!result) {
|
|
ERROR_LOG(Log::HTTP, "Error decompressing using zlib");
|
|
if (progress) {
|
|
progress->Update(0, 0, true);
|
|
}
|
|
return -1;
|
|
}
|
|
output->Append(decompressed);
|
|
}
|
|
}
|
|
|
|
if (progress) {
|
|
progress->Update(contentLength, contentLength, true);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
HTTPRequest::HTTPRequest(RequestMethod method, std::string_view url, std::string_view postData, std::string_view postMime, const Path &outfile, RequestFlags flags, net::ResolveFunc customResolve, std::string_view name)
|
|
: Request(method, url, name, outfile, flags), postData_(postData), postMime_(postMime), customResolve_(std::move(customResolve)) {
|
|
}
|
|
|
|
HTTPRequest::~HTTPRequest() {
|
|
g_OSD.RemoveProgressBar(url_, !failed_, 0.5f);
|
|
|
|
if (thread_.joinable()) {
|
|
_dbg_assert_msg_(false, "Download destructed without join");
|
|
}
|
|
}
|
|
|
|
void HTTPRequest::Start() {
|
|
thread_ = std::thread([this] { Do(); });
|
|
}
|
|
|
|
void HTTPRequest::Join() {
|
|
if (!thread_.joinable()) {
|
|
ERROR_LOG(Log::HTTP, "Already joined thread!");
|
|
_dbg_assert_(false);
|
|
}
|
|
thread_.join();
|
|
}
|
|
|
|
void HTTPRequest::SetFailed(int code) {
|
|
// TODO: Why are we not using code here?
|
|
|
|
failed_ = true;
|
|
progress_.Update(0, 0, true);
|
|
completed_ = true;
|
|
}
|
|
|
|
int HTTPRequest::Perform(const std::string &url) {
|
|
Url fileUrl(url);
|
|
if (!fileUrl.Valid()) {
|
|
return -1;
|
|
}
|
|
|
|
http::Client client(customResolve_);
|
|
if (!userAgent_.empty()) {
|
|
client.SetUserAgent(userAgent_);
|
|
}
|
|
|
|
if (!client.Resolve(fileUrl.Host().c_str(), fileUrl.Port())) {
|
|
ERROR_LOG(Log::HTTP, "Failed resolving %s", url.c_str());
|
|
return -1;
|
|
}
|
|
|
|
if (IsCancelled()) {
|
|
return -1;
|
|
}
|
|
|
|
if (!client.Connect(2, 20.0, &cancel_)) {
|
|
ERROR_LOG(Log::HTTP, "Failed connecting to server or cancelled (=%d).", (int)IsCancelled());
|
|
return -1;
|
|
}
|
|
|
|
if (IsCancelled()) {
|
|
return -1;
|
|
}
|
|
|
|
RequestParams req(fileUrl.Resource(), acceptMime_);
|
|
if (method_ == RequestMethod::GET) {
|
|
return client.GET(req, &buffer_, responseHeaders_, &progress_);
|
|
} else {
|
|
return client.POST(req, postData_, postMime_, &buffer_, &progress_);
|
|
}
|
|
}
|
|
|
|
std::string HTTPRequest::RedirectLocation(const std::string &baseUrl) const {
|
|
std::string redirectUrl;
|
|
if (GetHeaderValue(responseHeaders_, "Location", &redirectUrl)) {
|
|
Url url(baseUrl);
|
|
url = url.Relative(redirectUrl);
|
|
redirectUrl = url.ToString();
|
|
}
|
|
return redirectUrl;
|
|
}
|
|
|
|
void HTTPRequest::Do() {
|
|
SetCurrentThreadName("HTTPDownload::Do");
|
|
|
|
resultCode_ = 0;
|
|
|
|
std::string downloadURL = url_;
|
|
while (resultCode_ == 0) {
|
|
// This is where the new request is performed.
|
|
int resultCode = Perform(downloadURL);
|
|
if (resultCode == -1) {
|
|
SetFailed(resultCode);
|
|
return;
|
|
}
|
|
|
|
if (resultCode == 301 || resultCode == 302 || resultCode == 303 || resultCode == 307 || resultCode == 308) {
|
|
std::string redirectURL = RedirectLocation(downloadURL);
|
|
if (redirectURL.empty()) {
|
|
ERROR_LOG(Log::HTTP, "Could not find Location header for redirect");
|
|
resultCode_ = resultCode;
|
|
} else if (redirectURL == downloadURL || redirectURL == url_) {
|
|
// Simple loop detected, bail out.
|
|
resultCode_ = resultCode;
|
|
}
|
|
|
|
// Perform the next GET.
|
|
if (resultCode_ == 0) {
|
|
INFO_LOG(Log::HTTP, "Download of %s redirected to %s", downloadURL.c_str(), redirectURL.c_str());
|
|
buffer_.clear();
|
|
responseHeaders_.clear();
|
|
}
|
|
downloadURL = redirectURL;
|
|
continue;
|
|
}
|
|
|
|
if (resultCode == 200) {
|
|
INFO_LOG(Log::HTTP, "Completed requesting %s (storing result to %s)", url_.c_str(), outfile_.empty() ? "memory" : outfile_.c_str());
|
|
bool clear = !(flags_ & RequestFlags::KeepInMemory);
|
|
if (!outfile_.empty() && !buffer_.FlushToFile(outfile_, clear)) {
|
|
ERROR_LOG(Log::HTTP, "Failed writing download to '%s'", outfile_.c_str());
|
|
}
|
|
} else {
|
|
ERROR_LOG(Log::HTTP, "Error requesting '%s' (storing result to '%s'): %i", url_.c_str(), outfile_.empty() ? "memory" : outfile_.c_str(), resultCode);
|
|
}
|
|
resultCode_ = resultCode;
|
|
}
|
|
|
|
// Set this last to ensure no race conditions when checking Done. Users must always check
|
|
// Done before looking at the result code.
|
|
completed_ = true;
|
|
}
|
|
|
|
std::string RemoveHttpsIfNeeded(std::string_view url) {
|
|
if (!System_GetPropertyBool(SYSPROP_SUPPORTS_HTTPS)) {
|
|
// Try with http. Needed on Linux installs currently.
|
|
if (startsWith(url, "https://")) {
|
|
return "http://" + std::string(url.substr(8));
|
|
}
|
|
}
|
|
return std::string(url);
|
|
}
|
|
|
|
} // namespace http
|