1. Overview
The HttpClient module provides a lightweight, reusable HTTP utility for BastionGuard components that require outbound web requests, with primary usage in the Identity Leak subsystem (provider lookups such as HIBP and LeakCheck).
The implementation is built on libcurl and offers:
- Simple GET and POST interfaces
- Unified request execution through a shared
perform()path - Safe, one-time global initialization of curl
- Default “sane” headers (User-Agent and Accept) for compatibility with common APIs
- Redirect following with a bounded redirect count
- Timeout and connection timeout handling
- TLS verification enabled by default
- URL encoding helper (
urlEncode())
2. Dependencies
- libcurl – HTTP/TLS transport and URL encoding
- C++ standard library –
std::once_flag,std::call_once,std::map, string utilities
The module is designed to be stateless at the API level; each request creates and destroys its own CURL* easy handle.
3. Global Initialization Strategy
3.1 ensureCurlGlobalInit()
libcurl requires a global initialization step. The module enforces safe, single-execution initialization using:
std::once_flag g_curlInitOncestd::call_once()
Initialization is performed with:
curl_global_init(CURL_GLOBAL_DEFAULT);
curl_global_cleanup() is intentionally not called. For long-running applications, avoiding cleanup can reduce shutdown ordering issues and is operationally acceptable when the process lifetime is the dominant resource boundary.
4. Response Model
Requests return an HttpResponse structure with:
status– HTTP status code (0 on transport-level failure)body– response body as a stringerror– error string populated on libcurl failure
This model separates transport errors (libcurl) from HTTP-level non-2xx results, which are surfaced via status.
5. Public API
5.1 GET
Signature:
HttpResponse HttpClient::get(
const std::string& url,
const std::map<std::string, std::string>& headers,
long timeoutSeconds
)
Delegates to perform() with method GET.
5.2 POST
Signature:
HttpResponse HttpClient::post(
const std::string& url,
const std::string& body,
const std::map<std::string, std::string>& headers,
long timeoutSeconds
)
Delegates to perform() with method POST and includes a request body.
5.3 URL Encoding
Signature:
std::string HttpClient::urlEncode(const std::string& value)
Uses curl_easy_escape() to produce a percent-encoded string suitable for query strings and path components.
6. Request Execution: perform()
6.1 perform(url, method, body, headers, timeout)
All requests converge on:
HttpResponse HttpClient::perform(
const std::string& url,
const char* method,
const std::string& requestBody,
const std::map<std::string, std::string>& headers,
long timeoutSeconds
)
The method configures a curl easy handle, executes the request, and returns a populated HttpResponse.
6.2 Response Body Capture
Response payload is captured into a string using a write callback:
writeCallback()appends received chunks to astd::string- Configured via
CURLOPT_WRITEFUNCTIONandCURLOPT_WRITEDATA
This approach supports arbitrary payload sizes (bounded by memory), typical for JSON API responses in the Identity Leak workflow.
6.3 Header Handling
Caller-provided headers are merged with default headers to improve API compatibility.
Default headers applied when absent:
- User-Agent:
BastionGuard/IdentityLeak (GTKMM4) - Accept:
application/json
Headers are converted from std::map into a curl_slist* list via buildHeaderList(), then assigned through:
CURLOPT_HTTPHEADER
6.4 Redirect Policy
The client follows redirects with bounded limits:
CURLOPT_FOLLOWLOCATION = 1CURLOPT_MAXREDIRS = 5
This enables compatibility with endpoints that redirect to canonical hostnames while preventing unbounded redirect loops.
6.5 Timeouts
The client enforces:
- Overall request timeout:
CURLOPT_TIMEOUT = timeoutSeconds - Connection establishment timeout:
CURLOPT_CONNECTTIMEOUT = 10
This protects the monitoring subsystem from hanging on network stalls.
6.6 TLS Verification
TLS verification is explicitly enabled:
CURLOPT_SSL_VERIFYPEER = 1CURLOPT_SSL_VERIFYHOST = 2
This prevents silent downgrade to insecure transport and ensures server certificate validation remains active.
6.7 Method Selection
Method-specific configuration is applied as follows:
- POST:
CURLOPT_POST = 1CURLOPT_POSTFIELDSandCURLOPT_POSTFIELDSIZEset to the request body
- GET:
CURLOPT_HTTPGET = 1
Other HTTP verbs are not implemented by this module.
6.8 Error Handling
After execution, the module distinguishes between transport errors and HTTP status outcomes:
- If
curl_easy_perform()fails:status = 0errorcontainscurl_easy_strerror(code)bodyis cleared
- If transport succeeds:
- HTTP code retrieved via
CURLINFO_RESPONSE_CODE statusset to that valuebodycontains the captured payloaderroris cleared
- HTTP code retrieved via
6.9 Cleanup Semantics
Resources are released deterministically:
curl_slist_free_all(headerList)if allocatedcurl_easy_cleanup(curl)
This ensures no per-request heap structures persist across calls.
7. Runtime and Security Considerations
- Thread-safe global init:
std::call_onceguarantees curl global initialization is executed exactly once across threads - Per-request handles: separate
CURL*handles per request minimize shared-state complexity - Default headers: explicit User-Agent and Accept reduce API rejection rates and improve interoperability
- TLS enforcement: verification is enabled explicitly, preventing accidental insecure configuration
- Timeout discipline: connect and overall timeouts reduce monitoring thread stall risk
- Redirect bound: redirect following is limited to mitigate loops and abuse
- PII context: Identity Leak providers may transmit sensitive identifiers; this module does not log URLs or payloads, but upstream callers should treat request composition and error reporting carefully
- HTTP error handling: non-2xx responses are surfaced via
status; callers should explicitly validate expected codes and parse error payloads defensively