summaryrefslogtreecommitdiffstats
path: root/src/http.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/http.cpp')
-rw-r--r--src/http.cpp142
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