#include "http.hpp" #include #include #include #include #include #include #include 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; using HeaderList = std::unique_ptr; 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(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(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(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(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(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(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(c)); } else { out.push_back('%'); out.push_back(kHex[c >> 4]); out.push_back(kHex[c & 0x0F]); } } return out; } } // namespace cepis::detail