HTTPDownloader: Move to util project

This commit is contained in:
Stenzek
2023-11-06 19:59:02 +10:00
parent 3c6b6c5770
commit 0fe6e9170b
16 changed files with 107 additions and 106 deletions

View File

@ -23,8 +23,6 @@ add_library(common
hash_combine.h
heap_array.h
heterogeneous_containers.h
http_downloader.cpp
http_downloader.h
layered_settings_interface.cpp
layered_settings_interface.h
log.cpp
@ -67,13 +65,11 @@ target_link_libraries(common PRIVATE stb zlib minizip Zstd::Zstd "${CMAKE_DL_LIB
if(WIN32)
target_sources(common PRIVATE
http_downloader_winhttp.cpp
http_downloader_winhttp.h
thirdparty/StackWalker.cpp
thirdparty/StackWalker.h
windows_headers.h
)
target_link_libraries(common PRIVATE winhttp.lib OneCore.lib)
target_link_libraries(common PRIVATE OneCore.lib)
endif()
if(MSVC)
@ -98,19 +94,9 @@ if(APPLE)
target_link_libraries(common PRIVATE ${COCOA_LIBRARY})
endif()
if(NOT WIN32 AND NOT ANDROID)
target_sources(common PRIVATE
http_downloader_curl.cpp
http_downloader_curl.h
)
target_link_libraries(common PRIVATE
CURL::libcurl
)
if(LIBBACKTRACE_FOUND)
target_compile_definitions(common PRIVATE "-DENABLE_LIBBACKTRACE=1")
target_link_libraries(common PRIVATE libbacktrace::libbacktrace)
endif()
if(NOT WIN32 AND NOT ANDROID AND LIBBACKTRACE_FOUND)
target_compile_definitions(common PRIVATE "-DENABLE_LIBBACKTRACE=1")
target_link_libraries(common PRIVATE libbacktrace::libbacktrace)
endif()
if(ANDROID)

View File

@ -10,7 +10,7 @@
<ItemDefinitionGroup>
<Link>
<AdditionalDependencies>%(AdditionalDependencies);winhttp.lib;OneCore.lib</AdditionalDependencies>
<AdditionalDependencies>%(AdditionalDependencies);OneCore.lib</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
</Project>

View File

@ -17,8 +17,6 @@
<ClInclude Include="file_system.h" />
<ClInclude Include="hash_combine.h" />
<ClInclude Include="heap_array.h" />
<ClInclude Include="http_downloader.h" />
<ClInclude Include="http_downloader_winhttp.h" />
<ClInclude Include="image.h" />
<ClInclude Include="intrin.h" />
<ClInclude Include="layered_settings_interface.h" />
@ -51,8 +49,6 @@
<ClCompile Include="error.cpp" />
<ClCompile Include="fastjmp.cpp" />
<ClCompile Include="file_system.cpp" />
<ClCompile Include="http_downloader.cpp" />
<ClCompile Include="http_downloader_winhttp.cpp" />
<ClCompile Include="image.cpp" />
<ClCompile Include="layered_settings_interface.cpp" />
<ClCompile Include="log.cpp" />

View File

@ -28,8 +28,6 @@
<ClInclude Include="lru_cache.h" />
<ClInclude Include="easing.h" />
<ClInclude Include="error.h" />
<ClInclude Include="http_downloader_winhttp.h" />
<ClInclude Include="http_downloader.h" />
<ClInclude Include="path.h" />
<ClInclude Include="windows_headers.h" />
<ClInclude Include="settings_interface.h" />
@ -62,8 +60,6 @@
</ClCompile>
<ClCompile Include="crash_handler.cpp" />
<ClCompile Include="error.cpp" />
<ClCompile Include="http_downloader_winhttp.cpp" />
<ClCompile Include="http_downloader.cpp" />
<ClCompile Include="layered_settings_interface.cpp" />
<ClCompile Include="memory_settings_interface.cpp" />
<ClCompile Include="threading.cpp" />

View File

@ -1,359 +0,0 @@
// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#include "http_downloader.h"
#include "assert.h"
#include "log.h"
#include "string_util.h"
#include "timer.h"
Log_SetChannel(HTTPDownloader);
static constexpr float DEFAULT_TIMEOUT_IN_SECONDS = 30;
static constexpr u32 DEFAULT_MAX_ACTIVE_REQUESTS = 4;
namespace Common {
const char HTTPDownloader::DEFAULT_USER_AGENT[] =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0";
HTTPDownloader::HTTPDownloader()
: m_timeout(DEFAULT_TIMEOUT_IN_SECONDS), m_max_active_requests(DEFAULT_MAX_ACTIVE_REQUESTS)
{
}
HTTPDownloader::~HTTPDownloader() = default;
void HTTPDownloader::SetTimeout(float timeout)
{
m_timeout = timeout;
}
void HTTPDownloader::SetMaxActiveRequests(u32 max_active_requests)
{
Assert(max_active_requests > 0);
m_max_active_requests = max_active_requests;
}
void HTTPDownloader::CreateRequest(std::string url, Request::Callback callback)
{
Request* req = InternalCreateRequest();
req->parent = this;
req->type = Request::Type::Get;
req->url = std::move(url);
req->callback = std::move(callback);
req->start_time = Common::Timer::GetCurrentValue();
std::unique_lock<std::mutex> lock(m_pending_http_request_lock);
if (LockedGetActiveRequestCount() < m_max_active_requests)
{
if (!StartRequest(req))
return;
}
LockedAddRequest(req);
}
void HTTPDownloader::CreatePostRequest(std::string url, std::string post_data, Request::Callback callback)
{
Request* req = InternalCreateRequest();
req->parent = this;
req->type = Request::Type::Post;
req->url = std::move(url);
req->post_data = std::move(post_data);
req->callback = std::move(callback);
req->start_time = Common::Timer::GetCurrentValue();
std::unique_lock<std::mutex> lock(m_pending_http_request_lock);
if (LockedGetActiveRequestCount() < m_max_active_requests)
{
if (!StartRequest(req))
return;
}
LockedAddRequest(req);
}
bool HTTPDownloader::HasAnyRequests()
{
std::unique_lock<std::mutex> lock(m_pending_http_request_lock);
return !m_pending_http_requests.empty();
}
void HTTPDownloader::LockedPollRequests(std::unique_lock<std::mutex>& lock)
{
if (m_pending_http_requests.empty())
return;
InternalPollRequests();
const Common::Timer::Value current_time = Common::Timer::GetCurrentValue();
u32 active_requests = 0;
u32 unstarted_requests = 0;
for (size_t index = 0; index < m_pending_http_requests.size();)
{
Request* req = m_pending_http_requests[index];
if (req->state == Request::State::Pending)
{
unstarted_requests++;
index++;
continue;
}
if (req->state == Request::State::Started && current_time >= req->start_time &&
Common::Timer::ConvertValueToSeconds(current_time - req->start_time) >= m_timeout)
{
// request timed out
Log_ErrorPrintf("Request for '%s' timed out", req->url.c_str());
req->state.store(Request::State::Cancelled);
m_pending_http_requests.erase(m_pending_http_requests.begin() + index);
lock.unlock();
req->callback(HTTP_STATUS_TIMEOUT, std::string(), Request::Data());
CloseRequest(req);
lock.lock();
continue;
}
if (req->state != Request::State::Complete)
{
active_requests++;
index++;
continue;
}
// request complete
Log_VerbosePrintf("Request for '%s' complete, returned status code %u and %zu bytes", req->url.c_str(),
req->status_code, req->data.size());
m_pending_http_requests.erase(m_pending_http_requests.begin() + index);
// run callback with lock unheld
lock.unlock();
req->callback(req->status_code, std::move(req->content_type), std::move(req->data));
CloseRequest(req);
lock.lock();
}
// start new requests when we finished some
if (unstarted_requests > 0 && active_requests < m_max_active_requests)
{
for (size_t index = 0; index < m_pending_http_requests.size();)
{
Request* req = m_pending_http_requests[index];
if (req->state != Request::State::Pending)
{
index++;
continue;
}
if (!StartRequest(req))
{
m_pending_http_requests.erase(m_pending_http_requests.begin() + index);
continue;
}
active_requests++;
index++;
if (active_requests >= m_max_active_requests)
break;
}
}
}
void HTTPDownloader::PollRequests()
{
std::unique_lock<std::mutex> lock(m_pending_http_request_lock);
LockedPollRequests(lock);
}
void HTTPDownloader::WaitForAllRequests()
{
std::unique_lock<std::mutex> lock(m_pending_http_request_lock);
while (!m_pending_http_requests.empty())
LockedPollRequests(lock);
}
void HTTPDownloader::LockedAddRequest(Request* request)
{
m_pending_http_requests.push_back(request);
}
u32 HTTPDownloader::LockedGetActiveRequestCount()
{
u32 count = 0;
for (Request* req : m_pending_http_requests)
{
if (req->state == Request::State::Started || req->state == Request::State::Receiving)
count++;
}
return count;
}
std::string HTTPDownloader::URLEncode(const std::string_view& str)
{
std::string ret;
ret.reserve(str.length() + ((str.length() + 3) / 4) * 3);
for (size_t i = 0, l = str.size(); i < l; i++)
{
const char c = str[i];
if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '-' || c == '_' ||
c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' || c == ')')
{
ret.push_back(c);
}
else
{
ret.push_back('%');
const unsigned char n1 = static_cast<unsigned char>(c) >> 4;
const unsigned char n2 = static_cast<unsigned char>(c) & 0x0F;
ret.push_back((n1 >= 10) ? ('a' + (n1 - 10)) : ('0' + n1));
ret.push_back((n2 >= 10) ? ('a' + (n2 - 10)) : ('0' + n2));
}
}
return ret;
}
std::string HTTPDownloader::URLDecode(const std::string_view& str)
{
std::string ret;
ret.reserve(str.length());
for (size_t i = 0, l = str.size(); i < l; i++)
{
const char c = str[i];
if (c == '+')
{
ret.push_back(c);
}
else if (c == '%')
{
if ((i + 2) >= str.length())
break;
const char clower = str[i + 1];
const char cupper = str[i + 2];
const unsigned char lower =
(clower >= '0' && clower <= '9') ?
static_cast<unsigned char>(clower - '0') :
((clower >= 'a' && clower <= 'f') ?
static_cast<unsigned char>(clower - 'a') :
((clower >= 'A' && clower <= 'F') ? static_cast<unsigned char>(clower - 'A') : 0));
const unsigned char upper =
(cupper >= '0' && cupper <= '9') ?
static_cast<unsigned char>(cupper - '0') :
((cupper >= 'a' && cupper <= 'f') ?
static_cast<unsigned char>(cupper - 'a') :
((cupper >= 'A' && cupper <= 'F') ? static_cast<unsigned char>(cupper - 'A') : 0));
const char dch = static_cast<char>(lower | (upper << 4));
ret.push_back(dch);
}
else
{
ret.push_back(c);
}
}
return std::string(str);
}
std::string HTTPDownloader::GetExtensionForContentType(const std::string& content_type)
{
// Based on https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
static constexpr const char* table[][2] = {
{"audio/aac", "aac"},
{"application/x-abiword", "abw"},
{"application/x-freearc", "arc"},
{"image/avif", "avif"},
{"video/x-msvideo", "avi"},
{"application/vnd.amazon.ebook", "azw"},
{"application/octet-stream", "bin"},
{"image/bmp", "bmp"},
{"application/x-bzip", "bz"},
{"application/x-bzip2", "bz2"},
{"application/x-cdf", "cda"},
{"application/x-csh", "csh"},
{"text/css", "css"},
{"text/csv", "csv"},
{"application/msword", "doc"},
{"application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx"},
{"application/vnd.ms-fontobject", "eot"},
{"application/epub+zip", "epub"},
{"application/gzip", "gz"},
{"image/gif", "gif"},
{"text/html", "htm"},
{"image/vnd.microsoft.icon", "ico"},
{"text/calendar", "ics"},
{"application/java-archive", "jar"},
{"image/jpeg", "jpg"},
{"text/javascript", "js"},
{"application/json", "json"},
{"application/ld+json", "jsonld"},
{"audio/midi audio/x-midi", "mid"},
{"text/javascript", "mjs"},
{"audio/mpeg", "mp3"},
{"video/mp4", "mp4"},
{"video/mpeg", "mpeg"},
{"application/vnd.apple.installer+xml", "mpkg"},
{"application/vnd.oasis.opendocument.presentation", "odp"},
{"application/vnd.oasis.opendocument.spreadsheet", "ods"},
{"application/vnd.oasis.opendocument.text", "odt"},
{"audio/ogg", "oga"},
{"video/ogg", "ogv"},
{"application/ogg", "ogx"},
{"audio/opus", "opus"},
{"font/otf", "otf"},
{"image/png", "png"},
{"application/pdf", "pdf"},
{"application/x-httpd-php", "php"},
{"application/vnd.ms-powerpoint", "ppt"},
{"application/vnd.openxmlformats-officedocument.presentationml.presentation", "pptx"},
{"application/vnd.rar", "rar"},
{"application/rtf", "rtf"},
{"application/x-sh", "sh"},
{"image/svg+xml", "svg"},
{"application/x-tar", "tar"},
{"image/tiff", "tif"},
{"video/mp2t", "ts"},
{"font/ttf", "ttf"},
{"text/plain", "txt"},
{"application/vnd.visio", "vsd"},
{"audio/wav", "wav"},
{"audio/webm", "weba"},
{"video/webm", "webm"},
{"image/webp", "webp"},
{"font/woff", "woff"},
{"font/woff2", "woff2"},
{"application/xhtml+xml", "xhtml"},
{"application/vnd.ms-excel", "xls"},
{"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx"},
{"application/xml", "xml"},
{"text/xml", "xml"},
{"application/vnd.mozilla.xul+xml", "xul"},
{"application/zip", "zip"},
{"video/3gpp", "3gp"},
{"audio/3gpp", "3gp"},
{"video/3gpp2", "3g2"},
{"audio/3gpp2", "3g2"},
{"application/x-7z-compressed", "7z"},
};
std::string ret;
for (size_t i = 0; i < std::size(table); i++)
{
if (StringUtil::Strncasecmp(table[i][0], content_type.data(), content_type.length()) == 0)
{
ret = table[i][1];
break;
}
}
return ret;
}
} // namespace Common

View File

@ -1,98 +0,0 @@
// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#pragma once
#include "common/types.h"
#include <atomic>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <string_view>
#include <vector>
namespace Common {
class HTTPDownloader
{
public:
enum : s32
{
HTTP_STATUS_CANCELLED = -3,
HTTP_STATUS_TIMEOUT = -2,
HTTP_STATUS_ERROR = -1,
HTTP_STATUS_OK = 200
};
struct Request
{
using Data = std::vector<u8>;
using Callback = std::function<void(s32 status_code, std::string content_type, Data data)>;
enum class Type
{
Get,
Post,
};
enum class State
{
Pending,
Cancelled,
Started,
Receiving,
Complete,
};
HTTPDownloader* parent;
Callback callback;
std::string url;
std::string post_data;
std::string content_type;
Data data;
u64 start_time;
s32 status_code = 0;
u32 content_length = 0;
Type type = Type::Get;
std::atomic<State> state{State::Pending};
};
HTTPDownloader();
virtual ~HTTPDownloader();
static std::unique_ptr<HTTPDownloader> Create(const char* user_agent = DEFAULT_USER_AGENT);
static std::string URLEncode(const std::string_view& str);
static std::string URLDecode(const std::string_view& str);
static std::string GetExtensionForContentType(const std::string& content_type);
void SetTimeout(float timeout);
void SetMaxActiveRequests(u32 max_active_requests);
void CreateRequest(std::string url, Request::Callback callback);
void CreatePostRequest(std::string url, std::string post_data, Request::Callback callback);
bool HasAnyRequests();
void PollRequests();
void WaitForAllRequests();
static const char DEFAULT_USER_AGENT[];
protected:
virtual Request* InternalCreateRequest() = 0;
virtual void InternalPollRequests() = 0;
virtual bool StartRequest(Request* request) = 0;
virtual void CloseRequest(Request* request) = 0;
void LockedAddRequest(Request* request);
u32 LockedGetActiveRequestCount();
void LockedPollRequests(std::unique_lock<std::mutex>& lock);
float m_timeout;
u32 m_max_active_requests;
std::mutex m_pending_http_request_lock;
std::vector<Request*> m_pending_http_requests;
};
} // namespace FrontendCommon

View File

@ -1,167 +0,0 @@
#include "http_downloader_curl.h"
#include "common/assert.h"
#include "common/log.h"
#include "common/string_util.h"
#include "common/timer.h"
#include <algorithm>
#include <functional>
#include <pthread.h>
#include <signal.h>
Log_SetChannel(HTTPDownloader);
namespace Common {
HTTPDownloaderCurl::HTTPDownloaderCurl() : HTTPDownloader() {}
HTTPDownloaderCurl::~HTTPDownloaderCurl() = default;
std::unique_ptr<HTTPDownloader> HTTPDownloader::Create(const char* user_agent)
{
std::unique_ptr<HTTPDownloaderCurl> instance(std::make_unique<HTTPDownloaderCurl>());
if (!instance->Initialize(user_agent))
return {};
return instance;
}
static bool s_curl_initialized = false;
static std::once_flag s_curl_initialized_once_flag;
bool HTTPDownloaderCurl::Initialize(const char* user_agent)
{
if (!s_curl_initialized)
{
std::call_once(s_curl_initialized_once_flag, []() {
s_curl_initialized = curl_global_init(CURL_GLOBAL_ALL) == CURLE_OK;
if (s_curl_initialized)
{
std::atexit([]() {
curl_global_cleanup();
s_curl_initialized = false;
});
}
});
if (!s_curl_initialized)
{
Log_ErrorPrint("curl_global_init() failed");
return false;
}
}
m_user_agent = user_agent;
m_thread_pool = std::make_unique<cb::ThreadPool>(m_max_active_requests);
return true;
}
size_t HTTPDownloaderCurl::WriteCallback(char* ptr, size_t size, size_t nmemb, void* userdata)
{
Request* req = static_cast<Request*>(userdata);
const size_t current_size = req->data.size();
const size_t transfer_size = size * nmemb;
const size_t new_size = current_size + transfer_size;
req->data.resize(new_size);
std::memcpy(&req->data[current_size], ptr, transfer_size);
return nmemb;
}
void HTTPDownloaderCurl::ProcessRequest(Request* req)
{
std::unique_lock<std::mutex> cancel_lock(m_cancel_mutex);
if (req->closed.load())
return;
cancel_lock.unlock();
// Apparently OpenSSL can fire SIGPIPE...
sigset_t old_block_mask = {};
sigset_t new_block_mask = {};
sigemptyset(&old_block_mask);
sigemptyset(&new_block_mask);
sigaddset(&new_block_mask, SIGPIPE);
if (pthread_sigmask(SIG_BLOCK, &new_block_mask, &old_block_mask) != 0)
Log_WarningPrint("Failed to block SIGPIPE");
req->start_time = Common::Timer::GetCurrentValue();
int ret = curl_easy_perform(req->handle);
if (ret == CURLE_OK)
{
long response_code = 0;
curl_easy_getinfo(req->handle, CURLINFO_RESPONSE_CODE, &response_code);
req->status_code = static_cast<s32>(response_code);
char* content_type = nullptr;
if (!curl_easy_getinfo(req->handle, CURLINFO_CONTENT_TYPE, &content_type) && content_type)
req->content_type = content_type;
Log_DevPrintf("Request for '%s' returned status code %d and %zu bytes", req->url.c_str(), req->status_code,
req->data.size());
}
else
{
Log_ErrorPrintf("Request for '%s' returned %d", req->url.c_str(), ret);
}
curl_easy_cleanup(req->handle);
if (pthread_sigmask(SIG_UNBLOCK, &new_block_mask, &old_block_mask) != 0)
Log_WarningPrint("Failed to unblock SIGPIPE");
cancel_lock.lock();
req->state = Request::State::Complete;
if (req->closed.load())
delete req;
else
req->closed.store(true);
}
HTTPDownloader::Request* HTTPDownloaderCurl::InternalCreateRequest()
{
Request* req = new Request();
req->handle = curl_easy_init();
if (!req->handle)
{
delete req;
return nullptr;
}
return req;
}
void HTTPDownloaderCurl::InternalPollRequests()
{
// noop - uses thread pool
}
bool HTTPDownloaderCurl::StartRequest(HTTPDownloader::Request* request)
{
Request* req = static_cast<Request*>(request);
curl_easy_setopt(req->handle, CURLOPT_URL, request->url.c_str());
curl_easy_setopt(req->handle, CURLOPT_USERAGENT, m_user_agent.c_str());
curl_easy_setopt(req->handle, CURLOPT_WRITEFUNCTION, &HTTPDownloaderCurl::WriteCallback);
curl_easy_setopt(req->handle, CURLOPT_WRITEDATA, req);
curl_easy_setopt(req->handle, CURLOPT_NOSIGNAL, 1);
if (request->type == Request::Type::Post)
{
curl_easy_setopt(req->handle, CURLOPT_POST, 1L);
curl_easy_setopt(req->handle, CURLOPT_POSTFIELDS, request->post_data.c_str());
}
Log_DevPrintf("Started HTTP request for '%s'", req->url.c_str());
req->state = Request::State::Started;
req->start_time = Common::Timer::GetCurrentValue();
m_thread_pool->Schedule(std::bind(&HTTPDownloaderCurl::ProcessRequest, this, req));
return true;
}
void HTTPDownloaderCurl::CloseRequest(HTTPDownloader::Request* request)
{
std::unique_lock<std::mutex> cancel_lock(m_cancel_mutex);
Request* req = static_cast<Request*>(request);
if (req->closed.load())
delete req;
else
req->closed.store(true);
}
} // namespace Common

View File

@ -1,40 +0,0 @@
#pragma once
#include "http_downloader.h"
#include "common/thirdparty/thread_pool.h"
#include <atomic>
#include <memory>
#include <mutex>
#include <curl/curl.h>
namespace Common {
class HTTPDownloaderCurl final : public HTTPDownloader
{
public:
HTTPDownloaderCurl();
~HTTPDownloaderCurl() override;
bool Initialize(const char* user_agent);
protected:
Request* InternalCreateRequest() override;
void InternalPollRequests() override;
bool StartRequest(HTTPDownloader::Request* request) override;
void CloseRequest(HTTPDownloader::Request* request) override;
private:
struct Request : HTTPDownloader::Request
{
CURL* handle = nullptr;
std::atomic_bool closed{false};
};
static size_t WriteCallback(char* ptr, size_t size, size_t nmemb, void* userdata);
void ProcessRequest(Request* req);
std::string m_user_agent;
std::unique_ptr<cb::ThreadPool> m_thread_pool;
std::mutex m_cancel_mutex;
};
} // namespace FrontendCommon

View File

@ -1,315 +0,0 @@
// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#include "http_downloader_winhttp.h"
#include "assert.h"
#include "log.h"
#include "string_util.h"
#include "timer.h"
#include <VersionHelpers.h>
#include <algorithm>
Log_SetChannel(HTTPDownloader);
namespace Common {
HTTPDownloaderWinHttp::HTTPDownloaderWinHttp() : HTTPDownloader() {}
HTTPDownloaderWinHttp::~HTTPDownloaderWinHttp()
{
if (m_hSession)
{
WinHttpSetStatusCallback(m_hSession, nullptr, WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS, NULL);
WinHttpCloseHandle(m_hSession);
}
}
std::unique_ptr<HTTPDownloader> HTTPDownloader::Create(const char* user_agent)
{
std::unique_ptr<HTTPDownloaderWinHttp> instance(std::make_unique<HTTPDownloaderWinHttp>());
if (!instance->Initialize(user_agent))
return {};
return instance;
}
bool HTTPDownloaderWinHttp::Initialize(const char* user_agent)
{
// WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY is not supported before Win8.1.
const DWORD dwAccessType =
IsWindows8Point1OrGreater() ? WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY : WINHTTP_ACCESS_TYPE_DEFAULT_PROXY;
m_hSession = WinHttpOpen(StringUtil::UTF8StringToWideString(user_agent).c_str(), dwAccessType, nullptr, nullptr,
WINHTTP_FLAG_ASYNC);
if (m_hSession == NULL)
{
Log_ErrorPrintf("WinHttpOpen() failed: %u", GetLastError());
return false;
}
const DWORD notification_flags = WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS | WINHTTP_CALLBACK_FLAG_REQUEST_ERROR |
WINHTTP_CALLBACK_FLAG_HANDLES | WINHTTP_CALLBACK_FLAG_SECURE_FAILURE;
if (WinHttpSetStatusCallback(m_hSession, HTTPStatusCallback, notification_flags, NULL) ==
WINHTTP_INVALID_STATUS_CALLBACK)
{
Log_ErrorPrintf("WinHttpSetStatusCallback() failed: %u", GetLastError());
return false;
}
return true;
}
void CALLBACK HTTPDownloaderWinHttp::HTTPStatusCallback(HINTERNET hRequest, DWORD_PTR dwContext, DWORD dwInternetStatus,
LPVOID lpvStatusInformation, DWORD dwStatusInformationLength)
{
Request* req = reinterpret_cast<Request*>(dwContext);
switch (dwInternetStatus)
{
case WINHTTP_CALLBACK_STATUS_HANDLE_CREATED:
return;
case WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING:
{
if (!req)
return;
DebugAssert(hRequest == req->hRequest);
HTTPDownloaderWinHttp* parent = static_cast<HTTPDownloaderWinHttp*>(req->parent);
std::unique_lock<std::mutex> lock(parent->m_pending_http_request_lock);
Assert(std::none_of(parent->m_pending_http_requests.begin(), parent->m_pending_http_requests.end(),
[req](HTTPDownloader::Request* it) { return it == req; }));
// we can clean up the connection as well
DebugAssert(req->hConnection != NULL);
WinHttpCloseHandle(req->hConnection);
delete req;
return;
}
case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR:
{
const WINHTTP_ASYNC_RESULT* res = reinterpret_cast<const WINHTTP_ASYNC_RESULT*>(lpvStatusInformation);
Log_ErrorPrintf("WinHttp async function %p returned error %u", res->dwResult, res->dwError);
req->status_code = HTTP_STATUS_ERROR;
req->state.store(Request::State::Complete);
return;
}
case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE:
{
Log_DevPrintf("SendRequest complete");
if (!WinHttpReceiveResponse(hRequest, nullptr))
{
Log_ErrorPrintf("WinHttpReceiveResponse() failed: %u", GetLastError());
req->status_code = HTTP_STATUS_ERROR;
req->state.store(Request::State::Complete);
}
return;
}
case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE:
{
Log_DevPrintf("Headers available");
DWORD buffer_size = sizeof(req->status_code);
if (!WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX, &req->status_code, &buffer_size, WINHTTP_NO_HEADER_INDEX))
{
Log_ErrorPrintf("WinHttpQueryHeaders() for status code failed: %u", GetLastError());
req->status_code = HTTP_STATUS_ERROR;
req->state.store(Request::State::Complete);
return;
}
buffer_size = sizeof(req->content_length);
if (!WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_LENGTH | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX, &req->content_length, &buffer_size,
WINHTTP_NO_HEADER_INDEX))
{
if (GetLastError() != ERROR_WINHTTP_HEADER_NOT_FOUND)
Log_WarningPrintf("WinHttpQueryHeaders() for content length failed: %u", GetLastError());
req->content_length = 0;
}
DWORD content_type_length = 0;
if (!WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_TYPE, WINHTTP_HEADER_NAME_BY_INDEX,
WINHTTP_NO_OUTPUT_BUFFER, &content_type_length, WINHTTP_NO_HEADER_INDEX) &&
GetLastError() == ERROR_INSUFFICIENT_BUFFER && content_type_length >= sizeof(content_type_length))
{
std::wstring content_type_wstring;
content_type_wstring.resize((content_type_length / sizeof(wchar_t)) - 1);
if (WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_TYPE, WINHTTP_HEADER_NAME_BY_INDEX,
content_type_wstring.data(), &content_type_length, WINHTTP_NO_HEADER_INDEX))
{
req->content_type = StringUtil::WideStringToUTF8String(content_type_wstring);
}
}
Log_DevPrintf("Status code %d, content-length is %u", req->status_code, req->content_length);
req->data.reserve(req->content_length);
req->state = Request::State::Receiving;
// start reading
if (!WinHttpQueryDataAvailable(hRequest, nullptr) && GetLastError() != ERROR_IO_PENDING)
{
Log_ErrorPrintf("WinHttpQueryDataAvailable() failed: %u", GetLastError());
req->status_code = HTTP_STATUS_ERROR;
req->state.store(Request::State::Complete);
}
return;
}
case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE:
{
DWORD bytes_available;
std::memcpy(&bytes_available, lpvStatusInformation, sizeof(bytes_available));
if (bytes_available == 0)
{
// end of request
Log_DevPrintf("End of request '%s', %zu bytes received", req->url.c_str(), req->data.size());
req->state.store(Request::State::Complete);
return;
}
// start the transfer
Log_DevPrintf("%u bytes available", bytes_available);
req->io_position = static_cast<u32>(req->data.size());
req->data.resize(req->io_position + bytes_available);
if (!WinHttpReadData(hRequest, req->data.data() + req->io_position, bytes_available, nullptr) &&
GetLastError() != ERROR_IO_PENDING)
{
Log_ErrorPrintf("WinHttpReadData() failed: %u", GetLastError());
req->status_code = HTTP_STATUS_ERROR;
req->state.store(Request::State::Complete);
}
return;
}
case WINHTTP_CALLBACK_STATUS_READ_COMPLETE:
{
Log_DevPrintf("Read of %u complete", dwStatusInformationLength);
const u32 new_size = req->io_position + dwStatusInformationLength;
Assert(new_size <= req->data.size());
req->data.resize(new_size);
req->start_time = Common::Timer::GetCurrentValue();
if (!WinHttpQueryDataAvailable(hRequest, nullptr) && GetLastError() != ERROR_IO_PENDING)
{
Log_ErrorPrintf("WinHttpQueryDataAvailable() failed: %u", GetLastError());
req->status_code = HTTP_STATUS_ERROR;
req->state.store(Request::State::Complete);
}
return;
}
default:
// unhandled, ignore
return;
}
}
HTTPDownloader::Request* HTTPDownloaderWinHttp::InternalCreateRequest()
{
Request* req = new Request();
return req;
}
void HTTPDownloaderWinHttp::InternalPollRequests()
{
// noop - it uses windows's worker threads
}
bool HTTPDownloaderWinHttp::StartRequest(HTTPDownloader::Request* request)
{
Request* req = static_cast<Request*>(request);
std::wstring host_name;
host_name.resize(req->url.size());
req->object_name.resize(req->url.size());
URL_COMPONENTSW uc = {};
uc.dwStructSize = sizeof(uc);
uc.lpszHostName = host_name.data();
uc.dwHostNameLength = static_cast<DWORD>(host_name.size());
uc.lpszUrlPath = req->object_name.data();
uc.dwUrlPathLength = static_cast<DWORD>(req->object_name.size());
const std::wstring url_wide(StringUtil::UTF8StringToWideString(req->url));
if (!WinHttpCrackUrl(url_wide.c_str(), static_cast<DWORD>(url_wide.size()), 0, &uc))
{
Log_ErrorPrintf("WinHttpCrackUrl() failed: %u", GetLastError());
req->callback(HTTP_STATUS_ERROR, std::string(), req->data);
delete req;
return false;
}
host_name.resize(uc.dwHostNameLength);
req->object_name.resize(uc.dwUrlPathLength);
req->hConnection = WinHttpConnect(m_hSession, host_name.c_str(), uc.nPort, 0);
if (!req->hConnection)
{
Log_ErrorPrintf("Failed to start HTTP request for '%s': %u", req->url.c_str(), GetLastError());
req->callback(HTTP_STATUS_ERROR, std::string(), req->data);
delete req;
return false;
}
const DWORD request_flags = uc.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0;
req->hRequest =
WinHttpOpenRequest(req->hConnection, (req->type == HTTPDownloader::Request::Type::Post) ? L"POST" : L"GET",
req->object_name.c_str(), NULL, NULL, NULL, request_flags);
if (!req->hRequest)
{
Log_ErrorPrintf("WinHttpOpenRequest() failed: %u", GetLastError());
WinHttpCloseHandle(req->hConnection);
return false;
}
BOOL result;
if (req->type == HTTPDownloader::Request::Type::Post)
{
const std::wstring_view additional_headers(L"Content-Type: application/x-www-form-urlencoded\r\n");
result = WinHttpSendRequest(req->hRequest, additional_headers.data(), static_cast<DWORD>(additional_headers.size()),
req->post_data.data(), static_cast<DWORD>(req->post_data.size()),
static_cast<DWORD>(req->post_data.size()), reinterpret_cast<DWORD_PTR>(req));
}
else
{
result = WinHttpSendRequest(req->hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0,
reinterpret_cast<DWORD_PTR>(req));
}
if (!result && GetLastError() != ERROR_IO_PENDING)
{
Log_ErrorPrintf("WinHttpSendRequest() failed: %u", GetLastError());
req->status_code = HTTP_STATUS_ERROR;
req->state.store(Request::State::Complete);
}
Log_DevPrintf("Started HTTP request for '%s'", req->url.c_str());
req->state = Request::State::Started;
req->start_time = Common::Timer::GetCurrentValue();
return true;
}
void HTTPDownloaderWinHttp::CloseRequest(HTTPDownloader::Request* request)
{
Request* req = static_cast<Request*>(request);
if (req->hRequest != NULL)
{
// req will be freed by the callback.
// the callback can fire immediately here if there's nothing running async, so don't touch req afterwards
WinHttpCloseHandle(req->hRequest);
return;
}
if (req->hConnection != NULL)
WinHttpCloseHandle(req->hConnection);
delete req;
}
} // namespace Common

View File

@ -1,42 +0,0 @@
// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#pragma once
#include "http_downloader.h"
#include "common/windows_headers.h"
#include <winhttp.h>
namespace Common {
class HTTPDownloaderWinHttp final : public HTTPDownloader
{
public:
HTTPDownloaderWinHttp();
~HTTPDownloaderWinHttp() override;
bool Initialize(const char* user_agent);
protected:
Request* InternalCreateRequest() override;
void InternalPollRequests() override;
bool StartRequest(HTTPDownloader::Request* request) override;
void CloseRequest(HTTPDownloader::Request* request) override;
private:
struct Request : HTTPDownloader::Request
{
std::wstring object_name;
HINTERNET hConnection = NULL;
HINTERNET hRequest = NULL;
u32 io_position = 0;
};
static void CALLBACK HTTPStatusCallback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus,
LPVOID lpvStatusInformation, DWORD dwStatusInformationLength);
HINTERNET m_hSession = NULL;
};
} // namespace FrontendCommon