431 lines
14 KiB
C++
431 lines
14 KiB
C++
/*
|
|
{{copyright}}
|
|
*/
|
|
|
|
/*
|
|
{{version}}
|
|
*/
|
|
|
|
/*
|
|
{{license}}
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <curl/curl.h>
|
|
#include <filesystem>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <ostream>
|
|
#include <sdi_toolBox/logs/reLog.h>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
|
|
namespace sdi_ToolBox::communication
|
|
{
|
|
//--------------------------------------------------------------
|
|
class CurlGlobalInitializer
|
|
{
|
|
public:
|
|
static void init() // Initialize libcurl globally
|
|
{
|
|
static auto instance = std::unique_ptr<CurlGlobalInitializer>(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<std::string, std::string>;
|
|
|
|
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<std::string *>(userData);
|
|
response->append(static_cast<char *>(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<std::string> 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
|