From ce4ab4b7cc685320bd9d2a017390b2f12b8cff86 Mon Sep 17 00:00:00 2001 From: momo5502 Date: Sun, 12 Feb 2023 18:21:06 +0100 Subject: [PATCH] Use curl for http requests Fixes #150 --- .gitmodules | 3 + deps/curl | 1 + deps/premake/curl.lua | 73 ++++++++++++++++++++ src/common/utils/http.cpp | 138 ++++++++++++++++++++++++++++++-------- src/common/utils/http.hpp | 6 +- 5 files changed, 190 insertions(+), 31 deletions(-) create mode 160000 deps/curl create mode 100644 deps/premake/curl.lua diff --git a/.gitmodules b/.gitmodules index 9e7892ca..5c593a25 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,3 +28,6 @@ [submodule "deps/stb"] path = deps/stb url = https://github.com/nothings/stb.git +[submodule "deps/curl"] + path = deps/curl + url = https://github.com/curl/curl.git diff --git a/deps/curl b/deps/curl new file mode 160000 index 00000000..7ce140ba --- /dev/null +++ b/deps/curl @@ -0,0 +1 @@ +Subproject commit 7ce140ba97de1bf3e27299a72b0cc229c9e1364e diff --git a/deps/premake/curl.lua b/deps/premake/curl.lua new file mode 100644 index 00000000..8db164e1 --- /dev/null +++ b/deps/premake/curl.lua @@ -0,0 +1,73 @@ +curl = { + source = path.join(dependencies.basePath, "curl"), +} + +function curl.import() + links { "curl" } + + filter "toolset:msc*" + links { "Crypt32.lib" } + filter {} + + curl.includes() +end + +function curl.includes() +filter "toolset:msc*" + includedirs { + path.join(curl.source, "include"), + } + + defines { + "CURL_STRICTER", + "CURL_STATICLIB", + "CURL_DISABLE_LDAP", + } +filter {} +end + +function curl.project() + if not os.istarget("windows") then + return + end + + project "curl" + language "C" + + curl.includes() + + includedirs { + path.join(curl.source, "lib"), + } + + files { + path.join(curl.source, "lib/**.c"), + path.join(curl.source, "lib/**.h"), + } + + defines { + "BUILDING_LIBCURL", + } + + filter "toolset:msc*" + + defines { + "USE_SCHANNEL", + "USE_WINDOWS_SSPI", + "USE_THREADS_WIN32", + } + + filter "toolset:not msc*" + + defines { + "USE_GNUTLS", + "USE_THREADS_POSIX", + } + + filter {} + + warnings "Off" + kind "StaticLib" +end + +table.insert(dependencies, curl) \ No newline at end of file diff --git a/src/common/utils/http.cpp b/src/common/utils/http.cpp index 3cb59991..a208ac67 100644 --- a/src/common/utils/http.cpp +++ b/src/common/utils/http.cpp @@ -1,48 +1,128 @@ #include "http.hpp" -#include "nt.hpp" -#include +#include +#include "finally.hpp" + +#pragma comment(lib, "ws2_32.lib") namespace utils::http { - std::optional get_data(const std::string& url) + namespace { - CComPtr stream; + struct progress_helper + { + const std::function* callback{}; + std::exception_ptr exception{}; + }; - if (FAILED(URLOpenBlockingStreamA(nullptr, url.data(), &stream, 0, nullptr))) + int progress_callback(void* clientp, const curl_off_t /*dltotal*/, const curl_off_t dlnow, + const curl_off_t /*ultotal*/, const curl_off_t /*ulnow*/) + { + auto* helper = static_cast(clientp); + + try + { + if (*helper->callback) + { + (*helper->callback)(dlnow); + } + } + catch (...) + { + helper->exception = std::current_exception(); + return -1; + } + + return 0; + } + + size_t write_callback(void* contents, const size_t size, const size_t nmemb, void* userp) + { + auto* buffer = static_cast(userp); + + const auto total_size = size * nmemb; + buffer->append(static_cast(contents), total_size); + return total_size; + } + } + + std::optional get_data(const std::string& url, const headers& headers, + const std::function& callback, const uint32_t retries) + { + curl_slist* header_list = nullptr; + auto* curl = curl_easy_init(); + if (!curl) { return {}; } - char buffer[0x1000]; - std::string result; - - HRESULT status{}; - - do - { - DWORD bytes_read = 0; - status = stream->Read(buffer, sizeof(buffer), &bytes_read); - - if (bytes_read > 0) + auto _ = utils::finally([&]() { - result.append(buffer, bytes_read); + curl_slist_free_all(header_list); + curl_easy_cleanup(curl); + }); + + for (const auto& header : headers) + { + auto data = header.first + ": " + header.second; + header_list = curl_slist_append(header_list, data.data()); + } + + std::string buffer{}; + progress_helper helper{}; + helper.callback = &callback; + + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list); + curl_easy_setopt(curl, CURLOPT_URL, url.data()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buffer); + curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_callback); + curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &helper); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl, CURLOPT_USERAGENT, "xlabs-updater/1.0"); + curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); + + for (auto i = 0u; i < retries + 1; ++i) + { + // Due to CURLOPT_FAILONERROR, CURLE_OK will not be met when the server returns 400 or 500 + if (curl_easy_perform(curl) == CURLE_OK) + { + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + if (http_code >= 200) + { + return { std::move(buffer) }; + } + + throw std::runtime_error( + "Bad status code " + std::to_string(http_code) + " met while trying to download file " + url); + } + + if (helper.exception) + { + std::rethrow_exception(helper.exception); + } + + long http_code = 0; + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); + + if (http_code > 0) + { + break; } } - while (SUCCEEDED(status) && status != S_FALSE); - if (FAILED(status)) - { - return {}; - } - - return {result}; + return {}; } - std::future> get_data_async(const std::string& url) + std::future> get_data_async(const std::string& url, const headers& headers) { - return std::async(std::launch::async, [url]() - { - return get_data(url); - }); + return std::async(std::launch::async, [url, headers]() + { + return get_data(url, headers); + }); } } diff --git a/src/common/utils/http.hpp b/src/common/utils/http.hpp index 65628a9f..2fb650b3 100644 --- a/src/common/utils/http.hpp +++ b/src/common/utils/http.hpp @@ -6,6 +6,8 @@ namespace utils::http { - std::optional get_data(const std::string& url); - std::future> get_data_async(const std::string& url); + using headers = std::unordered_map; + + std::optional get_data(const std::string& url, const headers& headers = {}, const std::function& callback = {}, uint32_t retries = 2); + std::future> get_data_async(const std::string& url, const headers& headers = {}); }