// SPDX-License-Identifier: MIT // // Tiny libcurl wrapper used internally by the SDK. Not part of the public // surface — header lives under src/ on purpose. #pragma once #include #include #include #include #include #include #include #include #include namespace clawdforge::detail { /// One header line on the wire, as `Name: value`. Stored as a map so writers /// can replace by name without dupes. using HeaderMap = std::map; /// Parameters for a single HTTP exchange. struct Request { std::string method; ///< "GET" / "POST" / "DELETE" / ... std::string url; ///< Fully-qualified. HeaderMap headers; ///< Caller adds Authorization / Content-Type / Accept. std::optional body; ///< Raw bytes for application/json POSTs. /// Optional multipart filedata streamed off disk. When set, the SDK builds /// a libcurl `curl_mime` and adds the regular form fields below as text /// parts. `body` is ignored in that case. struct MultipartFile { std::string field_name; ///< form field name, e.g. "file" std::string filename; ///< Content-Disposition filename std::string content_type; ///< e.g. "application/octet-stream" std::string filesystem_path; ///< absolute or cwd-relative path on disk }; std::optional file; std::vector> form_fields; ///< extra text parts }; /// Result of an HTTP exchange. struct Response { long status{0}; std::string body; HeaderMap headers; }; /// Thin RAII wrapper around a `CURL*` easy handle plus the per-call mutables /// (header list, mime, etc.). class CurlSession { public: /// Throws `TransportError` if libcurl initialisation fails. CurlSession(std::chrono::seconds timeout, std::chrono::seconds connect_timeout, std::string user_agent, bool insecure_tls); ~CurlSession(); CurlSession(const CurlSession&) = delete; CurlSession& operator=(const CurlSession&) = delete; CurlSession(CurlSession&&) noexcept; CurlSession& operator=(CurlSession&&) noexcept; /// Perform a request, throwing `TransportError` on libcurl failure. The /// returned `Response::status` may still be non-2xx — that's the caller's /// problem to translate. Response perform(const Request& req); private: CURL* easy_{nullptr}; std::chrono::seconds timeout_{}; std::chrono::seconds connect_timeout_{}; std::string user_agent_; bool insecure_tls_{false}; }; /// Concatenate base + path with exactly one separating slash. Idempotent for /// trailing slashes on the base. [[nodiscard]] std::string join_url(std::string_view base, std::string_view path); /// Percent-encode a path segment (e.g. token name in /admin/tokens/). [[nodiscard]] std::string url_encode_path(std::string_view in); /// Truncate UTF-8-ish strings for error logs without slicing through the /// middle of a multibyte sequence. Best-effort, not perfect. [[nodiscard]] std::string truncate_for_log(std::string_view s, std::size_t max); /// Library-wide one-time `curl_global_init`, ref-counted via a static counter. /// Construct one of these in any TU that uses libcurl; the first one calls /// `curl_global_init`, subsequent ones bump the refcount, and destruction /// runs `curl_global_cleanup` once the last guard goes away. class CurlGlobalGuard { public: CurlGlobalGuard(); ~CurlGlobalGuard(); /// Copy ctor bumps the refcount — cheap and useful for `Client` move /// semantics where Impl wants to own one as a value member. CurlGlobalGuard(const CurlGlobalGuard&); CurlGlobalGuard& operator=(const CurlGlobalGuard&); CurlGlobalGuard(CurlGlobalGuard&&) noexcept; CurlGlobalGuard& operator=(CurlGlobalGuard&&) noexcept; private: bool active_{true}; }; } // namespace clawdforge::detail