/* {{copyright}} */ /* {{version}} */ /* {{license}} */ #pragma once #include #include #include #include #include #include #include #include namespace sdi_ToolBox::communication { //-------------------------------------------------------------- class CurlGlobalInitializer { public: static void init() // Initialize libcurl globally { static auto instance = std::unique_ptr(new CurlGlobalInitializer()); } public: virtual ~CurlGlobalInitializer() // Default destructor { curl_global_cleanup(); } CurlGlobalInitializer(const CurlGlobalInitializer &obj) = delete; // Copy constructor CurlGlobalInitializer(CurlGlobalInitializer &&obj) noexcept = delete; // Move constructor CurlGlobalInitializer &operator=(const CurlGlobalInitializer &obj) = delete; // Copy assignment operator CurlGlobalInitializer &operator=(CurlGlobalInitializer &&obj) noexcept = delete; // Move assignment operator protected: CurlGlobalInitializer() // Default constructor { if (curl_global_init(CURL_GLOBAL_DEFAULT) != CURLE_OK) throw std::runtime_error("ERROR: Failed to initialize cURL"); } }; //-------------------------------------------------------------- /* --- */ //-------------------------------------------------------------- class CurlHttpClient { public: enum class Method : uint8_t { http_GET, http_POST, http_PUT, http_DELETE, }; struct Settings { bool enableSSL = true; bool ssl_verifyPeer = true; bool ssl_verifyHost = true; std::filesystem::path ssl_caInfo; std::string ssl_certBlob; std::chrono::seconds timeout{ 10 }; bool operator==(const Settings &other) const { // Compare all fields against the other object return (enableSSL == other.enableSSL && ssl_verifyPeer == other.ssl_verifyPeer && ssl_verifyHost == other.ssl_verifyHost && ssl_caInfo == other.ssl_caInfo && ssl_certBlob == other.ssl_certBlob); } }; using Headers = std::unordered_map; struct Request { std::string url; Method method = Method::http_GET; std::string body; Headers headers; Settings settings; bool operator==(const Request &other) const { // Compare all fields against the other object return (url == other.url && method == other.method && body == other.body && headers == other.headers && settings == other.settings); } }; struct Response { int returnCode = -1; std::string content; }; public: CurlHttpClient(); // Default constructor virtual ~CurlHttpClient() = default; // Default destructor CurlHttpClient(const CurlHttpClient &obj) = delete; // Copy constructor CurlHttpClient(CurlHttpClient &&obj) noexcept = delete; // Move constructor CurlHttpClient &operator=(const CurlHttpClient &obj) = delete; // Copy assignment operator CurlHttpClient &operator=(CurlHttpClient &&obj) noexcept = delete; // Move assignment operator const Response &performRequest(const Request &request = {}); // Perform HTTP request const Response &performRequest_nothrow(const Request &request = {}); // Perform HTTP request const Request &getRequest() const; // Retrieves curl request const Response &getResponse() const; // Retrieves curl response protected: mutable std::mutex m_mtx; CURL *m_curl = nullptr; curl_slist *m_headerList = nullptr; Request m_request; Response m_response; private: CURLcode setRequestOptions(); // Set cURL request options static size_t write_callback(void *contents, // Callback for writing received data size_t size, size_t itemCount, void *userData); }; //-------------------------------------------------------------- /* Default constructor */ inline CurlHttpClient::CurlHttpClient() { CurlGlobalInitializer::init(); } //-------------------------------------------------------------- /* Perform HTTP request */ inline const CurlHttpClient::Response &CurlHttpClient::performRequest(const Request &request) { std::lock_guard lock(m_mtx); if (const Request default_req = {}; request != default_req) m_request = request; try { m_curl = curl_easy_init(); if (!m_curl) throw std::runtime_error("unable to initialize curl library"); // Set request options if (const auto res = setRequestOptions(); res != CURLE_OK) throw std::runtime_error(std::format("ERROR: Failed to set up CURL options: {}", curl_easy_strerror(res))); // Perform request if (const auto res = curl_easy_perform(m_curl); res != CURLE_OK) throw std::runtime_error(std::format("ERROR: curl_easy_perform() failed: {}", curl_easy_strerror(res))); if (const auto res = curl_easy_getinfo(m_curl, CURLINFO_RESPONSE_CODE, &m_response.returnCode); res != CURLE_OK) throw std::runtime_error(std::format("ERROR: Failed to get content code: {}", curl_easy_strerror(res))); } catch (const std::exception &e) { m_response.returnCode = -1; m_response.content = e.what(); } // Free resources if (m_curl) { curl_easy_cleanup(m_curl); // free the easy curl handle m_curl = nullptr; } if (m_headerList) { curl_slist_free_all(m_headerList); // free the custom headers m_curl = nullptr; } if (m_response.returnCode == -1) throw std::runtime_error(m_response.content); return m_response; } //-------------------------------------------------------------- /* Perform HTTP request */ inline const CurlHttpClient::Response &CurlHttpClient::performRequest_nothrow(const Request &request) { try { performRequest(request); } catch (const std::exception &e) { LogError() << e.what() << std::endl; } return m_response; } //-------------------------------------------------------------- /* Retrieves curl request */ inline const CurlHttpClient::Request &CurlHttpClient::getRequest() const { std::lock_guard lock(m_mtx); return m_request; } //-------------------------------------------------------------- /* Retrieves curl response */ inline const CurlHttpClient::Response &CurlHttpClient::getResponse() const { std::lock_guard lock(m_mtx); return m_response; } //-------------------------------------------------------------- /* Set cURL request options */ inline CURLcode CurlHttpClient::setRequestOptions() { // --- Common Options --- // Set the target URL if (const auto res = curl_easy_setopt(m_curl, CURLOPT_URL, m_request.url.c_str()); res != CURLE_OK) return res; // Set the write callback function if (const auto res = curl_easy_setopt(m_curl, CURLOPT_WRITEFUNCTION, write_callback); res != CURLE_OK) return res; // Set the user data pointer for the callback (our content buffer) if (const auto res = curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, &m_response.content); res != CURLE_OK) return res; // Set custom headers if any if (m_headerList) { // Prepare header list for (const auto &[key, value] : m_request.headers) { const auto header = format("{0:}: {1:}", key, value); m_headerList = curl_slist_append(m_headerList, header.c_str()); } // Append headers to the request if (const auto res = curl_easy_setopt(m_curl, CURLOPT_HTTPHEADER, m_headerList); res != CURLE_OK) return res; } // Set the max time in seconds for the entire operation (including connection and transfer) if (const auto res = curl_easy_setopt(m_curl, CURLOPT_TIMEOUT, m_request.settings.timeout.count()); res != CURLE_OK) return res; // --- SSL/TLS Options --- // curl documentation recommends setting both for full control if (m_request.settings.enableSSL) { // Enable peer certificate verification if (const auto res = curl_easy_setopt(m_curl, CURLOPT_SSL_VERIFYPEER, 1L); res != CURLE_OK) return res; // Enable host name verification (prevents 'Man-in-the-middle' attacks) if (const auto res = curl_easy_setopt(m_curl, CURLOPT_SSL_VERIFYHOST, 2L); res != CURLE_OK) return res; if (!m_request.settings.ssl_caInfo.empty()) { // Optionally, set the path to a CA bundle file (often not needed // if curl is built with a system-wide CA store) if (const auto res = curl_easy_setopt(m_curl, CURLOPT_CAINFO, m_request.settings.ssl_caInfo.c_str()); res != CURLE_OK) return res; } else if (!m_request.settings.ssl_certBlob.empty()) { if (const auto res = curl_easy_setopt(m_curl, CURLOPT_SSLKEY_BLOB, &m_request.settings.ssl_certBlob); res != CURLE_OK) return res; } } else { // Disable verification (USE WITH CAUTION: Insecure!) if (const auto res = curl_easy_setopt(m_curl, CURLOPT_SSL_VERIFYPEER, 0L); res != CURLE_OK) return res; if (const auto res = curl_easy_setopt(m_curl, CURLOPT_SSL_VERIFYHOST, 0L); res != CURLE_OK) return res; } // --- Method-Specific Options --- switch (m_request.method) { using enum Method; case http_GET: { if (const auto res = curl_easy_setopt(m_curl, CURLOPT_HTTPGET, 1L); res != CURLE_OK) return res; break; } case http_POST: { if (const auto res = curl_easy_setopt(m_curl, CURLOPT_POST, 1L); res != CURLE_OK) return res; // Set the data to be sent if (const auto res = curl_easy_setopt(m_curl, CURLOPT_POSTFIELDS, m_request.body.c_str()); res != CURLE_OK) return res; break; } case http_PUT: { if (const auto res = curl_easy_setopt(m_curl, CURLOPT_PUT, 1L); res != CURLE_OK) return res; if (const auto res = curl_easy_setopt(m_curl, CURLOPT_CUSTOMREQUEST, "PUT"); res != CURLE_OK) return res; if (const auto res = curl_easy_setopt(m_curl, CURLOPT_POSTFIELDS, m_request.body.c_str()); res != CURLE_OK) return res; break; } case http_DELETE: { if (const auto res = curl_easy_setopt(m_curl, CURLOPT_CUSTOMREQUEST, "DELETE"); res != CURLE_OK) return res; break; } } return CURLE_OK; } //-------------------------------------------------------------- /* Callback for writing received data */ inline size_t CurlHttpClient::write_callback(void *contents, const size_t size, const size_t itemCount, void *userData) { // Total size of the incoming buffer const auto realSize = size * itemCount; // Append the received data to the content string const auto &response = static_cast(userData); response->append(static_cast(contents), realSize); return realSize; } //-------------------------------------------------------------- /* --- */ //-------------------------------------------------------------- class CurlInfo { public: CurlInfo(); // Default constructor virtual ~CurlInfo() = default; // Default destructor CurlInfo(const CurlInfo &obj) = delete; // Copy constructor CurlInfo(CurlInfo &&obj) noexcept = delete; // Move constructor CurlInfo &operator=(const CurlInfo &obj) = delete; // Copy assignment operator CurlInfo &operator=(CurlInfo &&obj) noexcept = delete; // Move assignment operator void toStream(std::ostream &stream); // Print cURL information to stream object void print(); // Print cURL information to cout std::string version; uint32_t features; bool ssl_support = false; std::string ssl_version; std::vector availableProtocols; }; //-------------------------------------------------------------- /* Default constructor */ inline CurlInfo::CurlInfo() { CurlGlobalInitializer::init(); // Retrieves information from the library const auto info = curl_version_info(CURLVERSION_NOW); version = info->version; features = info->features; ssl_support = info->features & CURL_VERSION_SSL; ssl_version = info->ssl_version; if (info->protocols) { for (const char *const *proto = info->protocols; *proto; ++proto) availableProtocols.emplace_back(*proto); } } //-------------------------------------------------------------- /* Print cURL information to stream object */ inline void CurlInfo::toStream(std::ostream &stream) { stream << "--- libcurl Version Info ---" << "\n"; stream << "Version : " << version << "\n"; stream << "Features (bitmask) : " << std::format("0x{0:X}", features) << "\n"; stream << "SSL/TLS support : " << ((ssl_support) ? std::format("**Yes** [{0:}]", ssl_version) : "No") << "\n"; if (!availableProtocols.empty()) { stream << "Available protocols: "; bool first = true; for (const auto protocol : availableProtocols) { if (!first) stream << ", "; stream << protocol; first = false; } stream << "\n"; } } //-------------------------------------------------------------- /* Print cURL information to cout */ inline void CurlInfo::print() { toStream(std::cout); std::cout << std::flush; } //-------------------------------------------------------------- } // namespace sdi_ToolBox::communication