diff options
Diffstat (limited to 'src/http.cpp')
| -rw-r--r-- | src/http.cpp | 142 |
1 files changed, 142 insertions, 0 deletions
diff --git a/src/http.cpp b/src/http.cpp new file mode 100644 index 0000000..409fa6a --- /dev/null +++ b/src/http.cpp @@ -0,0 +1,142 @@ +#include "http.hpp" + +#include <curl/curl.h> + +#include <algorithm> +#include <cctype> +#include <cstddef> +#include <memory> +#include <mutex> +#include <string> + +namespace cepis::detail { +namespace { + +class GlobalInit { + public: + GlobalInit() { curl_global_init(CURL_GLOBAL_DEFAULT); } + ~GlobalInit() { curl_global_cleanup(); } + + GlobalInit(const GlobalInit&) = delete; + GlobalInit& operator=(const GlobalInit&) = delete; + GlobalInit(GlobalInit&&) = delete; + GlobalInit& operator=(GlobalInit&&) = delete; +}; + +void ensure_global_init() { + static const GlobalInit init; + (void)init; +} + +struct EasyDeleter { + void operator()(CURL* handle) const noexcept { curl_easy_cleanup(handle); } +}; + +struct SlistDeleter { + void operator()(curl_slist* list) const noexcept { curl_slist_free_all(list); } +}; + +using EasyHandle = std::unique_ptr<CURL, EasyDeleter>; +using HeaderList = std::unique_ptr<curl_slist, SlistDeleter>; + +std::size_t write_body(char* data, std::size_t size, std::size_t count, void* user_data) { + const std::size_t total = size * count; + static_cast<std::string*>(user_data)->append(data, total); + return total; +} + +std::size_t read_header(char* data, std::size_t size, std::size_t count, void* user_data) { + const std::size_t total = size * count; + const std::string_view line(data, total); + const std::size_t colon = line.find(':'); + if (colon != std::string_view::npos) { + std::string name(line.substr(0, colon)); + std::transform(name.begin(), name.end(), name.begin(), + [](unsigned char c) { return static_cast<char>(std::tolower(c)); }); + if (name == "retry-after") { + std::string_view value = line.substr(colon + 1); + while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) { + value.remove_prefix(1); + } + while (!value.empty() && (value.back() == '\r' || value.back() == '\n' || + value.back() == ' ' || value.back() == '\t')) { + value.remove_suffix(1); + } + *static_cast<std::string*>(user_data) = std::string(value); + } + } + return total; +} + +} // namespace + +HttpResult perform_get(const HttpRequest& request) { + ensure_global_init(); + + HttpResult result; + const EasyHandle handle(curl_easy_init()); + if (!handle) { + result.curl_error = "could not initialise libcurl"; + return result; + } + + HeaderList headers; + const auto add_header = [&headers](const char* value) { + curl_slist* owned = headers.release(); + curl_slist* grown = curl_slist_append(owned, value); + headers.reset(grown != nullptr ? grown : owned); + }; + add_header("Accept: application/json, text/csv;q=0.9, */*;q=0.1"); + add_header("Accept-Encoding: identity"); + + char error_buffer[CURL_ERROR_SIZE] = {}; + const long timeout_ms = static_cast<long>(request.timeout.count()); + + curl_easy_setopt(handle.get(), CURLOPT_URL, request.url.c_str()); + curl_easy_setopt(handle.get(), CURLOPT_HTTPGET, 1L); + curl_easy_setopt(handle.get(), CURLOPT_USERAGENT, request.user_agent.c_str()); + curl_easy_setopt(handle.get(), CURLOPT_HTTPHEADER, headers.get()); + curl_easy_setopt(handle.get(), CURLOPT_WRITEFUNCTION, write_body); + curl_easy_setopt(handle.get(), CURLOPT_WRITEDATA, &result.body); + curl_easy_setopt(handle.get(), CURLOPT_HEADERFUNCTION, read_header); + curl_easy_setopt(handle.get(), CURLOPT_HEADERDATA, &result.retry_after); + curl_easy_setopt(handle.get(), CURLOPT_TIMEOUT_MS, timeout_ms); + curl_easy_setopt(handle.get(), CURLOPT_CONNECTTIMEOUT_MS, timeout_ms); + curl_easy_setopt(handle.get(), CURLOPT_NOSIGNAL, 1L); + curl_easy_setopt(handle.get(), CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(handle.get(), CURLOPT_MAXREDIRS, 5L); + curl_easy_setopt(handle.get(), CURLOPT_ERRORBUFFER, error_buffer); + + const CURLcode code = curl_easy_perform(handle.get()); + if (code != CURLE_OK) { + result.curl_error = error_buffer[0] != '\0' ? error_buffer : curl_easy_strerror(code); + return result; + } + + long status = 0; + curl_easy_getinfo(handle.get(), CURLINFO_RESPONSE_CODE, &status); + result.completed = true; + result.status = static_cast<int>(status); + return result; +} + +std::string url_escape(std::string_view value) { + static constexpr char kHex[] = "0123456789ABCDEF"; + std::string out; + out.reserve(value.size()); + for (const char raw : value) { + const auto c = static_cast<unsigned char>(raw); + const bool unreserved = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '_' || c == '~'; + if (unreserved) { + out.push_back(static_cast<char>(c)); + } else { + out.push_back('%'); + out.push_back(kHex[c >> 4]); + out.push_back(kHex[c & 0x0F]); + } + } + return out; +} + +} // namespace cepis::detail |