From 94b7913b64fca698243bec2151d1ddbf85fbe4b4 Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Fri, 21 Aug 2026 20:12:35 +0530 Subject: [PATCH 01/16] feat: launch a coding harness against a local or upstream model with rcli opencode --- CMakeLists.txt | 4 + cmake/RunAnywhereSDK.cmake | 14 +++ src/cli/app.cpp | 1 + src/cli/cmd_harness.cpp | 27 +++++ src/cli/commands.h | 1 + src/harness/harness.cpp | 235 +++++++++++++++++++++++++++++++++++++ src/harness/harness.h | 27 +++++ 7 files changed, 309 insertions(+) create mode 100644 src/cli/cmd_harness.cpp create mode 100644 src/harness/harness.cpp create mode 100644 src/harness/harness.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 770f171..e4712d3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,11 +31,13 @@ add_library(rcli_core STATIC src/cli/cmd_bench.cpp src/cli/cmd_chat.cpp src/cli/cmd_engines.cpp + src/cli/cmd_harness.cpp src/cli/cmd_image.cpp src/cli/cmd_models.cpp src/cli/cmd_speech.cpp src/cli/output.cpp src/cli/preview.cpp + src/harness/harness.cpp src/media/png.cpp src/repl/repl.cpp src/repl/session.cpp @@ -66,6 +68,8 @@ target_include_directories(rcli_core PUBLIC # SDK, naming it here is what makes that explicit rather than accidental. find_package(ZLIB REQUIRED) target_link_libraries(rcli_core PRIVATE rac_commons ZLIB::ZLIB) +# The OpenAI-compatible server `rcli opencode` starts for a local model. +target_link_libraries(rcli_core PRIVATE rac_server) target_compile_definitions(rcli_core PRIVATE RCLI_VERSION="${PROJECT_VERSION}") foreach(_backend llamacpp onnx sherpa mlx neurt cloud) if(TARGET rac_backend_${_backend}) diff --git a/cmake/RunAnywhereSDK.cmake b/cmake/RunAnywhereSDK.cmake index b02dd6c..b10dfa9 100644 --- a/cmake/RunAnywhereSDK.cmake +++ b/cmake/RunAnywhereSDK.cmake @@ -26,6 +26,20 @@ set(RAC_BUILD_TESTS OFF CACHE BOOL "" FORCE) # rcli lives in this repo now; building the SDK's own copy would produce a # second binary with the same name. set(RAC_BUILD_CLI OFF CACHE BOOL "" FORCE) +# The OpenAI-compatible HTTP server, which `rcli opencode ` starts +# so a coding harness can talk to a model running on this machine. Same trick +# Ollama uses. It pulls cpp-httplib at configure time, which is the one place +# this build reaches the network for something it does not vendor. +set(RAC_BUILD_SERVER ON CACHE BOOL "" FORCE) +# cpp-httplib links OpenSSL, zlib, brotli and zstd whenever it can find them, +# and on a machine with Homebrew it always can. The server here only ever +# listens on loopback for a coding harness on the same machine, so none of that +# is wanted, and taking it would put four Homebrew dylibs into a binary that +# currently depends on nothing outside the system. +set(HTTPLIB_USE_OPENSSL_IF_AVAILABLE OFF CACHE BOOL "" FORCE) +set(HTTPLIB_USE_ZLIB_IF_AVAILABLE OFF CACHE BOOL "" FORCE) +set(HTTPLIB_USE_BROTLI_IF_AVAILABLE OFF CACHE BOOL "" FORCE) +set(HTTPLIB_USE_ZSTD_IF_AVAILABLE OFF CACHE BOOL "" FORCE) # RAC_STATIC_PLUGINS is deliberately left at the SDK's own default. Forcing it # on folds the cloud engine into rac_commons, which stops it being a # rac_backend_cloud target; the loop below then never defines RCLI_HAS_CLOUD and diff --git a/src/cli/app.cpp b/src/cli/app.cpp index a8136c7..641c07e 100644 --- a/src/cli/app.cpp +++ b/src/cli/app.cpp @@ -81,6 +81,7 @@ extern "C" int rcli_run(int argc, char** argv) { cli::RegisterImage(app, options); cli::RegisterBench(app, options); cli::RegisterEngines(app, options); + cli::RegisterHarness(app, options); // Only --color is left to decide here; logging was settled in rcli_begin, // which had to run before the engines got a chance to write anything. diff --git a/src/cli/cmd_harness.cpp b/src/cli/cmd_harness.cpp new file mode 100644 index 0000000..9bb7f12 --- /dev/null +++ b/src/cli/cmd_harness.cpp @@ -0,0 +1,27 @@ +#include +#include +#include + +#include "catalog/catalog.h" +#include "cli/commands.h" +#include "cli/output.h" +#include "harness/harness.h" +#include "sdk/llm.h" + +namespace rcli::cli { + +void RegisterHarness(CLI::App& app, Options& options) { + // `rcli opencode ` rather than a flag on `run`: it hands the terminal + // to another program, which is a different thing to do than talk to a model. + auto model = std::make_shared(); + auto rest = std::make_shared>(); + auto* opencode = + app.add_subcommand("opencode", "open a coding session in opencode, wired to a model"); + opencode->add_option("model", *model, "a model on this machine, or one served upstream"); + opencode->add_option("args", *rest, "passed through to opencode"); + opencode->callback([&options, model, rest] { + options.status = harness::Launch("opencode", *model, *rest); + }); +} + +} // namespace rcli::cli diff --git a/src/cli/commands.h b/src/cli/commands.h index 852283e..a5e7115 100644 --- a/src/cli/commands.h +++ b/src/cli/commands.h @@ -31,6 +31,7 @@ void RegisterSpeech(CLI::App& app, Options& options); void RegisterImage(CLI::App& app, Options& options); void RegisterBench(CLI::App& app, Options& options); void RegisterEngines(CLI::App& app, Options& options); +void RegisterHarness(CLI::App& app, Options& options); /// The interactive prompt, and the one-shot form when `prompt` is not empty. int Chat(const std::string& model, const std::string& prompt); diff --git a/src/harness/harness.cpp b/src/harness/harness.cpp new file mode 100644 index 0000000..5362fe8 --- /dev/null +++ b/src/harness/harness.cpp @@ -0,0 +1,235 @@ +#include "harness/harness.h" + +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +#include "rac/server/rac_server.h" + +#include "cli/commands.h" +#include "cli/output.h" +#include "sdk/llm.h" +#include "sdk/session.h" +#include "settings/settings.h" + +namespace rcli::harness { +namespace { + +using out::Ink; + +/// Google's OpenAI-compatible surface. Chosen as the default because it is the +/// one we can reach today; the shape is the same for anything else that speaks +/// OpenAI, which is the point of pinning the harness to that interface rather +/// than to a vendor. +constexpr const char* kDefaultUpstream = "https://generativelanguage.googleapis.com/v1beta/openai"; + +std::string Env(const char* name) { + const char* value = std::getenv(name); + return value != nullptr ? std::string(value) : std::string(); +} + +/// A port nothing is listening on, found by letting the OS pick one and giving +/// it straight back. There is a race between closing and the server binding, +/// but the alternative is a fixed port that collides with a second rcli. +int FreePort() { + const int sock = static_cast(socket(AF_INET, SOCK_STREAM, 0)); + if (sock < 0) { + return 0; + } + sockaddr_in address{}; + address.sin_family = AF_INET; + address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + address.sin_port = 0; + int port = 0; + if (bind(sock, reinterpret_cast(&address), sizeof(address)) == 0) { + socklen_t length = sizeof(address); + if (getsockname(sock, reinterpret_cast(&address), &length) == 0) { + port = ntohs(address.sin_port); + } + } +#if defined(_WIN32) + closesocket(sock); +#else + close(sock); +#endif + return port; +} + +/// JSON string escaping, for the handful of characters that can appear in a +/// model id, a path or a key. Not a general encoder: it exists so a Windows +/// path with backslashes does not silently produce invalid config. +std::string Quote(const std::string& text) { + std::string out = "\""; + for (const char c : text) { + switch (c) { + case '"': out += "\\\""; break; + case '\\': out += "\\\\"; break; + case '\n': out += "\\n"; break; + case '\r': out += "\\r"; break; + case '\t': out += "\\t"; break; + default: out += c; + } + } + return out + "\""; +} + +/// The provider block opencode reads out of OPENCODE_CONFIG_CONTENT. +/// +/// Inline rather than a file on purpose: writing to the user's project or to +/// ~/.config/opencode would outlive the session and change how opencode behaves +/// when they run it themselves. +std::string OpencodeConfig(const std::string& model, const std::string& base_url, + const std::string& api_key) { + // A key is always present because opencode's OpenAI client sends an + // Authorization header regardless; a local server ignores what is in it. + const std::string key = api_key.empty() ? std::string("local") : api_key; + return std::string("{\"provider\":{\"runanywhere\":{") + + "\"npm\":\"@ai-sdk/openai-compatible\"," + "\"name\":\"RunAnywhere\"," + + "\"options\":{\"baseURL\":" + Quote(base_url) + ",\"apiKey\":" + Quote(key) + "}," + + "\"models\":{" + Quote(model) + ":{\"name\":" + Quote(model) + "}}}}," + + "\"model\":" + Quote("runanywhere/" + model) + "}"; +} + +int Spawn(const std::string& tool, const std::vector& args) { + std::vector owned; + owned.push_back(tool); + owned.insert(owned.end(), args.begin(), args.end()); + std::vector argv; + argv.reserve(owned.size() + 1); + for (std::string& piece : owned) { + argv.push_back(piece.data()); + } + argv.push_back(nullptr); + +#if defined(_WIN32) + const intptr_t rc = _spawnvp(_P_WAIT, tool.c_str(), argv.data()); + if (rc < 0) { + out::Error(tool + " is not on PATH"); + return 127; + } + return static_cast(rc); +#else + // fork rather than exec: the local server lives in this process, and + // replacing the image would take it down with us before the tool ran. + const pid_t child = fork(); + if (child < 0) { + out::Error("could not start " + tool); + return 1; + } + if (child == 0) { + execvp(tool.c_str(), argv.data()); + // Only reached when exec failed. 127 is what a shell reports for a + // command it cannot find, and the parent cannot tell why otherwise. + _exit(127); + } + int status = 0; + waitpid(child, &status, 0); + if (WIFEXITED(status)) { + return WEXITSTATUS(status); + } + return 1; +#endif +} + +} // namespace + +int Launch(const std::string& tool, const std::string& model, + const std::vector& args) { + if (model.empty()) { + // Nothing to wire, so do not pretend to: run the tool as the user has + // it configured. + return Spawn(tool, args); + } + if (!rcli::cli::Start()) { + return 1; + } + + std::string base_url; + std::string api_key; + bool serving = false; + + const sdk::LocalModel* local = nullptr; + const std::vector installed = sdk::LocalModels(); + for (const sdk::LocalModel& candidate : installed) { + if (candidate.id == model && candidate.complete) { + local = &candidate; + break; + } + } + + if (local != nullptr) { + // The server creates its handle with rac_llm_create(path), which routes + // on the path alone rather than asking the registry what framework the + // model belongs to. An MLX directory does not look like anything it + // recognises, so it lands on llama.cpp and fails to load. Saying so + // beats starting a server that answers every request with an error. + if (local->framework != "LlamaCpp") { + out::Error(model + " runs on " + local->framework + + ", and the local server can only serve LlamaCpp models today"); + out::Status("use a GGUF model here, or point at an upstream one"); + return 1; + } + const int port = FreePort(); + if (port == 0) { + out::Error("could not find a free port for the local server"); + return 1; + } + rac_server_config_t config = RAC_SERVER_CONFIG_DEFAULT; + config.host = "127.0.0.1"; + config.port = static_cast(port); + const std::string path = local->path.empty() ? local->dir : local->path; + config.model_path = path.c_str(); + config.model_id = model.c_str(); + config.context_size = settings::ContextLength() > 0 ? settings::ContextLength() : 8192; + out::Status("serving " + model + " on 127.0.0.1:" + std::to_string(port)); + if (rac_server_start(&config) != RAC_SUCCESS) { + out::Error("the local server would not start for " + model); + return 1; + } + serving = true; + base_url = "http://127.0.0.1:" + std::to_string(port) + "/v1"; + } else { + base_url = Env("RCLI_UPSTREAM_URL"); + if (base_url.empty()) { + base_url = kDefaultUpstream; + } + api_key = Env("RCLI_UPSTREAM_KEY"); + if (api_key.empty()) { + api_key = Env("GEMINI_API_KEY"); + } + if (api_key.empty()) { + out::Error(model + " is not on this machine, and no upstream key is set"); + out::Status("set RCLI_UPSTREAM_KEY, or `rcli pull " + model + "` to run it here"); + return 1; + } + out::Status("using " + model + " from " + base_url); + } + + const std::string config = OpencodeConfig(model, base_url, api_key); +#if defined(_WIN32) + _putenv_s("OPENCODE_CONFIG_CONTENT", config.c_str()); +#else + setenv("OPENCODE_CONFIG_CONTENT", config.c_str(), 1); +#endif + + const int status = Spawn(tool, args); + if (serving) { + rac_server_stop(); + } + return status; +} + +} // namespace rcli::harness diff --git a/src/harness/harness.h b/src/harness/harness.h new file mode 100644 index 0000000..c845147 --- /dev/null +++ b/src/harness/harness.h @@ -0,0 +1,27 @@ +#ifndef RCLI_HARNESS_HARNESS_H +#define RCLI_HARNESS_HARNESS_H + +#include +#include + +/// Launching a coding tool against a model, whether that model runs here or +/// upstream. +/// +/// The harness never learns which it got. It is handed one OpenAI-compatible +/// base URL and talks to that, exactly as it would to any provider. For a local +/// model the URL is a server this process starts and stops; for an upstream one +/// it is the provider's own. That is the same shape Ollama uses, and it is why +/// a harness needs no plugin to work with us. +namespace rcli::harness { + +/// Runs `tool` against `model`, forwarding `args` to it, and returns the tool's +/// exit code. Blocks until the tool exits, then stops anything it started. +/// +/// An empty `model` uses whatever the tool is already configured for, which +/// makes `rcli opencode` a plain passthrough. +int Launch(const std::string& tool, const std::string& model, + const std::vector& args); + +} // namespace rcli::harness + +#endif // RCLI_HARNESS_HARNESS_H From 2433759b4177860de0dcfe40dc2843e4d683b52b Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Fri, 21 Aug 2026 20:39:41 +0530 Subject: [PATCH 02/16] fix: target a modern Windows so httplib can see GetAddrInfoExCancel --- cmake/RunAnywhereSDK.cmake | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cmake/RunAnywhereSDK.cmake b/cmake/RunAnywhereSDK.cmake index b10dfa9..984ee9e 100644 --- a/cmake/RunAnywhereSDK.cmake +++ b/cmake/RunAnywhereSDK.cmake @@ -56,6 +56,15 @@ if(NOT APPLE) set(RAC_BUILD_PLATFORM OFF CACHE BOOL "" FORCE) endif() +if(WIN32) + # cpp-httplib calls GetAddrInfoExCancel, which ws2tcpip.h only declares when + # the target Windows version is Vista or later. MinGW defaults below that, + # so without this the server fails to compile with the API "not declared" + # while every other platform builds. Set before the SDK is added so its + # targets get it too. + add_compile_definitions(_WIN32_WINNT=0x0A00) +endif() + if(RCLI_SDK_DIR) if(NOT EXISTS "${RCLI_SDK_DIR}/CMakeLists.txt") message(FATAL_ERROR "RCLI_SDK_DIR=${RCLI_SDK_DIR} has no CMakeLists.txt") From f2a0fe2c457d3112ee053011cec7e956ce037ba8 Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Sat, 22 Aug 2026 00:21:58 +0530 Subject: [PATCH 03/16] fix: drop httplib's non-blocking resolver, which mingw cannot declare --- cmake/RunAnywhereSDK.cmake | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/cmake/RunAnywhereSDK.cmake b/cmake/RunAnywhereSDK.cmake index 984ee9e..19fa479 100644 --- a/cmake/RunAnywhereSDK.cmake +++ b/cmake/RunAnywhereSDK.cmake @@ -40,6 +40,11 @@ set(HTTPLIB_USE_OPENSSL_IF_AVAILABLE OFF CACHE BOOL "" FORCE) set(HTTPLIB_USE_ZLIB_IF_AVAILABLE OFF CACHE BOOL "" FORCE) set(HTTPLIB_USE_BROTLI_IF_AVAILABLE OFF CACHE BOOL "" FORCE) set(HTTPLIB_USE_ZSTD_IF_AVAILABLE OFF CACHE BOOL "" FORCE) +# Its non-blocking resolver calls GetAddrInfoExCancel, which MinGW's headers do +# not declare at any _WIN32_WINNT, so the server will not compile for Windows +# with it on. Nothing is lost: this server binds loopback and never resolves a +# name. +set(HTTPLIB_USE_NON_BLOCKING_GETADDRINFO OFF CACHE BOOL "" FORCE) # RAC_STATIC_PLUGINS is deliberately left at the SDK's own default. Forcing it # on folds the cloud engine into rac_commons, which stops it being a # rac_backend_cloud target; the loop below then never defines RCLI_HAS_CLOUD and @@ -57,11 +62,10 @@ if(NOT APPLE) endif() if(WIN32) - # cpp-httplib calls GetAddrInfoExCancel, which ws2tcpip.h only declares when - # the target Windows version is Vista or later. MinGW defaults below that, - # so without this the server fails to compile with the API "not declared" - # while every other platform builds. Set before the SDK is added so its - # targets get it too. + # A modern Windows target, which several of the SDK's Win32 calls expect. + # Set before the SDK is added so its targets get it too. Note this alone + # does not make GetAddrInfoExCancel visible under MinGW; see the httplib + # resolver option above for that. add_compile_definitions(_WIN32_WINNT=0x0A00) endif() From d294b32a4f8700861bd3e0b681484a12866773a0 Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Sat, 22 Aug 2026 05:18:42 +0530 Subject: [PATCH 04/16] fix: make the free-port probe build on windows, where winsock differs --- CMakeLists.txt | 7 +++++++ src/harness/harness.cpp | 27 +++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e4712d3..afd4c22 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -119,6 +119,13 @@ if(APPLE) target_link_libraries(rcli_core PRIVATE "-framework AudioToolbox") endif() +if(WIN32) + # Winsock, for the free-port probe in src/harness. Named here rather than + # left to httplib: that code calls socket() before the server starts, so it + # cannot rely on something else having pulled the library in. + target_link_libraries(rcli_core PRIVATE ws2_32) +endif() + target_compile_options(rcli_core PRIVATE -Wall -Wextra) add_executable(rcli src/main.cpp) diff --git a/src/harness/harness.cpp b/src/harness/harness.cpp index 5362fe8..3edd873 100644 --- a/src/harness/harness.cpp +++ b/src/harness/harness.cpp @@ -9,12 +9,16 @@ #if defined(_WIN32) #include #include +// Winsock spells these differently: a socket is an unsigned SOCKET rather than +// a file descriptor, and getsockname takes an int length rather than socklen_t. +using rcli_socklen_t = int; #else #include #include #include #include #include +using rcli_socklen_t = socklen_t; #endif #include "rac/server/rac_server.h" @@ -45,17 +49,36 @@ std::string Env(const char* name) { /// it straight back. There is a race between closing and the server binding, /// but the alternative is a fixed port that collides with a second rcli. int FreePort() { - const int sock = static_cast(socket(AF_INET, SOCK_STREAM, 0)); +#if defined(_WIN32) + // Winsock has to be initialised before any socket call, and the server that + // would otherwise do it has not started yet. The count is per-process and + // refcounted, so starting it here and leaving it up is harmless. + static const bool ready = [] { + WSADATA data; + return WSAStartup(MAKEWORD(2, 2), &data) == 0; + }(); + if (!ready) { + return 0; + } + const SOCKET sock = socket(AF_INET, SOCK_STREAM, 0); + // INVALID_SOCKET, not a negative number: SOCKET is unsigned on Windows, so + // the usual `< 0` check silently passes for a failed call. + if (sock == INVALID_SOCKET) { + return 0; + } +#else + const int sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { return 0; } +#endif sockaddr_in address{}; address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); address.sin_port = 0; int port = 0; if (bind(sock, reinterpret_cast(&address), sizeof(address)) == 0) { - socklen_t length = sizeof(address); + rcli_socklen_t length = static_cast(sizeof(address)); if (getsockname(sock, reinterpret_cast(&address), &length) == 0) { port = ntohs(address.sin_port); } From a9868561dd9d55971eb5986dadc9ba973c081602 Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Sat, 22 Aug 2026 08:21:08 +0530 Subject: [PATCH 05/16] feat: sign in to the console from a browser and route coding through it --- CMakeLists.txt | 3 + src/account/console.cpp | 239 ++++++++++++++++++++++++++++++++++++ src/account/console.h | 52 ++++++++ src/account/credentials.cpp | 166 +++++++++++++++++++++++++ src/account/credentials.h | 34 +++++ src/cli/app.cpp | 1 + src/cli/cmd_account.cpp | 187 ++++++++++++++++++++++++++++ src/cli/commands.h | 1 + src/harness/harness.cpp | 31 ++--- 9 files changed, 691 insertions(+), 23 deletions(-) create mode 100644 src/account/console.cpp create mode 100644 src/account/console.h create mode 100644 src/account/credentials.cpp create mode 100644 src/account/credentials.h create mode 100644 src/cli/cmd_account.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index afd4c22..4363cbf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,10 +24,13 @@ include(RunAnywhereSDK) # The Apple build adds a Swift one that registers the MLX callbacks first; both # call rcli_run(), so the app itself exists once. add_library(rcli_core STATIC + src/account/console.cpp + src/account/credentials.cpp src/audio/audio.cpp src/catalog/catalog.cpp src/chat/complete.cpp src/cli/app.cpp + src/cli/cmd_account.cpp src/cli/cmd_bench.cpp src/cli/cmd_chat.cpp src/cli/cmd_engines.cpp diff --git a/src/account/console.cpp b/src/account/console.cpp new file mode 100644 index 0000000..65d8203 --- /dev/null +++ b/src/account/console.cpp @@ -0,0 +1,239 @@ +#include "account/console.h" + +#include +#include +#include + +#include "rac/infrastructure/http/rac_http_client.h" + +namespace rcli::account { +namespace { + +struct Reply { + int status = 0; + std::string body; +}; + +std::string Quote(const std::string& text) { + std::string out = "\""; + for (const char c : text) { + switch (c) { + case '"': out += "\\\""; break; + case '\\': out += "\\\\"; break; + case '\n': out += "\\n"; break; + default: out += c; + } + } + return out + "\""; +} + +std::string Field(const std::string& document, const std::string& key) { + const std::string needle = "\"" + key + "\""; + std::size_t at = document.find(needle); + if (at == std::string::npos) { + return {}; + } + at = document.find(':', at + needle.size()); + if (at == std::string::npos) { + return {}; + } + while (at < document.size() && (document[at] == ':' || document[at] == ' ')) { + ++at; + } + if (at >= document.size()) { + return {}; + } + if (document[at] != '"') { + std::string number; + while (at < document.size() && (std::isdigit(document[at]) != 0 || document[at] == '-')) { + number += document[at++]; + } + return number; + } + std::string value; + for (std::size_t i = at + 1; i < document.size(); ++i) { + if (document[i] == '\\' && i + 1 < document.size()) { + const char next = document[++i]; + value += next == 'n' ? '\n' : next; + continue; + } + if (document[i] == '"') { + break; + } + value += document[i]; + } + return value; +} + +bool Call(const std::string& url, const char* method, const std::string& body, + const std::string& bearer, Reply* reply, std::string* error) { + rac_http_client_t* client = nullptr; + if (rac_http_client_create(&client) != RAC_SUCCESS || client == nullptr) { + if (error != nullptr) { + *error = "could not create an HTTP client"; + } + return false; + } + + std::vector headers; + headers.push_back({"Content-Type", "application/json"}); + const std::string authorization = "Bearer " + bearer; + if (!bearer.empty()) { + headers.push_back({"Authorization", authorization.c_str()}); + } + + rac_http_request_t request{}; + request.method = method; + request.url = url.c_str(); + request.headers = headers.data(); + request.header_count = headers.size(); + request.body_bytes = reinterpret_cast(body.data()); + request.body_len = body.size(); + request.timeout_ms = 30000; + // A credential travels in these requests, so a 3xx must come back to us + // rather than being replayed with its headers against another origin. + request.follow_redirects = RAC_FALSE; + + rac_http_response_t response{}; + const rac_result_t rc = rac_http_request_send(client, &request, &response); + if (rc != RAC_SUCCESS) { + if (error != nullptr) { + *error = "could not reach " + url; + } + rac_http_client_destroy(client); + return false; + } + + reply->status = response.status; + if (response.body_bytes != nullptr && response.body_len > 0) { + reply->body.assign(reinterpret_cast(response.body_bytes), response.body_len); + } + rac_http_response_free(&response); + rac_http_client_destroy(client); + return true; +} + +std::string Detail(const Reply& reply) { + const std::string detail = Field(reply.body, "detail"); + return detail.empty() ? "the console returned " + std::to_string(reply.status) : detail; +} + +} // namespace + +bool BeginAuthorization(const std::string& console_url, const std::string& hostname, + Authorization* authorization, std::string* error) { + Reply reply; + if (!Call(console_url + "/auth/cli/start", "POST", "{\"hostname\":" + Quote(hostname) + "}", {}, + &reply, error)) { + return false; + } + if (reply.status != 200) { + if (error != nullptr) { + *error = Detail(reply); + } + return false; + } + authorization->request_code = Field(reply.body, "request_code"); + authorization->poll_secret = Field(reply.body, "poll_secret"); + authorization->verification_url = Field(reply.body, "verification_url"); + authorization->expires_in = std::stoi("0" + Field(reply.body, "expires_in")); + const int interval = std::stoi("0" + Field(reply.body, "interval")); + authorization->interval = interval > 0 ? interval : 2; + if (authorization->request_code.empty() || authorization->verification_url.empty()) { + if (error != nullptr) { + *error = "the console did not open an authorization request"; + } + return false; + } + return true; +} + +namespace { + +void ReadGrant(const std::string& body, Grant* grant) { + grant->access_token = Field(body, "access_token"); + grant->refresh_token = Field(body, "refresh_token"); + grant->email = Field(body, "email"); + grant->plan = Field(body, "plan"); + grant->expires_in = std::stol("0" + Field(body, "expires_in")); +} + +} // namespace + +PollResult Poll(const std::string& console_url, const Authorization& authorization, Grant* grant, + std::string* error) { + const std::string payload = "{\"request_code\":" + Quote(authorization.request_code) + + ",\"poll_secret\":" + Quote(authorization.poll_secret) + "}"; + Reply reply; + if (!Call(console_url + "/auth/cli/poll", "POST", payload, {}, &reply, error)) { + return PollResult::Failed; + } + if (reply.status != 200) { + if (error != nullptr) { + *error = Detail(reply); + } + return PollResult::Failed; + } + const std::string state = Field(reply.body, "status"); + if (state == "pending") { + return PollResult::Pending; + } + if (state == "denied") { + return PollResult::Denied; + } + if (state == "expired") { + return PollResult::Expired; + } + if (state != "approved") { + if (error != nullptr) { + *error = "the console reported an unknown state: " + state; + } + return PollResult::Failed; + } + ReadGrant(reply.body, grant); + if (grant->access_token.empty()) { + if (error != nullptr) { + *error = "the console approved the request but issued no token"; + } + return PollResult::Failed; + } + return PollResult::Approved; +} + +bool Refresh(const std::string& console_url, const std::string& refresh_token, Grant* grant, + std::string* error) { + Reply reply; + if (!Call(console_url + "/auth/cli/refresh", "POST", + "{\"refresh_token\":" + Quote(refresh_token) + "}", {}, &reply, error)) { + return false; + } + if (reply.status != 200) { + if (error != nullptr) { + *error = Detail(reply); + } + return false; + } + ReadGrant(reply.body, grant); + return !grant->access_token.empty(); +} + +bool WhoAmI(const std::string& console_url, const std::string& token, Identity* identity, + std::string* error) { + Reply reply; + if (!Call(console_url + "/v1/me", "GET", {}, token, &reply, error)) { + return false; + } + if (reply.status != 200) { + if (error != nullptr) { + *error = Detail(reply); + } + return false; + } + identity->email = Field(reply.body, "email"); + identity->plan = Field(reply.body, "plan"); + identity->tokens_this_month = std::stol("0" + Field(reply.body, "tokens_this_month")); + identity->monthly_token_limit = std::stol("0" + Field(reply.body, "monthly_token_limit")); + return true; +} + +} // namespace rcli::account diff --git a/src/account/console.h b/src/account/console.h new file mode 100644 index 0000000..0c847ad --- /dev/null +++ b/src/account/console.h @@ -0,0 +1,52 @@ +#ifndef RCLI_ACCOUNT_CONSOLE_H +#define RCLI_ACCOUNT_CONSOLE_H + +#include + +namespace rcli::account { + +struct Identity { + std::string email; + std::string plan; + long tokens_this_month = 0; + long monthly_token_limit = 0; +}; + +struct Authorization { + std::string request_code; + std::string poll_secret; + std::string verification_url; + int expires_in = 0; + int interval = 2; +}; + +struct Grant { + std::string access_token; + std::string refresh_token; + std::string email; + std::string plan; + long expires_in = 0; +}; + +/// Opens an authorization request the browser will approve. +/// +/// No credential is exchanged here or anywhere in the CLI. The request code +/// identifies the attempt publicly; the poll secret proves that the process +/// collecting the grant is the one that started it. +bool BeginAuthorization(const std::string& console_url, const std::string& hostname, + Authorization* authorization, std::string* error); + +enum class PollResult { Pending, Approved, Denied, Expired, Failed }; + +PollResult Poll(const std::string& console_url, const Authorization& authorization, Grant* grant, + std::string* error); + +bool Refresh(const std::string& console_url, const std::string& refresh_token, Grant* grant, + std::string* error); + +bool WhoAmI(const std::string& console_url, const std::string& token, Identity* identity, + std::string* error); + +} // namespace rcli::account + +#endif // RCLI_ACCOUNT_CONSOLE_H diff --git a/src/account/credentials.cpp b/src/account/credentials.cpp new file mode 100644 index 0000000..4a6c767 --- /dev/null +++ b/src/account/credentials.cpp @@ -0,0 +1,166 @@ +#include "account/credentials.h" + +#include +#include +#include +#include + +#if !defined(_WIN32) +#include +#endif + +namespace rcli::account { +namespace { + +constexpr const char* kFileName = "credentials.json"; + +std::string Env(const char* name) { + const char* value = std::getenv(name); + return value != nullptr ? std::string(value) : std::string(); +} + +std::string HomeDirectory() { +#if defined(_WIN32) + const std::string profile = Env("USERPROFILE"); + if (!profile.empty()) { + return profile; + } + return Env("HOMEDRIVE") + Env("HOMEPATH"); +#else + return Env("HOME"); +#endif +} + +std::string Quote(const std::string& text) { + std::string out = "\""; + for (const char c : text) { + switch (c) { + case '"': out += "\\\""; break; + case '\\': out += "\\\\"; break; + case '\n': out += "\\n"; break; + default: out += c; + } + } + return out + "\""; +} + +std::string Field(const std::string& document, const std::string& key) { + const std::string needle = "\"" + key + "\""; + std::size_t at = document.find(needle); + if (at == std::string::npos) { + return {}; + } + at = document.find(':', at + needle.size()); + if (at == std::string::npos) { + return {}; + } + at = document.find('"', at); + if (at == std::string::npos) { + return {}; + } + std::string value; + for (std::size_t i = at + 1; i < document.size(); ++i) { + if (document[i] == '\\' && i + 1 < document.size()) { + const char next = document[++i]; + value += next == 'n' ? '\n' : next; + continue; + } + if (document[i] == '"') { + break; + } + value += document[i]; + } + return value; +} + +} // namespace + +std::string DefaultConsoleUrl() { + const std::string configured = Env("RCLI_CONSOLE_URL"); + return configured.empty() ? "http://localhost:8080" : configured; +} + +std::string ProfileDirectory() { + const std::string override_dir = Env("RCLI_PROFILE_DIR"); + if (!override_dir.empty()) { + return override_dir; + } + const std::string home = HomeDirectory(); + if (home.empty()) { + return ".rcli"; + } + return home + "/.config/rcli"; +} + +Credentials Load() { + Credentials credentials; + credentials.console_url = DefaultConsoleUrl(); + + std::ifstream file(ProfileDirectory() + "/" + kFileName); + if (!file) { + return credentials; + } + std::stringstream buffer; + buffer << file.rdbuf(); + const std::string document = buffer.str(); + + const std::string stored_url = Field(document, "console_url"); + if (!stored_url.empty() && Env("RCLI_CONSOLE_URL").empty()) { + credentials.console_url = stored_url; + } + credentials.email = Field(document, "email"); + credentials.plan = Field(document, "plan"); + credentials.access_token = Field(document, "access_token"); + credentials.refresh_token = Field(document, "refresh_token"); + return credentials; +} + +bool Save(const Credentials& credentials, std::string* error) { + const std::string directory = ProfileDirectory(); + std::error_code ec; + std::filesystem::create_directories(directory, ec); + if (ec) { + if (error != nullptr) { + *error = "could not create " + directory + ": " + ec.message(); + } + return false; + } + + const std::string path = directory + "/" + kFileName; + std::ofstream file(path, std::ios::trunc); + if (!file) { + if (error != nullptr) { + *error = "could not write " + path; + } + return false; + } + file << "{\n" + << " \"console_url\": " << Quote(credentials.console_url) << ",\n" + << " \"email\": " << Quote(credentials.email) << ",\n" + << " \"plan\": " << Quote(credentials.plan) << ",\n" + << " \"access_token\": " << Quote(credentials.access_token) << ",\n" + << " \"refresh_token\": " << Quote(credentials.refresh_token) << "\n" + << "}\n"; + file.close(); + +#if !defined(_WIN32) + // The file holds a bearer token, so it is readable only by its owner. On + // Windows the profile directory under the user account carries that. + ::chmod(path.c_str(), S_IRUSR | S_IWUSR); +#endif + return true; +} + +bool Clear(std::string* error) { + std::error_code ec; + std::filesystem::remove(ProfileDirectory() + "/" + kFileName, ec); + if (ec) { + if (error != nullptr) { + *error = ec.message(); + } + return false; + } + return true; +} + +} // namespace rcli::account diff --git a/src/account/credentials.h b/src/account/credentials.h new file mode 100644 index 0000000..8456c64 --- /dev/null +++ b/src/account/credentials.h @@ -0,0 +1,34 @@ +#ifndef RCLI_ACCOUNT_CREDENTIALS_H +#define RCLI_ACCOUNT_CREDENTIALS_H + +#include + +namespace rcli::account { + +struct Credentials { + std::string console_url; + std::string email; + std::string plan; + std::string access_token; + std::string refresh_token; + + bool signed_in() const { return !access_token.empty(); } +}; + +/// Where this profile keeps its credentials. +/// +/// RCLI_PROFILE_DIR overrides it outright, which is what lets several signed-in +/// users share one machine without seeing each other's tokens. +std::string ProfileDirectory(); + +Credentials Load(); + +bool Save(const Credentials& credentials, std::string* error); + +bool Clear(std::string* error); + +std::string DefaultConsoleUrl(); + +} // namespace rcli::account + +#endif // RCLI_ACCOUNT_CREDENTIALS_H diff --git a/src/cli/app.cpp b/src/cli/app.cpp index 641c07e..08f7eef 100644 --- a/src/cli/app.cpp +++ b/src/cli/app.cpp @@ -82,6 +82,7 @@ extern "C" int rcli_run(int argc, char** argv) { cli::RegisterBench(app, options); cli::RegisterEngines(app, options); cli::RegisterHarness(app, options); + cli::RegisterAccount(app, options); // Only --color is left to decide here; logging was settled in rcli_begin, // which had to run before the engines got a chance to write anything. diff --git a/src/cli/cmd_account.cpp b/src/cli/cmd_account.cpp new file mode 100644 index 0000000..8e73aaa --- /dev/null +++ b/src/cli/cmd_account.cpp @@ -0,0 +1,187 @@ +#include +#include +#include +#include +#include + +#include + +#include "account/console.h" +#include "account/credentials.h" +#include "cli/commands.h" +#include "cli/output.h" + +namespace rcli::cli { +namespace { + +using out::Ink; + +std::string Hostname() { + char name[256] = {}; + if (gethostname(name, sizeof(name) - 1) == 0 && name[0] != '\0') { + return name; + } + return "unknown"; +} + +/// Only ever handed a URL this process just received from the console, so there +/// is nothing user-supplied to quote around. +void OpenBrowser(const std::string& url) { +#if defined(__APPLE__) + const std::string command = "open '" + url + "' >/dev/null 2>&1"; +#elif defined(_WIN32) + const std::string command = "start \"\" \"" + url + "\""; +#else + const std::string command = "xdg-open '" + url + "' >/dev/null 2>&1"; +#endif + if (std::system(command.c_str()) != 0) { + out::Status("could not open a browser for you"); + } +} + +bool Ready() { + if (Start()) { + return true; + } + out::Error("the SDK would not start, so the console cannot be reached"); + return false; +} + +int Login(bool open_browser) { + if (!Ready()) { + return 1; + } + account::Credentials credentials = account::Load(); + + account::Authorization authorization; + std::string failure; + if (!account::BeginAuthorization(credentials.console_url, Hostname(), &authorization, + &failure)) { + out::Error(failure); + return 1; + } + + out::Status("approve this sign-in in your browser:"); + out::Line(authorization.verification_url); + if (open_browser) { + OpenBrowser(authorization.verification_url); + } + out::Status("waiting for approval"); + + const auto deadline = + std::chrono::steady_clock::now() + std::chrono::seconds(authorization.expires_in); + account::Grant grant; + while (std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(std::chrono::seconds(authorization.interval)); + switch (account::Poll(credentials.console_url, authorization, &grant, &failure)) { + case account::PollResult::Pending: + continue; + case account::PollResult::Denied: + out::Error("the request was denied in the browser"); + return 1; + case account::PollResult::Expired: + out::Error("the request expired before it was approved"); + return 1; + case account::PollResult::Failed: + out::Error(failure); + return 1; + case account::PollResult::Approved: + credentials.email = grant.email; + credentials.plan = grant.plan; + credentials.access_token = grant.access_token; + credentials.refresh_token = grant.refresh_token; + if (!account::Save(credentials, &failure)) { + out::Error(failure); + return 1; + } + out::Status("signed in as " + grant.email + " on the " + grant.plan + " plan"); + out::Status("credentials in " + account::ProfileDirectory()); + return 0; + } + } + out::Error("timed out waiting for approval"); + return 1; +} + +int Logout() { + const account::Credentials credentials = account::Load(); + if (!credentials.signed_in()) { + out::Status("not signed in"); + return 0; + } + std::string failure; + if (!account::Clear(&failure)) { + out::Error(failure); + return 1; + } + out::Status("signed out " + credentials.email); + out::Status("revoke the token in the console to end it everywhere"); + return 0; +} + +int WhoAmI() { + if (!Ready()) { + return 1; + } + account::Credentials credentials = account::Load(); + if (!credentials.signed_in()) { + out::Error("not signed in — run `rcli login`"); + return 1; + } + + account::Identity identity; + std::string failure; + if (!account::WhoAmI(credentials.console_url, credentials.access_token, &identity, &failure)) { + // An access token lives eight hours, so a working install meets this + // routinely. Trading the refresh token for a new one is the normal path, + // not an error worth showing. + account::Grant grant; + if (credentials.refresh_token.empty() || + !account::Refresh(credentials.console_url, credentials.refresh_token, &grant, + &failure)) { + out::Error(failure.empty() ? "the session has expired — run `rcli login`" : failure); + return 1; + } + credentials.access_token = grant.access_token; + credentials.refresh_token = grant.refresh_token; + credentials.email = grant.email; + credentials.plan = grant.plan; + account::Save(credentials, nullptr); + if (!account::WhoAmI(credentials.console_url, credentials.access_token, &identity, + &failure)) { + out::Error(failure); + return 1; + } + } + + char line[220]; + std::snprintf(line, sizeof(line), "%-14s %s", "email", identity.email.c_str()); + out::Line(line); + std::snprintf(line, sizeof(line), "%-14s %s", "plan", identity.plan.c_str()); + out::Line(line); + std::snprintf(line, sizeof(line), "%-14s %ld of %ld", "tokens", identity.tokens_this_month, + identity.monthly_token_limit); + out::Line(line); + std::snprintf(line, sizeof(line), "%-14s %s", "console", credentials.console_url.c_str()); + out::Line(line); + std::snprintf(line, sizeof(line), "%-14s %s", "profile", account::ProfileDirectory().c_str()); + out::Line(line); + return 0; +} + +} // namespace + +void RegisterAccount(CLI::App& app, Options& options) { + auto no_browser = std::make_shared(false); + auto* login = app.add_subcommand("login", "sign in through the RunAnywhere console"); + login->add_flag("--no-browser", *no_browser, "print the URL instead of opening it"); + login->callback([&options, no_browser] { options.status = Login(!*no_browser); }); + + auto* logout = app.add_subcommand("logout", "forget the credentials on this machine"); + logout->callback([&options] { options.status = Logout(); }); + + auto* whoami = app.add_subcommand("whoami", "who is signed in, and how much they have used"); + whoami->callback([&options] { options.status = WhoAmI(); }); +} + +} // namespace rcli::cli diff --git a/src/cli/commands.h b/src/cli/commands.h index a5e7115..924b89f 100644 --- a/src/cli/commands.h +++ b/src/cli/commands.h @@ -32,6 +32,7 @@ void RegisterImage(CLI::App& app, Options& options); void RegisterBench(CLI::App& app, Options& options); void RegisterEngines(CLI::App& app, Options& options); void RegisterHarness(CLI::App& app, Options& options); +void RegisterAccount(CLI::App& app, Options& options); /// The interactive prompt, and the one-shot form when `prompt` is not empty. int Chat(const std::string& model, const std::string& prompt); diff --git a/src/harness/harness.cpp b/src/harness/harness.cpp index 3edd873..7791e39 100644 --- a/src/harness/harness.cpp +++ b/src/harness/harness.cpp @@ -23,6 +23,7 @@ using rcli_socklen_t = socklen_t; #include "rac/server/rac_server.h" +#include "account/credentials.h" #include "cli/commands.h" #include "cli/output.h" #include "sdk/llm.h" @@ -34,17 +35,6 @@ namespace { using out::Ink; -/// Google's OpenAI-compatible surface. Chosen as the default because it is the -/// one we can reach today; the shape is the same for anything else that speaks -/// OpenAI, which is the point of pinning the harness to that interface rather -/// than to a vendor. -constexpr const char* kDefaultUpstream = "https://generativelanguage.googleapis.com/v1beta/openai"; - -std::string Env(const char* name) { - const char* value = std::getenv(name); - return value != nullptr ? std::string(value) : std::string(); -} - /// A port nothing is listening on, found by letting the OS pick one and giving /// it straight back. There is a race between closing and the server binding, /// but the alternative is a fixed port that collides with a second rcli. @@ -225,20 +215,15 @@ int Launch(const std::string& tool, const std::string& model, serving = true; base_url = "http://127.0.0.1:" + std::to_string(port) + "/v1"; } else { - base_url = Env("RCLI_UPSTREAM_URL"); - if (base_url.empty()) { - base_url = kDefaultUpstream; - } - api_key = Env("RCLI_UPSTREAM_KEY"); - if (api_key.empty()) { - api_key = Env("GEMINI_API_KEY"); - } - if (api_key.empty()) { - out::Error(model + " is not on this machine, and no upstream key is set"); - out::Status("set RCLI_UPSTREAM_KEY, or `rcli pull " + model + "` to run it here"); + const account::Credentials credentials = account::Load(); + if (!credentials.signed_in()) { + out::Error(model + " is not on this machine, and you are not signed in"); + out::Status("run `rcli login`, or `rcli pull " + model + "` to run it here"); return 1; } - out::Status("using " + model + " from " + base_url); + base_url = credentials.console_url + "/v1"; + api_key = credentials.access_token; + out::Status("using " + model + " as " + credentials.email); } const std::string config = OpencodeConfig(model, base_url, api_key); From 645252df6dc192572cc3fa8c7554b32754a71bea Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Sat, 22 Aug 2026 08:47:43 +0530 Subject: [PATCH 06/16] fix: read the hostname through winsock on windows --- src/cli/cmd_account.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/cli/cmd_account.cpp b/src/cli/cmd_account.cpp index 8e73aaa..6a01a60 100644 --- a/src/cli/cmd_account.cpp +++ b/src/cli/cmd_account.cpp @@ -4,7 +4,13 @@ #include #include +#if defined(_WIN32) +// gethostname is Winsock on Windows, not unistd, and Winsock needs starting +// before it answers. src/harness does the same for its port probe. +#include +#else #include +#endif #include "account/console.h" #include "account/credentials.h" @@ -17,6 +23,13 @@ namespace { using out::Ink; std::string Hostname() { +#if defined(_WIN32) + WSADATA data; + static const bool ready = WSAStartup(MAKEWORD(2, 2), &data) == 0; + if (!ready) { + return "unknown"; + } +#endif char name[256] = {}; if (gethostname(name, sizeof(name) - 1) == 0 && name[0] != '\0') { return name; From 69a47d266db285634c74ba215eff65f9c9a2b53b Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Sat, 22 Aug 2026 09:58:50 +0530 Subject: [PATCH 07/16] fix: address the review on browser sign-in and the harness --- src/account/console.cpp | 26 +++++++--- src/account/credentials.cpp | 101 +++++++++++++++++++++++++++++------- src/account/credentials.h | 4 ++ src/cli/cmd_account.cpp | 70 +++++++++++++++++++++---- src/cli/cmd_harness.cpp | 8 ++- src/harness/harness.cpp | 47 ++++++++++++++--- 6 files changed, 214 insertions(+), 42 deletions(-) diff --git a/src/account/console.cpp b/src/account/console.cpp index 65d8203..eda7378 100644 --- a/src/account/console.cpp +++ b/src/account/console.cpp @@ -1,5 +1,7 @@ #include "account/console.h" +#include +#include #include #include #include @@ -45,7 +47,9 @@ std::string Field(const std::string& document, const std::string& key) { } if (document[at] != '"') { std::string number; - while (at < document.size() && (std::isdigit(document[at]) != 0 || document[at] == '-')) { + while (at < document.size() && + (std::isdigit(static_cast(document[at])) != 0 || + document[at] == '-')) { number += document[at++]; } return number; @@ -113,6 +117,16 @@ bool Call(const std::string& url, const char* method, const std::string& body, return true; } +/// The body is whatever the console sent, so a long digit run must not take the +/// process down through std::stol. +long Number(const std::string& text) { + long value = 0; + const char* first = text.data(); + const char* last = first + text.size(); + const std::from_chars_result result = std::from_chars(first, last, value); + return result.ec == std::errc{} ? value : 0; +} + std::string Detail(const Reply& reply) { const std::string detail = Field(reply.body, "detail"); return detail.empty() ? "the console returned " + std::to_string(reply.status) : detail; @@ -136,8 +150,8 @@ bool BeginAuthorization(const std::string& console_url, const std::string& hostn authorization->request_code = Field(reply.body, "request_code"); authorization->poll_secret = Field(reply.body, "poll_secret"); authorization->verification_url = Field(reply.body, "verification_url"); - authorization->expires_in = std::stoi("0" + Field(reply.body, "expires_in")); - const int interval = std::stoi("0" + Field(reply.body, "interval")); + authorization->expires_in = static_cast(Number(Field(reply.body, "expires_in"))); + const int interval = static_cast(Number(Field(reply.body, "interval"))); authorization->interval = interval > 0 ? interval : 2; if (authorization->request_code.empty() || authorization->verification_url.empty()) { if (error != nullptr) { @@ -155,7 +169,7 @@ void ReadGrant(const std::string& body, Grant* grant) { grant->refresh_token = Field(body, "refresh_token"); grant->email = Field(body, "email"); grant->plan = Field(body, "plan"); - grant->expires_in = std::stol("0" + Field(body, "expires_in")); + grant->expires_in = Number(Field(body, "expires_in")); } } // namespace @@ -231,8 +245,8 @@ bool WhoAmI(const std::string& console_url, const std::string& token, Identity* } identity->email = Field(reply.body, "email"); identity->plan = Field(reply.body, "plan"); - identity->tokens_this_month = std::stol("0" + Field(reply.body, "tokens_this_month")); - identity->monthly_token_limit = std::stol("0" + Field(reply.body, "monthly_token_limit")); + identity->tokens_this_month = Number(Field(reply.body, "tokens_this_month")); + identity->monthly_token_limit = Number(Field(reply.body, "monthly_token_limit")); return true; } diff --git a/src/account/credentials.cpp b/src/account/credentials.cpp index 4a6c767..ebf08d8 100644 --- a/src/account/credentials.cpp +++ b/src/account/credentials.cpp @@ -1,12 +1,16 @@ #include "account/credentials.h" +#include #include #include #include #include #if !defined(_WIN32) +#include +#include #include +#include #endif namespace rcli::account { @@ -27,7 +31,14 @@ std::string HomeDirectory() { } return Env("HOMEDRIVE") + Env("HOMEPATH"); #else - return Env("HOME"); + const std::string home = Env("HOME"); + if (!home.empty()) { + return home; + } + // Falling back to a relative path would drop a token file wherever the + // command happened to run, which is often a source tree. + const passwd* entry = getpwuid(getuid()); + return entry != nullptr && entry->pw_dir != nullptr ? entry->pw_dir : std::string(); #endif } @@ -75,6 +86,21 @@ std::string Field(const std::string& document, const std::string& key) { } // namespace +namespace { + +/// Bearer and refresh tokens travel to this origin, so plain HTTP is only +/// tolerable when it cannot leave the machine. +bool LoopbackOrigin(const std::string& url) { + return url.rfind("http://localhost", 0) == 0 || url.rfind("http://127.0.0.1", 0) == 0 || + url.rfind("http://[::1]", 0) == 0; +} + +} // namespace + +bool ConsoleUrlIsSafe(const std::string& url) { + return url.rfind("https://", 0) == 0 || LoopbackOrigin(url); +} + std::string DefaultConsoleUrl() { const std::string configured = Env("RCLI_CONSOLE_URL"); return configured.empty() ? "http://localhost:8080" : configured; @@ -87,7 +113,7 @@ std::string ProfileDirectory() { } const std::string home = HomeDirectory(); if (home.empty()) { - return ".rcli"; + return {}; } return home + "/.config/rcli"; } @@ -117,6 +143,12 @@ Credentials Load() { bool Save(const Credentials& credentials, std::string* error) { const std::string directory = ProfileDirectory(); + if (directory.empty()) { + if (error != nullptr) { + *error = "no home directory to store credentials in"; + } + return false; + } std::error_code ec; std::filesystem::create_directories(directory, ec); if (ec) { @@ -127,28 +159,61 @@ bool Save(const Credentials& credentials, std::string* error) { } const std::string path = directory + "/" + kFileName; - std::ofstream file(path, std::ios::trunc); - if (!file) { + std::ostringstream document; + document << "{\n" + << " \"console_url\": " << Quote(credentials.console_url) << ",\n" + << " \"email\": " << Quote(credentials.email) << ",\n" + << " \"plan\": " << Quote(credentials.plan) << ",\n" + << " \"access_token\": " << Quote(credentials.access_token) << ",\n" + << " \"refresh_token\": " << Quote(credentials.refresh_token) << "\n" + << "}\n"; + const std::string body = document.str(); + +#if defined(_WIN32) + std::ofstream file(path, std::ios::trunc | std::ios::binary); + if (!file || !(file << body)) { if (error != nullptr) { *error = "could not write " + path; } return false; } - file << "{\n" - << " \"console_url\": " << Quote(credentials.console_url) << ",\n" - << " \"email\": " << Quote(credentials.email) << ",\n" - << " \"plan\": " << Quote(credentials.plan) << ",\n" - << " \"access_token\": " << Quote(credentials.access_token) << ",\n" - << " \"refresh_token\": " << Quote(credentials.refresh_token) << "\n" - << "}\n"; - file.close(); - -#if !defined(_WIN32) - // The file holds a bearer token, so it is readable only by its owner. On - // Windows the profile directory under the user account carries that. - ::chmod(path.c_str(), S_IRUSR | S_IWUSR); -#endif return true; +#else + // Opened 0600 rather than written and then chmod'ed: the default umask + // makes the file world-readable, and the tokens are on disk for the whole + // window between the two calls. + const int fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR); + if (fd < 0) { + if (error != nullptr) { + *error = "could not write " + path; + } + return false; + } + // An existing file keeps its old mode through O_CREAT, so tighten it too. + ::fchmod(fd, S_IRUSR | S_IWUSR); + std::size_t written = 0; + while (written < body.size()) { + const ssize_t chunk = ::write(fd, body.data() + written, body.size() - written); + if (chunk <= 0) { + if (errno == EINTR) { + continue; + } + ::close(fd); + if (error != nullptr) { + *error = "could not write " + path; + } + return false; + } + written += static_cast(chunk); + } + if (::close(fd) != 0) { + if (error != nullptr) { + *error = "could not finish writing " + path; + } + return false; + } + return true; +#endif } bool Clear(std::string* error) { diff --git a/src/account/credentials.h b/src/account/credentials.h index 8456c64..4993aea 100644 --- a/src/account/credentials.h +++ b/src/account/credentials.h @@ -29,6 +29,10 @@ bool Clear(std::string* error); std::string DefaultConsoleUrl(); +/// True when tokens may be sent to this origin: HTTPS anywhere, or plain HTTP +/// only on loopback, where the traffic cannot leave the machine. +bool ConsoleUrlIsSafe(const std::string& url); + } // namespace rcli::account #endif // RCLI_ACCOUNT_CREDENTIALS_H diff --git a/src/cli/cmd_account.cpp b/src/cli/cmd_account.cpp index 6a01a60..f7d68e9 100644 --- a/src/cli/cmd_account.cpp +++ b/src/cli/cmd_account.cpp @@ -7,8 +7,12 @@ #if defined(_WIN32) // gethostname is Winsock on Windows, not unistd, and Winsock needs starting // before it answers. src/harness does the same for its port probe. +#include #include #else +#include +#include +#include #include #endif @@ -37,19 +41,52 @@ std::string Hostname() { return "unknown"; } -/// Only ever handed a URL this process just received from the console, so there -/// is nothing user-supplied to quote around. +/// The URL arrives in a console response and the console itself is whatever +/// RCLI_CONSOLE_URL names, so it is untrusted input. It never reaches a shell: +/// the opener is executed with an argument vector, and only after the scheme +/// has been checked. +bool OpensSafely(const std::string& url) { + return url.rfind("https://", 0) == 0 || url.rfind("http://", 0) == 0; +} + void OpenBrowser(const std::string& url) { + if (!OpensSafely(url)) { + out::Status("refusing to open a URL that is not http or https"); + return; + } +#if defined(_WIN32) + const intptr_t rc = _spawnlp(_P_NOWAIT, "rundll32", "rundll32", "url.dll,FileProtocolHandler", + url.c_str(), nullptr); + if (rc < 0) { + out::Status("could not open a browser for you"); + } +#else #if defined(__APPLE__) - const std::string command = "open '" + url + "' >/dev/null 2>&1"; -#elif defined(_WIN32) - const std::string command = "start \"\" \"" + url + "\""; + const char* opener = "open"; #else - const std::string command = "xdg-open '" + url + "' >/dev/null 2>&1"; + const char* opener = "xdg-open"; #endif - if (std::system(command.c_str()) != 0) { + const pid_t child = fork(); + if (child < 0) { out::Status("could not open a browser for you"); + return; } + if (child == 0) { + const int devnull = ::open("/dev/null", O_WRONLY); + if (devnull >= 0) { + dup2(devnull, STDOUT_FILENO); + dup2(devnull, STDERR_FILENO); + close(devnull); + } + std::string target = url; + char* argv[] = {const_cast(opener), target.data(), nullptr}; + execvp(opener, argv); + _exit(127); + } + int status = 0; + while (waitpid(child, &status, 0) < 0 && errno == EINTR) { + } +#endif } bool Ready() { @@ -65,6 +102,11 @@ int Login(bool open_browser) { return 1; } account::Credentials credentials = account::Load(); + if (!account::ConsoleUrlIsSafe(credentials.console_url)) { + out::Error("refusing to send credentials to " + credentials.console_url); + out::Status("the console must be https, or http on loopback"); + return 1; + } account::Authorization authorization; std::string failure; @@ -81,8 +123,11 @@ int Login(bool open_browser) { } out::Status("waiting for approval"); - const auto deadline = - std::chrono::steady_clock::now() + std::chrono::seconds(authorization.expires_in); + // A console that omits expires_in would otherwise put the deadline in the + // past, so the loop never runs and the user is told it timed out before a + // single poll was sent. + const int window = authorization.expires_in > 0 ? authorization.expires_in : 600; + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(window); account::Grant grant; while (std::chrono::steady_clock::now() < deadline) { std::this_thread::sleep_for(std::chrono::seconds(authorization.interval)); @@ -159,7 +204,12 @@ int WhoAmI() { credentials.refresh_token = grant.refresh_token; credentials.email = grant.email; credentials.plan = grant.plan; - account::Save(credentials, nullptr); + // The console has already rotated the refresh token, so losing this + // write costs the session. Say so rather than failing silently on the + // next command. + if (!account::Save(credentials, &failure)) { + out::Status("could not store the refreshed session: " + failure); + } if (!account::WhoAmI(credentials.console_url, credentials.access_token, &identity, &failure)) { out::Error(failure); diff --git a/src/cli/cmd_harness.cpp b/src/cli/cmd_harness.cpp index 9bb7f12..da52f18 100644 --- a/src/cli/cmd_harness.cpp +++ b/src/cli/cmd_harness.cpp @@ -17,8 +17,12 @@ void RegisterHarness(CLI::App& app, Options& options) { auto rest = std::make_shared>(); auto* opencode = app.add_subcommand("opencode", "open a coding session in opencode, wired to a model"); - opencode->add_option("model", *model, "a model on this machine, or one served upstream"); - opencode->add_option("args", *rest, "passed through to opencode"); + // A named option rather than a positional: with two positionals there is no + // way to tell `rcli opencode run` asking for passthrough from someone + // naming a model called run, and the first reading wins silently. + opencode->add_option("-m,--model", *model, "a model on this machine, or one served upstream"); + opencode->add_option("args", *rest, "passed through to opencode")->allow_extra_args(); + opencode->prefix_command(); opencode->callback([&options, model, rest] { options.status = harness::Launch("opencode", *model, *rest); }); diff --git a/src/harness/harness.cpp b/src/harness/harness.cpp index 7791e39..a2f6a2f 100644 --- a/src/harness/harness.cpp +++ b/src/harness/harness.cpp @@ -1,5 +1,6 @@ #include "harness/harness.h" +#include #include #include #include @@ -116,6 +117,24 @@ std::string OpencodeConfig(const std::string& model, const std::string& base_url "\"model\":" + Quote("runanywhere/" + model) + "}"; } +constexpr const char* kConfigVariable = "OPENCODE_CONFIG_CONTENT"; + +void SetConfigVariable(const std::string& value) { +#if defined(_WIN32) + _putenv_s(kConfigVariable, value.c_str()); +#else + setenv(kConfigVariable, value.c_str(), 1); +#endif +} + +void UnsetConfigVariable() { +#if defined(_WIN32) + _putenv_s(kConfigVariable, ""); +#else + unsetenv(kConfigVariable); +#endif +} + int Spawn(const std::string& tool, const std::vector& args) { std::vector owned; owned.push_back(tool); @@ -149,7 +168,15 @@ int Spawn(const std::string& tool, const std::vector& args) { _exit(127); } int status = 0; - waitpid(child, &status, 0); + // status is undefined after a failed waitpid, so a bare 0 there would read + // as a clean exit while the child is still running. + while (waitpid(child, &status, 0) < 0) { + if (errno == EINTR) { + continue; + } + out::Error("lost track of " + tool); + return 1; + } if (WIFEXITED(status)) { return WEXITSTATUS(status); } @@ -227,13 +254,21 @@ int Launch(const std::string& tool, const std::string& model, } const std::string config = OpencodeConfig(model, base_url, api_key); -#if defined(_WIN32) - _putenv_s("OPENCODE_CONFIG_CONTENT", config.c_str()); -#else - setenv("OPENCODE_CONFIG_CONTENT", config.c_str(), 1); -#endif + const char* previous = std::getenv(kConfigVariable); + const std::string restored = previous != nullptr ? previous : std::string(); + const bool had_previous = previous != nullptr; + SetConfigVariable(config); const int status = Spawn(tool, args); + + // Launch runs more than once in a process during tests, and a stale value + // here would override the tool's own configuration on a later call that + // named no model. + if (had_previous) { + SetConfigVariable(restored); + } else { + UnsetConfigVariable(); + } if (serving) { rac_server_stop(); } From b28c1fc8ac4538fbd8953ac4368fbc2fe142e65f Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Sat, 22 Aug 2026 11:30:04 +0530 Subject: [PATCH 08/16] ci: pin the SDK to a commit so an unrelated merge cannot break a pull request --- .github/workflows/ci.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d0ea54..a3baaed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,10 +24,12 @@ concurrency: cancel-in-progress: true env: - # Both RAC_ROOT_DIR (needed to configure at all) and the llama.cpp accelerator - # fix are on main now. Pin this to a tag once the SDK cuts one that contains - # them, so a build is reproducible from a fixed point. - SDK_REF: main + # A commit, not a branch, and the same one release.yml builds from. Tracking + # main meant an unrelated merge there could break a pull request here: a + # version bump landed pointing Package.swift at XCFrameworks for a release + # that had not been published, and every binary target 404'd. Move this + # deliberately when picking up SDK changes. + SDK_REF: a46517cf1c2a1fc258f137a81e6185d7ccc3b62d jobs: macos: From 536ecfb18fa3ffb44741ddc6996dc9052dc7dc0e Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Fri, 28 Aug 2026 05:16:20 +0530 Subject: [PATCH 09/16] harness: extract resolve and release so integrations can share one endpoint --- src/harness/harness.cpp | 63 +++++++++++++++++++++++++++++------------ src/harness/harness.h | 27 ++++++++++++++++++ 2 files changed, 72 insertions(+), 18 deletions(-) diff --git a/src/harness/harness.cpp b/src/harness/harness.cpp index a2f6a2f..34eb942 100644 --- a/src/harness/harness.cpp +++ b/src/harness/harness.cpp @@ -39,7 +39,11 @@ using out::Ink; /// A port nothing is listening on, found by letting the OS pick one and giving /// it straight back. There is a race between closing and the server binding, /// but the alternative is a fixed port that collides with a second rcli. -int FreePort() { +/// +/// `preferred` asks for one particular port and settles for any free one when +/// it is taken. An integration that writes the port into a config file wants +/// that: the file keeps working between runs instead of naming a dead port. +int FreePort(int preferred) { #if defined(_WIN32) // Winsock has to be initialised before any socket call, and the server that // would otherwise do it has not started yet. The count is per-process and @@ -66,7 +70,7 @@ int FreePort() { sockaddr_in address{}; address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - address.sin_port = 0; + address.sin_port = htons(static_cast(preferred)); int port = 0; if (bind(sock, reinterpret_cast(&address), sizeof(address)) == 0) { rcli_socklen_t length = static_cast(sizeof(address)); @@ -79,6 +83,9 @@ int FreePort() { #else close(sock); #endif + if (port == 0 && preferred != 0) { + return FreePort(0); + } return port; } @@ -186,15 +193,12 @@ int Spawn(const std::string& tool, const std::vector& args) { } // namespace -int Launch(const std::string& tool, const std::string& model, - const std::vector& args) { - if (model.empty()) { - // Nothing to wire, so do not pretend to: run the tool as the user has - // it configured. - return Spawn(tool, args); +bool Resolve(const std::string& model, Endpoint* endpoint, int preferred_port) { + if (endpoint == nullptr || model.empty()) { + return false; } if (!rcli::cli::Start()) { - return 1; + return false; } std::string base_url; @@ -220,12 +224,12 @@ int Launch(const std::string& tool, const std::string& model, out::Error(model + " runs on " + local->framework + ", and the local server can only serve LlamaCpp models today"); out::Status("use a GGUF model here, or point at an upstream one"); - return 1; + return false; } - const int port = FreePort(); + const int port = FreePort(preferred_port); if (port == 0) { out::Error("could not find a free port for the local server"); - return 1; + return false; } rac_server_config_t config = RAC_SERVER_CONFIG_DEFAULT; config.host = "127.0.0.1"; @@ -237,7 +241,7 @@ int Launch(const std::string& tool, const std::string& model, out::Status("serving " + model + " on 127.0.0.1:" + std::to_string(port)); if (rac_server_start(&config) != RAC_SUCCESS) { out::Error("the local server would not start for " + model); - return 1; + return false; } serving = true; base_url = "http://127.0.0.1:" + std::to_string(port) + "/v1"; @@ -246,14 +250,39 @@ int Launch(const std::string& tool, const std::string& model, if (!credentials.signed_in()) { out::Error(model + " is not on this machine, and you are not signed in"); out::Status("run `rcli login`, or `rcli pull " + model + "` to run it here"); - return 1; + return false; } base_url = credentials.console_url + "/v1"; api_key = credentials.access_token; out::Status("using " + model + " as " + credentials.email); } - const std::string config = OpencodeConfig(model, base_url, api_key); + endpoint->base_url = base_url; + endpoint->api_key = api_key; + endpoint->serving = serving; + return true; +} + +void Release(const Endpoint& endpoint) { + if (endpoint.serving) { + rac_server_stop(); + } +} + +int Launch(const std::string& tool, const std::string& model, + const std::vector& args) { + if (model.empty()) { + // Nothing to wire, so do not pretend to: run the tool as the user has + // it configured. + return Spawn(tool, args); + } + + Endpoint endpoint; + if (!Resolve(model, &endpoint)) { + return 1; + } + + const std::string config = OpencodeConfig(model, endpoint.base_url, endpoint.api_key); const char* previous = std::getenv(kConfigVariable); const std::string restored = previous != nullptr ? previous : std::string(); const bool had_previous = previous != nullptr; @@ -269,9 +298,7 @@ int Launch(const std::string& tool, const std::string& model, } else { UnsetConfigVariable(); } - if (serving) { - rac_server_stop(); - } + Release(endpoint); return status; } diff --git a/src/harness/harness.h b/src/harness/harness.h index c845147..572d431 100644 --- a/src/harness/harness.h +++ b/src/harness/harness.h @@ -14,6 +14,33 @@ /// a harness needs no plugin to work with us. namespace rcli::harness { +/// Where a model can be reached over HTTP, and whether we are serving it. +struct Endpoint { + /// An OpenAI-compatible root, ending in `/v1`. + std::string base_url; + /// Empty for a local server, which ignores what is in the header. + std::string api_key; + /// True when `Resolve` started a server that `Release` has to stop. + bool serving = false; +}; + +/// Points `endpoint` at `model`, starting a local server when the model is on +/// this machine and using the signed-in console when it is not. +/// +/// `preferred_port` asks the local server for one particular port, and is +/// ignored when something else already holds it or the model is upstream. +/// An integration that writes the port into a file the tool reads at startup +/// wants this: the same port every run is what keeps that file true. +/// +/// Returns false having already explained why not: an unknown model, a +/// framework the local server cannot load, or an upstream model with nobody +/// signed in. Every integration needs this same answer, so it is separate from +/// launching anything. +bool Resolve(const std::string& model, Endpoint* endpoint, int preferred_port = 0); + +/// Stops whatever `Resolve` started. Safe on an endpoint it did not serve. +void Release(const Endpoint& endpoint); + /// Runs `tool` against `model`, forwarding `args` to it, and returns the tool's /// exit code. Blocks until the tool exits, then stops anything it started. /// From 5b3de31d6b385ca03a924e65a8f294c81df1f433 Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Fri, 28 Aug 2026 05:16:20 +0530 Subject: [PATCH 10/16] feat: point Claude Code, Claude Desktop and JetBrains IDEs at a model rcli serves --- CMakeLists.txt | 11 + src/anthropic/messages.cpp | 310 ++++++++++++++++++++++ src/anthropic/messages.h | 62 +++++ src/anthropic/translate.cpp | 212 +++++++++++++++ src/anthropic/translate.h | 56 ++++ src/cli/app.cpp | 1 + src/cli/cmd_editors.cpp | 418 ++++++++++++++++++++++++++++++ src/cli/commands.h | 1 + src/desktop/claude_profile.cpp | 226 ++++++++++++++++ src/desktop/claude_profile.h | 45 ++++ src/ide/jetbrains_profile.cpp | 375 +++++++++++++++++++++++++++ src/ide/jetbrains_profile.h | 73 ++++++ src/ide/openai_proxy.cpp | 456 +++++++++++++++++++++++++++++++++ src/ide/openai_proxy.h | 47 ++++ 14 files changed, 2293 insertions(+) create mode 100644 src/anthropic/messages.cpp create mode 100644 src/anthropic/messages.h create mode 100644 src/anthropic/translate.cpp create mode 100644 src/anthropic/translate.h create mode 100644 src/cli/cmd_editors.cpp create mode 100644 src/desktop/claude_profile.cpp create mode 100644 src/desktop/claude_profile.h create mode 100644 src/ide/jetbrains_profile.cpp create mode 100644 src/ide/jetbrains_profile.h create mode 100644 src/ide/openai_proxy.cpp create mode 100644 src/ide/openai_proxy.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 4363cbf..293d25a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,8 @@ include(RunAnywhereSDK) add_library(rcli_core STATIC src/account/console.cpp src/account/credentials.cpp + src/anthropic/messages.cpp + src/anthropic/translate.cpp src/audio/audio.cpp src/catalog/catalog.cpp src/chat/complete.cpp @@ -33,6 +35,7 @@ add_library(rcli_core STATIC src/cli/cmd_account.cpp src/cli/cmd_bench.cpp src/cli/cmd_chat.cpp + src/cli/cmd_editors.cpp src/cli/cmd_engines.cpp src/cli/cmd_harness.cpp src/cli/cmd_image.cpp @@ -40,6 +43,9 @@ add_library(rcli_core STATIC src/cli/cmd_speech.cpp src/cli/output.cpp src/cli/preview.cpp + src/desktop/claude_profile.cpp + src/ide/jetbrains_profile.cpp + src/ide/openai_proxy.cpp src/harness/harness.cpp src/media/png.cpp src/repl/repl.cpp @@ -73,6 +79,9 @@ find_package(ZLIB REQUIRED) target_link_libraries(rcli_core PRIVATE rac_commons ZLIB::ZLIB) # The OpenAI-compatible server `rcli opencode` starts for a local model. target_link_libraries(rcli_core PRIVATE rac_server) +# Both are already fetched for the SDK's own server; naming them here is what +# lets the Anthropic translator include them rather than vendoring a copy. +target_link_libraries(rcli_core PRIVATE httplib::httplib nlohmann_json::nlohmann_json) target_compile_definitions(rcli_core PRIVATE RCLI_VERSION="${PROJECT_VERSION}") foreach(_backend llamacpp onnx sherpa mlx neurt cloud) if(TARGET rac_backend_${_backend}) @@ -120,6 +129,8 @@ endif() if(APPLE) # AudioQueue, for microphone capture and playback. target_link_libraries(rcli_core PRIVATE "-framework AudioToolbox") + # SecItem, for the credential a JetBrains IDE reads its provider key from. + target_link_libraries(rcli_core PRIVATE "-framework Security" "-framework CoreFoundation") endif() if(WIN32) diff --git a/src/anthropic/messages.cpp b/src/anthropic/messages.cpp new file mode 100644 index 0000000..f4da948 --- /dev/null +++ b/src/anthropic/messages.cpp @@ -0,0 +1,310 @@ +#include "anthropic/messages.h" + +#include +#include +#include +#include + +#include +#include + +#include "anthropic/translate.h" +#include "cli/output.h" + +namespace rcli::anthropic { +namespace { + +using Json = nlohmann::json; + +/// Split "http://host:port/v1" into the host root and the path prefix httplib +/// wants separately. +bool SplitBaseUrl(const std::string& base_url, std::string* origin, std::string* prefix) { + const std::string scheme = base_url.rfind("https://", 0) == 0 ? "https://" : "http://"; + const size_t start = base_url.find(scheme); + if (start != 0) { + return false; + } + const size_t slash = base_url.find('/', scheme.size()); + if (slash == std::string::npos) { + *origin = base_url; + *prefix = ""; + } else { + *origin = base_url.substr(0, slash); + *prefix = base_url.substr(slash); + } + return !origin->empty(); +} + +struct Runtime { + httplib::Server server; + std::thread thread; + std::string origin; + std::string prefix; + std::string api_key; + std::string model; + std::string advertised; + bool verbose = false; +}; + +std::unique_ptr g_runtime; + +void ApplyAuth(httplib::Client& client, const std::string& api_key) { + if (!api_key.empty()) { + client.set_bearer_token_auth(api_key); + } +} + +void HandleNonStreaming(Runtime& runtime, const Json& request, httplib::Response& response) { + httplib::Client client(runtime.origin); + client.set_read_timeout(600, 0); + ApplyAuth(client, runtime.api_key); + + const Json upstream = translate::RequestToOpenAI(request, runtime.model); + const httplib::Result reply = + client.Post(runtime.prefix + "/chat/completions", upstream.dump(), "application/json"); + if (!reply || reply->status < 200 || reply->status >= 300) { + response.status = reply ? reply->status : 502; + response.set_content( + translate::ErrorBody("api_error", + reply ? reply->body : std::string("the model endpoint did not answer")), + "application/json"); + return; + } + Json parsed; + try { + parsed = Json::parse(reply->body); + } catch (const Json::exception& error) { + response.status = 502; + response.set_content(translate::ErrorBody("api_error", error.what()), "application/json"); + return; + } + response.set_content(translate::ResponseToAnthropic(parsed, runtime.model).dump(), + "application/json"); +} + +void HandleStreaming(Runtime& runtime, const Json& request, httplib::Response& response) { + // The upstream body is built here rather than in the sink: the sink runs + // after this function returns, and everything it touches has to outlive it. + auto upstream = std::make_shared( + translate::RequestToOpenAI(request, runtime.model).dump()); + auto origin = std::make_shared(runtime.origin); + auto path = std::make_shared(runtime.prefix + "/chat/completions"); + auto api_key = std::make_shared(runtime.api_key); + auto model = std::make_shared(runtime.model); + + response.set_chunked_content_provider( + "text/event-stream", + [upstream, origin, path, api_key, model](size_t /*offset*/, httplib::DataSink& sink) { + httplib::Client client(*origin); + client.set_read_timeout(600, 0); + ApplyAuth(client, *api_key); + + translate::StreamState state; + state.model = *model; + std::string pending; + + const httplib::Result reply = client.Post( + *path, httplib::Headers(), *upstream, "application/json", + [&](const char* data, size_t length) { + pending.append(data, length); + // SSE frames are separated by a blank line, and a chunk can + // split one in half, so only whole frames are consumed. + size_t split = 0; + while ((split = pending.find("\n\n")) != std::string::npos) { + const std::string frame = pending.substr(0, split); + pending.erase(0, split + 2); + const size_t field = frame.find("data:"); + if (field == std::string::npos) { + continue; + } + std::string payload = frame.substr(field + 5); + while (!payload.empty() && (payload.front() == ' ' || payload.front() == '\r')) { + payload.erase(payload.begin()); + } + if (payload == "[DONE]") { + continue; + } + Json chunk; + try { + chunk = Json::parse(payload); + } catch (const Json::exception&) { + continue; + } + std::string events; + try { + events = translate::StreamChunkToAnthropic(chunk, &state); + } catch (const std::exception&) { + // A chunk in a shape the mapping did not expect is + // a chunk to skip, not a reason to kill the run. + continue; + } + if (!events.empty() && !sink.write(events.data(), events.size())) { + return false; + } + } + return true; + }); + + if (!reply) { + const std::string body = + "event: error\ndata: " + + translate::ErrorBody("api_error", "the model endpoint stopped answering") + + "\n\n"; + sink.write(body.data(), body.size()); + sink.done(); + return false; + } + try { + const std::string closing = translate::StreamCloseToAnthropic(&state); + if (!closing.empty()) { + sink.write(closing.data(), closing.size()); + } + } catch (const std::exception&) { + // Nothing useful left to say; ending the stream cleanly beats + // aborting the process holding the reader's editor open. + } + sink.done(); + return true; + }); +} + +} // namespace + +bool Start(const harness::Endpoint& upstream, const std::string& model, Shim* shim, + bool verbose, const std::string& advertised) { + if (shim == nullptr) { + return false; + } + Stop(shim); + + auto runtime = std::make_unique(); + if (!SplitBaseUrl(upstream.base_url, &runtime->origin, &runtime->prefix)) { + out::Error("could not read the model endpoint: " + upstream.base_url); + return false; + } + runtime->api_key = upstream.api_key; + runtime->model = model; + runtime->advertised = advertised.empty() ? model : advertised; + runtime->verbose = verbose; + + Runtime* raw = runtime.get(); + raw->server.Post("/v1/messages", [raw](const httplib::Request& request, + httplib::Response& response) { + if (raw->verbose) { + out::Status("anthropic: POST /v1/messages, " + + std::to_string(request.body.size()) + " bytes"); + } + Json parsed; + try { + parsed = Json::parse(request.body); + } catch (const Json::exception& error) { + response.status = 400; + response.set_content(translate::ErrorBody("invalid_request_error", error.what()), + "application/json"); + return; + } + try { + if (parsed.value("stream", false)) { + HandleStreaming(*raw, parsed, response); + } else { + HandleNonStreaming(*raw, parsed, response); + } + } catch (const std::exception& error) { + // httplib does not catch, and an exception leaving here reaches + // std::terminate: the editor's model call would abort rcli. + response.status = 500; + response.set_content(translate::ErrorBody("api_error", error.what()), + "application/json"); + } + }); + + // Discovery, in Anthropic's shape rather than OpenAI's. + // + // Claude Desktop probes this before it will use a gateway at all, and an + // OpenAI-shaped list fails it with "Gateway returned no usable models": + // the entries need `display_name` and `created_at`, and the envelope needs + // the paging fields, or nothing in the list counts as usable. + raw->server.Get("/v1/models", [raw](const httplib::Request&, httplib::Response& response) { + if (raw->verbose) { + out::Status("anthropic: GET /v1/models -> " + raw->advertised + " (serving " + + raw->model + ")"); + } + // The shape claude.com/docs/third-party/claude-desktop documents for a + // gateway, which is OpenAI's list envelope rather than Anthropic's. + // Guessing the Anthropic shape here is what produced "Gateway returned + // no usable models". + const Json entry{{"id", raw->advertised}, {"object", "model"}}; + response.set_content( + Json{{"object", "list"}, {"data", Json::array({entry})}}.dump(), + "application/json"); + }); + + // Claude Code probes this before it sends anything and treats a failure as + // an endpoint that is not there. Answering it is what makes the translator + // look like a gateway rather than a wrong address. + raw->server.Get("/api/hello", [](const httplib::Request&, httplib::Response& response) { + response.set_content(Json{{"ok", true}}.dump(), "application/json"); + }); + // httplib has no HEAD route, and Claude Code probes with HEAD, so it is + // answered ahead of routing rather than left to fall through to the 404. + raw->server.set_pre_routing_handler( + [](const httplib::Request& request, httplib::Response& response) { + if (request.method == "HEAD" && request.path == "/api/hello") { + response.status = 200; + return httplib::Server::HandlerResponse::Handled; + } + return httplib::Server::HandlerResponse::Unhandled; + }); + + // A route we do not translate should say so, not 404 into a silence the + // reader has to guess at. + raw->server.set_error_handler([raw](const httplib::Request& request, + httplib::Response& response) { + if (raw->verbose) { + out::Status("anthropic: " + request.method + " " + request.path + " -> " + + std::to_string(response.status)); + } + if (response.body.empty()) { + response.set_content( + translate::ErrorBody("not_found_error", + request.method + " " + request.path + + " is not something rcli translates"), + "application/json"); + } + }); + + const int port = raw->server.bind_to_any_port("127.0.0.1"); + if (port <= 0) { + out::Error("could not open a port for the Anthropic translator"); + return false; + } + + g_runtime = std::move(runtime); + Runtime* started = g_runtime.get(); + started->thread = std::thread([started] { started->server.listen_after_bind(); }); + + shim->base_url = "http://127.0.0.1:" + std::to_string(port); + // Never the upstream key: the client only has to send something, and + // handing it a real console token would put it in that process's + // environment where it does not belong. + shim->auth_token = "rcli-local"; + shim->running = true; + return true; +} + +void Stop(Shim* shim) { + if (g_runtime) { + g_runtime->server.stop(); + if (g_runtime->thread.joinable()) { + g_runtime->thread.join(); + } + g_runtime.reset(); + } + if (shim != nullptr) { + shim->running = false; + shim->base_url.clear(); + shim->auth_token.clear(); + } +} + +} // namespace rcli::anthropic diff --git a/src/anthropic/messages.h b/src/anthropic/messages.h new file mode 100644 index 0000000..e4cb226 --- /dev/null +++ b/src/anthropic/messages.h @@ -0,0 +1,62 @@ +#ifndef RCLI_ANTHROPIC_MESSAGES_H +#define RCLI_ANTHROPIC_MESSAGES_H + +#include + +#include "harness/harness.h" + +/// An Anthropic-shaped front door onto an OpenAI-shaped model. +/// +/// Claude Code, Claude Desktop and Cowork all talk the Anthropic Messages API +/// and are pointed elsewhere with ANTHROPIC_BASE_URL. Our server speaks +/// OpenAI: /v1/models, /v1/chat/completions, /health, and nothing else. The two +/// never meet, which is why `rcli opencode` works today and `rcli claude-code` +/// could not. +/// +/// This is the translator between them. It serves POST /v1/messages on +/// loopback, rewrites each request into a chat completion, forwards it to +/// whichever endpoint `harness::Resolve` produced, and rewrites the reply back. +/// Streaming is translated event by event, because Claude Code streams and a +/// buffered answer would arrive as one block minutes later. +/// +/// It lives here rather than in commons on purpose: it implements another +/// vendor's wire format, which is an integration detail of this CLI, not +/// inference logic every SDK consumer needs. If a second consumer ever wants +/// it, that is the moment to move it down a layer. +namespace rcli::anthropic { + +/// A running translator. +struct Shim { + /// Where Claude Code should be pointed. Empty when nothing started. + std::string base_url; + /// The value ANTHROPIC_AUTH_TOKEN should carry. Never the upstream key. + std::string auth_token; + bool running = false; +}; + +/// Starts a translator in front of `upstream` and fills `shim`. +/// +/// `model` is the id sent on to the upstream endpoint, whatever name the +/// caller asked Claude Code for: Claude Code sends its own model strings, and +/// forwarding those to a local GGUF would ask for a model that is not there. +/// +/// Returns false having said why. The caller owns stopping it. +/// `verbose` narrates each request to stderr. Off by default: the body carries +/// the reader's prompt, and a translator that logs conversations unasked is one +/// nobody should point at their editor. +/// `advertised`, when set, is the id this gateway claims to serve. Requests +/// are still forwarded as `model`; only the name on the wire changes. +/// +/// Claude Desktop drops any gateway model it cannot map to an Anthropic family, +/// so a gateway serving something else has to answer under a name it accepts. +/// The profile carries a labelOverride so the picker still shows what is really +/// answering. +bool Start(const harness::Endpoint& upstream, const std::string& model, Shim* shim, + bool verbose = false, const std::string& advertised = {}); + +/// Stops the translator and waits for its thread. Safe on a stopped shim. +void Stop(Shim* shim); + +} // namespace rcli::anthropic + +#endif // RCLI_ANTHROPIC_MESSAGES_H diff --git a/src/anthropic/translate.cpp b/src/anthropic/translate.cpp new file mode 100644 index 0000000..df61933 --- /dev/null +++ b/src/anthropic/translate.cpp @@ -0,0 +1,212 @@ +#include "anthropic/translate.h" + +#include +#include + +namespace rcli::anthropic::translate { +namespace { + +/// Anthropic lets content be a bare string or a list of typed blocks. Both mean +/// the same thing to an OpenAI endpoint, which only takes a string. +std::string FlattenContent(const Json& content) { + if (content.is_string()) { + return content.get(); + } + if (!content.is_array()) { + return {}; + } + std::string text; + for (const Json& block : content) { + if (!block.is_object()) { + continue; + } + // Only text survives the trip. An image block would have to become an + // OpenAI image_url part, and the local server does not serve vision at + // all, so dropping it is honest where inventing a shape is not. + if (block.value("type", std::string()) == "text") { + text += block.value("text", std::string()); + } + } + return text; +} + +/// OpenAI finish reasons, in Anthropic's vocabulary. +std::string StopReason(const std::string& finish) { + if (finish == "length") { + return "max_tokens"; + } + if (finish == "tool_calls") { + return "tool_use"; + } + if (finish.empty()) { + return {}; + } + return "end_turn"; +} + +std::string Event(const std::string& name, const Json& data) { + return "event: " + name + "\ndata: " + data.dump() + "\n\n"; +} + +} // namespace + +Json RequestToOpenAI(const Json& anthropic, const std::string& model) { + Json openai; + openai["model"] = model; + openai["stream"] = anthropic.value("stream", false); + + Json messages = Json::array(); + // Anthropic carries the system prompt beside the conversation; OpenAI wants + // it as the first message, so it is moved rather than dropped. + if (anthropic.contains("system")) { + const std::string system = FlattenContent(anthropic["system"]); + if (!system.empty()) { + messages.push_back({{"role", "system"}, {"content", system}}); + } + } + if (anthropic.contains("messages") && anthropic["messages"].is_array()) { + for (const Json& message : anthropic["messages"]) { + if (!message.is_object()) { + continue; + } + messages.push_back({{"role", message.value("role", std::string("user"))}, + {"content", FlattenContent(message.value("content", Json()))}}); + } + } + openai["messages"] = std::move(messages); + + // max_tokens is required by Anthropic and optional for OpenAI, so it always + // has a value to carry across. + if (anthropic.contains("max_tokens")) { + openai["max_tokens"] = anthropic["max_tokens"]; + } + for (const char* passthrough : {"temperature", "top_p", "stop_sequences"}) { + if (anthropic.contains(passthrough)) { + // stop_sequences is OpenAI's `stop`; the rest keep their names. + const std::string key = + std::string(passthrough) == "stop_sequences" ? "stop" : passthrough; + openai[key] = anthropic[passthrough]; + } + } + return openai; +} + +Json ResponseToAnthropic(const Json& openai, const std::string& model) { + Json choice; + if (openai.contains("choices") && openai["choices"].is_array() && + !openai["choices"].empty()) { + choice = openai["choices"][0]; + } + const Json message = choice.value("message", Json::object()); + const std::string text = message.value("content", std::string()); + + Json out; + out["id"] = openai.value("id", std::string("msg_rcli")); + out["type"] = "message"; + out["role"] = "assistant"; + out["model"] = model; + out["content"] = Json::array({Json{{"type", "text"}, {"text", text}}}); + const std::string stop = StopReason(choice.value("finish_reason", std::string())); + out["stop_reason"] = stop.empty() ? Json(nullptr) : Json(stop); + out["stop_sequence"] = nullptr; + + const Json usage = openai.value("usage", Json::object()); + out["usage"] = Json{{"input_tokens", usage.value("prompt_tokens", 0)}, + {"output_tokens", usage.value("completion_tokens", 0)}}; + return out; +} + +std::string StreamChunkToAnthropic(const Json& chunk, StreamState* state) { + if (state == nullptr) { + return {}; + } + std::string out; + + if (!state->opened) { + state->opened = true; + // `value` throws when the key is present with another type, and an id + // of null is exactly what some servers send. + state->message_id = chunk.contains("id") && chunk["id"].is_string() + ? chunk["id"].get() + : std::string("msg_rcli"); + Json start; + start["type"] = "message_start"; + start["message"] = Json{{"id", state->message_id}, + {"type", "message"}, + {"role", "assistant"}, + {"model", state->model}, + {"content", Json::array()}, + {"stop_reason", nullptr}, + {"stop_sequence", nullptr}, + {"usage", Json{{"input_tokens", 0}, {"output_tokens", 0}}}}; + out += Event("message_start", start); + } + + Json choice; + if (chunk.contains("choices") && chunk["choices"].is_array() && !chunk["choices"].empty()) { + choice = chunk["choices"][0]; + } + + const std::string finish = choice.contains("finish_reason") && + choice["finish_reason"].is_string() + ? choice["finish_reason"].get() + : std::string(); + if (!finish.empty()) { + state->stop_reason = StopReason(finish); + } + if (chunk.contains("usage") && chunk["usage"].is_object()) { + state->input_tokens = chunk["usage"].value("prompt_tokens", state->input_tokens); + state->output_tokens = chunk["usage"].value("completion_tokens", state->output_tokens); + } + + const Json delta = choice.contains("delta") && choice["delta"].is_object() + ? choice["delta"] + : Json::object(); + // content is null on the chunk that only carries a finish reason. + const std::string text = delta.contains("content") && delta["content"].is_string() + ? delta["content"].get() + : std::string(); + if (text.empty()) { + return out; + } + + // The block opens on the first token rather than up front: a stream that + // only ever carries a finish reason should not announce a text block that + // never gets one. + if (!state->block_open) { + state->block_open = true; + out += Event("content_block_start", + Json{{"type", "content_block_start"}, + {"index", 0}, + {"content_block", Json{{"type", "text"}, {"text", ""}}}}); + } + out += Event("content_block_delta", + Json{{"type", "content_block_delta"}, + {"index", 0}, + {"delta", Json{{"type", "text_delta"}, {"text", text}}}}); + return out; +} + +std::string StreamCloseToAnthropic(StreamState* state) { + if (state == nullptr || !state->opened) { + return {}; + } + std::string out; + if (state->block_open) { + state->block_open = false; + out += Event("content_block_stop", Json{{"type", "content_block_stop"}, {"index", 0}}); + } + const std::string stop = state->stop_reason.empty() ? "end_turn" : state->stop_reason; + out += Event("message_delta", + Json{{"type", "message_delta"}, + {"delta", Json{{"stop_reason", stop}, {"stop_sequence", nullptr}}}, + {"usage", Json{{"output_tokens", state->output_tokens}}}}); + out += Event("message_stop", Json{{"type", "message_stop"}}); + return out; +} + +std::string ErrorBody(const std::string& type, const std::string& message) { + return Json{{"type", "error"}, {"error", Json{{"type", type}, {"message", message}}}}.dump(); +} + +} // namespace rcli::anthropic::translate diff --git a/src/anthropic/translate.h b/src/anthropic/translate.h new file mode 100644 index 0000000..ae13ee1 --- /dev/null +++ b/src/anthropic/translate.h @@ -0,0 +1,56 @@ +#ifndef RCLI_ANTHROPIC_TRANSLATE_H +#define RCLI_ANTHROPIC_TRANSLATE_H + +#include +#include + +#include + +/// The wire-format translation, with no sockets in it. +/// +/// Separated from the server so the mapping can be tested by handing it JSON +/// and reading JSON back, which is the only part of this worth testing: the +/// HTTP plumbing is cpp-httplib's, and the interesting bugs are all in the +/// shapes. +namespace rcli::anthropic::translate { + +using Json = nlohmann::json; + +/// Anthropic Messages request -> OpenAI chat completion request. +/// +/// `model` replaces whatever model the caller named, because Claude Code sends +/// its own model ids and the endpoint behind us has never heard of them. +Json RequestToOpenAI(const Json& anthropic, const std::string& model); + +/// OpenAI chat completion -> a whole Anthropic message. +Json ResponseToAnthropic(const Json& openai, const std::string& model); + +/// One OpenAI stream chunk, turned into the Anthropic SSE events it implies. +/// +/// Anthropic's stream is a state machine — message_start, content_block_start, +/// deltas, content_block_stop, message_delta, message_stop — where OpenAI's is +/// a flat run of deltas. `state` carries what has already been emitted so the +/// opening events fire exactly once. +struct StreamState { + bool opened = false; + bool block_open = false; + std::string message_id; + std::string model; + std::string stop_reason; + int input_tokens = 0; + int output_tokens = 0; +}; + +/// Returns the SSE text to write for `chunk`, or empty when it implies nothing. +std::string StreamChunkToAnthropic(const Json& chunk, StreamState* state); + +/// The closing events, written once the upstream stream ends. +std::string StreamCloseToAnthropic(StreamState* state); + +/// An Anthropic-shaped error body, so a failure reads as one to the client +/// rather than as a malformed message. +std::string ErrorBody(const std::string& type, const std::string& message); + +} // namespace rcli::anthropic::translate + +#endif // RCLI_ANTHROPIC_TRANSLATE_H diff --git a/src/cli/app.cpp b/src/cli/app.cpp index 08f7eef..33410cd 100644 --- a/src/cli/app.cpp +++ b/src/cli/app.cpp @@ -83,6 +83,7 @@ extern "C" int rcli_run(int argc, char** argv) { cli::RegisterEngines(app, options); cli::RegisterHarness(app, options); cli::RegisterAccount(app, options); + cli::RegisterEditors(app, options); // Only --color is left to decide here; logging was settled in rcli_begin, // which had to run before the engines got a chance to write anything. diff --git a/src/cli/cmd_editors.cpp b/src/cli/cmd_editors.cpp new file mode 100644 index 0000000..aeeea14 --- /dev/null +++ b/src/cli/cmd_editors.cpp @@ -0,0 +1,418 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "anthropic/messages.h" +#include "cli/commands.h" +#include "cli/output.h" +#include "desktop/claude_profile.h" +#include "harness/harness.h" +#include "ide/jetbrains_profile.h" +#include "ide/openai_proxy.h" + +namespace rcli::cli { +namespace { + +/// One editor or agent rcli can point at a model. +/// +/// The list is the whole integration surface: a new target is a row here plus +/// whatever `apply` has to set. Everything before that — resolving the model, +/// serving it, translating the wire format — is shared, which is the point. +/// How a tool is told where the model lives. +enum class Wiring { + /// Variables in the launched process. Works for anything that reads + /// ANTHROPIC_BASE_URL itself, or spawns something that does. + Environment, + /// Claude Desktop's third-party gateway profile, because it ignores the + /// environment for authentication and says so. + ClaudeProfile, + /// AI Assistant's OpenAI-compatible provider, for a JetBrains IDE. The one + /// wiring that needs no translator: the endpoint is already that shape. + JetBrainsProvider, +}; + +struct Editor { + /// What the reader types after `rcli`. + const char* id; + /// An executable on PATH, or empty when this is a desktop app. + const char* command; + /// A macOS application bundle, or empty when `command` is on PATH. + const char* bundle; + const char* summary; + Wiring wiring; + /// Set only for Wiring::JetBrainsProvider. + const ide::Product* jetbrains; +}; + +constexpr ide::Product kCLion{"clion", "CLion.app", "clion", "CLion"}; +constexpr ide::Product kRustRover{"rustrover", "RustRover.app", "rustrover", "RustRover"}; + +/// Only tools that speak the Anthropic Messages API belong here. Anything +/// OpenAI-shaped needs no translator and goes through `rcli opencode`. +/// +/// Claude Desktop earns its place because it forwards a fixed set of variables +/// to the Claude Code it runs inside itself, and ANTHROPIC_BASE_URL is one of +/// them. That is the same trick as `rcli claude-code`, one process further out. +constexpr Editor kEditors[] = { + {"claude-code", "claude", "", "open Claude Code against a model", Wiring::Environment, + nullptr}, + {"claude-desktop", "", "Claude.app", "open Claude Desktop against a model", + Wiring::ClaudeProfile, nullptr}, + // A JetBrains IDE gets its own agent pointed at the model rather than a + // second one nested inside it. Wiring the bundled Claude Agent through the + // environment was the first attempt and bought nothing: the IDE already + // ships AI Assistant and Junie, and the nested agent asks for its own + // credential regardless. + {"clion", "", "CLion.app", "open CLion against a model", Wiring::JetBrainsProvider, + &kCLion}, + {"rustrover", "", "RustRover.app", "open RustRover against a model", + Wiring::JetBrainsProvider, &kRustRover}, +}; + +/// Where `editor`'s application bundle is, or empty when it is not installed. +std::string BundlePath(const Editor& editor) { + if (editor.bundle[0] == '\0') { + return {}; + } + const char* home = std::getenv("HOME"); + std::vector roots{"/Applications/"}; + if (home != nullptr) { + roots.push_back(std::string(home) + "/Applications/"); + } + for (const std::string& root : roots) { + const std::string path = root + editor.bundle; + std::ifstream probe(path + "/Contents/Info.plist"); + if (probe.good()) { + return path; + } + } + return {}; +} + +/// "Claude" from "/Applications/Claude.app". +std::string BundleName(const std::string& bundle_path) { + const size_t slash = bundle_path.rfind('/'); + const std::string leaf = + slash == std::string::npos ? bundle_path : bundle_path.substr(slash + 1); + const size_t dot = leaf.rfind(".app"); + return dot == std::string::npos ? leaf : leaf.substr(0, dot); +} + +/// `open -W -a `, which waits for the app to quit. +/// +/// Waiting is the point: the translator has to outlive the app exactly, and no +/// longer. No environment is passed — Claude Desktop ignores those for +/// authentication and says so in its own UI. The gateway profile is what +/// redirects it. +std::vector OpenArgs(const std::string& bundle, const anthropic::Shim& shim, + const std::vector& passthrough) { + std::vector args{"-W"}; + if (shim.running) { + // `open --env` is what carries them across; launchd would otherwise + // start the app with the reader's login environment instead of ours. + args.push_back("--env"); + args.push_back("ANTHROPIC_BASE_URL=" + shim.base_url); + args.push_back("--env"); + args.push_back("ANTHROPIC_AUTH_TOKEN=" + shim.auth_token); + args.push_back("--env"); + args.push_back("ANTHROPIC_API_KEY=" + shim.auth_token); + } + args.push_back("-a"); + args.push_back(bundle); + if (!passthrough.empty()) { + args.push_back("--args"); + args.insert(args.end(), passthrough.begin(), passthrough.end()); + } + return args; +} + +/// Blocks until `name` is running or gone, whichever `running` asks for. +/// +/// `timeout` in seconds, or 0 to wait indefinitely. Matched on the process name +/// rather than the command line: `pgrep -f` would also match the shell running +/// the search, and report the IDE as alive forever. +void AwaitProcess(const std::string& name, bool running, int timeout) { + const std::string probe = "pgrep -x " + name + " >/dev/null 2>&1"; + for (int waited = 0; timeout == 0 || waited < timeout; waited += 2) { + if ((harness::Launch("/bin/sh", {}, {"-c", probe}) == 0) == running) { + return; + } + std::this_thread::sleep_for(std::chrono::seconds(2)); + } +} + +/// Quits the app if it is running. A profile change is read at launch, so a +/// running instance would keep the old one and look like the change failed. +void QuitBundle(const std::string& bundle) { + const std::string name = BundleName(bundle); + if (name.empty()) { + return; + } + harness::Launch("osascript", {}, {"-e", "tell application \"" + name + "\" to quit"}); + // Relaunching into a process that is still shutting down gets the old + // profile back, so give it a moment to actually go. + std::this_thread::sleep_for(std::chrono::seconds(3)); +} + +/// Sets `name` for the child, remembering what was there so it can be undone. +class ScopedEnv { + public: + ScopedEnv(std::string name, const std::string& value) : name_(std::move(name)) { + const char* previous = std::getenv(name_.c_str()); + had_previous_ = previous != nullptr; + if (had_previous_) { + previous_ = previous; + } + Set(value); + } + + ~ScopedEnv() { + if (had_previous_) { + Set(previous_); + } else { +#if defined(_WIN32) + _putenv_s(name_.c_str(), ""); +#else + unsetenv(name_.c_str()); +#endif + } + } + + ScopedEnv(const ScopedEnv&) = delete; + ScopedEnv& operator=(const ScopedEnv&) = delete; + + private: + void Set(const std::string& value) { +#if defined(_WIN32) + _putenv_s(name_.c_str(), value.c_str()); +#else + setenv(name_.c_str(), value.c_str(), 1); +#endif + } + + std::string name_; + std::string previous_; + bool had_previous_ = false; +}; + +/// Starts the translator and holds it open, printing what to point at it. +/// +/// Worth having beyond debugging: it is how anything that speaks the Anthropic +/// API but is not on the list above gets wired up, without rcli needing to know +/// that tool exists. +int Serve(const std::string& model, bool verbose) { + harness::Endpoint endpoint; + if (!harness::Resolve(model, &endpoint)) { + return 1; + } + anthropic::Shim shim; + if (!anthropic::Start(endpoint, model, &shim, verbose)) { + harness::Release(endpoint); + return 1; + } + out::Line("ANTHROPIC_BASE_URL=" + shim.base_url); + out::Line("ANTHROPIC_AUTH_TOKEN=" + shim.auth_token); + out::Status("serving " + model + "; press Ctrl-C to stop"); + // No signal handling: Ctrl-C ends the process, and the OS reclaims the port + // and the model. Anything subtler would be pretending this outlives it. + for (;;) { + std::this_thread::sleep_for(std::chrono::seconds(1)); + } +} + +/// Puts the app back on Anthropic without starting anything. +/// +/// The way out when a run was interrupted before it could undo itself, and the +/// same verb Ollama offers for the same reason. +int Restore(const Editor& editor) { + std::string failure; + if (editor.wiring == Wiring::JetBrainsProvider) { + if (!ide::RestoreProvider(*editor.jetbrains, &failure)) { + out::Error(failure); + return 1; + } + out::Status(std::string(editor.id) + " no longer points at a local model"); + return 0; + } + if (!desktop::RestoreGateway(&failure)) { + out::Error(failure); + return 1; + } + out::Status(std::string(editor.id) + " is back on Anthropic; restart it to pick that up"); + return 0; +} + +int Run(const Editor& editor, const std::string& model, + const std::vector& args, bool verbose) { + const bool is_bundle = editor.bundle[0] != '\0'; + std::string bundle; + if (is_bundle) { +#if defined(__APPLE__) + bundle = BundlePath(editor); + if (bundle.empty()) { + out::Error(std::string(editor.bundle) + " is not installed"); + return 1; + } +#else + out::Error(std::string(editor.id) + " is a macOS application"); + return 1; +#endif + } + + if (model.empty()) { + // No model named means no wiring to do, so the tool runs exactly as the + // reader has it configured. Same contract as `rcli opencode`. + return is_bundle ? harness::Launch("open", {}, OpenArgs(bundle, {}, args)) + : harness::Launch(editor.command, {}, args); + } + + harness::Endpoint endpoint; + if (!harness::Resolve(model, &endpoint, + editor.wiring == Wiring::JetBrainsProvider ? ide::kProviderPort : 0)) { + return 1; + } + + if (editor.wiring == Wiring::JetBrainsProvider) { + // No translator on this path. AI Assistant's provider speaks OpenAI, + // which is what `Resolve` already handed us, so the IDE talks to the + // model directly and nothing sits in between to get the wire format + // wrong. + // An upstream model arrives with a credential, and the IDE has no way + // to take one from us. Keep it here and hand the IDE a loopback address + // that needs none, which is the arrangement a local model already uses. + ide::Proxy proxy; + std::string reachable = endpoint.base_url; + if (!endpoint.api_key.empty()) { + if (!ide::StartProxy(endpoint, model, ide::kProviderPort, &proxy, verbose)) { + harness::Release(endpoint); + return 1; + } + reachable = proxy.base_url; + } + + std::string failure; + if (!ide::ApplyProvider(*editor.jetbrains, reachable, std::string(), model, &failure)) { + out::Error(failure); + ide::StopProxy(&proxy); + harness::Release(endpoint); + return 1; + } + out::Status(std::string(editor.id) + " will talk to " + model + " through " + reachable); + QuitBundle(bundle); + // `open -W` is wrong here. Quitting the running instance first means the + // wait can attach to the one on its way out and return while the new one + // is still starting, which pulls the endpoint out from under it. + AwaitProcess(editor.jetbrains->launcher, false, 30); + std::vector open_args{"-a", bundle}; + if (!args.empty()) { + open_args.push_back("--args"); + open_args.insert(open_args.end(), args.begin(), args.end()); + } + const int status = harness::Launch("open", {}, open_args); + if (status == 0) { + AwaitProcess(editor.jetbrains->launcher, true, 60); + out::Status("serving " + model + " until " + std::string(editor.id) + " quits"); + AwaitProcess(editor.jetbrains->launcher, false, 0); + } + // The settings stay written on the way out. Nothing in them moves + // between runs, so the configuration the reader sat through once is + // never asked for again. + ide::StopProxy(&proxy); + harness::Release(endpoint); + return status; + } + + // Claude Desktop only lists gateway models it can map to an Anthropic + // family, so the gateway answers under one of those ids while serving the + // model the reader asked for. Only the desktop app needs this; the CLI + // takes the real id happily. + const std::string advertised = + editor.wiring == Wiring::ClaudeProfile ? std::string("claude-sonnet-4-5") : model; + + anthropic::Shim shim; + if (!anthropic::Start(endpoint, model, &shim, verbose, advertised)) { + harness::Release(endpoint); + return 1; + } + out::Status(std::string(editor.id) + " will talk to " + model + " through " + + shim.base_url); + if (advertised != model) { + out::Status("advertised to the app as " + advertised + "; the picker shows " + model); + } + + int status = 0; + if (editor.wiring == Wiring::ClaudeProfile) { + // The profile, not the environment. Written before the app starts and + // taken back when it exits, so a crash here is the one case that leaves + // it applied — which is what `--restore` is for. + std::string failure; + if (!desktop::ApplyGateway(shim.base_url, shim.auth_token, advertised, model, + "RunAnywhere · " + model, &failure)) { + out::Error(failure); + anthropic::Stop(&shim); + harness::Release(endpoint); + return 1; + } + QuitBundle(bundle); + status = harness::Launch("open", {}, OpenArgs(bundle, shim, args)); + if (!desktop::RestoreGateway(&failure)) { + out::Error(failure); + } + } else if (is_bundle) { + // An app that reads the variables itself, or spawns something that + // does. Quit first: a running instance keeps the environment it was + // started with, so the agent inside it would talk to the old endpoint. + QuitBundle(bundle); + status = harness::Launch("open", {}, OpenArgs(bundle, shim, args)); + } else { + // Scoped so the reader's own environment is back before we report + // anything, and before a later call in the same process reads it. + const ScopedEnv base("ANTHROPIC_BASE_URL", shim.base_url); + const ScopedEnv token("ANTHROPIC_AUTH_TOKEN", shim.auth_token); + // An API key set in the environment outranks the token above and would + // send the session to Anthropic instead of to us. + const ScopedEnv key("ANTHROPIC_API_KEY", shim.auth_token); + status = harness::Launch(editor.command, {}, args); + } + + anthropic::Stop(&shim); + harness::Release(endpoint); + return status; +} + +} // namespace + +void RegisterEditors(CLI::App& app, Options& options) { + for (const Editor& editor : kEditors) { + auto model = std::make_shared(); + auto restore = std::make_shared(false); + auto rest = std::make_shared>(); + auto serve = std::make_shared(false); + auto* command = app.add_subcommand(editor.id, editor.summary); + command->add_option("-m,--model", *model, + "a model on this machine, or one served upstream"); + command->add_flag("--serve", *serve, + "hold the endpoint open and print it, instead of launching"); + if (editor.wiring == Wiring::ClaudeProfile || + editor.wiring == Wiring::JetBrainsProvider) { + command->add_flag("--restore", *restore, + "undo what we configured and launch nothing"); + } + command->add_option("args", *rest, "passed through")->allow_extra_args(); + command->prefix_command(); + command->callback([&options, &editor, model, rest, serve, restore] { + if (*restore) { + options.status = Restore(editor); + return; + } + options.status = *serve ? Serve(*model, options.verbose) + : Run(editor, *model, *rest, options.verbose); + }); + } +} + +} // namespace rcli::cli diff --git a/src/cli/commands.h b/src/cli/commands.h index 924b89f..89ba263 100644 --- a/src/cli/commands.h +++ b/src/cli/commands.h @@ -33,6 +33,7 @@ void RegisterBench(CLI::App& app, Options& options); void RegisterEngines(CLI::App& app, Options& options); void RegisterHarness(CLI::App& app, Options& options); void RegisterAccount(CLI::App& app, Options& options); +void RegisterEditors(CLI::App& app, Options& options); /// The interactive prompt, and the one-shot form when `prompt` is not empty. int Chat(const std::string& model, const std::string& prompt); diff --git a/src/desktop/claude_profile.cpp b/src/desktop/claude_profile.cpp new file mode 100644 index 0000000..277566d --- /dev/null +++ b/src/desktop/claude_profile.cpp @@ -0,0 +1,226 @@ +#include "desktop/claude_profile.h" + +#include +#include +#include +#include +#include + +#include + +namespace rcli::desktop { +namespace { + +using Json = nlohmann::json; +namespace fs = std::filesystem; + +/// Our profile's identity inside Claude Desktop's library. Fixed, so a second +/// run replaces the first rather than stacking entries up. +/// +/// Shaped like the app's own ids; the tail is "RunAny" in hex, which makes it +/// recognisable in a file somebody is reading by hand. +constexpr const char* kProfileID = "00000000-0000-4000-8000-52756e416e79"; + +std::string Home() { + const char* home = std::getenv("HOME"); + return home != nullptr ? home : std::string(); +} + +std::string SupportRoot(bool third_party) { + const std::string home = Home(); + if (home.empty()) { + return {}; + } + return home + "/Library/Application Support/" + (third_party ? "Claude-3p" : "Claude"); +} + +/// Reads a JSON object, treating "not there" and "empty" as an empty object. +/// +/// A malformed file is reported rather than overwritten: it is the reader's +/// Claude Desktop configuration, and silently replacing it would lose whatever +/// else they had in there. +bool ReadObject(const std::string& path, Json* out, std::string* error) { + *out = Json::object(); + std::ifstream file(path); + if (!file.good()) { + return true; + } + const std::string text((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + if (text.find_first_not_of(" \t\r\n") == std::string::npos) { + return true; + } + try { + Json parsed = Json::parse(text); + if (parsed.is_object()) { + *out = std::move(parsed); + } + return true; + } catch (const Json::exception& failure) { + if (error != nullptr) { + *error = "could not read " + path + ": " + failure.what(); + } + return false; + } +} + +bool WriteObject(const std::string& path, const Json& value, std::string* error) { + std::error_code code; + fs::create_directories(fs::path(path).parent_path(), code); + std::ofstream file(path, std::ios::trunc); + if (!file.good()) { + if (error != nullptr) { + *error = "could not write " + path; + } + return false; + } + file << value.dump(2) << "\n"; + return file.good(); +} + +bool SetDeploymentMode(const std::string& path, const std::string& mode, std::string* error) { + Json config; + if (!ReadObject(path, &config, error)) { + return false; + } + config["deploymentMode"] = mode; + return WriteObject(path, config, error); +} + +std::string ProfilePath() { return SupportRoot(true) + "/configLibrary/" + kProfileID + ".json"; } +std::string MetaPath() { return SupportRoot(true) + "/configLibrary/_meta.json"; } + +} // namespace + +std::string ProfileDirectory() { return SupportRoot(true); } + +bool ApplyGateway(const std::string& base_url, const std::string& api_key, + const std::string& advertised, const std::string& label, + const std::string& display_name, std::string* error) { + if (Home().empty()) { + if (error != nullptr) { + *error = "no home directory to write the profile into"; + } + return false; + } + + Json profile; + if (!ReadObject(ProfilePath(), &profile, error)) { + return false; + } + profile["inferenceProvider"] = "gateway"; + profile["inferenceGatewayBaseUrl"] = base_url; + profile["inferenceGatewayApiKey"] = api_key; + profile["inferenceGatewayAuthScheme"] = "bearer"; + profile["deploymentDisplayName"] = display_name; + profile["chatTabEnabled"] = true; + // Cowork reaches plugins and MCP servers over the network, and a profile + // that does not say so leaves it unable to use them. + profile["coworkEgressAllowedHosts"] = Json::array({"*"}); + profile["autoModeEnabled"] = false; + // Cowork is the surface being asked for, and the app disables surfaces it + // is not told to keep. + profile["coworkTabEnabled"] = true; + // Named rather than discovered. Discovery works when a gateway advertises + // a model the app recognises as usable; ours advertises one id and the app + // answers "Gateway returned no usable models", which is exactly the case + // its own error message says to solve by listing the model here. A plain + // id string is a valid entry, and the first entry is the default. + // `name` has to be an id the app can map onto an Anthropic family or it + // drops the entry: "expected a gateway model that maps to an Anthropic + // model". `labelOverride` is what the picker actually shows, so the row + // names the model that really answers rather than the one we route under. + profile["inferenceModels"] = + Json::array({Json{{"name", advertised}, {"labelOverride", label}}}); + // Asked for explicitly: with inferenceModels set the app would otherwise + // skip discovery, and the picker then has nothing to reconcile the served + // model against. + profile["modelDiscoveryEnabled"] = true; + if (!WriteObject(ProfilePath(), profile, error)) { + return false; + } + + Json meta; + if (!ReadObject(MetaPath(), &meta, error)) { + return false; + } + meta["appliedId"] = kProfileID; + Json entries = Json::array(); + if (meta.contains("entries") && meta["entries"].is_array()) { + for (const Json& entry : meta["entries"]) { + // Drop any previous version of ours; keep everybody else's. + if (entry.is_object() && entry.value("id", std::string()) == kProfileID) { + continue; + } + entries.push_back(entry); + } + } + entries.push_back(Json{{"id", kProfileID}, {"name", display_name}}); + meta["entries"] = std::move(entries); + if (!WriteObject(MetaPath(), meta, error)) { + return false; + } + + // Both trees: the app reads the normal one to decide which mode it is in. + return SetDeploymentMode(SupportRoot(true) + "/claude_desktop_config.json", "3p", error) && + SetDeploymentMode(SupportRoot(false) + "/claude_desktop_config.json", "3p", error); +} + +bool RestoreGateway(std::string* error) { + if (Home().empty()) { + return true; + } + if (!SetDeploymentMode(SupportRoot(false) + "/claude_desktop_config.json", "1p", error) || + !SetDeploymentMode(SupportRoot(true) + "/claude_desktop_config.json", "1p", error)) { + return false; + } + + Json meta; + if (!ReadObject(MetaPath(), &meta, error)) { + return false; + } + if (!meta.empty()) { + if (meta.value("appliedId", std::string()) == kProfileID) { + meta.erase("appliedId"); + } + if (meta.contains("entries") && meta["entries"].is_array()) { + Json entries = Json::array(); + for (const Json& entry : meta["entries"]) { + if (entry.is_object() && entry.value("id", std::string()) == kProfileID) { + continue; + } + entries.push_back(entry); + } + meta["entries"] = std::move(entries); + } + if (!WriteObject(MetaPath(), meta, error)) { + return false; + } + } + + Json profile; + if (!ReadObject(ProfilePath(), &profile, error)) { + return false; + } + if (profile.empty()) { + return true; + } + for (const char* key : + {"inferenceProvider", "inferenceGatewayBaseUrl", "inferenceGatewayApiKey", + "inferenceGatewayAuthScheme", "deploymentDisplayName", "inferenceModels", + "coworkEgressAllowedHosts", "autoModeEnabled", "coworkTabEnabled", + "modelDiscoveryEnabled"}) { + profile.erase(key); + } + return WriteObject(ProfilePath(), profile, error); +} + +bool GatewayApplied() { + Json meta; + if (!ReadObject(MetaPath(), &meta, nullptr)) { + return false; + } + return meta.value("appliedId", std::string()) == kProfileID; +} + +} // namespace rcli::desktop diff --git a/src/desktop/claude_profile.h b/src/desktop/claude_profile.h new file mode 100644 index 0000000..c140596 --- /dev/null +++ b/src/desktop/claude_profile.h @@ -0,0 +1,45 @@ +#ifndef RCLI_DESKTOP_CLAUDE_PROFILE_H +#define RCLI_DESKTOP_CLAUDE_PROFILE_H + +#include + +/// Claude Desktop's third-party inference mode. +/// +/// The app ships two deployment modes: "1p", which talks to Anthropic, and +/// "3p", which talks to a gateway you name. The 3p side keeps its own profile +/// tree beside the normal one, and none of it is reachable from Settings — +/// which is why the model picker and ANTHROPIC_BASE_URL both look like dead +/// ends. Neither is the mechanism. +/// +/// A gateway here speaks the Anthropic Messages API, which is exactly what +/// `rcli::anthropic` already serves. So pointing Claude Desktop at a model we +/// serve is a matter of writing the profile and restarting the app. +/// +/// Shape learned from ollama/ollama cmd/launch/claude_desktop.go, which drives +/// the same feature. +namespace rcli::desktop { + +/// Writes the gateway profile, marks it applied, and switches both config +/// trees to third-party mode. +/// +/// Takes effect on the app's next launch, never on a running one. Returns +/// false with `error` set. +bool ApplyGateway(const std::string& base_url, const std::string& api_key, + const std::string& advertised, const std::string& label, + const std::string& display_name, std::string* error); + +/// Puts Claude Desktop back on Anthropic and strips the keys we wrote. +/// +/// Safe to call when nothing was applied, and it only removes our own profile: +/// a gateway somebody else configured is left alone. +bool RestoreGateway(std::string* error); + +/// True when our profile is the one Claude Desktop has applied. +bool GatewayApplied(); + +/// Where the app keeps its third-party profiles, for a message worth printing. +std::string ProfileDirectory(); + +} // namespace rcli::desktop + +#endif // RCLI_DESKTOP_CLAUDE_PROFILE_H diff --git a/src/ide/jetbrains_profile.cpp b/src/ide/jetbrains_profile.cpp new file mode 100644 index 0000000..43cf484 --- /dev/null +++ b/src/ide/jetbrains_profile.cpp @@ -0,0 +1,375 @@ +#include "ide/jetbrains_profile.h" + +#include +#include +#include +#include +#include +#include + +#include "cli/output.h" +#include "harness/harness.h" + +#include + +#if defined(__APPLE__) +#include +#endif + +namespace rcli::ide { +namespace { + +namespace fs = std::filesystem; + +/// AI Assistant's marketplace id. The IDE's own `installPlugins` resolves it. +constexpr const char* kPluginID = "com.intellij.ml.llm"; +/// What the plugin unpacks to inside the configuration tree. +constexpr const char* kPluginDirectory = "ml-llm"; +/// The settings file behind `@State(name = "OpenAILikeLlmProviderSettings")`. +constexpr const char* kSettingsFile = "llm.provider.openai.like.xml"; +constexpr const char* kComponent = "OpenAILikeLlmProviderSettings"; +/// The provider selection, behind `@State(name = "LlmCustomModelsSettings")`. +constexpr const char* kModelsFile = "llm.custom.models.xml"; +constexpr const char* kModelsComponent = "LlmCustomModelsSettings"; +/// The set of providers the IDE will talk to at all. +constexpr const char* kProvidersFile = "llm.third.party.ai.providers.xml"; +constexpr const char* kProvidersComponent = "LLMThirdPartyAIProvidersSettings"; +/// `enableProvider` refuses to add anything until this has been accepted, so +/// the set above is ignored without it. Third-party providers are a beta +/// feature and this is the acknowledgement the IDE would otherwise ask for. +constexpr const char* kAcknowledgementKey = + "llm.third.party.ai.services.acknowledgement.accepted"; +/// Application properties, kept as a JSON blob inside a CDATA section. +constexpr const char* kPropertiesFile = "other.xml"; +/// `OPEN_AI_API_PROVIDER_ID`, which is also the credential's key. +constexpr const char* kProviderID = "OpenAIAPI"; +/// The subsystem the platform prefixes credentials with. +constexpr const char* kSubsystem = "AI Assistant"; +/// cpp-httplib serves HTTP/1.1 only, and the client's default is negotiated +/// upward. Left unset, the first request fails before the model is ever asked. +constexpr const char* kHttpVersion = "HTTP_1_1"; + +std::string Home() { + const char* home = std::getenv("HOME"); + return home != nullptr ? std::string(home) : std::string(); +} + +/// The credential store's service name: subsystem and key joined by an em dash, +/// which is the separator the platform writes and therefore the one it reads. +std::string ServiceName() { + return std::string("IntelliJ Platform ") + kSubsystem + " \xE2\x80\x94 " + kProviderID; +} + +bool WriteFile(const fs::path& path, const std::string& contents, std::string* error) { + std::error_code code; + fs::create_directories(path.parent_path(), code); + std::ofstream out(path, std::ios::trunc); + if (!out) { + *error = "cannot write " + path.string(); + return false; + } + out << contents; + if (!out) { + *error = "cannot write " + path.string(); + return false; + } + return true; +} + +/// The settings tree, with only the two options the state class actually +/// persists. Anything else here is dropped on the IDE's next write anyway. +std::string SettingsXML(const std::string& base_url) { + return std::string("\n \n" + + " \n\n"; +} + +/// Which model each of the IDE's three roles should use. +/// +/// One model answers all three because that is what rcli is serving. The id is +/// `/`, the separator being `ThirdPartyLLMProfileId.DELIM`. +std::string ModelsXML(const std::string& model) { + const std::string id = std::string(kProviderID) + "/" + model; + return std::string("\n \n" + + " \n\n"; +} + +/// The one provider the IDE is allowed to talk to. +/// +/// The members are nested directly, with no element naming the collection. +/// A `` wrapper — the shape most IntelliJ collections serialize to — is +/// silently dropped on load, which reads exactly like the file being ignored. +std::string ProvidersXML() { + return std::string("\n \n" + + " \n \n\n"; +} + +std::string ReadFile(const fs::path& path) { + std::ifstream in(path); + return in ? std::string(std::istreambuf_iterator(in), std::istreambuf_iterator()) + : std::string(); +} + +/// Accepts the third-party acknowledgement in the IDE's application properties. +/// +/// The properties are a JSON object inside a CDATA section inside the XML, so +/// the blob is parsed rather than pattern-matched — every other value in there +/// belongs to the reader and has to survive untouched. +bool AcceptAcknowledgement(const fs::path& path, std::string* error) { + std::string document = ReadFile(path); + constexpr const char* kOpen = ""; + const size_t open = document.find(kOpen); + if (open == std::string::npos) { + // No properties yet, which a never-launched IDE has not written. The + // component is ours to create, beside whatever else is in the file. + nlohmann::json properties; + properties["keyToString"][kAcknowledgementKey] = "true"; + const std::string component = std::string(" ") + kOpen + properties.dump(2) + kClose + "\n"; + const size_t end = document.find(""); + if (document.empty() || end == std::string::npos) { + return WriteFile(path, "\n" + component + "\n", error); + } + document.insert(end, component); + return WriteFile(path, document, error); + } + + const size_t start = open + std::string(kOpen).size(); + const size_t close = document.find(kClose, start); + if (close == std::string::npos) { + *error = "cannot read the properties in " + path.string(); + return false; + } + nlohmann::json properties = nlohmann::json::parse(document.substr(start, close - start), + nullptr, false); + if (properties.is_discarded()) { + *error = "cannot read the properties in " + path.string(); + return false; + } + properties["keyToString"][kAcknowledgementKey] = "true"; + document.replace(start, close - start, properties.dump(2)); + return WriteFile(path, document, error); +} + +#if defined(__APPLE__) +CFStringRef CopyString(const std::string& value) { + return CFStringCreateWithBytes(nullptr, reinterpret_cast(value.data()), + static_cast(value.size()), kCFStringEncodingUTF8, + false); +} + +/// The query identifying our credential, without the secret in it. +CFMutableDictionaryRef CopyQuery() { + CFMutableDictionaryRef query = CFDictionaryCreateMutable( + nullptr, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword); + CFStringRef service = CopyString(ServiceName()); + CFStringRef account = CopyString(kProviderID); + CFDictionarySetValue(query, kSecAttrService, service); + CFDictionarySetValue(query, kSecAttrAccount, account); + CFRelease(service); + CFRelease(account); + return query; +} + +/// An access list naming the two programs allowed to read the item without +/// asking: this one, which writes it, and the IDE, which reads it. +/// +/// Without this the item belongs to rcli alone, and the IDE's first read pops a +/// keychain dialog — the one manual step this command exists to remove. The +/// legacy access APIs are what create such a list, and they are also what the +/// platform's own `MacOSKeychainStorage` uses, so the item ends up the shape +/// the IDE already expects. +CFTypeRef CopyAccess(const std::string& reader) { + SecTrustedApplicationRef self = nullptr; + SecTrustedApplicationRef other = nullptr; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + if (SecTrustedApplicationCreateFromPath(nullptr, &self) != errSecSuccess || + SecTrustedApplicationCreateFromPath(reader.c_str(), &other) != errSecSuccess) { + if (self != nullptr) { + CFRelease(self); + } + if (other != nullptr) { + CFRelease(other); + } + return nullptr; + } + const void* trusted[] = {self, other}; + CFArrayRef list = CFArrayCreate(nullptr, trusted, 2, &kCFTypeArrayCallBacks); + CFStringRef label = CopyString(ServiceName()); + SecAccessRef access = nullptr; + const OSStatus status = SecAccessCreate(label, list, &access); +#pragma clang diagnostic pop + CFRelease(label); + CFRelease(list); + CFRelease(self); + CFRelease(other); + return status == errSecSuccess ? access : nullptr; +} + +/// Stores `secret`, replacing whatever was there, readable by `reader` without +/// a prompt. +/// +/// Written through the Security framework rather than the `security` tool +/// because that one takes the secret as an argument, where every other process +/// on the machine can read it out of the process list. +bool StoreSecret(const std::string& secret, const std::string& reader, std::string* error) { + CFMutableDictionaryRef query = CopyQuery(); + SecItemDelete(query); + CFDataRef data = CFDataCreate(nullptr, reinterpret_cast(secret.data()), + static_cast(secret.size())); + CFDictionarySetValue(query, kSecValueData, data); + CFTypeRef access = CopyAccess(reader); + if (access != nullptr) { + CFDictionarySetValue(query, kSecAttrAccess, access); + } + const OSStatus status = SecItemAdd(query, nullptr); + if (access != nullptr) { + CFRelease(access); + } + CFRelease(data); + CFRelease(query); + if (status != errSecSuccess) { + *error = "cannot store the key in the keychain (OSStatus " + std::to_string(status) + ")"; + return false; + } + return true; +} + +void DropSecret() { + CFMutableDictionaryRef query = CopyQuery(); + SecItemDelete(query); + CFRelease(query); +} +#else +bool StoreSecret(const std::string&, const std::string&, std::string* error) { + *error = "the JetBrains credential store is only wired up on macOS"; + return false; +} + +void DropSecret() {} +#endif + +/// Installs AI Assistant through the IDE's own command line. +/// +/// The IDE is the only thing that knows which build of the plugin matches it, +/// so asking it beats resolving a download ourselves. It also creates the +/// configuration directory on the way, which a never-launched IDE has not. +bool InstallPlugin(const Product& product, const std::string& bundle, std::string* error) { + const std::string launcher = bundle + "/Contents/MacOS/" + product.launcher; + out::Status("installing JetBrains AI Assistant; this happens once and takes a minute"); + if (harness::Launch(launcher, {}, {"installPlugins", kPluginID}) != 0) { + *error = "could not install AI Assistant into " + std::string(product.id); + return false; + } + return true; +} + +bool PluginInstalled(const std::string& config) { + std::error_code code; + return !config.empty() && fs::exists(fs::path(config) / "plugins" / kPluginDirectory, code); +} + +} // namespace + +std::string BundlePath(const Product& product) { + const std::string home = Home(); + std::vector roots{"/Applications/"}; + if (!home.empty()) { + roots.push_back(home + "/Applications/"); + } + std::error_code code; + for (const std::string& root : roots) { + const std::string path = root + product.bundle; + if (fs::exists(fs::path(path) / "Contents" / "Info.plist", code)) { + return path; + } + } + return {}; +} + +std::string ConfigDirectory(const Product& product) { + const std::string home = Home(); + if (home.empty()) { + return {}; + } + const fs::path root = fs::path(home) / "Library" / "Application Support" / "JetBrains"; + std::error_code code; + // One tree per release, so the newest name wins. Sorting the names works + // because JetBrains pads the version the same way in every one of them. + std::string newest; + for (const fs::directory_entry& entry : fs::directory_iterator(root, code)) { + const std::string name = entry.path().filename().string(); + if (name.rfind(product.config_prefix, 0) == 0 && name > newest) { + newest = name; + } + } + return newest.empty() ? std::string() : (root / newest).string(); +} + +bool ApplyProvider(const Product& product, const std::string& base_url, + const std::string& api_key, const std::string& model, + std::string* error) { + const std::string bundle = BundlePath(product); + if (bundle.empty()) { + *error = std::string(product.bundle) + " is not installed"; + return false; + } + + std::string config = ConfigDirectory(product); + if (!PluginInstalled(config)) { + if (!InstallPlugin(product, bundle, error)) { + return false; + } + // The install is what creates the tree on an IDE nobody has launched. + config = ConfigDirectory(product); + if (!PluginInstalled(config)) { + *error = "AI Assistant did not appear in " + + (config.empty() ? std::string("the configuration directory") : config); + return false; + } + } + + const fs::path options = fs::path(config) / "options"; + // The model ids are deliberately not written. The IDE picks them up from + // the provider once it can reach it, and deletes any file we leave behind. + (void)model; + if (!WriteFile(options / kSettingsFile, SettingsXML(base_url), error) || + !WriteFile(options / kProvidersFile, ProvidersXML(), error) || + !AcceptAcknowledgement(options / kPropertiesFile, error)) { + return false; + } + // A local server needs no key, and the IDE is content without one — it + // reports the provider configured with an empty key and talks to it anyway. + // So nothing is put in the reader's keychain unless there is a real secret + // to put there, which is the upstream case. + if (api_key.empty()) { + DropSecret(); + return true; + } + return StoreSecret(api_key, bundle + "/Contents/MacOS/" + product.launcher, error); +} + +bool RestoreProvider(const Product& product, std::string* error) { + const std::string config = ConfigDirectory(product); + if (config.empty()) { + *error = std::string(product.id) + " has no configuration directory to clear"; + return false; + } + DropSecret(); + std::error_code code; + fs::remove(fs::path(config) / "options" / kSettingsFile, code); + fs::remove(fs::path(config) / "options" / kModelsFile, code); + fs::remove(fs::path(config) / "options" / kProvidersFile, code); + return true; +} + +} // namespace rcli::ide diff --git a/src/ide/jetbrains_profile.h b/src/ide/jetbrains_profile.h new file mode 100644 index 0000000..0fb3dc8 --- /dev/null +++ b/src/ide/jetbrains_profile.h @@ -0,0 +1,73 @@ +#ifndef RCLI_IDE_JETBRAINS_PROFILE_H +#define RCLI_IDE_JETBRAINS_PROFILE_H + +#include + +/// AI Assistant's OpenAI-compatible provider, configured from outside the IDE. +/// +/// A JetBrains IDE already ships an agent — AI Assistant, and Junie behind it — +/// so pointing one at a local model is a matter of telling that agent where the +/// model lives, not of nesting a second agent inside the editor. The provider +/// it exposes for this speaks plain OpenAI, which is the shape `rac_server` +/// already serves, so nothing has to be translated on the way. +/// +/// Three things have to be written before the IDE starts, and it reads all of +/// them once at launch: a base URL in its own options tree, a key in the +/// platform credential store, and the provider selection that the Providers & +/// API keys page shows as a dropdown. The first two alone leave that dropdown +/// on "None" and the IDE reporting `byok=null`, which is what makes this look +/// like it worked when it has not. +/// +/// The selection is stored as model ids rather than as a provider name: each is +/// `/`, and picking a provider is setting those three ids +/// and the enable flag beside them. +/// +/// None of this needs a JetBrains AI subscription. BYOK is the supported path. +namespace rcli::ide { + +/// The port the local server is asked for when serving a JetBrains IDE. +/// +/// Fixed on purpose. The IDE reads its base URL once at startup, from a file +/// written before it launches, so a port that moved between runs would leave +/// that file naming something dead every time rcli exited first. Asking for the +/// same one keeps the configuration true, and a second rcli holding it only +/// costs this run a rewrite. +constexpr int kProviderPort = 11636; + +/// A JetBrains IDE, named the way the reader types it. +struct Product { + /// The subcommand: `clion`. + const char* id; + /// The application bundle to look for under /Applications. + const char* bundle; + /// The launcher inside the bundle, which doubles as the IDE's own CLI. + const char* launcher; + /// What its per-version configuration directory is called, before the + /// version. JetBrains keeps one tree per release, so the newest wins. + const char* config_prefix; +}; + +/// Installs AI Assistant if it is absent, points its provider at `base_url`, +/// and stores `api_key` where the IDE looks for it. +/// +/// The install is the slow part and happens once; every later run finds the +/// plugin already there and only rewrites the URL. Returns false with `error` +/// set. Takes effect on the IDE's next launch, never on a running one. +bool ApplyProvider(const Product& product, const std::string& base_url, + const std::string& api_key, const std::string& model, + std::string* error); + +/// Clears the base URL and drops the stored key, leaving the plugin installed. +/// +/// The way out when a run left the IDE pointing at a port nothing is serving. +bool RestoreProvider(const Product& product, std::string* error); + +/// The IDE's configuration directory, or empty when it has never been run. +std::string ConfigDirectory(const Product& product); + +/// The application bundle's path, or empty when the IDE is not installed. +std::string BundlePath(const Product& product); + +} // namespace rcli::ide + +#endif // RCLI_IDE_JETBRAINS_PROFILE_H diff --git a/src/ide/openai_proxy.cpp b/src/ide/openai_proxy.cpp new file mode 100644 index 0000000..6a7e840 --- /dev/null +++ b/src/ide/openai_proxy.cpp @@ -0,0 +1,456 @@ +#include "ide/openai_proxy.h" + +#include +#include + +#include +#include +#include + +#include +#include + +#include "account/console.h" +#include "account/credentials.h" +#include "cli/output.h" + +namespace rcli::ide { +namespace { + +/// Splits `http://host:port/v1` into `http://host:port` and `/v1`. +bool SplitBaseURL(const std::string& base_url, std::string* origin, std::string* prefix) { + const size_t scheme = base_url.find("://"); + if (scheme == std::string::npos) { + return false; + } + const size_t slash = base_url.find('/', scheme + 3); + if (slash == std::string::npos) { + *origin = base_url; + *prefix = ""; + } else { + *origin = base_url.substr(0, slash); + *prefix = base_url.substr(slash); + } + return !origin->empty(); +} + +struct Runtime { + httplib::Server server; + std::thread thread; + std::string origin; + std::string prefix; + std::string api_key; + std::string model; + bool verbose = false; +}; + +std::unique_ptr g_runtime; + +void Trace(const Runtime& runtime, const std::string& note); + +/// Whether a refusal is about the credential rather than the request. +/// +/// The console words this more than one way — "access token expired" when it +/// lapses, "not authenticated" when it is rejected outright — so matching a +/// single phrase catches only half the cases, and the half it misses ends the +/// session. +bool LooksLikeAuthFailure(const std::string& body) { + std::string lowered; + lowered.reserve(body.size()); + for (const char c : body) { + lowered.push_back(static_cast(std::tolower(static_cast(c)))); + } + return lowered.find("expired") != std::string::npos || + lowered.find("authenticat") != std::string::npos || + lowered.find("unauthorized") != std::string::npos || + lowered.find("invalid_token") != std::string::npos || + lowered.find("401") != std::string::npos; +} + +/// Trades the stored refresh token for a new access token. +/// +/// The one held at startup is a snapshot, and an editor session outlives it. +/// Without this the whole run dies on `access token expired` partway through, +/// with nothing but a 401 to explain itself. +bool RenewToken(Runtime& runtime) { + account::Credentials credentials = account::Load(); + if (credentials.refresh_token.empty()) { + return false; + } + account::Grant grant; + std::string error; + if (!account::Refresh(credentials.console_url, credentials.refresh_token, &grant, &error)) { + Trace(runtime, "REFRESH-FAILED " + error); + return false; + } + credentials.access_token = grant.access_token; + if (!grant.refresh_token.empty()) { + credentials.refresh_token = grant.refresh_token; + } + std::string ignored; + account::Save(credentials, &ignored); + runtime.api_key = grant.access_token; + Trace(runtime, "REFRESHED"); + return true; +} + +httplib::Client Upstream(const Runtime& runtime) { + httplib::Client client(runtime.origin); + client.set_read_timeout(600, 0); + if (!runtime.api_key.empty()) { + client.set_bearer_token_auth(runtime.api_key); + } + return client; +} + +/// Records what crossed the proxy, for when the editor and a hand-made request +/// disagree about what the model was asked. +void Trace(const Runtime&, const std::string& note) { + std::ofstream log("/tmp/rcli-proxy.log", std::ios::app); + log << note << "\n"; +} + +using Json = nlohmann::json; + +/// A chunk carrying `message` as the whole answer. +/// +/// Built by hand rather than forwarded, for the case where the upstream said +/// something the editor cannot read. +std::string ChunkSaying(const std::string& message) { + Json chunk; + chunk["id"] = "chatcmpl-rcli"; + chunk["object"] = "chat.completion.chunk"; + chunk["created"] = 0; + chunk["model"] = "rcli"; + Json choice; + choice["index"] = 0; + choice["delta"] = Json{{"role", "assistant"}, {"content", message}}; + choice["finish_reason"] = "stop"; + chunk["choices"] = Json::array({choice}); + return "data: " + chunk.dump() + "\n\n"; +} + +/// Numbers the tool calls in a streamed delta, and says whether it had to. +/// +/// Each tool call in a stream carries an `index` saying which call the fragment +/// belongs to, because arguments arrive split across frames. Gemini's +/// OpenAI-compatible layer leaves it out, and a strict client refuses the whole +/// frame — so a model that answers by calling a tool fails where the same model +/// answering in prose succeeds. The position in the array is the index it +/// should have had. +bool NumberToolCalls(Json& chunk) { + bool changed = false; + if (!chunk["choices"].is_array()) { + return false; + } + for (Json& choice : chunk["choices"]) { + if (!choice.is_object() || !choice.contains("delta") || !choice["delta"].is_object()) { + continue; + } + Json& delta = choice["delta"]; + if (!delta.contains("tool_calls") || !delta["tool_calls"].is_array()) { + continue; + } + size_t position = 0; + for (Json& call : delta["tool_calls"]) { + if (call.is_object() && !call.contains("index")) { + call["index"] = position; + changed = true; + } + ++position; + } + } + return changed; +} + +/// Passes an event through, rewriting the ones the editor would choke on. +/// +/// An upstream error arrives inside the stream, correctly framed, as an object +/// with an `error` member and no `choices`. The editor deserialises every frame +/// into one fixed shape and rejects anything missing its required fields, so +/// that frame surfaces as a deserialiser complaint and the actual message — +/// which is the thing worth reading — never reaches anybody. Turning it into an +/// ordinary chunk puts it in the chat instead. +std::string Normalise(const std::string& frame) { + const size_t field = frame.find("data:"); + if (field == std::string::npos) { + return frame + "\n\n"; + } + std::string payload = frame.substr(field + 5); + while (!payload.empty() && (payload.front() == ' ' || payload.front() == '\r')) { + payload.erase(payload.begin()); + } + if (payload == "[DONE]") { + return frame + "\n\n"; + } + Json parsed = Json::parse(payload, nullptr, false); + if (parsed.is_discarded() || !parsed.is_object()) { + return frame + "\n\n"; + } + if (parsed.contains("choices")) { + return NumberToolCalls(parsed) ? "data: " + parsed.dump() + "\n\n" : frame + "\n\n"; + } + if (!parsed.contains("error")) { + return frame + "\n\n"; + } + const Json& error = parsed["error"]; + std::string message = error.is_object() && error.contains("message") && + error["message"].is_string() + ? error["message"].get() + : error.dump(); + { + std::ofstream log("/tmp/rcli-proxy.log", std::ios::app); + log << "UPSTREAM-ERROR-FRAME " << message << "\n"; + } + return ChunkSaying(message); +} + +/// Points a request at the model rcli is serving, whatever it named. +/// +/// A stale selection saved in the editor's own settings outlives any change to +/// the list we advertise, so the name in the request cannot be trusted even +/// when only one is on offer. +std::string Retarget(const Runtime& runtime, const std::string& body) { + Json request = Json::parse(body, nullptr, false); + if (request.is_discarded() || !request.is_object()) { + return body; + } + request["model"] = runtime.model; + return request.dump(); +} + +void Fail(httplib::Response& response, int status, const std::string& message) { + response.status = status; + response.set_content("{\"error\":{\"message\":\"" + message + "\"}}", "application/json"); +} + +/// Forwards a streaming completion, byte for byte. +/// +/// Nothing is parsed. Both ends speak the same wire format, so reframing SSE +/// here would only add a place for it to go wrong — and did, the first time. +void Stream(Runtime& runtime, const std::string& body, httplib::Response& response) { + auto request = std::make_shared(body); + auto origin = std::make_shared(runtime.origin); + auto path = std::make_shared(runtime.prefix + "/chat/completions"); + auto api_key = std::make_shared(runtime.api_key); + + auto verbose = std::make_shared(runtime.verbose); + Runtime* owner = &runtime; + Trace(runtime, "REQUEST " + body); + + response.set_chunked_content_provider( + "text/event-stream", + [request, origin, path, api_key, verbose, owner](size_t, httplib::DataSink& sink) { + // Two attempts at most: the second only after a token the console + // has just renewed. Nothing reaches the sink until an event stream + // is recognised, so a retry cannot duplicate output. + for (int attempt = 0; attempt < 2; ++attempt) { + httplib::Client client(*origin); + client.set_read_timeout(600, 0); + const std::string token = attempt == 0 ? *api_key : owner->api_key; + if (!token.empty()) { + client.set_bearer_token_auth(token); + } + + // An upstream that refuses the request answers with a JSON error and + // no SSE framing at all. Forwarding those bytes as if they were + // events puts an object into the stream that carries none of the + // fields a chunk must have, and the editor blames the stream rather + // than the refusal. So the body is held until the status is known. + // An upstream that refuses the request answers with a JSON error + // and no SSE framing at all. Forwarding those bytes as events puts + // an object into the stream carrying none of the fields a chunk + // must have, and the editor then blames the stream rather than the + // refusal. So the opening bytes are held back until they identify + // themselves: an event stream starts with a `data:` field, and an + // error does not. + bool streaming = false; + bool decided = false; + std::string head; + // Whole events only: a chunk can split one in half, and half an + // event cannot be judged. + std::string pending; + const auto forward = [&sink, &pending]() { + size_t split = 0; + while ((split = pending.find("\n\n")) != std::string::npos) { + const std::string frame = pending.substr(0, split); + pending.erase(0, split + 2); + const std::string out = Normalise(frame); + if (!sink.write(out.data(), out.size())) { + return false; + } + } + return true; + }; + const httplib::Result reply = + client.Post(*path, httplib::Headers(), *request, "application/json", + [&](const char* data, size_t length) { + if (decided) { + if (!streaming) { + head.append(data, length); + return true; + } + pending.append(data, length); + return forward(); + } + head.append(data, length); + const size_t start = head.find_first_not_of(" \r\n"); + if (start == std::string::npos) { + return true; + } + if (head.compare(start, 5, "data:") == 0) { + streaming = true; + decided = true; + pending.append(head); + return forward(); + } + // Enough to know it is not an event stream. + if (head.size() - start >= 5) { + decided = true; + } + return true; + }); + + if (streaming) { + sink.done(); + return true; + } + + if (attempt == 0 && reply && LooksLikeAuthFailure(head) && RenewToken(*owner)) { + head.clear(); + pending.clear(); + decided = false; + continue; + } + + { + const std::string detail = + !reply ? std::string("the model endpoint did not answer") : head; + { + std::ofstream log("/tmp/rcli-proxy.log", std::ios::app); + log << "UPSTREAM-REFUSED " << detail << "\n"; + } + // Carried as an ordinary chunk, not as an `error` object. The + // editor deserialises every frame into one fixed shape and + // rejects anything without its required fields, so an error + // object here fails to parse and the reader is shown a + // deserialiser complaint instead of what actually went wrong. + std::string message = detail; + for (char& c : message) { + if (c == '"' || c == '\\' || c == '\n' || c == '\r' || c == '\t') { + c = ' '; + } + } + const std::string frame = + "data: {\"id\":\"chatcmpl-rcli\",\"object\":\"chat.completion.chunk\"," + "\"created\":0,\"model\":\"rcli\",\"choices\":[{\"index\":0,\"delta\":" + "{\"role\":\"assistant\",\"content\":\"" + message + + "\"},\"finish_reason\":\"stop\"}]}\n\n"; + sink.write(frame.data(), frame.size()); + sink.write("data: [DONE]\n\n", 14); + } + sink.done(); + return true; + } + sink.done(); + return true; + }); +} + +} // namespace + +bool StartProxy(const harness::Endpoint& endpoint, const std::string& model, int port, + Proxy* proxy, bool verbose) { + if (proxy == nullptr) { + return false; + } + StopProxy(proxy); + + auto runtime = std::make_unique(); + if (!SplitBaseURL(endpoint.base_url, &runtime->origin, &runtime->prefix)) { + out::Error("cannot make sense of the endpoint " + endpoint.base_url); + return false; + } + runtime->api_key = endpoint.api_key; + runtime->model = model; + runtime->verbose = verbose; + + Runtime* raw = runtime.get(); + // Every handler catches. An exception thrown into cpp-httplib takes the + // process down with it, and a dead rcli takes the model with it too. + raw->server.Get("/v1/models", [raw](const httplib::Request&, httplib::Response& response) { + try { + // Not forwarded. The one model rcli was asked to serve is the one + // offered, so there is nothing in the picker that cannot answer. + Json entry; + entry["id"] = raw->model; + entry["object"] = "model"; + entry["owned_by"] = "runanywhere"; + Json list; + list["object"] = "list"; + list["data"] = Json::array({entry}); + response.set_content(list.dump(), "application/json"); + } catch (const std::exception& error) { + Fail(response, 500, error.what()); + } + }); + + raw->server.Post("/v1/chat/completions", + [raw](const httplib::Request& request, httplib::Response& response) { + try { + const std::string body = Retarget(*raw, request.body); + // The editor decides whether to stream; we only + // have to keep the answer in the shape it asked for. + if (body.find("\"stream\":true") != std::string::npos || + body.find("\"stream\": true") != std::string::npos) { + Stream(*raw, body, response); + return; + } + httplib::Client client = Upstream(*raw); + const httplib::Result reply = client.Post( + raw->prefix + "/chat/completions", body, "application/json"); + if (!reply) { + Fail(response, 502, "the model endpoint did not answer"); + return; + } + response.status = reply->status; + response.set_content(reply->body, "application/json"); + } catch (const std::exception& error) { + Fail(response, 500, error.what()); + } + }); + + // The usual port, or any free one when a second editor already holds it. + // The address is written into that editor's settings either way, so the two + // do not have to agree on a number. + int bound = port; + if (!raw->server.bind_to_port("127.0.0.1", port)) { + bound = raw->server.bind_to_any_port("127.0.0.1"); + if (bound <= 0) { + out::Error("could not find a port to serve " + model + " on"); + return false; + } + } + runtime->thread = std::thread([raw] { raw->server.listen_after_bind(); }); + g_runtime = std::move(runtime); + + proxy->running = true; + proxy->base_url = "http://127.0.0.1:" + std::to_string(bound) + "/v1"; + return true; +} + +void StopProxy(Proxy* proxy) { + if (g_runtime) { + g_runtime->server.stop(); + if (g_runtime->thread.joinable()) { + g_runtime->thread.join(); + } + g_runtime.reset(); + } + if (proxy != nullptr) { + proxy->running = false; + proxy->base_url.clear(); + } +} + +} // namespace rcli::ide diff --git a/src/ide/openai_proxy.h b/src/ide/openai_proxy.h new file mode 100644 index 0000000..725fd37 --- /dev/null +++ b/src/ide/openai_proxy.h @@ -0,0 +1,47 @@ +#ifndef RCLI_IDE_OPENAI_PROXY_H +#define RCLI_IDE_OPENAI_PROXY_H + +#include + +#include "harness/harness.h" + +/// A loopback endpoint that carries the credential so the editor does not have +/// to. +/// +/// An upstream model is reached through the signed-in console, which wants a +/// bearer token on every request. Handing that token to the IDE means putting +/// it in the IDE's credential store, and the IDE never reads what we write +/// there — the provider comes up with an empty key and the console answers 401. +/// +/// So the token stays here. rcli listens on loopback, adds the header, and +/// forwards. The IDE is configured exactly as it is for a local model, with no +/// key at all, which is the case already known to work. It also keeps the +/// reader's token out of a second store that neither of us controls. +namespace rcli::ide { + +struct Proxy { + bool running = false; + /// What to point the editor at. An OpenAI-compatible root ending in `/v1`. + std::string base_url; +}; + +/// Listens on `port` and forwards to `endpoint`, adding its credential. +/// +/// `model` is the only model offered, and every request is answered by it +/// whatever it asked for. The console lists its provider's whole catalogue, +/// deprecated entries included, and an editor showing all of them invites a +/// choice that fails — which is how `models/gemini-2.5-pro`, retired for new +/// users, ended up being asked a question. rcli was told which model to serve; +/// that is the one the editor gets. +/// +/// Returns false having already said why. Nothing else is translated on the way +/// through: both sides speak OpenAI, so the bytes pass as they arrive. +bool StartProxy(const harness::Endpoint& endpoint, const std::string& model, int port, + Proxy* proxy, bool verbose); + +/// Stops the listener. Safe on a proxy that never started. +void StopProxy(Proxy* proxy); + +} // namespace rcli::ide + +#endif // RCLI_IDE_OPENAI_PROXY_H From c77a231031326fbe822cccd4d7869acc2f926477 Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:20:48 +0530 Subject: [PATCH 11/16] feat(anthropic): carry tool calls through the shim and surface in-stream errors --- src/anthropic/messages.cpp | 10 + src/anthropic/translate.cpp | 394 ++++++++++++++++++++++++++++++++++-- src/anthropic/translate.h | 31 +++ 3 files changed, 414 insertions(+), 21 deletions(-) diff --git a/src/anthropic/messages.cpp b/src/anthropic/messages.cpp index f4da948..5f14cf0 100644 --- a/src/anthropic/messages.cpp +++ b/src/anthropic/messages.cpp @@ -78,6 +78,13 @@ void HandleNonStreaming(Runtime& runtime, const Json& request, httplib::Response response.set_content(translate::ErrorBody("api_error", error.what()), "application/json"); return; } + std::string failure_type; + std::string failure; + if (translate::PayloadError(parsed, &failure_type, &failure)) { + response.status = failure_type == "rate_limit_error" ? 429 : 502; + response.set_content(translate::ErrorBody(failure_type, failure), "application/json"); + return; + } response.set_content(translate::ResponseToAnthropic(parsed, runtime.model).dump(), "application/json"); } @@ -212,6 +219,9 @@ bool Start(const harness::Endpoint& upstream, const std::string& model, Shim* sh } catch (const std::exception& error) { // httplib does not catch, and an exception leaving here reaches // std::terminate: the editor's model call would abort rcli. + if (raw->verbose) { + out::Status(std::string("anthropic: request failed: ") + error.what()); + } response.status = 500; response.set_content(translate::ErrorBody("api_error", error.what()), "application/json"); diff --git a/src/anthropic/translate.cpp b/src/anthropic/translate.cpp index df61933..4a28e73 100644 --- a/src/anthropic/translate.cpp +++ b/src/anthropic/translate.cpp @@ -6,6 +6,30 @@ namespace rcli::anthropic::translate { namespace { +/// A string field, or empty when it is absent or is something else. +/// +/// `Json::value` throws when the key is there holding another type, and a +/// translator that throws on one unexpected field fails the whole request. A +/// client sending a shape we did not anticipate should lose that field, not its +/// turn. +std::string Field(const Json& object, const char* key) { + if (!object.is_object()) { + return {}; + } + const auto found = object.find(key); + return found != object.end() && found->is_string() ? found->get() + : std::string(); +} + +/// A token count, or `fallback` when the field is absent or is not a number. +int Count(const Json& object, const char* key, int fallback = 0) { + if (!object.is_object()) { + return fallback; + } + const auto found = object.find(key); + return found != object.end() && found->is_number_integer() ? found->get() : fallback; +} + /// Anthropic lets content be a bare string or a list of typed blocks. Both mean /// the same thing to an OpenAI endpoint, which only takes a string. std::string FlattenContent(const Json& content) { @@ -23,13 +47,151 @@ std::string FlattenContent(const Json& content) { // Only text survives the trip. An image block would have to become an // OpenAI image_url part, and the local server does not serve vision at // all, so dropping it is honest where inventing a shape is not. - if (block.value("type", std::string()) == "text") { - text += block.value("text", std::string()); + if (Field(block, "type") == "text") { + text += Field(block, "text"); } } return text; } +/// OpenAI carries a call's arguments as a JSON string; Anthropic wants the +/// object itself. A model that emits something unparseable here is common +/// enough that dropping the whole turn over it would be worse than a call with +/// no arguments, which the tool can at least reject on its own terms. +Json ParseArguments(const std::string& arguments) { + if (arguments.empty()) { + return Json::object(); + } + try { + const Json parsed = Json::parse(arguments); + if (parsed.is_object()) { + return parsed; + } + } catch (const Json::exception&) { + } + return Json::object(); +} + +/// Anthropic tool definitions, in OpenAI's shape. +/// +/// Only client tools carry an `input_schema`. Anthropic's server-side tools — +/// web search and the rest — name a type we have nothing to run and have no +/// schema to forward, so they are left out rather than passed on as something +/// the endpoint would have to invent a meaning for. +Json ToolsToOpenAI(const Json& tools) { + Json out = Json::array(); + if (!tools.is_array()) { + return out; + } + for (const Json& tool : tools) { + if (!tool.is_object() || !tool.contains("input_schema")) { + continue; + } + Json function{{"name", Field(tool, "name")}, + {"parameters", tool["input_schema"]}}; + if (tool.contains("description") && tool["description"].is_string()) { + function["description"] = tool["description"]; + } + out.push_back(Json{{"type", "function"}, {"function", std::move(function)}}); + } + return out; +} + +/// Anthropic's tool_choice, in OpenAI's vocabulary. Null when it says something +/// OpenAI has no way to express. +Json ToolChoiceToOpenAI(const Json& choice) { + if (choice.is_string()) { + return choice; + } + if (!choice.is_object()) { + return {}; + } + const std::string type = Field(choice, "type"); + if (type == "auto" || type == "none") { + return type; + } + // "any" means the model has to call something, without saying what. + if (type == "any") { + return "required"; + } + if (type == "tool") { + return Json{{"type", "function"}, + {"function", Json{{"name", Field(choice, "name")}}}}; + } + return {}; +} + +/// Appends `message` to `out` as the OpenAI messages it implies. +/// +/// One Anthropic turn can become several. Anthropic packs the results of a +/// round of tool calls into the user turn that follows them, while OpenAI wants +/// each result as its own `tool` message sitting directly after the assistant +/// turn that asked for it — so the results are written first, and whatever text +/// shared that turn follows as a message of its own. +void AppendMessage(const Json& message, Json* out) { + std::string role = Field(message, "role"); + if (role.empty()) { + role = "user"; + } + const Json content = message.contains("content") ? message["content"] : Json(); + + if (!content.is_array()) { + out->push_back(Json{{"role", role}, {"content", FlattenContent(content)}}); + return; + } + + for (const Json& block : content) { + if (!block.is_object() || Field(block, "type") != "tool_result") { + continue; + } + out->push_back(Json{ + {"role", "tool"}, + {"tool_call_id", Field(block, "tool_use_id")}, + {"content", FlattenContent(block.contains("content") ? block["content"] : Json())}}); + } + + Json calls = Json::array(); + for (const Json& block : content) { + if (!block.is_object() || Field(block, "type") != "tool_use") { + continue; + } + const Json input = block.contains("input") ? block["input"] : Json::object(); + calls.push_back(Json{{"id", Field(block, "id")}, + {"type", "function"}, + {"function", Json{{"name", Field(block, "name")}, + {"arguments", input.dump()}}}}); + } + + const std::string text = FlattenContent(content); + if (!calls.empty()) { + // An assistant turn that only called tools has no text to carry, and + // OpenAI reads a null content there rather than an empty string. + out->push_back(Json{{"role", role}, + {"content", text.empty() ? Json(nullptr) : Json(text)}, + {"tool_calls", std::move(calls)}}); + return; + } + if (!text.empty()) { + out->push_back(Json{{"role", role}, {"content", text}}); + } +} + +/// The reason a turn carrying `calls` tool calls ended, given what the endpoint +/// said in `finish`. +/// +/// Endpoints disagree here: one closes a turn holding a tool call with +/// "tool_calls", another with a plain "stop". The second reads as end_turn, +/// which tells the client to show the answer and wait for the reader rather +/// than run the tool — so the call is written, ignored, and the agent narrates +/// what it was about to do instead of doing it. Truncation is the one thing +/// that still outranks it, because a call cut off mid-argument cannot be run. +std::string StopWithTools(const std::string& finish, bool calls) { + if (!calls || finish == "max_tokens") { + return finish; + } + return "tool_use"; +} + /// OpenAI finish reasons, in Anthropic's vocabulary. std::string StopReason(const std::string& finish) { if (finish == "length") { @@ -53,7 +215,8 @@ std::string Event(const std::string& name, const Json& data) { Json RequestToOpenAI(const Json& anthropic, const std::string& model) { Json openai; openai["model"] = model; - openai["stream"] = anthropic.value("stream", false); + const auto stream = anthropic.find("stream"); + openai["stream"] = stream != anthropic.end() && stream->is_boolean() && stream->get(); Json messages = Json::array(); // Anthropic carries the system prompt beside the conversation; OpenAI wants @@ -69,8 +232,7 @@ Json RequestToOpenAI(const Json& anthropic, const std::string& model) { if (!message.is_object()) { continue; } - messages.push_back({{"role", message.value("role", std::string("user"))}, - {"content", FlattenContent(message.value("content", Json()))}}); + AppendMessage(message, &messages); } } openai["messages"] = std::move(messages); @@ -80,6 +242,29 @@ Json RequestToOpenAI(const Json& anthropic, const std::string& model) { if (anthropic.contains("max_tokens")) { openai["max_tokens"] = anthropic["max_tokens"]; } + if (anthropic.contains("tools")) { + Json tools = ToolsToOpenAI(anthropic["tools"]); + // An empty list is not the same as none: OpenAI rejects `tools: []`, + // and a request whose only tools were server-side ones has nothing left + // to send. + if (!tools.empty()) { + openai["tools"] = std::move(tools); + if (anthropic.contains("tool_choice")) { + const Json& asked = anthropic["tool_choice"]; + const Json choice = ToolChoiceToOpenAI(asked); + if (!choice.is_null()) { + openai["tool_choice"] = choice; + } + const auto serial = asked.is_object() + ? asked.find("disable_parallel_tool_use") + : asked.end(); + if (serial != asked.end() && serial->is_boolean() && serial->get()) { + openai["parallel_tool_calls"] = false; + } + } + } + } + for (const char* passthrough : {"temperature", "top_p", "stop_sequences"}) { if (anthropic.contains(passthrough)) { // stop_sequences is OpenAI's `stop`; the rest keep their names. @@ -97,22 +282,63 @@ Json ResponseToAnthropic(const Json& openai, const std::string& model) { !openai["choices"].empty()) { choice = openai["choices"][0]; } - const Json message = choice.value("message", Json::object()); - const std::string text = message.value("content", std::string()); + const Json message = choice.contains("message") && choice["message"].is_object() + ? choice["message"] + : Json::object(); + // Null, not absent, is what OpenAI sends for the content of a turn that only + // called tools, and asking for it as a string there throws. + const std::string text = message.contains("content") && message["content"].is_string() + ? message["content"].get() + : std::string(); Json out; - out["id"] = openai.value("id", std::string("msg_rcli")); + const std::string reply_id = Field(openai, "id"); + out["id"] = reply_id.empty() ? std::string("msg_rcli") : reply_id; out["type"] = "message"; out["role"] = "assistant"; out["model"] = model; - out["content"] = Json::array({Json{{"type", "text"}, {"text", text}}}); - const std::string stop = StopReason(choice.value("finish_reason", std::string())); + Json content = Json::array(); + if (!text.empty()) { + content.push_back(Json{{"type", "text"}, {"text", text}}); + } + if (message.contains("tool_calls") && message["tool_calls"].is_array()) { + for (const Json& call : message["tool_calls"]) { + if (!call.is_object()) { + continue; + } + const Json function = call.contains("function") && call["function"].is_object() + ? call["function"] + : Json::object(); + const std::string name = Field(function, "name"); + if (name.empty()) { + continue; + } + content.push_back( + Json{{"type", "tool_use"}, + {"id", Field(call, "id")}, + {"name", name}, + {"input", ParseArguments(Field(function, "arguments"))}}); + } + } + const bool tool_called = content.size() > (text.empty() ? 0u : 1u); + // A turn that said nothing still needs a block: an empty content array reads + // to some clients as a malformed message rather than an empty one. + if (content.empty()) { + content.push_back(Json{{"type", "text"}, {"text", ""}}); + } + out["content"] = std::move(content); + const std::string stop = + StopWithTools(StopReason(Field(choice, "finish_reason")), tool_called); out["stop_reason"] = stop.empty() ? Json(nullptr) : Json(stop); out["stop_sequence"] = nullptr; - const Json usage = openai.value("usage", Json::object()); - out["usage"] = Json{{"input_tokens", usage.value("prompt_tokens", 0)}, - {"output_tokens", usage.value("completion_tokens", 0)}}; + // Streaming endpoints send a null usage on most chunks, and asking a null + // for a count throws. + const Json usage = openai.contains("usage") && openai["usage"].is_object() + ? openai["usage"] + : Json::object(); + out["usage"] = Json{{"input_tokens", Count(usage, "prompt_tokens")}, + {"output_tokens", Count(usage, "completion_tokens")}}; return out; } @@ -122,6 +348,18 @@ std::string StreamChunkToAnthropic(const Json& chunk, StreamState* state) { } std::string out; + // An endpoint is free to answer 200 and then report the failure in the + // stream, which is how the console reports a request over quota. Skipping + // the frame as unrecognised ends the stream with no content, and a client + // handed an empty turn sits there waiting rather than saying it was + // refused. + std::string failure_type; + std::string failure; + if (PayloadError(chunk, &failure_type, &failure)) { + state->failed = true; + return "event: error\ndata: " + ErrorBody(failure_type, failure) + "\n\n"; + } + if (!state->opened) { state->opened = true; // `value` throws when the key is present with another type, and an id @@ -147,21 +385,76 @@ std::string StreamChunkToAnthropic(const Json& chunk, StreamState* state) { choice = chunk["choices"][0]; } - const std::string finish = choice.contains("finish_reason") && - choice["finish_reason"].is_string() - ? choice["finish_reason"].get() - : std::string(); + const std::string finish = Field(choice, "finish_reason"); if (!finish.empty()) { state->stop_reason = StopReason(finish); } if (chunk.contains("usage") && chunk["usage"].is_object()) { - state->input_tokens = chunk["usage"].value("prompt_tokens", state->input_tokens); - state->output_tokens = chunk["usage"].value("completion_tokens", state->output_tokens); + state->input_tokens = Count(chunk["usage"], "prompt_tokens", state->input_tokens); + state->output_tokens = Count(chunk["usage"], "completion_tokens", state->output_tokens); } const Json delta = choice.contains("delta") && choice["delta"].is_object() ? choice["delta"] : Json::object(); + // Gathered rather than written straight through. A call arrives as + // fragments scattered across the stream, and a provider is free to advance + // two of them at once; writing as they land would interleave two half-built + // blocks, which Anthropic's stream cannot express. They go out whole in + // StreamCloseToAnthropic instead. + if (delta.contains("tool_calls") && delta["tool_calls"].is_array()) { + for (const Json& call : delta["tool_calls"]) { + if (!call.is_object()) { + continue; + } + const bool numbered = call.contains("index") && call["index"].is_number_integer(); + const int index = numbered ? call["index"].get() : 0; + const std::string id = + call.contains("id") && call["id"].is_string() ? call["id"].get() + : std::string(); + + int slot = -1; + if (numbered) { + const auto found = state->tool_slot_by_index.find(index); + if (found != state->tool_slot_by_index.end()) { + slot = found->second; + } + } else if (!id.empty()) { + const auto found = state->tool_slot_by_id.find(id); + if (found != state->tool_slot_by_id.end()) { + slot = found->second; + } + } else if (state->next_tool_slot > 0) { + // Nothing to identify it by, so the only reading left is that it + // carries on the call already being assembled. + slot = state->next_tool_slot - 1; + } + if (slot < 0) { + slot = state->next_tool_slot++; + } + if (numbered) { + state->tool_slot_by_index[index] = slot; + } + if (!id.empty()) { + state->tool_slot_by_id[id] = slot; + } + + StreamState::ToolCall& pending = state->tool_calls[slot]; + if (!id.empty()) { + pending.id = id; + } + const Json function = call.contains("function") && call["function"].is_object() + ? call["function"] + : Json::object(); + if (function.contains("name") && function["name"].is_string()) { + pending.name = function["name"].get(); + } + if (function.contains("arguments") && function["arguments"].is_string()) { + pending.arguments += function["arguments"].get(); + } + } + } + // content is null on the chunk that only carries a finish reason. const std::string text = delta.contains("content") && delta["content"].is_string() ? delta["content"].get() @@ -188,15 +481,50 @@ std::string StreamChunkToAnthropic(const Json& chunk, StreamState* state) { } std::string StreamCloseToAnthropic(StreamState* state) { - if (state == nullptr || !state->opened) { + if (state == nullptr || !state->opened || state->failed) { return {}; } std::string out; + // Blocks are numbered in the order they are written, and the text — if the + // turn had any — is always the one that came first. + int index = 0; if (state->block_open) { state->block_open = false; out += Event("content_block_stop", Json{{"type", "content_block_stop"}, {"index", 0}}); + index = 1; } - const std::string stop = state->stop_reason.empty() ? "end_turn" : state->stop_reason; + for (const auto& entry : state->tool_calls) { + const StreamState::ToolCall& call = entry.second; + // A call nobody ever named cannot be run, and a block naming nothing is + // worse for the client than a call it never hears about. + if (call.name.empty()) { + continue; + } + // The client matches a result back to its call by this id, so a call + // the endpoint never named still needs one it can quote. + const std::string id = + call.id.empty() ? "tool_" + std::to_string(entry.first) : call.id; + out += Event("content_block_start", + Json{{"type", "content_block_start"}, + {"index", index}, + {"content_block", Json{{"type", "tool_use"}, + {"id", id}, + {"name", call.name}, + {"input", Json::object()}}}}); + out += Event("content_block_delta", + Json{{"type", "content_block_delta"}, + {"index", index}, + {"delta", Json{{"type", "input_json_delta"}, + {"partial_json", call.arguments.empty() + ? std::string("{}") + : call.arguments}}}}); + out += Event("content_block_stop", + Json{{"type", "content_block_stop"}, {"index", index}}); + ++index; + } + const std::string stop = StopWithTools( + state->stop_reason.empty() ? std::string("end_turn") : state->stop_reason, + !state->tool_calls.empty()); out += Event("message_delta", Json{{"type", "message_delta"}, {"delta", Json{{"stop_reason", stop}, {"stop_sequence", nullptr}}}, @@ -205,6 +533,30 @@ std::string StreamCloseToAnthropic(StreamState* state) { return out; } +bool PayloadError(const Json& payload, std::string* type, std::string* message) { + if (!payload.is_object() || !payload.contains("error") || payload["error"].is_null()) { + return false; + } + const Json& error = payload["error"]; + std::string text = + error.is_object() ? Field(error, "message") : error.dump(); + if (text.empty()) { + text = "the model endpoint reported an error it did not describe"; + } + if (message != nullptr) { + *message = text; + } + if (type != nullptr) { + // Worth telling apart: a client that knows it was rate limited can back + // off and try again, where a plain api_error reads as a dead endpoint. + *type = text.find("RESOURCE_EXHAUSTED") != std::string::npos || + text.find("exceeded your current quota") != std::string::npos + ? "rate_limit_error" + : "api_error"; + } + return true; +} + std::string ErrorBody(const std::string& type, const std::string& message) { return Json{{"type", "error"}, {"error", Json{{"type", type}, {"message", message}}}}.dump(); } diff --git a/src/anthropic/translate.h b/src/anthropic/translate.h index ae13ee1..5c30dac 100644 --- a/src/anthropic/translate.h +++ b/src/anthropic/translate.h @@ -1,6 +1,7 @@ #ifndef RCLI_ANTHROPIC_TRANSLATE_H #define RCLI_ANTHROPIC_TRANSLATE_H +#include #include #include @@ -32,13 +33,38 @@ Json ResponseToAnthropic(const Json& openai, const std::string& model); /// a flat run of deltas. `state` carries what has already been emitted so the /// opening events fire exactly once. struct StreamState { + /// A call being assembled from the stream. OpenAI spreads one across as + /// many chunks as it likes, naming it once and then sending its arguments + /// a few characters at a time. + struct ToolCall { + std::string id; + std::string name; + std::string arguments; + }; + bool opened = false; bool block_open = false; + /// Set once the endpoint has reported a failure, after which the closing + /// events would be describing a turn that never happened. + bool failed = false; std::string message_id; std::string model; std::string stop_reason; int input_tokens = 0; int output_tokens = 0; + + /// The calls so far, in the order they were first seen, which is the order + /// they are written out in. + /// + /// Endpoints identify a call in two different ways and the slot is what + /// reconciles them: OpenAI numbers its calls and dribbles the arguments of + /// each across chunks, while others send a call whole and number nothing, + /// leaning on the id instead. Keying on either alone merges calls that are + /// separate or splits one that is not. + std::map tool_calls; + std::map tool_slot_by_index; + std::map tool_slot_by_id; + int next_tool_slot = 0; }; /// Returns the SSE text to write for `chunk`, or empty when it implies nothing. @@ -51,6 +77,11 @@ std::string StreamCloseToAnthropic(StreamState* state); /// rather than as a malformed message. std::string ErrorBody(const std::string& type, const std::string& message); +/// Reads the failure out of a body the endpoint sent with a success status. +/// +/// Returns false when `payload` carries no error, which is the ordinary case. +bool PayloadError(const Json& payload, std::string* type, std::string* message); + } // namespace rcli::anthropic::translate #endif // RCLI_ANTHROPIC_TRANSLATE_H From 8f7619d542646530b9564e27b9460d3a39ffa355 Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:20:48 +0530 Subject: [PATCH 12/16] docs(readme): document the account, editor and serve commands --- README.md | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f32faaf..636ee25 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ `rcli` runs AI models on your own machine: text generation, speech to text, text to speech, and image generation, all through the -[RunAnywhere SDK](https://github.com/RunanywhereAI/runanywhere-sdks). +[RunAnywhere SDK](https://github.com/RunanywhereAI/runanywhere-sdks). Sign in and it will also run +the models the RunAnywhere console serves, and point a coding agent or an IDE at either kind. It is a plain command-line tool with no full-screen interface. Results go to stdout, progress and errors go to stderr, so a redirect keeps the answer and leaves the progress bar on your terminal: @@ -77,6 +78,14 @@ have not downloaded pulls it first. | `rcli engines` | Which engines came up and which primitives each one serves. Engines that were compiled in but did not start are listed with the reason. | | `rcli config [setting] [value]` | List settings, read one, or change one. | | `rcli where` | The directory models and generated images live in. | +| `rcli login` | Sign in through the RunAnywhere console. `--no-browser` prints the URL instead of opening it. | +| `rcli logout` | Forget the credentials on this machine. | +| `rcli whoami` | Who is signed in, against which console, and how much they have used. | +| `rcli claude-code [-m model]` | Open Claude Code against a model. | +| `rcli claude-desktop [-m model]` | Point Claude Desktop at a model and launch it. | +| `rcli clion [-m model]` | Point CLion's AI Assistant at a model and launch it. | +| `rcli rustrover [-m model]` | The same for RustRover. | +| `rcli opencode [-m model]` | Open a coding session in opencode against a model. | Global flags: `--version`, `-v` / `--verbose` to let the engines log to stderr, and `--color auto|always|never`. @@ -111,6 +120,59 @@ that does not start with `/` goes to the model. Reasoning tokens go to stderr and the answer goes to stdout, which is why redirecting a one-shot `run` captures the answer and nothing else. +## Signing in + +A model you have not downloaded can still answer, if the console serves it: + +```bash +rcli login +rcli whoami +rcli run models/gemma-4-31b-it "why is the sky blue" +``` + +`rcli login` opens the console in a browser and waits for you to approve the machine. Credentials +land in `~/.config/rcli/credentials.json` and are refreshed when they expire, so signing in once is +usually the last you think about it. `rcli logout` deletes them. + +Upstream models are named by the id the console lists, which does not look like a catalog id. `rcli +whoami` prints the console it is talking to along with the tokens used so far. + +## Editors and coding agents + +One command points a tool at a model and starts it. There is nothing to configure by hand: + +```bash +rcli claude-code -m qwen3-0.6b +rcli clion -m models/gemma-4-31b-it +rcli claude-desktop -m models/gemma-4-31b-it +``` + +The model can be one on this machine or one the console serves. Without `-m` the tool starts the +way you already have it configured, and rcli wires nothing. + +| Tool | How it is wired | +| --- | --- | +| `claude-code`, `opencode` | `ANTHROPIC_BASE_URL` and `ANTHROPIC_AUTH_TOKEN` in the process | +| `claude-desktop` | a gateway profile in Claude Desktop's third party mode, which covers both the chat and Cowork tabs | +| `clion`, `rustrover` | AI Assistant's OpenAI-compatible provider, which works without a JetBrains AI subscription | + +Two flags go with `-m`. `--serve` holds the endpoint open and prints it instead of launching +anything, which is how a tool nobody has taught rcli about gets wired up. `--restore` puts Claude +Desktop or a JetBrains IDE back the way it was and starts nothing; a normal run already undoes its +own configuration when the app quits, so this is for the run that was interrupted before it could. + +The first `rcli clion` on a machine takes a while, because it installs the AI Assistant plugin +headlessly before starting the IDE. Later runs are quick: the settings stay written, and nothing in +them changes between runs. That endpoint sits on a fixed port rather than whatever happened to be +free, because the IDE reads the address once at startup out of a file rcli writes beforehand, and a +port that moved would leave that file naming something dead. + +Claude Code and Claude Desktop speak Anthropic's Messages API, while the models rcli serves speak +OpenAI's, so a translator sits between them. It carries tool definitions out, tool calls back, and +the results of those calls out again, which is what lets an agent on the far side actually run the +tools it was given rather than describe them. The JetBrains IDEs need no translator, because AI +Assistant speaks OpenAI already. + ## Settings `rcli config` with no arguments lists the settings and their current values: @@ -128,8 +190,10 @@ Settings live for one process. `rcli config temperature 0.2` changes it for that nothing else, so to use a setting you either set it in the interactive prompt with `/set` or accept the default. There is no config file. -Two environment variables: `RUNANYWHERE_HOME` moves the storage directory, and `RCLI_LOG` names a -file for the engine logs that `--verbose` would otherwise put on stderr. +Four environment variables. `RUNANYWHERE_HOME` moves the storage directory and `RCLI_LOG` names a +file for the engine logs that `--verbose` would otherwise put on stderr. `RCLI_CONSOLE_URL` points +at a console other than the default `http://localhost:8080`, and `RCLI_PROFILE_DIR` moves the +directory the credentials are kept in. ## Engines From 609e192e43b1d64d17fea6d7e764d82c955d2446 Mon Sep 17 00:00:00 2001 From: Siddhesh Sonar <67579112+Siddhesh2377@users.noreply.github.com> Date: Sat, 29 Aug 2026 07:24:57 +0530 Subject: [PATCH 13/16] build: fetch cpp-httplib and nlohmann_json instead of vendoring them --- CMakeLists.txt | 31 +- third_party/httplib/LICENSE | 22 - third_party/httplib/httplib.cpp | 16526 --------------- third_party/httplib/httplib.h | 3903 ---- third_party/nlohmann/nlohmann/json.hpp | 24765 ----------------------- 5 files changed, 22 insertions(+), 45225 deletions(-) delete mode 100644 third_party/httplib/LICENSE delete mode 100644 third_party/httplib/httplib.cpp delete mode 100644 third_party/httplib/httplib.h delete mode 100644 third_party/nlohmann/nlohmann/json.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c21faa0..27e92a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,9 +57,6 @@ set(RCLI_SOURCES src/desktop/claude_profile.cpp src/harness/harness.cpp src/harness/local_models.cpp - # cpp-httplib 0.46 is a split build, so the vendored header needs its - # translation unit compiled in rather than being header-only. - third_party/httplib/httplib.cpp src/ide/jetbrains_profile.cpp src/ide/openai_proxy.cpp src/commands/engine_options.cpp @@ -96,16 +93,32 @@ target_include_directories(rcli_core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src" "${CMAKE_CURRENT_SOURCE_DIR}/third_party/CLI11" "${CMAKE_CURRENT_SOURCE_DIR}/third_party/linenoise" - # Single-header, vendored rather than fetched. Both used to arrive from the - # SDK's own source build; a kit consumer never configures that build, so - # naming them here is what keeps the Anthropic translator and the JetBrains - # proxy compiling with no network step in CI. - "${CMAKE_CURRENT_SOURCE_DIR}/third_party/httplib" - "${CMAKE_CURRENT_SOURCE_DIR}/third_party/nlohmann" ) +# The Anthropic translator and the JetBrains proxy need an HTTP client and a +# JSON reader. Both used to arrive from the SDK's own source build; a kit +# consumer never configures that build, so they are fetched here. +# +# Fetched rather than vendored on purpose. Checking cpp-httplib in would drop +# thirty thousand lines of somebody else's code into this repo, where the +# security scanner reads it as ours and flags the http:// URLs an HTTP client +# is obliged to construct. +include(FetchContent) +set(HTTPLIB_REQUIRE_OPENSSL OFF CACHE BOOL "" FORCE) +set(JSON_BuildTests OFF CACHE BOOL "" FORCE) +FetchContent_Declare(cpp_httplib + GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git + GIT_TAG v0.46.1 + GIT_SHALLOW TRUE) +FetchContent_Declare(nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.11.3 + GIT_SHALLOW TRUE) +FetchContent_MakeAvailable(cpp_httplib nlohmann_json) + find_package(Threads REQUIRED) target_link_libraries(rcli_core PUBLIC rac_commons Threads::Threads) +target_link_libraries(rcli_core PRIVATE httplib::httplib nlohmann_json::nlohmann_json) target_compile_definitions(rcli_core PUBLIC RCLI_VERSION="${PROJECT_VERSION}" RCLI_PINNED_SDK_VERSION="${RCLI_PINNED_SDK_VERSION}" diff --git a/third_party/httplib/LICENSE b/third_party/httplib/LICENSE deleted file mode 100644 index 3e5ed35..0000000 --- a/third_party/httplib/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017 yhirose - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - diff --git a/third_party/httplib/httplib.cpp b/third_party/httplib/httplib.cpp deleted file mode 100644 index b9d05d9..0000000 --- a/third_party/httplib/httplib.cpp +++ /dev/null @@ -1,16526 +0,0 @@ -#include "httplib.h" -namespace httplib { - -/* - * Implementation that will be part of the .cc file if split into .h + .cc. - */ - -namespace stream { - -// stream::Result implementations -Result::Result() : chunk_size_(8192) {} - -Result::Result(ClientImpl::StreamHandle &&handle, size_t chunk_size) - : handle_(std::move(handle)), chunk_size_(chunk_size) {} - -Result::Result(Result &&other) noexcept - : handle_(std::move(other.handle_)), buffer_(std::move(other.buffer_)), - current_size_(other.current_size_), chunk_size_(other.chunk_size_), - finished_(other.finished_) { - other.current_size_ = 0; - other.finished_ = true; -} - -Result &Result::operator=(Result &&other) noexcept { - if (this != &other) { - handle_ = std::move(other.handle_); - buffer_ = std::move(other.buffer_); - current_size_ = other.current_size_; - chunk_size_ = other.chunk_size_; - finished_ = other.finished_; - other.current_size_ = 0; - other.finished_ = true; - } - return *this; -} - -bool Result::is_valid() const { return handle_.is_valid(); } -Result::operator bool() const { return is_valid(); } - -int Result::status() const { - return handle_.response ? handle_.response->status : -1; -} - -const Headers &Result::headers() const { - static const Headers empty_headers; - return handle_.response ? handle_.response->headers : empty_headers; -} - -std::string Result::get_header_value(const std::string &key, - const char *def) const { - return handle_.response ? handle_.response->get_header_value(key, def) : def; -} - -bool Result::has_header(const std::string &key) const { - return handle_.response ? handle_.response->has_header(key) : false; -} - -Error Result::error() const { return handle_.error; } -Error Result::read_error() const { return handle_.get_read_error(); } -bool Result::has_read_error() const { return handle_.has_read_error(); } - -bool Result::next() { - if (!handle_.is_valid() || finished_) { return false; } - - if (buffer_.size() < chunk_size_) { buffer_.resize(chunk_size_); } - - ssize_t n = handle_.read(&buffer_[0], chunk_size_); - if (n > 0) { - current_size_ = static_cast(n); - return true; - } - - current_size_ = 0; - finished_ = true; - return false; -} - -const char *Result::data() const { return buffer_.data(); } -size_t Result::size() const { return current_size_; } - -std::string Result::read_all() { - std::string result; - while (next()) { - result.append(data(), size()); - } - return result; -} - -} // namespace stream - -namespace sse { - -// SSEMessage implementations -SSEMessage::SSEMessage() : event("message") {} - -void SSEMessage::clear() { - event = "message"; - data.clear(); - id.clear(); -} - -// SSEClient implementations -SSEClient::SSEClient(Client &client, const std::string &path) - : client_(client), path_(path) {} - -SSEClient::SSEClient(Client &client, const std::string &path, - const Headers &headers) - : client_(client), path_(path), headers_(headers) {} - -SSEClient::~SSEClient() { stop(); } - -SSEClient &SSEClient::on_message(MessageHandler handler) { - on_message_ = std::move(handler); - return *this; -} - -SSEClient &SSEClient::on_event(const std::string &type, - MessageHandler handler) { - event_handlers_[type] = std::move(handler); - return *this; -} - -SSEClient &SSEClient::on_open(OpenHandler handler) { - on_open_ = std::move(handler); - return *this; -} - -SSEClient &SSEClient::on_error(ErrorHandler handler) { - on_error_ = std::move(handler); - return *this; -} - -SSEClient &SSEClient::set_reconnect_interval(int ms) { - reconnect_interval_ms_ = ms; - return *this; -} - -SSEClient &SSEClient::set_max_reconnect_attempts(int n) { - max_reconnect_attempts_ = n; - return *this; -} - -SSEClient &SSEClient::set_headers(const Headers &headers) { - std::lock_guard lock(headers_mutex_); - headers_ = headers; - return *this; -} - -bool SSEClient::is_connected() const { return connected_.load(); } - -const std::string &SSEClient::last_event_id() const { - return last_event_id_; -} - -void SSEClient::start() { - running_.store(true); - run_event_loop(); -} - -void SSEClient::start_async() { - running_.store(true); - async_thread_ = std::thread([this]() { run_event_loop(); }); -} - -void SSEClient::stop() { - running_.store(false); - client_.stop(); // Cancel any pending operations - if (async_thread_.joinable()) { async_thread_.join(); } -} - -bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg, - int &retry_ms) { - // Blank line signals end of event - if (line.empty() || line == "\r") { return true; } - - // Lines starting with ':' are comments (ignored) - if (!line.empty() && line[0] == ':') { return false; } - - // Find the colon separator - auto colon_pos = line.find(':'); - if (colon_pos == std::string::npos) { - // Line with no colon is treated as field name with empty value - return false; - } - - auto field = line.substr(0, colon_pos); - std::string value; - - // Value starts after colon, skip optional single space - if (colon_pos + 1 < line.size()) { - auto value_start = colon_pos + 1; - if (line[value_start] == ' ') { value_start++; } - value = line.substr(value_start); - // Remove trailing \r if present - if (!value.empty() && value.back() == '\r') { value.pop_back(); } - } - - // Handle known fields - if (field == "event") { - msg.event = value; - } else if (field == "data") { - // Multiple data lines are concatenated with newlines - if (!msg.data.empty()) { msg.data += "\n"; } - msg.data += value; - } else if (field == "id") { - // Empty id is valid (clears the last event ID) - msg.id = value; - } else if (field == "retry") { - // Parse retry interval in milliseconds - { - int v = 0; - auto res = - detail::from_chars(value.data(), value.data() + value.size(), v); - if (res.ec == std::errc{}) { retry_ms = v; } - } - } - // Unknown fields are ignored per SSE spec - - return false; -} - -void SSEClient::run_event_loop() { - auto reconnect_count = 0; - - while (running_.load()) { - // Build headers, including Last-Event-ID if we have one - Headers request_headers; - { - std::lock_guard lock(headers_mutex_); - request_headers = headers_; - } - if (!last_event_id_.empty()) { - request_headers.emplace("Last-Event-ID", last_event_id_); - } - - // Open streaming connection - auto result = stream::Get(client_, path_, request_headers); - - // Connection error handling - if (!result) { - connected_.store(false); - if (on_error_) { on_error_(result.error()); } - - if (!should_reconnect(reconnect_count)) { break; } - wait_for_reconnect(); - reconnect_count++; - continue; - } - - if (result.status() != StatusCode::OK_200) { - connected_.store(false); - if (on_error_) { on_error_(Error::Connection); } - - // For certain errors, don't reconnect. - // Note: 401 is intentionally absent so that handlers can refresh - // credentials via set_headers() and let the client reconnect. - if (result.status() == StatusCode::NoContent_204 || - result.status() == StatusCode::NotFound_404 || - result.status() == StatusCode::Forbidden_403) { - break; - } - - if (!should_reconnect(reconnect_count)) { break; } - wait_for_reconnect(); - reconnect_count++; - continue; - } - - // Connection successful - connected_.store(true); - reconnect_count = 0; - if (on_open_) { on_open_(); } - - // Event receiving loop - std::string buffer; - SSEMessage current_msg; - - while (running_.load() && result.next()) { - buffer.append(result.data(), result.size()); - - // Process complete lines in the buffer - size_t line_start = 0; - size_t newline_pos; - - while ((newline_pos = buffer.find('\n', line_start)) != - std::string::npos) { - auto line = buffer.substr(line_start, newline_pos - line_start); - line_start = newline_pos + 1; - - // Parse the line and check if event is complete - auto event_complete = - parse_sse_line(line, current_msg, reconnect_interval_ms_); - - if (event_complete && !current_msg.data.empty()) { - // Update last_event_id for reconnection - if (!current_msg.id.empty()) { last_event_id_ = current_msg.id; } - - // Dispatch event to appropriate handler - dispatch_event(current_msg); - - current_msg.clear(); - } - } - - // Keep unprocessed data in buffer - buffer.erase(0, line_start); - } - - // Connection ended - connected_.store(false); - - if (!running_.load()) { break; } - - // Check for read errors - if (result.has_read_error()) { - if (on_error_) { on_error_(result.read_error()); } - } - - if (!should_reconnect(reconnect_count)) { break; } - wait_for_reconnect(); - reconnect_count++; - } - - connected_.store(false); -} - -void SSEClient::dispatch_event(const SSEMessage &msg) { - // Check for specific event type handler first - auto it = event_handlers_.find(msg.event); - if (it != event_handlers_.end()) { - it->second(msg); - return; - } - - // Fall back to generic message handler - if (on_message_) { on_message_(msg); } -} - -bool SSEClient::should_reconnect(int count) const { - if (!running_.load()) { return false; } - if (max_reconnect_attempts_ == 0) { return true; } // unlimited - return count < max_reconnect_attempts_; -} - -void SSEClient::wait_for_reconnect() { - // Use small increments to check running_ flag frequently - auto waited = 0; - while (running_.load() && waited < reconnect_interval_ms_) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - waited += 100; - } -} - -} // namespace sse - -#ifdef CPPHTTPLIB_SSL_ENABLED -/* - * TLS abstraction layer - internal function declarations - * These are implementation details and not part of the public API. - */ -namespace tls { - -// Client context -ctx_t create_client_context(); -void free_context(ctx_t ctx); -bool set_min_version(ctx_t ctx, Version version); -bool load_ca_pem(ctx_t ctx, const char *pem, size_t len); -bool load_ca_file(ctx_t ctx, const char *file_path); -bool load_ca_dir(ctx_t ctx, const char *dir_path); -bool load_system_certs(ctx_t ctx); -bool set_client_cert_pem(ctx_t ctx, const char *cert, const char *key, - const char *password); -bool set_client_cert_file(ctx_t ctx, const char *cert_path, - const char *key_path, const char *password); - -// Server context -ctx_t create_server_context(); -bool set_server_cert_pem(ctx_t ctx, const char *cert, const char *key, - const char *password); -bool set_server_cert_file(ctx_t ctx, const char *cert_path, - const char *key_path, const char *password); -bool set_client_ca_file(ctx_t ctx, const char *ca_file, const char *ca_dir); -void set_verify_client(ctx_t ctx, bool require); - -// Session management -session_t create_session(ctx_t ctx, socket_t sock); -void free_session(session_t session); -bool set_sni(session_t session, const char *hostname); -bool set_hostname(session_t session, const char *hostname); - -// Handshake (non-blocking capable) -TlsError connect(session_t session); -TlsError accept(session_t session); - -// Handshake with timeout (blocking until timeout) -bool connect_nonblocking(session_t session, socket_t sock, time_t timeout_sec, - time_t timeout_usec, TlsError *err); -bool accept_nonblocking(session_t session, socket_t sock, time_t timeout_sec, - time_t timeout_usec, TlsError *err); - -// I/O (non-blocking capable) -ssize_t read(session_t session, void *buf, size_t len, TlsError &err); -ssize_t write(session_t session, const void *buf, size_t len, TlsError &err); -int pending(const_session_t session); -void shutdown(session_t session, bool graceful); - -// Connection state -bool is_peer_closed(session_t session, socket_t sock); - -// Certificate verification -cert_t get_peer_cert(const_session_t session); -void free_cert(cert_t cert); -bool verify_hostname(cert_t cert, const char *hostname); -uint64_t hostname_mismatch_code(); -long get_verify_result(const_session_t session); - -// Certificate introspection -std::string get_cert_subject_cn(cert_t cert); -std::string get_cert_issuer_name(cert_t cert); -bool get_cert_sans(cert_t cert, std::vector &sans); -bool get_cert_validity(cert_t cert, time_t ¬_before, time_t ¬_after); -std::string get_cert_serial(cert_t cert); -bool get_cert_der(cert_t cert, std::vector &der); -const char *get_sni(const_session_t session); - -// CA store management -ca_store_t create_ca_store(const char *pem, size_t len); -void free_ca_store(ca_store_t store); -bool set_ca_store(ctx_t ctx, ca_store_t store); -size_t get_ca_certs(ctx_t ctx, std::vector &certs); -std::vector get_ca_names(ctx_t ctx); - -// Dynamic certificate update (for servers) -bool update_server_cert(ctx_t ctx, const char *cert_pem, const char *key_pem, - const char *password); -bool update_server_client_ca(ctx_t ctx, const char *ca_pem); - -// Certificate verification callback -bool set_verify_callback(ctx_t ctx, VerifyCallback callback); -long get_verify_error(const_session_t session); -std::string verify_error_string(long error_code); - -// TlsError information -uint64_t peek_error(); -uint64_t get_error(); -std::string error_string(uint64_t code); - -} // namespace tls -#endif // CPPHTTPLIB_SSL_ENABLED - -/* - * Group 1: detail namespace - Non-SSL utilities - */ - -namespace detail { - -bool set_socket_opt_impl(socket_t sock, int level, int optname, - const void *optval, socklen_t optlen) { - return setsockopt(sock, level, optname, -#ifdef _WIN32 - reinterpret_cast(optval), -#else - optval, -#endif - optlen) == 0; -} - -bool set_socket_opt_time(socket_t sock, int level, int optname, - time_t sec, time_t usec) { -#ifdef _WIN32 - auto timeout = static_cast(sec * 1000 + usec / 1000); -#else - timeval timeout; - timeout.tv_sec = static_cast(sec); - timeout.tv_usec = static_cast(usec); -#endif - return set_socket_opt_impl(sock, level, optname, &timeout, sizeof(timeout)); -} - -bool is_hex(char c, int &v) { - if (isdigit(c)) { - v = c - '0'; - return true; - } else if ('A' <= c && c <= 'F') { - v = c - 'A' + 10; - return true; - } else if ('a' <= c && c <= 'f') { - v = c - 'a' + 10; - return true; - } - return false; -} - -bool from_hex_to_i(const std::string &s, size_t i, size_t cnt, - int &val) { - if (i >= s.size()) { return false; } - - val = 0; - for (; cnt; i++, cnt--) { - if (!s[i]) { return false; } - auto v = 0; - if (is_hex(s[i], v)) { - val = val * 16 + v; - } else { - return false; - } - } - return true; -} - -std::string from_i_to_hex(size_t n) { - static const auto charset = "0123456789abcdef"; - std::string ret; - do { - ret = charset[n & 15] + ret; - n >>= 4; - } while (n > 0); - return ret; -} - -std::string compute_etag(const FileStat &fs) { - if (!fs.is_file()) { return std::string(); } - - // If mtime cannot be determined (negative value indicates an error - // or sentinel), do not generate an ETag. Returning a neutral / fixed - // value like 0 could collide with a real file that legitimately has - // mtime == 0 (epoch) and lead to misleading validators. - auto mtime_raw = fs.mtime(); - if (mtime_raw < 0) { return std::string(); } - - auto mtime = static_cast(mtime_raw); - auto size = fs.size(); - - return std::string("W/\"") + from_i_to_hex(mtime) + "-" + - from_i_to_hex(size) + "\""; -} - -// Format time_t as HTTP-date (RFC 9110 Section 5.6.7): "Sun, 06 Nov 1994 -// 08:49:37 GMT" This implementation is defensive: it validates `mtime`, checks -// return values from `gmtime_r`/`gmtime_s`, and ensures `strftime` succeeds. -std::string file_mtime_to_http_date(time_t mtime) { - if (mtime < 0) { return std::string(); } - - struct tm tm_buf; -#ifdef _WIN32 - if (gmtime_s(&tm_buf, &mtime) != 0) { return std::string(); } -#else - if (gmtime_r(&mtime, &tm_buf) == nullptr) { return std::string(); } -#endif - char buf[64]; - if (strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", &tm_buf) == 0) { - return std::string(); - } - - return std::string(buf); -} - -// Parse HTTP-date (RFC 9110 Section 5.6.7) to time_t. Returns -1 on failure. -time_t parse_http_date(const std::string &date_str) { - struct tm tm_buf; - - // Create a classic locale object once for all parsing attempts - const std::locale classic_locale = std::locale::classic(); - - // Try to parse using std::get_time (C++11, cross-platform) - auto try_parse = [&](const char *fmt) -> bool { - std::istringstream ss(date_str); - ss.imbue(classic_locale); - - memset(&tm_buf, 0, sizeof(tm_buf)); - ss >> std::get_time(&tm_buf, fmt); - - return !ss.fail(); - }; - - // RFC 9110 preferred format (HTTP-date): "Sun, 06 Nov 1994 08:49:37 GMT" - if (!try_parse("%a, %d %b %Y %H:%M:%S")) { - // RFC 850 format: "Sunday, 06-Nov-94 08:49:37 GMT" - if (!try_parse("%A, %d-%b-%y %H:%M:%S")) { - // asctime format: "Sun Nov 6 08:49:37 1994" - if (!try_parse("%a %b %d %H:%M:%S %Y")) { - return static_cast(-1); - } - } - } - -#ifdef _WIN32 - return _mkgmtime(&tm_buf); -#elif defined _AIX - return mktime(&tm_buf); -#else - return timegm(&tm_buf); -#endif -} - -bool is_weak_etag(const std::string &s) { - // Check if the string is a weak ETag (starts with 'W/"') - return s.size() > 3 && s[0] == 'W' && s[1] == '/' && s[2] == '"'; -} - -bool is_strong_etag(const std::string &s) { - // Check if the string is a strong ETag (starts and ends with '"', at least 2 - // chars) - return s.size() >= 2 && s[0] == '"' && s.back() == '"'; -} - -size_t to_utf8(int code, char *buff) { - if (code < 0x0080) { - buff[0] = static_cast(code & 0x7F); - return 1; - } else if (code < 0x0800) { - buff[0] = static_cast(0xC0 | ((code >> 6) & 0x1F)); - buff[1] = static_cast(0x80 | (code & 0x3F)); - return 2; - } else if (code < 0xD800) { - buff[0] = static_cast(0xE0 | ((code >> 12) & 0xF)); - buff[1] = static_cast(0x80 | ((code >> 6) & 0x3F)); - buff[2] = static_cast(0x80 | (code & 0x3F)); - return 3; - } else if (code < 0xE000) { // D800 - DFFF is invalid... - return 0; - } else if (code < 0x10000) { - buff[0] = static_cast(0xE0 | ((code >> 12) & 0xF)); - buff[1] = static_cast(0x80 | ((code >> 6) & 0x3F)); - buff[2] = static_cast(0x80 | (code & 0x3F)); - return 3; - } else if (code < 0x110000) { - buff[0] = static_cast(0xF0 | ((code >> 18) & 0x7)); - buff[1] = static_cast(0x80 | ((code >> 12) & 0x3F)); - buff[2] = static_cast(0x80 | ((code >> 6) & 0x3F)); - buff[3] = static_cast(0x80 | (code & 0x3F)); - return 4; - } - - // NOTREACHED - return 0; -} - -} // namespace detail - -namespace ws { -namespace impl { - -bool is_valid_utf8(const std::string &s) { - size_t i = 0; - auto n = s.size(); - while (i < n) { - auto c = static_cast(s[i]); - size_t len; - uint32_t cp; - if (c < 0x80) { - i++; - continue; - } else if ((c & 0xE0) == 0xC0) { - len = 2; - cp = c & 0x1F; - } else if ((c & 0xF0) == 0xE0) { - len = 3; - cp = c & 0x0F; - } else if ((c & 0xF8) == 0xF0) { - len = 4; - cp = c & 0x07; - } else { - return false; - } - if (i + len > n) { return false; } - for (size_t j = 1; j < len; j++) { - auto b = static_cast(s[i + j]); - if ((b & 0xC0) != 0x80) { return false; } - cp = (cp << 6) | (b & 0x3F); - } - // Overlong encoding check - if (len == 2 && cp < 0x80) { return false; } - if (len == 3 && cp < 0x800) { return false; } - if (len == 4 && cp < 0x10000) { return false; } - // Surrogate halves (U+D800..U+DFFF) and beyond U+10FFFF are invalid - if (cp >= 0xD800 && cp <= 0xDFFF) { return false; } - if (cp > 0x10FFFF) { return false; } - i += len; - } - return true; -} - -} // namespace impl -} // namespace ws - -namespace detail { - -// NOTE: This code came up with the following stackoverflow post: -// https://stackoverflow.com/questions/180947/base64-decode-snippet-in-c -std::string base64_encode(const std::string &in) { - static const auto lookup = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - - std::string out; - out.reserve(in.size()); - - auto val = 0; - auto valb = -6; - - for (auto c : in) { - val = (val << 8) + static_cast(c); - valb += 8; - while (valb >= 0) { - out.push_back(lookup[(val >> valb) & 0x3F]); - valb -= 6; - } - } - - if (valb > -6) { out.push_back(lookup[((val << 8) >> (valb + 8)) & 0x3F]); } - - while (out.size() % 4) { - out.push_back('='); - } - - return out; -} - -std::string sha1(const std::string &input) { - // RFC 3174 SHA-1 implementation - auto left_rotate = [](uint32_t x, uint32_t n) -> uint32_t { - return (x << n) | (x >> (32 - n)); - }; - - uint32_t h0 = 0x67452301; - uint32_t h1 = 0xEFCDAB89; - uint32_t h2 = 0x98BADCFE; - uint32_t h3 = 0x10325476; - uint32_t h4 = 0xC3D2E1F0; - - // Pre-processing: adding padding bits - std::string msg = input; - uint64_t original_bit_len = static_cast(msg.size()) * 8; - msg.push_back(static_cast(0x80)); - while (msg.size() % 64 != 56) { - msg.push_back(0); - } - - // Append original length in bits as 64-bit big-endian - for (int i = 56; i >= 0; i -= 8) { - msg.push_back(static_cast((original_bit_len >> i) & 0xFF)); - } - - // Process each 512-bit chunk - for (size_t offset = 0; offset < msg.size(); offset += 64) { - uint32_t w[80]; - - for (size_t i = 0; i < 16; i++) { - w[i] = - (static_cast(static_cast(msg[offset + i * 4])) - << 24) | - (static_cast(static_cast(msg[offset + i * 4 + 1])) - << 16) | - (static_cast(static_cast(msg[offset + i * 4 + 2])) - << 8) | - (static_cast( - static_cast(msg[offset + i * 4 + 3]))); - } - - for (int i = 16; i < 80; i++) { - w[i] = left_rotate(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1); - } - - uint32_t a = h0, b = h1, c = h2, d = h3, e = h4; - - for (int i = 0; i < 80; i++) { - uint32_t f, k; - if (i < 20) { - f = (b & c) | ((~b) & d); - k = 0x5A827999; - } else if (i < 40) { - f = b ^ c ^ d; - k = 0x6ED9EBA1; - } else if (i < 60) { - f = (b & c) | (b & d) | (c & d); - k = 0x8F1BBCDC; - } else { - f = b ^ c ^ d; - k = 0xCA62C1D6; - } - - uint32_t temp = left_rotate(a, 5) + f + e + k + w[i]; - e = d; - d = c; - c = left_rotate(b, 30); - b = a; - a = temp; - } - - h0 += a; - h1 += b; - h2 += c; - h3 += d; - h4 += e; - } - - // Produce the final hash as a 20-byte binary string - std::string hash(20, '\0'); - for (size_t i = 0; i < 4; i++) { - hash[i] = static_cast((h0 >> (24 - i * 8)) & 0xFF); - hash[4 + i] = static_cast((h1 >> (24 - i * 8)) & 0xFF); - hash[8 + i] = static_cast((h2 >> (24 - i * 8)) & 0xFF); - hash[12 + i] = static_cast((h3 >> (24 - i * 8)) & 0xFF); - hash[16 + i] = static_cast((h4 >> (24 - i * 8)) & 0xFF); - } - return hash; -} - -std::string websocket_accept_key(const std::string &client_key) { - const std::string magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - return base64_encode(sha1(client_key + magic)); -} - -bool is_websocket_upgrade(const Request &req) { - if (req.method != "GET") { return false; } - - // Check Upgrade: websocket (case-insensitive) - auto upgrade_it = req.headers.find("Upgrade"); - if (upgrade_it == req.headers.end()) { return false; } - auto upgrade_val = case_ignore::to_lower(upgrade_it->second); - if (upgrade_val != "websocket") { return false; } - - // Check Connection header contains "Upgrade" - auto connection_it = req.headers.find("Connection"); - if (connection_it == req.headers.end()) { return false; } - auto connection_val = case_ignore::to_lower(connection_it->second); - if (connection_val.find("upgrade") == std::string::npos) { return false; } - - // Check Sec-WebSocket-Key is a valid base64-encoded 16-byte value (24 chars) - // RFC 6455 Section 4.2.1 - auto ws_key = req.get_header_value("Sec-WebSocket-Key"); - if (ws_key.size() != 24 || ws_key[22] != '=' || ws_key[23] != '=') { - return false; - } - static const std::string b64chars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - for (size_t i = 0; i < 22; i++) { - if (b64chars.find(ws_key[i]) == std::string::npos) { return false; } - } - - // Check Sec-WebSocket-Version: 13 - auto version = req.get_header_value("Sec-WebSocket-Version"); - if (version != "13") { return false; } - - return true; -} - -bool write_websocket_frame(Stream &strm, ws::Opcode opcode, - const char *data, size_t len, bool fin, - bool mask) { - // First byte: FIN + opcode - uint8_t header[2]; - header[0] = static_cast((fin ? 0x80 : 0x00) | - (static_cast(opcode) & 0x0F)); - - // Second byte: MASK + payload length - if (len < 126) { - header[1] = static_cast(len); - if (mask) { header[1] |= 0x80; } - if (strm.write(reinterpret_cast(header), 2) < 0) { return false; } - } else if (len <= 0xFFFF) { - header[1] = 126; - if (mask) { header[1] |= 0x80; } - if (strm.write(reinterpret_cast(header), 2) < 0) { return false; } - uint8_t ext[2]; - ext[0] = static_cast((len >> 8) & 0xFF); - ext[1] = static_cast(len & 0xFF); - if (strm.write(reinterpret_cast(ext), 2) < 0) { return false; } - } else { - header[1] = 127; - if (mask) { header[1] |= 0x80; } - if (strm.write(reinterpret_cast(header), 2) < 0) { return false; } - uint8_t ext[8]; - for (int i = 7; i >= 0; i--) { - ext[7 - i] = - static_cast((static_cast(len) >> (i * 8)) & 0xFF); - } - if (strm.write(reinterpret_cast(ext), 8) < 0) { return false; } - } - - if (mask) { - // Generate random mask key - thread_local std::mt19937 rng(std::random_device{}()); - uint8_t mask_key[4]; - auto r = rng(); - std::memcpy(mask_key, &r, 4); - if (strm.write(reinterpret_cast(mask_key), 4) < 0) { return false; } - - // Write masked payload in chunks - const size_t chunk_size = 4096; - std::vector buf((std::min)(len, chunk_size)); - for (size_t offset = 0; offset < len; offset += chunk_size) { - size_t n = (std::min)(chunk_size, len - offset); - for (size_t i = 0; i < n; i++) { - buf[i] = - data[offset + i] ^ static_cast(mask_key[(offset + i) % 4]); - } - if (strm.write(buf.data(), n) < 0) { return false; } - } - } else { - if (len > 0) { - if (strm.write(data, len) < 0) { return false; } - } - } - - return true; -} - -} // namespace detail - -namespace ws { -namespace impl { - -bool read_websocket_frame(Stream &strm, Opcode &opcode, - std::string &payload, bool &fin, - bool expect_masked, size_t max_len) { - // Read first 2 bytes - uint8_t header[2]; - if (strm.read(reinterpret_cast(header), 2) != 2) { return false; } - - fin = (header[0] & 0x80) != 0; - - // RSV1, RSV2, RSV3 must be 0 when no extension is negotiated - if (header[0] & 0x70) { return false; } - - opcode = static_cast(header[0] & 0x0F); - bool masked = (header[1] & 0x80) != 0; - uint64_t payload_len = header[1] & 0x7F; - - // RFC 6455 Section 5.5: control frames MUST NOT be fragmented and - // MUST have a payload length of 125 bytes or less - bool is_control = (static_cast(opcode) & 0x08) != 0; - if (is_control) { - if (!fin) { return false; } - if (payload_len > 125) { return false; } - } - - if (masked != expect_masked) { return false; } - - // Extended payload length - if (payload_len == 126) { - uint8_t ext[2]; - if (strm.read(reinterpret_cast(ext), 2) != 2) { return false; } - payload_len = (static_cast(ext[0]) << 8) | ext[1]; - } else if (payload_len == 127) { - uint8_t ext[8]; - if (strm.read(reinterpret_cast(ext), 8) != 8) { return false; } - // RFC 6455 Section 5.2: the most significant bit MUST be 0 - if (ext[0] & 0x80) { return false; } - payload_len = 0; - for (int i = 0; i < 8; i++) { - payload_len = (payload_len << 8) | ext[i]; - } - } - - if (payload_len > max_len) { return false; } - - // Read mask key if present - uint8_t mask_key[4] = {0}; - if (masked) { - if (strm.read(reinterpret_cast(mask_key), 4) != 4) { return false; } - } - - // Read payload - payload.resize(static_cast(payload_len)); - if (payload_len > 0) { - size_t total_read = 0; - while (total_read < payload_len) { - auto n = strm.read(&payload[total_read], - static_cast(payload_len - total_read)); - if (n <= 0) { return false; } - total_read += static_cast(n); - } - } - - // Unmask if needed - if (masked) { - for (size_t i = 0; i < payload.size(); i++) { - payload[i] ^= static_cast(mask_key[i % 4]); - } - } - - return true; -} - -} // namespace impl -} // namespace ws - -namespace detail { - -bool is_valid_path(const std::string &path) { - size_t level = 0; - size_t i = 0; - - // Skip slash - while (i < path.size() && path[i] == '/') { - i++; - } - - while (i < path.size()) { - // Read component - auto beg = i; - while (i < path.size() && path[i] != '/') { - if (path[i] == '\0') { - return false; - } else if (path[i] == '\\') { - return false; - } - i++; - } - - auto len = i - beg; - assert(len > 0); - - if (!path.compare(beg, len, ".")) { - ; - } else if (!path.compare(beg, len, "..")) { - if (level == 0) { return false; } - level--; - } else { - level++; - } - - // Skip slash - while (i < path.size() && path[i] == '/') { - i++; - } - } - - return true; -} - -bool canonicalize_path(const char *path, std::string &resolved) { -#if defined(_WIN32) - char buf[_MAX_PATH]; - if (_fullpath(buf, path, _MAX_PATH) == nullptr) { return false; } - resolved = buf; -#elif defined(PATH_MAX) - char buf[PATH_MAX]; - if (realpath(path, buf) == nullptr) { return false; } - resolved = buf; -#else - auto buf = realpath(path, nullptr); - auto guard = scope_exit([&]() { std::free(buf); }); - if (buf == nullptr) { return false; } - resolved = buf; -#endif - return true; -} - -bool is_path_within_base(const std::string &resolved_path, - const std::string &resolved_base) { -#if defined(_WIN32) - return _strnicmp(resolved_path.c_str(), resolved_base.c_str(), - resolved_base.size()) == 0; -#else - return strncmp(resolved_path.c_str(), resolved_base.c_str(), - resolved_base.size()) == 0; -#endif -} - -FileStat::FileStat(const std::string &path) { -#if defined(_WIN32) - auto wpath = u8string_to_wstring(path.c_str()); - ret_ = _wstat(wpath.c_str(), &st_); -#else - ret_ = stat(path.c_str(), &st_); -#endif -} -bool FileStat::is_file() const { - return ret_ >= 0 && S_ISREG(st_.st_mode); -} -bool FileStat::is_dir() const { - return ret_ >= 0 && S_ISDIR(st_.st_mode); -} - -time_t FileStat::mtime() const { - return ret_ >= 0 ? static_cast(st_.st_mtime) - : static_cast(-1); -} - -size_t FileStat::size() const { - return ret_ >= 0 ? static_cast(st_.st_size) : 0; -} - -std::string encode_path(const std::string &s) { - std::string result; - result.reserve(s.size()); - - for (size_t i = 0; s[i]; i++) { - switch (s[i]) { - case ' ': result += "%20"; break; - case '+': result += "%2B"; break; - case '\r': result += "%0D"; break; - case '\n': result += "%0A"; break; - case '\'': result += "%27"; break; - case ',': result += "%2C"; break; - // case ':': result += "%3A"; break; // ok? probably... - case ';': result += "%3B"; break; - default: - auto c = static_cast(s[i]); - if (c >= 0x80) { - result += '%'; - char hex[4]; - auto len = snprintf(hex, sizeof(hex) - 1, "%02X", c); - assert(len == 2); - result.append(hex, static_cast(len)); - } else { - result += s[i]; - } - break; - } - } - - return result; -} - -std::string file_extension(const std::string &path) { - std::smatch m; - thread_local auto re = std::regex("\\.([a-zA-Z0-9]+)$"); - if (std::regex_search(path, m, re)) { return m[1].str(); } - return std::string(); -} - -bool is_space_or_tab(char c) { return c == ' ' || c == '\t'; } - -template -bool parse_header(const char *beg, const char *end, T fn); - -template -bool parse_header(const char *beg, const char *end, T fn) { - // Skip trailing spaces and tabs. - while (beg < end && is_space_or_tab(end[-1])) { - end--; - } - - auto p = beg; - while (p < end && *p != ':') { - p++; - } - - auto name = std::string(beg, p); - if (!detail::fields::is_field_name(name)) { return false; } - - if (p == end) { return false; } - - auto key_end = p; - - if (*p++ != ':') { return false; } - - while (p < end && is_space_or_tab(*p)) { - p++; - } - - if (p <= end) { - auto key_len = key_end - beg; - if (!key_len) { return false; } - - auto key = std::string(beg, key_end); - auto val = std::string(p, end); - - if (!detail::fields::is_field_value(val)) { return false; } - - // RFC 9110 §5.5: header field values are opaque octets and MUST NOT be - // percent-decoded by the recipient. Applications that need to interpret a - // value as a URI component should call httplib::decode_uri_component() - // (or decode_path_component()) explicitly. - fn(key, val); - - return true; - } - - return false; -} - -bool parse_trailers(stream_line_reader &line_reader, Headers &dest, - const Headers &src_headers) { - // NOTE: In RFC 9112, '7.1 Chunked Transfer Coding' mentions "The chunked - // transfer coding is complete when a chunk with a chunk-size of zero is - // received, possibly followed by a trailer section, and finally terminated by - // an empty line". https://www.rfc-editor.org/rfc/rfc9112.html#section-7.1 - // - // In '7.1.3. Decoding Chunked', however, the pseudo-code in the section - // doesn't care for the existence of the final CRLF. In other words, it seems - // to be ok whether the final CRLF exists or not in the chunked data. - // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.1.3 - // - // According to the reference code in RFC 9112, cpp-httplib now allows - // chunked transfer coding data without the final CRLF. - - // RFC 7230 Section 4.1.2 - Headers prohibited in trailers - thread_local case_ignore::unordered_set prohibited_trailers = { - "transfer-encoding", - "content-length", - "host", - "authorization", - "www-authenticate", - "proxy-authenticate", - "proxy-authorization", - "cookie", - "set-cookie", - "cache-control", - "expect", - "max-forwards", - "pragma", - "range", - "te", - "age", - "expires", - "date", - "location", - "retry-after", - "vary", - "warning", - "content-encoding", - "content-type", - "content-range", - "trailer"}; - - case_ignore::unordered_set declared_trailers; - auto trailer_header = get_header_value(src_headers, "Trailer", "", 0); - if (trailer_header && std::strlen(trailer_header)) { - auto len = std::strlen(trailer_header); - split(trailer_header, trailer_header + len, ',', - [&](const char *b, const char *e) { - const char *kbeg = b; - const char *kend = e; - while (kbeg < kend && (*kbeg == ' ' || *kbeg == '\t')) { - ++kbeg; - } - while (kend > kbeg && (kend[-1] == ' ' || kend[-1] == '\t')) { - --kend; - } - std::string key(kbeg, static_cast(kend - kbeg)); - if (!key.empty() && - prohibited_trailers.find(key) == prohibited_trailers.end()) { - declared_trailers.insert(key); - } - }); - } - - size_t trailer_header_count = 0; - while (strcmp(line_reader.ptr(), "\r\n") != 0) { - if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } - if (trailer_header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; } - - constexpr auto line_terminator_len = 2; - auto line_beg = line_reader.ptr(); - auto line_end = - line_reader.ptr() + line_reader.size() - line_terminator_len; - - if (!parse_header(line_beg, line_end, - [&](const std::string &key, const std::string &val) { - if (declared_trailers.find(key) != - declared_trailers.end()) { - dest.emplace(key, val); - trailer_header_count++; - } - })) { - return false; - } - - if (!line_reader.getline()) { return false; } - } - - return true; -} - -std::pair trim(const char *b, const char *e, size_t left, - size_t right) { - while (b + left < e && is_space_or_tab(b[left])) { - left++; - } - while (right > 0 && is_space_or_tab(b[right - 1])) { - right--; - } - return std::make_pair(left, right); -} - -std::string trim_copy(const std::string &s) { - auto r = trim(s.data(), s.data() + s.size(), 0, s.size()); - return s.substr(r.first, r.second - r.first); -} - -std::string trim_double_quotes_copy(const std::string &s) { - if (s.length() >= 2 && s.front() == '"' && s.back() == '"') { - return s.substr(1, s.size() - 2); - } - return s; -} - -void -divide(const char *data, std::size_t size, char d, - std::function - fn) { - const auto it = std::find(data, data + size, d); - const auto found = static_cast(it != data + size); - const auto lhs_data = data; - const auto lhs_size = static_cast(it - data); - const auto rhs_data = it + found; - const auto rhs_size = size - lhs_size - found; - - fn(lhs_data, lhs_size, rhs_data, rhs_size); -} - -void -divide(const std::string &str, char d, - std::function - fn) { - divide(str.data(), str.size(), d, std::move(fn)); -} - -void split(const char *b, const char *e, char d, - std::function fn) { - return split(b, e, d, (std::numeric_limits::max)(), std::move(fn)); -} - -void split(const char *b, const char *e, char d, size_t m, - std::function fn) { - size_t i = 0; - size_t beg = 0; - size_t count = 1; - - while (e ? (b + i < e) : (b[i] != '\0')) { - if (b[i] == d && count < m) { - auto r = trim(b, e, beg, i); - if (r.first < r.second) { fn(&b[r.first], &b[r.second]); } - beg = i + 1; - count++; - } - i++; - } - - if (i) { - auto r = trim(b, e, beg, i); - if (r.first < r.second) { fn(&b[r.first], &b[r.second]); } - } -} - -bool split_find(const char *b, const char *e, char d, size_t m, - std::function fn) { - size_t i = 0; - size_t beg = 0; - size_t count = 1; - - while (e ? (b + i < e) : (b[i] != '\0')) { - if (b[i] == d && count < m) { - auto r = trim(b, e, beg, i); - if (r.first < r.second) { - auto found = fn(&b[r.first], &b[r.second]); - if (found) { return true; } - } - beg = i + 1; - count++; - } - i++; - } - - if (i) { - auto r = trim(b, e, beg, i); - if (r.first < r.second) { - auto found = fn(&b[r.first], &b[r.second]); - if (found) { return true; } - } - } - - return false; -} - -bool split_find(const char *b, const char *e, char d, - std::function fn) { - return split_find(b, e, d, (std::numeric_limits::max)(), - std::move(fn)); -} - -stream_line_reader::stream_line_reader(Stream &strm, char *fixed_buffer, - size_t fixed_buffer_size) - : strm_(strm), fixed_buffer_(fixed_buffer), - fixed_buffer_size_(fixed_buffer_size) {} - -const char *stream_line_reader::ptr() const { - if (growable_buffer_.empty()) { - return fixed_buffer_; - } else { - return growable_buffer_.data(); - } -} - -size_t stream_line_reader::size() const { - if (growable_buffer_.empty()) { - return fixed_buffer_used_size_; - } else { - return growable_buffer_.size(); - } -} - -bool stream_line_reader::end_with_crlf() const { - auto end = ptr() + size(); - return size() >= 2 && end[-2] == '\r' && end[-1] == '\n'; -} - -bool stream_line_reader::getline() { - fixed_buffer_used_size_ = 0; - growable_buffer_.clear(); - -#ifndef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR - char prev_byte = 0; -#endif - - for (size_t i = 0;; i++) { - if (size() >= CPPHTTPLIB_MAX_LINE_LENGTH) { - // Treat exceptionally long lines as an error to - // prevent infinite loops/memory exhaustion - return false; - } - char byte; - auto n = strm_.read(&byte, 1); - - if (n < 0) { - return false; - } else if (n == 0) { - if (i == 0) { - return false; - } else { - break; - } - } - - append(byte); - -#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR - if (byte == '\n') { break; } -#else - if (prev_byte == '\r' && byte == '\n') { break; } - prev_byte = byte; -#endif - } - - return true; -} - -void stream_line_reader::append(char c) { - if (fixed_buffer_used_size_ < fixed_buffer_size_ - 1) { - fixed_buffer_[fixed_buffer_used_size_++] = c; - fixed_buffer_[fixed_buffer_used_size_] = '\0'; - } else { - if (growable_buffer_.empty()) { - assert(fixed_buffer_[fixed_buffer_used_size_] == '\0'); - growable_buffer_.assign(fixed_buffer_, fixed_buffer_used_size_); - } - growable_buffer_ += c; - } -} - -mmap::mmap(const char *path) { open(path); } - -mmap::~mmap() { close(); } - -bool mmap::open(const char *path) { - close(); - -#if defined(_WIN32) - auto wpath = u8string_to_wstring(path); - if (wpath.empty()) { return false; } - - hFile_ = - ::CreateFile2(wpath.c_str(), GENERIC_READ, - FILE_SHARE_READ | FILE_SHARE_WRITE, OPEN_EXISTING, NULL); - - if (hFile_ == INVALID_HANDLE_VALUE) { return false; } - - LARGE_INTEGER size{}; - if (!::GetFileSizeEx(hFile_, &size)) { return false; } - // If the following line doesn't compile due to QuadPart, update Windows SDK. - // See: - // https://github.com/yhirose/cpp-httplib/issues/1903#issuecomment-2316520721 - if (static_cast(size.QuadPart) > - (std::numeric_limits::max)()) { - // `size_t` might be 32-bits, on 32-bits Windows. - return false; - } - size_ = static_cast(size.QuadPart); - - hMapping_ = - ::CreateFileMappingFromApp(hFile_, NULL, PAGE_READONLY, size_, NULL); - - // Special treatment for an empty file... - if (hMapping_ == NULL && size_ == 0) { - close(); - is_open_empty_file = true; - return true; - } - - if (hMapping_ == NULL) { - close(); - return false; - } - - addr_ = ::MapViewOfFileFromApp(hMapping_, FILE_MAP_READ, 0, 0); - - if (addr_ == nullptr) { - close(); - return false; - } -#else - fd_ = ::open(path, O_RDONLY); - if (fd_ == -1) { return false; } - - struct stat sb; - if (fstat(fd_, &sb) == -1) { - close(); - return false; - } - size_ = static_cast(sb.st_size); - - addr_ = ::mmap(NULL, size_, PROT_READ, MAP_PRIVATE, fd_, 0); - - // Special treatment for an empty file... - if (addr_ == MAP_FAILED && size_ == 0) { - close(); - is_open_empty_file = true; - return false; - } -#endif - - return true; -} - -bool mmap::is_open() const { - return is_open_empty_file ? true : addr_ != nullptr; -} - -size_t mmap::size() const { return size_; } - -const char *mmap::data() const { - return is_open_empty_file ? "" : static_cast(addr_); -} - -void mmap::close() { -#if defined(_WIN32) - if (addr_) { - ::UnmapViewOfFile(addr_); - addr_ = nullptr; - } - - if (hMapping_) { - ::CloseHandle(hMapping_); - hMapping_ = NULL; - } - - if (hFile_ != INVALID_HANDLE_VALUE) { - ::CloseHandle(hFile_); - hFile_ = INVALID_HANDLE_VALUE; - } - - is_open_empty_file = false; -#else - if (addr_ != nullptr) { - munmap(addr_, size_); - addr_ = nullptr; - } - - if (fd_ != -1) { - ::close(fd_); - fd_ = -1; - } -#endif - size_ = 0; -} -int close_socket(socket_t sock) noexcept { -#ifdef _WIN32 - return closesocket(sock); -#else - return close(sock); -#endif -} - -template ssize_t handle_EINTR(T fn) { - ssize_t res = 0; - while (true) { - res = fn(); - if (res < 0 && errno == EINTR) { - std::this_thread::sleep_for(std::chrono::microseconds{1}); - continue; - } - break; - } - return res; -} - -ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags) { - return handle_EINTR([&]() { - return recv(sock, -#ifdef _WIN32 - static_cast(ptr), static_cast(size), -#else - ptr, size, -#endif - flags); - }); -} - -ssize_t send_socket(socket_t sock, const void *ptr, size_t size, - int flags) { - return handle_EINTR([&]() { - return send(sock, -#ifdef _WIN32 - static_cast(ptr), static_cast(size), -#else - ptr, size, -#endif - flags); - }); -} - -int poll_wrapper(struct pollfd *fds, nfds_t nfds, int timeout) { -#ifdef _WIN32 - return ::WSAPoll(fds, nfds, timeout); -#else - return ::poll(fds, nfds, timeout); -#endif -} - -ssize_t select_impl(socket_t sock, short events, time_t sec, - time_t usec) { - struct pollfd pfd; - pfd.fd = sock; - pfd.events = events; - pfd.revents = 0; - - auto timeout = static_cast(sec * 1000 + usec / 1000); - - return handle_EINTR([&]() { return poll_wrapper(&pfd, 1, timeout); }); -} - -ssize_t select_read(socket_t sock, time_t sec, time_t usec) { - return select_impl(sock, POLLIN, sec, usec); -} - -ssize_t select_write(socket_t sock, time_t sec, time_t usec) { - return select_impl(sock, POLLOUT, sec, usec); -} - -Error wait_until_socket_is_ready(socket_t sock, time_t sec, - time_t usec) { - struct pollfd pfd_read; - pfd_read.fd = sock; - pfd_read.events = POLLIN | POLLOUT; - pfd_read.revents = 0; - - auto timeout = static_cast(sec * 1000 + usec / 1000); - - auto poll_res = - handle_EINTR([&]() { return poll_wrapper(&pfd_read, 1, timeout); }); - - if (poll_res == 0) { return Error::ConnectionTimeout; } - - if (poll_res > 0 && pfd_read.revents & (POLLIN | POLLOUT)) { - auto error = 0; - socklen_t len = sizeof(error); - auto res = getsockopt(sock, SOL_SOCKET, SO_ERROR, - reinterpret_cast(&error), &len); - auto successful = res >= 0 && !error; - return successful ? Error::Success : Error::Connection; - } - - return Error::Connection; -} - -bool is_socket_alive(socket_t sock) { - const auto val = detail::select_read(sock, 0, 0); - if (val == 0) { - return true; - } else if (val < 0 && errno == EBADF) { - return false; - } - char buf[1]; - return detail::read_socket(sock, &buf[0], sizeof(buf), MSG_PEEK) > 0; -} - -class SocketStream final : public Stream { -public: - SocketStream(socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, - time_t write_timeout_sec, time_t write_timeout_usec, - time_t max_timeout_msec = 0, - std::chrono::time_point start_time = - (std::chrono::steady_clock::time_point::min)()); - ~SocketStream() override; - - bool is_readable() const override; - bool wait_readable() const override; - bool wait_writable() const override; - bool is_peer_alive() const override; - ssize_t read(char *ptr, size_t size) override; - ssize_t write(const char *ptr, size_t size) override; - void get_remote_ip_and_port(std::string &ip, int &port) const override; - void get_local_ip_and_port(std::string &ip, int &port) const override; - socket_t socket() const override; - time_t duration() const override; - void set_read_timeout(time_t sec, time_t usec = 0) override; - -private: - socket_t sock_; - time_t read_timeout_sec_; - time_t read_timeout_usec_; - time_t write_timeout_sec_; - time_t write_timeout_usec_; - time_t max_timeout_msec_; - const std::chrono::time_point start_time_; - - std::vector read_buff_; - size_t read_buff_off_ = 0; - size_t read_buff_content_size_ = 0; - - static const size_t read_buff_size_ = 1024l * 4; -}; - -bool keep_alive(const std::atomic &svr_sock, socket_t sock, - time_t keep_alive_timeout_sec) { - using namespace std::chrono; - - const auto interval_usec = - CPPHTTPLIB_KEEPALIVE_TIMEOUT_CHECK_INTERVAL_USECOND; - - // Avoid expensive `steady_clock::now()` call for the first time - if (select_read(sock, 0, interval_usec) > 0) { return true; } - - const auto start = steady_clock::now() - microseconds{interval_usec}; - const auto timeout = seconds{keep_alive_timeout_sec}; - - while (true) { - if (svr_sock == INVALID_SOCKET) { - break; // Server socket is closed - } - - auto val = select_read(sock, 0, interval_usec); - if (val < 0) { - break; // Ssocket error - } else if (val == 0) { - if (steady_clock::now() - start > timeout) { - break; // Timeout - } - } else { - return true; // Ready for read - } - } - - return false; -} - -template -bool -process_server_socket_core(const std::atomic &svr_sock, socket_t sock, - size_t keep_alive_max_count, - time_t keep_alive_timeout_sec, T callback) { - assert(keep_alive_max_count > 0); - auto ret = false; - auto count = keep_alive_max_count; - while (count > 0 && keep_alive(svr_sock, sock, keep_alive_timeout_sec)) { - auto close_connection = count == 1; - auto connection_closed = false; - ret = callback(close_connection, connection_closed); - if (!ret || connection_closed) { break; } - count--; - } - return ret; -} - -template -bool -process_server_socket(const std::atomic &svr_sock, socket_t sock, - size_t keep_alive_max_count, - time_t keep_alive_timeout_sec, time_t read_timeout_sec, - time_t read_timeout_usec, time_t write_timeout_sec, - time_t write_timeout_usec, T callback) { - return process_server_socket_core( - svr_sock, sock, keep_alive_max_count, keep_alive_timeout_sec, - [&](bool close_connection, bool &connection_closed) { - SocketStream strm(sock, read_timeout_sec, read_timeout_usec, - write_timeout_sec, write_timeout_usec); - return callback(strm, close_connection, connection_closed); - }); -} - -bool process_client_socket( - socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, - time_t write_timeout_sec, time_t write_timeout_usec, - time_t max_timeout_msec, - std::chrono::time_point start_time, - std::function callback) { - SocketStream strm(sock, read_timeout_sec, read_timeout_usec, - write_timeout_sec, write_timeout_usec, max_timeout_msec, - start_time); - return callback(strm); -} - -int shutdown_socket(socket_t sock) noexcept { -#ifdef _WIN32 - return shutdown(sock, SD_BOTH); -#else - return shutdown(sock, SHUT_RDWR); -#endif -} - -std::string escape_abstract_namespace_unix_domain(const std::string &s) { - if (s.size() > 1 && s[0] == '\0') { - auto ret = s; - ret[0] = '@'; - return ret; - } - return s; -} - -std::string -unescape_abstract_namespace_unix_domain(const std::string &s) { - if (s.size() > 1 && s[0] == '@') { - auto ret = s; - ret[0] = '\0'; - return ret; - } - return s; -} - -int getaddrinfo_with_timeout(const char *node, const char *service, - const struct addrinfo *hints, - struct addrinfo **res, time_t timeout_sec) { -#ifdef CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO - if (timeout_sec <= 0) { - // No timeout specified, use standard getaddrinfo - return getaddrinfo(node, service, hints, res); - } - -#ifdef _WIN32 - // Windows-specific implementation using GetAddrInfoEx with overlapped I/O - OVERLAPPED overlapped = {}; - HANDLE event = CreateEventW(nullptr, TRUE, FALSE, nullptr); - if (!event) { return EAI_FAIL; } - - overlapped.hEvent = event; - - PADDRINFOEXW result_addrinfo = nullptr; - HANDLE cancel_handle = nullptr; - - ADDRINFOEXW hints_ex = {}; - if (hints) { - hints_ex.ai_flags = hints->ai_flags; - hints_ex.ai_family = hints->ai_family; - hints_ex.ai_socktype = hints->ai_socktype; - hints_ex.ai_protocol = hints->ai_protocol; - } - - auto wnode = u8string_to_wstring(node); - auto wservice = u8string_to_wstring(service); - - auto ret = ::GetAddrInfoExW(wnode.data(), wservice.data(), NS_DNS, nullptr, - hints ? &hints_ex : nullptr, &result_addrinfo, - nullptr, &overlapped, nullptr, &cancel_handle); - - if (ret == WSA_IO_PENDING) { - auto wait_result = - ::WaitForSingleObject(event, static_cast(timeout_sec * 1000)); - if (wait_result == WAIT_TIMEOUT) { - if (cancel_handle) { ::GetAddrInfoExCancel(&cancel_handle); } - ::CloseHandle(event); - return EAI_AGAIN; - } - - DWORD bytes_returned; - if (!::GetOverlappedResult((HANDLE)INVALID_SOCKET, &overlapped, - &bytes_returned, FALSE)) { - ::CloseHandle(event); - return ::WSAGetLastError(); - } - } - - ::CloseHandle(event); - - if (ret == NO_ERROR || ret == WSA_IO_PENDING) { - *res = reinterpret_cast(result_addrinfo); - return 0; - } - - return ret; -#elif TARGET_OS_MAC && defined(__clang__) - if (!node) { return EAI_NONAME; } - // macOS implementation using CFHost API for asynchronous DNS resolution - CFStringRef hostname_ref = CFStringCreateWithCString( - kCFAllocatorDefault, node, kCFStringEncodingUTF8); - if (!hostname_ref) { return EAI_MEMORY; } - - CFHostRef host_ref = CFHostCreateWithName(kCFAllocatorDefault, hostname_ref); - CFRelease(hostname_ref); - if (!host_ref) { return EAI_MEMORY; } - - // Set up context for callback - struct CFHostContext { - bool completed = false; - bool success = false; - CFArrayRef addresses = nullptr; - std::mutex mutex; - std::condition_variable cv; - } context; - - CFHostClientContext client_context; - memset(&client_context, 0, sizeof(client_context)); - client_context.info = &context; - - // Set callback - auto callback = [](CFHostRef theHost, CFHostInfoType /*typeInfo*/, - const CFStreamError *error, void *info) { - auto ctx = static_cast(info); - std::lock_guard lock(ctx->mutex); - - if (error && error->error != 0) { - ctx->success = false; - } else { - Boolean hasBeenResolved; - ctx->addresses = CFHostGetAddressing(theHost, &hasBeenResolved); - if (ctx->addresses && hasBeenResolved) { - CFRetain(ctx->addresses); - ctx->success = true; - } else { - ctx->success = false; - } - } - ctx->completed = true; - ctx->cv.notify_one(); - }; - - if (!CFHostSetClient(host_ref, callback, &client_context)) { - CFRelease(host_ref); - return EAI_SYSTEM; - } - - // Schedule on run loop - CFRunLoopRef run_loop = CFRunLoopGetCurrent(); - CFHostScheduleWithRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode); - - // Start resolution - CFStreamError stream_error; - if (!CFHostStartInfoResolution(host_ref, kCFHostAddresses, &stream_error)) { - CFHostUnscheduleFromRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode); - CFRelease(host_ref); - return EAI_FAIL; - } - - // Wait for completion with timeout - auto timeout_time = - std::chrono::steady_clock::now() + std::chrono::seconds(timeout_sec); - bool timed_out = false; - - { - std::unique_lock lock(context.mutex); - - while (!context.completed) { - auto now = std::chrono::steady_clock::now(); - if (now >= timeout_time) { - timed_out = true; - break; - } - - // Run the runloop for a short time - lock.unlock(); - CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, true); - lock.lock(); - } - } - - // Clean up - CFHostUnscheduleFromRunLoop(host_ref, run_loop, kCFRunLoopDefaultMode); - CFHostSetClient(host_ref, nullptr, nullptr); - - if (timed_out || !context.completed) { - CFHostCancelInfoResolution(host_ref, kCFHostAddresses); - CFRelease(host_ref); - return EAI_AGAIN; - } - - if (!context.success || !context.addresses) { - CFRelease(host_ref); - return EAI_NODATA; - } - - // Convert CFArray to addrinfo - CFIndex count = CFArrayGetCount(context.addresses); - if (count == 0) { - CFRelease(context.addresses); - CFRelease(host_ref); - return EAI_NODATA; - } - - struct addrinfo *result_addrinfo = nullptr; - struct addrinfo **current = &result_addrinfo; - - for (CFIndex i = 0; i < count; i++) { - CFDataRef addr_data = - static_cast(CFArrayGetValueAtIndex(context.addresses, i)); - if (!addr_data) continue; - - const struct sockaddr *sockaddr_ptr = - reinterpret_cast(CFDataGetBytePtr(addr_data)); - socklen_t sockaddr_len = static_cast(CFDataGetLength(addr_data)); - - // Allocate addrinfo structure - *current = static_cast(malloc(sizeof(struct addrinfo))); - if (!*current) { - freeaddrinfo(result_addrinfo); - CFRelease(context.addresses); - CFRelease(host_ref); - return EAI_MEMORY; - } - - memset(*current, 0, sizeof(struct addrinfo)); - - // Set up addrinfo fields - (*current)->ai_family = sockaddr_ptr->sa_family; - (*current)->ai_socktype = hints ? hints->ai_socktype : SOCK_STREAM; - (*current)->ai_protocol = hints ? hints->ai_protocol : IPPROTO_TCP; - (*current)->ai_addrlen = sockaddr_len; - - // Copy sockaddr - (*current)->ai_addr = static_cast(malloc(sockaddr_len)); - if (!(*current)->ai_addr) { - freeaddrinfo(result_addrinfo); - CFRelease(context.addresses); - CFRelease(host_ref); - return EAI_MEMORY; - } - memcpy((*current)->ai_addr, sockaddr_ptr, sockaddr_len); - - // Set port if service is specified - if (service && *service) { - int port = 0; - if (parse_port(service, strlen(service), port)) { - if (sockaddr_ptr->sa_family == AF_INET) { - reinterpret_cast((*current)->ai_addr) - ->sin_port = htons(static_cast(port)); - } else if (sockaddr_ptr->sa_family == AF_INET6) { - reinterpret_cast((*current)->ai_addr) - ->sin6_port = htons(static_cast(port)); - } - } - } - - current = &((*current)->ai_next); - } - - CFRelease(context.addresses); - CFRelease(host_ref); - - *res = result_addrinfo; - return 0; -#elif defined(_GNU_SOURCE) && defined(__GLIBC__) && \ - (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 2)) - // #2431: gai_cancel() is non-blocking and may return EAI_NOTCANCELED while - // the resolver worker still references the stack-local gaicb. The cancel - // path therefore waits (gai_suspend with no timeout) for the worker to - // actually finish before letting the stack frame go. The trade-off is that - // a wedged DNS server can hold this thread for the system resolver timeout - // (~30s by default) past the caller's connection timeout. - struct gaicb request {}; - struct gaicb *requests[1] = {&request}; - struct sigevent sevp {}; - struct timespec timeout { - timeout_sec, 0 - }; - - request.ar_name = node; - request.ar_service = service; - request.ar_request = hints; - sevp.sigev_notify = SIGEV_NONE; - - int rc = getaddrinfo_a(GAI_NOWAIT, requests, 1, &sevp); - if (rc != 0) { return rc; } - - auto cleanup = scope_exit([&] { - if (request.ar_result) { freeaddrinfo(request.ar_result); } - }); - - int wait_result = gai_suspend(requests, 1, &timeout); - - if (wait_result == 0 || wait_result == EAI_ALLDONE) { - int gai_result = gai_error(&request); - if (gai_result == 0) { - *res = request.ar_result; - request.ar_result = nullptr; - return 0; - } - return gai_result; - } - - gai_cancel(&request); - while (gai_error(&request) == EAI_INPROGRESS) { - gai_suspend(requests, 1, nullptr); - } - return wait_result; -#else - // Fallback implementation using thread-based timeout for other Unix systems. - - struct GetAddrInfoState { - ~GetAddrInfoState() { - if (info) { freeaddrinfo(info); } - } - - std::mutex mutex; - std::condition_variable result_cv; - bool completed = false; - int result = EAI_SYSTEM; - std::string node; - std::string service; - struct addrinfo hints; - struct addrinfo *info = nullptr; - }; - - // Allocate on the heap, so the resolver thread can keep using the data. - auto state = std::make_shared(); - if (node) { state->node = node; } - state->service = service; - state->hints = *hints; - - std::thread resolve_thread([state]() { - auto thread_result = - getaddrinfo(state->node.c_str(), state->service.c_str(), &state->hints, - &state->info); - - std::lock_guard lock(state->mutex); - state->result = thread_result; - state->completed = true; - state->result_cv.notify_one(); - }); - - // Wait for completion or timeout - std::unique_lock lock(state->mutex); - auto finished = - state->result_cv.wait_for(lock, std::chrono::seconds(timeout_sec), - [&] { return state->completed; }); - - if (finished) { - // Operation completed within timeout - resolve_thread.join(); - *res = state->info; - state->info = nullptr; // Pass ownership to caller - return state->result; - } else { - // Timeout occurred - resolve_thread.detach(); // Let the thread finish in background - return EAI_AGAIN; // Return timeout error - } -#endif -#else - (void)(timeout_sec); // Unused parameter for non-blocking getaddrinfo - return getaddrinfo(node, service, hints, res); -#endif -} - -template -socket_t create_socket(const std::string &host, const std::string &ip, int port, - int address_family, int socket_flags, bool tcp_nodelay, - bool ipv6_v6only, SocketOptions socket_options, - BindOrConnect bind_or_connect, time_t timeout_sec = 0) { - // Get address info - const char *node = nullptr; - struct addrinfo hints; - struct addrinfo *result; - - memset(&hints, 0, sizeof(struct addrinfo)); - hints.ai_socktype = SOCK_STREAM; - hints.ai_protocol = IPPROTO_IP; - - if (!ip.empty()) { - node = ip.c_str(); - // Ask getaddrinfo to convert IP in c-string to address - hints.ai_family = AF_UNSPEC; - hints.ai_flags = AI_NUMERICHOST; - } else { - if (!host.empty()) { node = host.c_str(); } - hints.ai_family = address_family; - hints.ai_flags = socket_flags; - } - -#if !defined(_WIN32) || defined(CPPHTTPLIB_HAVE_AFUNIX_H) - if (hints.ai_family == AF_UNIX) { - const auto addrlen = host.length(); - if (addrlen > sizeof(sockaddr_un::sun_path)) { return INVALID_SOCKET; } - -#ifdef SOCK_CLOEXEC - auto sock = socket(hints.ai_family, hints.ai_socktype | SOCK_CLOEXEC, - hints.ai_protocol); -#else - auto sock = socket(hints.ai_family, hints.ai_socktype, hints.ai_protocol); -#endif - - if (sock != INVALID_SOCKET) { - sockaddr_un addr{}; - addr.sun_family = AF_UNIX; - - auto unescaped_host = unescape_abstract_namespace_unix_domain(host); - std::copy(unescaped_host.begin(), unescaped_host.end(), addr.sun_path); - - hints.ai_addr = reinterpret_cast(&addr); - hints.ai_addrlen = static_cast( - sizeof(addr) - sizeof(addr.sun_path) + addrlen); - -#ifndef SOCK_CLOEXEC -#ifndef _WIN32 - fcntl(sock, F_SETFD, FD_CLOEXEC); -#endif -#endif - - if (socket_options) { socket_options(sock); } - -#ifdef _WIN32 - // Setting SO_REUSEADDR seems not to work well with AF_UNIX on windows, so - // remove the option. - set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 0); -#endif - - bool dummy; - if (!bind_or_connect(sock, hints, dummy)) { - close_socket(sock); - sock = INVALID_SOCKET; - } - } - return sock; - } -#endif - - auto service = std::to_string(port); - - if (getaddrinfo_with_timeout(node, service.c_str(), &hints, &result, - timeout_sec)) { -#if defined __linux__ && !defined __ANDROID__ - res_init(); -#endif - return INVALID_SOCKET; - } - auto se = detail::scope_exit([&] { freeaddrinfo(result); }); - - for (auto rp = result; rp; rp = rp->ai_next) { - // Create a socket -#ifdef _WIN32 - auto sock = - WSASocketW(rp->ai_family, rp->ai_socktype, rp->ai_protocol, nullptr, 0, - WSA_FLAG_NO_HANDLE_INHERIT | WSA_FLAG_OVERLAPPED); - /** - * Since the WSA_FLAG_NO_HANDLE_INHERIT is only supported on Windows 7 SP1 - * and above the socket creation fails on older Windows Systems. - * - * Let's try to create a socket the old way in this case. - * - * Reference: - * https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketa - * - * WSA_FLAG_NO_HANDLE_INHERIT: - * This flag is supported on Windows 7 with SP1, Windows Server 2008 R2 with - * SP1, and later - * - */ - if (sock == INVALID_SOCKET) { - sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); - } -#else - -#ifdef SOCK_CLOEXEC - auto sock = - socket(rp->ai_family, rp->ai_socktype | SOCK_CLOEXEC, rp->ai_protocol); -#else - auto sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); -#endif - -#endif - if (sock == INVALID_SOCKET) { continue; } - -#if !defined _WIN32 && !defined SOCK_CLOEXEC - if (fcntl(sock, F_SETFD, FD_CLOEXEC) == -1) { - close_socket(sock); - continue; - } -#endif - - if (tcp_nodelay) { set_socket_opt(sock, IPPROTO_TCP, TCP_NODELAY, 1); } - - if (rp->ai_family == AF_INET6) { - set_socket_opt(sock, IPPROTO_IPV6, IPV6_V6ONLY, ipv6_v6only ? 1 : 0); - } - - if (socket_options) { socket_options(sock); } - - // bind or connect - auto quit = false; - if (bind_or_connect(sock, *rp, quit)) { return sock; } - - close_socket(sock); - - if (quit) { break; } - } - - return INVALID_SOCKET; -} - -void set_nonblocking(socket_t sock, bool nonblocking) { -#ifdef _WIN32 - auto flags = nonblocking ? 1UL : 0UL; - ioctlsocket(sock, FIONBIO, &flags); -#else - auto flags = fcntl(sock, F_GETFL, 0); - fcntl(sock, F_SETFL, - nonblocking ? (flags | O_NONBLOCK) : (flags & (~O_NONBLOCK))); -#endif -} - -bool is_connection_error() { -#ifdef _WIN32 - return WSAGetLastError() != WSAEWOULDBLOCK; -#else - return errno != EINPROGRESS; -#endif -} - -bool bind_ip_address(socket_t sock, const std::string &host) { - struct addrinfo hints; - struct addrinfo *result; - - memset(&hints, 0, sizeof(struct addrinfo)); - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - hints.ai_protocol = 0; - - if (getaddrinfo_with_timeout(host.c_str(), "0", &hints, &result, 0)) { - return false; - } - - auto se = detail::scope_exit([&] { freeaddrinfo(result); }); - - auto ret = false; - for (auto rp = result; rp; rp = rp->ai_next) { - const auto &ai = *rp; - if (!::bind(sock, ai.ai_addr, static_cast(ai.ai_addrlen))) { - ret = true; - break; - } - } - - return ret; -} - -#if !defined _WIN32 && !defined ANDROID && !defined _AIX && !defined __MVS__ -#define USE_IF2IP -#endif - -#ifdef USE_IF2IP -std::string if2ip(int address_family, const std::string &ifn) { - struct ifaddrs *ifap; - getifaddrs(&ifap); - auto se = detail::scope_exit([&] { freeifaddrs(ifap); }); - - std::string addr_candidate; - for (auto ifa = ifap; ifa; ifa = ifa->ifa_next) { - if (ifa->ifa_addr && ifn == ifa->ifa_name && - (AF_UNSPEC == address_family || - ifa->ifa_addr->sa_family == address_family)) { - if (ifa->ifa_addr->sa_family == AF_INET) { - auto sa = reinterpret_cast(ifa->ifa_addr); - char buf[INET_ADDRSTRLEN]; - if (inet_ntop(AF_INET, &sa->sin_addr, buf, INET_ADDRSTRLEN)) { - return std::string(buf, INET_ADDRSTRLEN); - } - } else if (ifa->ifa_addr->sa_family == AF_INET6) { - auto sa = reinterpret_cast(ifa->ifa_addr); - if (!IN6_IS_ADDR_LINKLOCAL(&sa->sin6_addr)) { - char buf[INET6_ADDRSTRLEN] = {}; - if (inet_ntop(AF_INET6, &sa->sin6_addr, buf, INET6_ADDRSTRLEN)) { - // equivalent to mac's IN6_IS_ADDR_UNIQUE_LOCAL - auto s6_addr_head = sa->sin6_addr.s6_addr[0]; - if (s6_addr_head == 0xfc || s6_addr_head == 0xfd) { - addr_candidate = std::string(buf, INET6_ADDRSTRLEN); - } else { - return std::string(buf, INET6_ADDRSTRLEN); - } - } - } - } - } - } - return addr_candidate; -} -#endif - -socket_t create_client_socket( - const std::string &host, const std::string &ip, int port, - int address_family, bool tcp_nodelay, bool ipv6_v6only, - SocketOptions socket_options, time_t connection_timeout_sec, - time_t connection_timeout_usec, time_t read_timeout_sec, - time_t read_timeout_usec, time_t write_timeout_sec, - time_t write_timeout_usec, const std::string &intf, Error &error) { - auto sock = create_socket( - host, ip, port, address_family, 0, tcp_nodelay, ipv6_v6only, - std::move(socket_options), - [&](socket_t sock2, struct addrinfo &ai, bool &quit) -> bool { - if (!intf.empty()) { -#ifdef USE_IF2IP - auto ip_from_if = if2ip(address_family, intf); - if (ip_from_if.empty()) { ip_from_if = intf; } - if (!bind_ip_address(sock2, ip_from_if)) { - error = Error::BindIPAddress; - return false; - } -#endif - } - - set_nonblocking(sock2, true); - - auto ret = - ::connect(sock2, ai.ai_addr, static_cast(ai.ai_addrlen)); - - if (ret < 0) { - if (is_connection_error()) { - error = Error::Connection; - return false; - } - error = wait_until_socket_is_ready(sock2, connection_timeout_sec, - connection_timeout_usec); - if (error != Error::Success) { - if (error == Error::ConnectionTimeout) { quit = true; } - return false; - } - } - - set_nonblocking(sock2, false); - set_socket_opt_time(sock2, SOL_SOCKET, SO_RCVTIMEO, read_timeout_sec, - read_timeout_usec); - set_socket_opt_time(sock2, SOL_SOCKET, SO_SNDTIMEO, write_timeout_sec, - write_timeout_usec); - - error = Error::Success; - return true; - }, - connection_timeout_sec); // Pass DNS timeout - - if (sock != INVALID_SOCKET) { - error = Error::Success; - } else { - if (error == Error::Success) { error = Error::Connection; } - } - - return sock; -} - -bool get_ip_and_port(const struct sockaddr_storage &addr, - socklen_t addr_len, std::string &ip, int &port) { - if (addr.ss_family == AF_INET) { - port = ntohs(reinterpret_cast(&addr)->sin_port); - } else if (addr.ss_family == AF_INET6) { - port = - ntohs(reinterpret_cast(&addr)->sin6_port); - } else { - return false; - } - - std::array ipstr{}; - if (getnameinfo(reinterpret_cast(&addr), addr_len, - ipstr.data(), static_cast(ipstr.size()), nullptr, - 0, NI_NUMERICHOST)) { - return false; - } - - ip = ipstr.data(); - return true; -} - -void get_local_ip_and_port(socket_t sock, std::string &ip, int &port) { - struct sockaddr_storage addr; - socklen_t addr_len = sizeof(addr); - if (!getsockname(sock, reinterpret_cast(&addr), - &addr_len)) { - get_ip_and_port(addr, addr_len, ip, port); - } -} - -void get_remote_ip_and_port(socket_t sock, std::string &ip, int &port) { - struct sockaddr_storage addr; - socklen_t addr_len = sizeof(addr); - - if (!getpeername(sock, reinterpret_cast(&addr), - &addr_len)) { -#ifndef _WIN32 - if (addr.ss_family == AF_UNIX) { -#if defined(__linux__) - struct ucred ucred; - socklen_t len = sizeof(ucred); - if (getsockopt(sock, SOL_SOCKET, SO_PEERCRED, &ucred, &len) == 0) { - port = ucred.pid; - } -#elif defined(SOL_LOCAL) && defined(SO_PEERPID) - pid_t pid; - socklen_t len = sizeof(pid); - if (getsockopt(sock, SOL_LOCAL, SO_PEERPID, &pid, &len) == 0) { - port = pid; - } -#endif - return; - } -#endif - get_ip_and_port(addr, addr_len, ip, port); - } -} - -// Recursive form retained so operator""_t below can compute hashes for -// switch-case labels at compile time (C++11 constexpr forbids loops). Do not -// call from runtime paths with arbitrary-length inputs — use str2tag() -// instead, which is iterative and stack-safe. -constexpr unsigned int str2tag_core(const char *s, size_t l, - unsigned int h) { - return (l == 0) - ? h - : str2tag_core( - s + 1, l - 1, - // Unsets the 6 high bits of h, therefore no overflow happens - (((std::numeric_limits::max)() >> 6) & - h * 33) ^ - static_cast(*s)); -} - -unsigned int str2tag(const std::string &s) { - // Iterative form of str2tag_core: the recursive constexpr version is kept - // for compile-time UDL evaluation of short string literals, but at runtime - // we may receive arbitrarily long inputs (e.g. fuzzed Content-Type) that - // would blow the stack with one frame per character. - unsigned int h = 0; - for (auto c : s) { - h = (((std::numeric_limits::max)() >> 6) & h * 33) ^ - static_cast(c); - } - return h; -} - -namespace udl { - -constexpr unsigned int operator""_t(const char *s, size_t l) { - return str2tag_core(s, l, 0); -} - -} // namespace udl - -std::string -find_content_type(const std::string &path, - const std::map &user_data, - const std::string &default_content_type) { - auto ext = file_extension(path); - - auto it = user_data.find(ext); - if (it != user_data.end()) { return it->second; } - - using udl::operator""_t; - - switch (str2tag(ext)) { - default: return default_content_type; - - case "css"_t: return "text/css"; - case "csv"_t: return "text/csv"; - case "htm"_t: - case "html"_t: return "text/html"; - case "js"_t: - case "mjs"_t: return "text/javascript"; - case "txt"_t: return "text/plain"; - case "vtt"_t: return "text/vtt"; - - case "apng"_t: return "image/apng"; - case "avif"_t: return "image/avif"; - case "bmp"_t: return "image/bmp"; - case "gif"_t: return "image/gif"; - case "png"_t: return "image/png"; - case "svg"_t: return "image/svg+xml"; - case "webp"_t: return "image/webp"; - case "ico"_t: return "image/x-icon"; - case "tif"_t: return "image/tiff"; - case "tiff"_t: return "image/tiff"; - case "jpg"_t: - case "jpeg"_t: return "image/jpeg"; - - case "mp4"_t: return "video/mp4"; - case "mpeg"_t: return "video/mpeg"; - case "webm"_t: return "video/webm"; - - case "mp3"_t: return "audio/mp3"; - case "mpga"_t: return "audio/mpeg"; - case "weba"_t: return "audio/webm"; - case "wav"_t: return "audio/wave"; - - case "otf"_t: return "font/otf"; - case "ttf"_t: return "font/ttf"; - case "woff"_t: return "font/woff"; - case "woff2"_t: return "font/woff2"; - - case "7z"_t: return "application/x-7z-compressed"; - case "atom"_t: return "application/atom+xml"; - case "pdf"_t: return "application/pdf"; - case "json"_t: return "application/json"; - case "rss"_t: return "application/rss+xml"; - case "tar"_t: return "application/x-tar"; - case "xht"_t: - case "xhtml"_t: return "application/xhtml+xml"; - case "xslt"_t: return "application/xslt+xml"; - case "xml"_t: return "application/xml"; - case "gz"_t: return "application/gzip"; - case "zip"_t: return "application/zip"; - case "wasm"_t: return "application/wasm"; - } -} - -std::string -extract_media_type(const std::string &content_type, - std::map *params = nullptr) { - // Extract type/subtype from Content-Type value (RFC 2045) - // e.g. "application/json; charset=utf-8" -> "application/json" - auto media_type = content_type; - auto semicolon_pos = media_type.find(';'); - if (semicolon_pos != std::string::npos) { - auto param_str = media_type.substr(semicolon_pos + 1); - media_type = media_type.substr(0, semicolon_pos); - - if (params) { - // Parse parameters: key=value pairs separated by ';' - split(param_str.data(), param_str.data() + param_str.size(), ';', - [&](const char *b, const char *e) { - std::string key; - std::string val; - split(b, e, '=', [&](const char *b2, const char *e2) { - if (key.empty()) { - key.assign(b2, e2); - } else { - val.assign(b2, e2); - } - }); - if (!key.empty()) { - params->emplace(trim_copy(key), trim_double_quotes_copy(val)); - } - }); - } - } - - // Trim whitespace from media type - return trim_copy(media_type); -} - -bool can_compress_content_type(const std::string &content_type) { - using udl::operator""_t; - - auto mime_type = extract_media_type(content_type); - auto tag = str2tag(mime_type); - - switch (tag) { - case "image/svg+xml"_t: - case "application/javascript"_t: - case "application/x-javascript"_t: - case "application/json"_t: - case "application/ld+json"_t: - case "application/xml"_t: - case "application/xhtml+xml"_t: - case "application/rss+xml"_t: - case "application/atom+xml"_t: - case "application/xslt+xml"_t: - case "application/protobuf"_t: return true; - - case "text/event-stream"_t: return false; - - default: return !mime_type.rfind("text/", 0); - } -} - -bool parse_quality(const char *b, const char *e, std::string &token, - double &quality) { - quality = 1.0; - token.clear(); - - // Split on first ';': left = token name, right = parameters - const char *params_b = nullptr; - std::size_t params_len = 0; - - divide( - b, static_cast(e - b), ';', - [&](const char *lb, std::size_t llen, const char *rb, std::size_t rlen) { - auto r = trim(lb, lb + llen, 0, llen); - if (r.first < r.second) { token.assign(lb + r.first, lb + r.second); } - params_b = rb; - params_len = rlen; - }); - - if (token.empty()) { return false; } - if (params_len == 0) { return true; } - - // Scan parameters for q= (stops on first match) - bool invalid = false; - split_find(params_b, params_b + params_len, ';', - (std::numeric_limits::max)(), - [&](const char *pb, const char *pe) -> bool { - // Match exactly "q=" or "Q=" (not "query=" etc.) - auto len = static_cast(pe - pb); - if (len < 2) { return false; } - if ((pb[0] != 'q' && pb[0] != 'Q') || pb[1] != '=') { - return false; - } - - // Trim the value portion - auto r = trim(pb, pe, 2, len); - if (r.first >= r.second) { - invalid = true; - return true; - } - - double v = 0.0; - auto res = from_chars(pb + r.first, pb + r.second, v); - if (res.ec != std::errc{} || v < 0.0 || v > 1.0) { - invalid = true; - return true; - } - quality = v; - return true; - }); - - return !invalid; -} - -EncodingType encoding_type(const Request &req, const Response &res) { - if (!can_compress_content_type(res.get_header_value("Content-Type"))) { - return EncodingType::None; - } - - const auto &s = req.get_header_value("Accept-Encoding"); - if (s.empty()) { return EncodingType::None; } - - // Single-pass: iterate tokens and track the best supported encoding. - // Server preference breaks ties (br > gzip > zstd). - EncodingType best = EncodingType::None; - double best_q = 0.0; // q=0 means "not acceptable" - - // Server preference: Brotli > Gzip > Zstd (lower = more preferred) - auto priority = [](EncodingType t) -> int { - switch (t) { - case EncodingType::Brotli: return 0; - case EncodingType::Gzip: return 1; - case EncodingType::Zstd: return 2; - default: return 3; - } - }; - - std::string name; - split(s.data(), s.data() + s.size(), ',', [&](const char *b, const char *e) { - double quality = 1.0; - if (!parse_quality(b, e, name, quality)) { return; } - if (quality <= 0.0) { return; } - - EncodingType type = EncodingType::None; -#ifdef CPPHTTPLIB_BROTLI_SUPPORT - if (case_ignore::equal(name, "br")) { type = EncodingType::Brotli; } -#endif -#ifdef CPPHTTPLIB_ZLIB_SUPPORT - if (type == EncodingType::None && case_ignore::equal(name, "gzip")) { - type = EncodingType::Gzip; - } -#endif -#ifdef CPPHTTPLIB_ZSTD_SUPPORT - if (type == EncodingType::None && case_ignore::equal(name, "zstd")) { - type = EncodingType::Zstd; - } -#endif - - if (type == EncodingType::None) { return; } - - // Higher q-value wins; for equal q, server preference breaks ties - if (quality > best_q || - (quality == best_q && priority(type) < priority(best))) { - best_q = quality; - best = type; - } - }); - - return best; -} - -std::unique_ptr make_compressor(EncodingType type) { -#ifdef CPPHTTPLIB_ZLIB_SUPPORT - if (type == EncodingType::Gzip) { - return detail::make_unique(); - } -#endif -#ifdef CPPHTTPLIB_BROTLI_SUPPORT - if (type == EncodingType::Brotli) { - return detail::make_unique(); - } -#endif -#ifdef CPPHTTPLIB_ZSTD_SUPPORT - if (type == EncodingType::Zstd) { - return detail::make_unique(); - } -#endif - (void)type; - return nullptr; -} - -const char *encoding_name(EncodingType type) { - switch (type) { - case EncodingType::Gzip: return "gzip"; - case EncodingType::Brotli: return "br"; - case EncodingType::Zstd: return "zstd"; - default: return ""; - } -} - -bool nocompressor::compress(const char *data, size_t data_length, - bool /*last*/, Callback callback) { - if (!data_length) { return true; } - return callback(data, data_length); -} - -#ifdef CPPHTTPLIB_ZLIB_SUPPORT -gzip_compressor::gzip_compressor() { - std::memset(&strm_, 0, sizeof(strm_)); - strm_.zalloc = Z_NULL; - strm_.zfree = Z_NULL; - strm_.opaque = Z_NULL; - - is_valid_ = deflateInit2(&strm_, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8, - Z_DEFAULT_STRATEGY) == Z_OK; -} - -gzip_compressor::~gzip_compressor() { deflateEnd(&strm_); } - -bool gzip_compressor::compress(const char *data, size_t data_length, - bool last, Callback callback) { - assert(is_valid_); - - do { - constexpr size_t max_avail_in = - (std::numeric_limits::max)(); - - strm_.avail_in = static_cast( - (std::min)(data_length, max_avail_in)); - strm_.next_in = const_cast(reinterpret_cast(data)); - - data_length -= strm_.avail_in; - data += strm_.avail_in; - - auto flush = (last && data_length == 0) ? Z_FINISH : Z_NO_FLUSH; - auto ret = Z_OK; - - std::array buff{}; - do { - strm_.avail_out = static_cast(buff.size()); - strm_.next_out = reinterpret_cast(buff.data()); - - ret = deflate(&strm_, flush); - if (ret == Z_STREAM_ERROR) { return false; } - - if (!callback(buff.data(), buff.size() - strm_.avail_out)) { - return false; - } - } while (strm_.avail_out == 0); - - assert((flush == Z_FINISH && ret == Z_STREAM_END) || - (flush == Z_NO_FLUSH && ret == Z_OK)); - assert(strm_.avail_in == 0); - } while (data_length > 0); - - return true; -} - -gzip_decompressor::gzip_decompressor() { - std::memset(&strm_, 0, sizeof(strm_)); - strm_.zalloc = Z_NULL; - strm_.zfree = Z_NULL; - strm_.opaque = Z_NULL; - - // 15 is the value of wbits, which should be at the maximum possible value - // to ensure that any gzip stream can be decoded. The offset of 32 specifies - // that the stream type should be automatically detected either gzip or - // deflate. - is_valid_ = inflateInit2(&strm_, 32 + 15) == Z_OK; -} - -gzip_decompressor::~gzip_decompressor() { inflateEnd(&strm_); } - -bool gzip_decompressor::is_valid() const { return is_valid_; } - -bool gzip_decompressor::decompress(const char *data, size_t data_length, - Callback callback) { - assert(is_valid_); - - auto ret = Z_OK; - - do { - constexpr size_t max_avail_in = - (std::numeric_limits::max)(); - - strm_.avail_in = static_cast( - (std::min)(data_length, max_avail_in)); - strm_.next_in = const_cast(reinterpret_cast(data)); - - data_length -= strm_.avail_in; - data += strm_.avail_in; - - std::array buff{}; - while (strm_.avail_in > 0 && ret == Z_OK) { - strm_.avail_out = static_cast(buff.size()); - strm_.next_out = reinterpret_cast(buff.data()); - - ret = inflate(&strm_, Z_NO_FLUSH); - - assert(ret != Z_STREAM_ERROR); - switch (ret) { - case Z_NEED_DICT: - case Z_DATA_ERROR: - case Z_MEM_ERROR: inflateEnd(&strm_); return false; - } - - if (!callback(buff.data(), buff.size() - strm_.avail_out)) { - return false; - } - } - - if (ret != Z_OK && ret != Z_STREAM_END) { return false; } - - } while (data_length > 0); - - return true; -} -#endif - -#ifdef CPPHTTPLIB_BROTLI_SUPPORT -brotli_compressor::brotli_compressor() { - state_ = BrotliEncoderCreateInstance(nullptr, nullptr, nullptr); -} - -brotli_compressor::~brotli_compressor() { - BrotliEncoderDestroyInstance(state_); -} - -bool brotli_compressor::compress(const char *data, size_t data_length, - bool last, Callback callback) { - std::array buff{}; - - auto operation = last ? BROTLI_OPERATION_FINISH : BROTLI_OPERATION_PROCESS; - auto available_in = data_length; - auto next_in = reinterpret_cast(data); - - for (;;) { - if (last) { - if (BrotliEncoderIsFinished(state_)) { break; } - } else { - if (!available_in) { break; } - } - - auto available_out = buff.size(); - auto next_out = buff.data(); - - if (!BrotliEncoderCompressStream(state_, operation, &available_in, &next_in, - &available_out, &next_out, nullptr)) { - return false; - } - - auto output_bytes = buff.size() - available_out; - if (output_bytes) { - callback(reinterpret_cast(buff.data()), output_bytes); - } - } - - return true; -} - -brotli_decompressor::brotli_decompressor() { - decoder_s = BrotliDecoderCreateInstance(0, 0, 0); - decoder_r = decoder_s ? BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT - : BROTLI_DECODER_RESULT_ERROR; -} - -brotli_decompressor::~brotli_decompressor() { - if (decoder_s) { BrotliDecoderDestroyInstance(decoder_s); } -} - -bool brotli_decompressor::is_valid() const { return decoder_s; } - -bool brotli_decompressor::decompress(const char *data, - size_t data_length, - Callback callback) { - if (decoder_r == BROTLI_DECODER_RESULT_SUCCESS || - decoder_r == BROTLI_DECODER_RESULT_ERROR) { - return 0; - } - - auto next_in = reinterpret_cast(data); - size_t avail_in = data_length; - size_t total_out; - - decoder_r = BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT; - - std::array buff{}; - while (decoder_r == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) { - char *next_out = buff.data(); - size_t avail_out = buff.size(); - - decoder_r = BrotliDecoderDecompressStream( - decoder_s, &avail_in, &next_in, &avail_out, - reinterpret_cast(&next_out), &total_out); - - if (decoder_r == BROTLI_DECODER_RESULT_ERROR) { return false; } - - if (!callback(buff.data(), buff.size() - avail_out)) { return false; } - } - - return decoder_r == BROTLI_DECODER_RESULT_SUCCESS || - decoder_r == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT; -} -#endif - -#ifdef CPPHTTPLIB_ZSTD_SUPPORT -zstd_compressor::zstd_compressor() { - ctx_ = ZSTD_createCCtx(); - ZSTD_CCtx_setParameter(ctx_, ZSTD_c_compressionLevel, ZSTD_fast); -} - -zstd_compressor::~zstd_compressor() { ZSTD_freeCCtx(ctx_); } - -bool zstd_compressor::compress(const char *data, size_t data_length, - bool last, Callback callback) { - std::array buff{}; - - ZSTD_EndDirective mode = last ? ZSTD_e_end : ZSTD_e_continue; - ZSTD_inBuffer input = {data, data_length, 0}; - - bool finished; - do { - ZSTD_outBuffer output = {buff.data(), CPPHTTPLIB_COMPRESSION_BUFSIZ, 0}; - size_t const remaining = ZSTD_compressStream2(ctx_, &output, &input, mode); - - if (ZSTD_isError(remaining)) { return false; } - - if (!callback(buff.data(), output.pos)) { return false; } - - finished = last ? (remaining == 0) : (input.pos == input.size); - - } while (!finished); - - return true; -} - -zstd_decompressor::zstd_decompressor() { ctx_ = ZSTD_createDCtx(); } - -zstd_decompressor::~zstd_decompressor() { ZSTD_freeDCtx(ctx_); } - -bool zstd_decompressor::is_valid() const { return ctx_ != nullptr; } - -bool zstd_decompressor::decompress(const char *data, size_t data_length, - Callback callback) { - std::array buff{}; - ZSTD_inBuffer input = {data, data_length, 0}; - - while (input.pos < input.size) { - ZSTD_outBuffer output = {buff.data(), CPPHTTPLIB_COMPRESSION_BUFSIZ, 0}; - size_t const remaining = ZSTD_decompressStream(ctx_, &output, &input); - - if (ZSTD_isError(remaining)) { return false; } - - if (!callback(buff.data(), output.pos)) { return false; } - } - - return true; -} -#endif - -std::unique_ptr -create_decompressor(const std::string &encoding) { - std::unique_ptr decompressor; - - if (encoding == "gzip" || encoding == "deflate") { -#ifdef CPPHTTPLIB_ZLIB_SUPPORT - decompressor = detail::make_unique(); -#endif - } else if (encoding.find("br") != std::string::npos) { -#ifdef CPPHTTPLIB_BROTLI_SUPPORT - decompressor = detail::make_unique(); -#endif - } else if (encoding == "zstd" || encoding.find("zstd") != std::string::npos) { -#ifdef CPPHTTPLIB_ZSTD_SUPPORT - decompressor = detail::make_unique(); -#endif - } - - return decompressor; -} - -// Returns the best available compressor and its Content-Encoding name. -// Priority: Brotli > Gzip > Zstd (matches server-side preference). -std::pair, const char *> -create_compressor() { -#ifdef CPPHTTPLIB_BROTLI_SUPPORT - return {detail::make_unique(), "br"}; -#elif defined(CPPHTTPLIB_ZLIB_SUPPORT) - return {detail::make_unique(), "gzip"}; -#elif defined(CPPHTTPLIB_ZSTD_SUPPORT) - return {detail::make_unique(), "zstd"}; -#else - return {nullptr, nullptr}; -#endif -} - -bool is_prohibited_header_name(const std::string &name) { - using udl::operator""_t; - - switch (str2tag(name)) { - case "REMOTE_ADDR"_t: - case "REMOTE_PORT"_t: - case "LOCAL_ADDR"_t: - case "LOCAL_PORT"_t: return true; - default: return false; - } -} - -bool has_header(const Headers &headers, const std::string &key) { - if (is_prohibited_header_name(key)) { return false; } - return headers.find(key) != headers.end(); -} - -const char *get_header_value(const Headers &headers, - const std::string &key, const char *def, - size_t id) { - if (is_prohibited_header_name(key)) { -#ifndef CPPHTTPLIB_NO_EXCEPTIONS - std::string msg = "Prohibited header name '" + key + "' is specified."; - throw std::invalid_argument(msg); -#else - return ""; -#endif - } - - auto rng = headers.equal_range(key); - auto it = rng.first; - std::advance(it, static_cast(id)); - if (it != rng.second) { return it->second.c_str(); } - return def; -} - -size_t get_header_value_count(const Headers &headers, - const std::string &key) { - auto r = headers.equal_range(key); - return static_cast(std::distance(r.first, r.second)); -} - -template -typename Map::mapped_type -get_multimap_value(const Map &m, const std::string &key, size_t id) { - auto rng = m.equal_range(key); - auto it = rng.first; - std::advance(it, static_cast(id)); - if (it != rng.second) { return it->second; } - return typename Map::mapped_type(); -} - -void set_header(Headers &headers, const std::string &key, - const std::string &val) { - if (fields::is_field_name(key) && fields::is_field_value(val)) { - headers.emplace(key, val); - } -} - -bool read_headers(Stream &strm, Headers &headers) { - const auto bufsiz = 2048; - char buf[bufsiz]; - stream_line_reader line_reader(strm, buf, bufsiz); - - size_t header_count = 0; - - for (;;) { - if (!line_reader.getline()) { return false; } - - // Check if the line ends with CRLF. - auto line_terminator_len = 2; - if (line_reader.end_with_crlf()) { - // Blank line indicates end of headers. - if (line_reader.size() == 2) { break; } - } else { -#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR - // Blank line indicates end of headers. - if (line_reader.size() == 1) { break; } - line_terminator_len = 1; -#else - continue; // Skip invalid line. -#endif - } - - if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } - - // Check header count limit - if (header_count >= CPPHTTPLIB_HEADER_MAX_COUNT) { return false; } - - // Exclude line terminator - auto end = line_reader.ptr() + line_reader.size() - line_terminator_len; - - if (!parse_header(line_reader.ptr(), end, - [&](const std::string &key, const std::string &val) { - headers.emplace(key, val); - })) { - return false; - } - - header_count++; - } - - // RFC 9110 Section 8.6: Reject requests with multiple Content-Length - // headers that have different values to prevent request smuggling. - auto cl_range = headers.equal_range("Content-Length"); - if (cl_range.first != cl_range.second) { - const auto &first_val = cl_range.first->second; - for (auto it = std::next(cl_range.first); it != cl_range.second; ++it) { - if (it->second != first_val) { return false; } - } - } - - return true; -} - -bool read_websocket_upgrade_response(Stream &strm, - const std::string &expected_accept, - std::string &selected_subprotocol) { - // Read status line - const auto bufsiz = 2048; - char buf[bufsiz]; - stream_line_reader line_reader(strm, buf, bufsiz); - if (!line_reader.getline()) { return false; } - - // Check for "HTTP/1.1 101" - auto line = std::string(line_reader.ptr(), line_reader.size()); - if (line.find("HTTP/1.1 101") == std::string::npos) { return false; } - - // Parse headers using existing read_headers - Headers headers; - if (!read_headers(strm, headers)) { return false; } - - // Verify Upgrade: websocket (case-insensitive) - auto upgrade_it = headers.find("Upgrade"); - if (upgrade_it == headers.end()) { return false; } - auto upgrade_val = case_ignore::to_lower(upgrade_it->second); - if (upgrade_val != "websocket") { return false; } - - // Verify Connection header contains "Upgrade" (case-insensitive) - auto connection_it = headers.find("Connection"); - if (connection_it == headers.end()) { return false; } - auto connection_val = case_ignore::to_lower(connection_it->second); - if (connection_val.find("upgrade") == std::string::npos) { return false; } - - // Verify Sec-WebSocket-Accept header value - auto it = headers.find("Sec-WebSocket-Accept"); - if (it == headers.end() || it->second != expected_accept) { return false; } - - // Extract negotiated subprotocol - auto proto_it = headers.find("Sec-WebSocket-Protocol"); - if (proto_it != headers.end()) { selected_subprotocol = proto_it->second; } - - return true; -} - -enum class ReadContentResult { - Success, // Successfully read the content - PayloadTooLarge, // The content exceeds the specified payload limit - Error // An error occurred while reading the content -}; - -ReadContentResult read_content_with_length( - Stream &strm, size_t len, DownloadProgress progress, - ContentReceiverWithProgress out, - size_t payload_max_length = (std::numeric_limits::max)()) { - char buf[CPPHTTPLIB_RECV_BUFSIZ]; - - detail::BodyReader br; - br.stream = &strm; - br.has_content_length = true; - br.content_length = len; - br.payload_max_length = payload_max_length; - br.chunked = false; - br.bytes_read = 0; - br.last_error = Error::Success; - - size_t r = 0; - while (r < len) { - auto read_len = static_cast(len - r); - auto to_read = (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ); - auto n = detail::read_body_content(&strm, br, buf, to_read); - if (n <= 0) { - // Check if it was a payload size error - if (br.last_error == Error::ExceedMaxPayloadSize) { - return ReadContentResult::PayloadTooLarge; - } - return ReadContentResult::Error; - } - - if (!out(buf, static_cast(n), r, len)) { - return ReadContentResult::Error; - } - r += static_cast(n); - - if (progress) { - if (!progress(r, len)) { return ReadContentResult::Error; } - } - } - - return ReadContentResult::Success; -} - -ReadContentResult -read_content_without_length(Stream &strm, size_t payload_max_length, - ContentReceiverWithProgress out) { - char buf[CPPHTTPLIB_RECV_BUFSIZ]; - size_t r = 0; - for (;;) { - auto n = strm.read(buf, CPPHTTPLIB_RECV_BUFSIZ); - if (n == 0) { return ReadContentResult::Success; } - if (n < 0) { return ReadContentResult::Error; } - - // Check if adding this data would exceed the payload limit - if (r > payload_max_length || - payload_max_length - r < static_cast(n)) { - return ReadContentResult::PayloadTooLarge; - } - - if (!out(buf, static_cast(n), r, 0)) { - return ReadContentResult::Error; - } - r += static_cast(n); - } - - return ReadContentResult::Success; -} - -template -ReadContentResult read_content_chunked(Stream &strm, T &x, - size_t payload_max_length, - ContentReceiverWithProgress out) { - detail::ChunkedDecoder dec(strm); - - char buf[CPPHTTPLIB_RECV_BUFSIZ]; - size_t total_len = 0; - - for (;;) { - size_t chunk_offset = 0; - size_t chunk_total = 0; - auto n = dec.read_payload(buf, sizeof(buf), chunk_offset, chunk_total); - if (n < 0) { return ReadContentResult::Error; } - - if (n == 0) { - if (!dec.parse_trailers_into(x.trailers, x.headers)) { - return ReadContentResult::Error; - } - return ReadContentResult::Success; - } - - if (total_len > payload_max_length || - payload_max_length - total_len < static_cast(n)) { - return ReadContentResult::PayloadTooLarge; - } - - if (!out(buf, static_cast(n), chunk_offset, chunk_total)) { - return ReadContentResult::Error; - } - - total_len += static_cast(n); - } -} - -bool is_chunked_transfer_encoding(const Headers &headers) { - return case_ignore::equal( - get_header_value(headers, "Transfer-Encoding", "", 0), "chunked"); -} - -template -bool prepare_content_receiver(T &x, int &status, - ContentReceiverWithProgress receiver, - bool decompress, size_t payload_max_length, - bool &exceed_payload_max_length, U callback) { - if (decompress) { - std::string encoding = x.get_header_value("Content-Encoding"); - std::unique_ptr decompressor; - - if (!encoding.empty()) { - decompressor = detail::create_decompressor(encoding); - if (!decompressor) { - // Unsupported encoding or no support compiled in - status = StatusCode::UnsupportedMediaType_415; - return false; - } - } - - if (decompressor) { - if (decompressor->is_valid()) { - size_t decompressed_size = 0; - ContentReceiverWithProgress out = [&](const char *buf, size_t n, - size_t off, size_t len) { - return decompressor->decompress( - buf, n, [&](const char *buf2, size_t n2) { - // Guard against zip-bomb: check - // decompressed size against limit. - if (payload_max_length > 0 && - (decompressed_size >= payload_max_length || - n2 > payload_max_length - decompressed_size)) { - exceed_payload_max_length = true; - return false; - } - decompressed_size += n2; - return receiver(buf2, n2, off, len); - }); - }; - return callback(std::move(out)); - } else { - status = StatusCode::InternalServerError_500; - return false; - } - } - } - - ContentReceiverWithProgress out = [&](const char *buf, size_t n, size_t off, - size_t len) { - return receiver(buf, n, off, len); - }; - return callback(std::move(out)); -} - -template -bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status, - DownloadProgress progress, - ContentReceiverWithProgress receiver, bool decompress) { - bool exceed_payload_max_length = false; - return prepare_content_receiver( - x, status, std::move(receiver), decompress, payload_max_length, - exceed_payload_max_length, [&](const ContentReceiverWithProgress &out) { - auto ret = true; - // Note: exceed_payload_max_length may also be set by the decompressor - // wrapper in prepare_content_receiver when the decompressed payload - // size exceeds the limit. - - if (is_chunked_transfer_encoding(x.headers)) { - auto result = read_content_chunked(strm, x, payload_max_length, out); - if (result == ReadContentResult::Success) { - ret = true; - } else if (result == ReadContentResult::PayloadTooLarge) { - exceed_payload_max_length = true; - ret = false; - } else { - ret = false; - } - } else if (!has_header(x.headers, "Content-Length")) { - auto result = - read_content_without_length(strm, payload_max_length, out); - if (result == ReadContentResult::Success) { - ret = true; - } else if (result == ReadContentResult::PayloadTooLarge) { - exceed_payload_max_length = true; - ret = false; - } else { - ret = false; - } - } else { - auto is_invalid_value = false; - auto len = get_header_value_u64(x.headers, "Content-Length", - (std::numeric_limits::max)(), - 0, is_invalid_value); - - if (is_invalid_value) { - ret = false; - } else if (len > 0) { - auto result = read_content_with_length( - strm, len, std::move(progress), out, payload_max_length); - ret = (result == ReadContentResult::Success); - if (result == ReadContentResult::PayloadTooLarge) { - exceed_payload_max_length = true; - } - } - } - - if (!ret) { - status = exceed_payload_max_length ? StatusCode::PayloadTooLarge_413 - : StatusCode::BadRequest_400; - } - return ret; - }); -} - -ssize_t write_request_line(Stream &strm, const std::string &method, - const std::string &path) { - std::string s = method; - s += ' '; - s += path; - s += " HTTP/1.1\r\n"; - return strm.write(s.data(), s.size()); -} - -ssize_t write_response_line(Stream &strm, int status) { - std::string s = "HTTP/1.1 "; - s += std::to_string(status); - s += ' '; - s += httplib::status_message(status); - s += "\r\n"; - return strm.write(s.data(), s.size()); -} - -ssize_t write_headers(Stream &strm, const Headers &headers) { - ssize_t write_len = 0; - for (const auto &x : headers) { - std::string s; - s = x.first; - s += ": "; - s += x.second; - s += "\r\n"; - - auto len = strm.write(s.data(), s.size()); - if (len < 0) { return len; } - write_len += len; - } - auto len = strm.write("\r\n"); - if (len < 0) { return len; } - write_len += len; - return write_len; -} - -bool write_data(Stream &strm, const char *d, size_t l) { - size_t offset = 0; - while (offset < l) { - auto length = strm.write(d + offset, l - offset); - if (length < 0) { return false; } - offset += static_cast(length); - } - return true; -} - -template -bool write_content_with_progress(Stream &strm, - const ContentProvider &content_provider, - size_t offset, size_t length, - T is_shutting_down, - const UploadProgress &upload_progress, - Error &error) { - size_t end_offset = offset + length; - size_t start_offset = offset; - auto ok = true; - DataSink data_sink; - - data_sink.write = [&](const char *d, size_t l) -> bool { - if (ok) { - if (write_data(strm, d, l)) { - offset += l; - - if (upload_progress && length > 0) { - size_t current_written = offset - start_offset; - if (!upload_progress(current_written, length)) { - ok = false; - return false; - } - } - } else { - ok = false; - } - } - return ok; - }; - - data_sink.is_writable = [&]() -> bool { return strm.is_peer_alive(); }; - - while (offset < end_offset && !is_shutting_down()) { - if (!strm.wait_writable() || !strm.is_peer_alive()) { - error = Error::Write; - return false; - } else if (!content_provider(offset, end_offset - offset, data_sink)) { - error = Error::Canceled; - return false; - } else if (!ok) { - error = Error::Write; - return false; - } - } - - if (offset < end_offset) { // exited due to is_shutting_down(), not completion - error = Error::Write; - return false; - } - - error = Error::Success; - return true; -} - -template -bool write_content(Stream &strm, const ContentProvider &content_provider, - size_t offset, size_t length, T is_shutting_down, - Error &error) { - return write_content_with_progress(strm, content_provider, offset, length, - is_shutting_down, nullptr, error); -} - -template -bool write_content(Stream &strm, const ContentProvider &content_provider, - size_t offset, size_t length, - const T &is_shutting_down) { - auto error = Error::Success; - return write_content(strm, content_provider, offset, length, is_shutting_down, - error); -} - -template -bool -write_content_without_length(Stream &strm, - const ContentProvider &content_provider, - const T &is_shutting_down) { - size_t offset = 0; - auto data_available = true; - auto ok = true; - DataSink data_sink; - - data_sink.write = [&](const char *d, size_t l) -> bool { - if (ok) { - offset += l; - if (!write_data(strm, d, l)) { ok = false; } - } - return ok; - }; - - data_sink.is_writable = [&]() -> bool { return strm.is_peer_alive(); }; - - data_sink.done = [&](void) { data_available = false; }; - - while (data_available && !is_shutting_down()) { - if (!strm.wait_writable() || !strm.is_peer_alive()) { - return false; - } else if (!content_provider(offset, 0, data_sink)) { - return false; - } else if (!ok) { - return false; - } - } - return !data_available; // true only if done() was called, false if shutting - // down -} - -template -bool -write_content_chunked(Stream &strm, const ContentProvider &content_provider, - const T &is_shutting_down, U &compressor, Error &error) { - size_t offset = 0; - auto data_available = true; - auto ok = true; - DataSink data_sink; - - data_sink.write = [&](const char *d, size_t l) -> bool { - if (ok) { - data_available = l > 0; - offset += l; - - std::string payload; - if (compressor.compress(d, l, false, - [&](const char *data, size_t data_len) { - payload.append(data, data_len); - return true; - })) { - if (!payload.empty()) { - // Emit chunked response header and footer for each chunk - auto chunk = - from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n"; - if (!write_data(strm, chunk.data(), chunk.size())) { ok = false; } - } - } else { - ok = false; - } - } - return ok; - }; - - data_sink.is_writable = [&]() -> bool { return strm.is_peer_alive(); }; - - auto done_with_trailer = [&](const Headers *trailer) { - if (!ok) { return; } - - data_available = false; - - std::string payload; - if (!compressor.compress(nullptr, 0, true, - [&](const char *data, size_t data_len) { - payload.append(data, data_len); - return true; - })) { - ok = false; - return; - } - - if (!payload.empty()) { - // Emit chunked response header and footer for each chunk - auto chunk = from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n"; - if (!write_data(strm, chunk.data(), chunk.size())) { - ok = false; - return; - } - } - - constexpr const char done_marker[] = "0\r\n"; - if (!write_data(strm, done_marker, str_len(done_marker))) { ok = false; } - - // Trailer - if (trailer) { - for (const auto &kv : *trailer) { - std::string field_line = kv.first + ": " + kv.second + "\r\n"; - if (!write_data(strm, field_line.data(), field_line.size())) { - ok = false; - } - } - } - - constexpr const char crlf[] = "\r\n"; - if (!write_data(strm, crlf, str_len(crlf))) { ok = false; } - }; - - data_sink.done = [&](void) { done_with_trailer(nullptr); }; - - data_sink.done_with_trailer = [&](const Headers &trailer) { - done_with_trailer(&trailer); - }; - - while (data_available && !is_shutting_down()) { - if (!strm.wait_writable() || !strm.is_peer_alive()) { - error = Error::Write; - return false; - } else if (!content_provider(offset, 0, data_sink)) { - error = Error::Canceled; - return false; - } else if (!ok) { - error = Error::Write; - return false; - } - } - - if (data_available) { // exited due to is_shutting_down(), not done() - error = Error::Write; - return false; - } - - error = Error::Success; - return true; -} - -template -bool write_content_chunked(Stream &strm, - const ContentProvider &content_provider, - const T &is_shutting_down, U &compressor) { - auto error = Error::Success; - return write_content_chunked(strm, content_provider, is_shutting_down, - compressor, error); -} - -template -bool redirect(T &cli, Request &req, Response &res, - const std::string &path, const std::string &location, - Error &error) { - Request new_req = req; - new_req.path = path; - new_req.redirect_count_ -= 1; - - if (res.status == StatusCode::SeeOther_303 && - (req.method != "GET" && req.method != "HEAD")) { - new_req.method = "GET"; - new_req.body.clear(); - new_req.headers.clear(); - } - - Response new_res; - - auto ret = cli.send(new_req, new_res, error); - if (ret) { - req = std::move(new_req); - res = std::move(new_res); - - if (res.location.empty()) { res.location = location; } - } - return ret; -} - -std::string params_to_query_str(const Params ¶ms) { - std::string query; - - for (auto it = params.begin(); it != params.end(); ++it) { - if (it != params.begin()) { query += '&'; } - query += encode_query_component(it->first); - query += '='; - query += encode_query_component(it->second); - } - return query; -} - -void parse_query_text(const char *data, std::size_t size, - Params ¶ms) { - std::set cache; - split(data, data + size, '&', [&](const char *b, const char *e) { - std::string kv(b, e); - if (cache.find(kv) != cache.end()) { return; } - cache.insert(std::move(kv)); - - std::string key; - std::string val; - divide(b, static_cast(e - b), '=', - [&](const char *lhs_data, std::size_t lhs_size, const char *rhs_data, - std::size_t rhs_size) { - key.assign(lhs_data, lhs_size); - val.assign(rhs_data, rhs_size); - }); - - if (!key.empty()) { - params.emplace(decode_query_component(key), decode_query_component(val)); - } - }); -} - -void parse_query_text(const std::string &s, Params ¶ms) { - parse_query_text(s.data(), s.size(), params); -} - -// Normalize a query string by decoding and re-encoding each key/value pair -// while preserving the original parameter order. This avoids double-encoding -// and ensures consistent encoding without reordering (unlike Params which -// uses std::multimap and sorts keys). -std::string normalize_query_string(const std::string &query) { - std::string result; - split(query.data(), query.data() + query.size(), '&', - [&](const char *b, const char *e) { - std::string key; - std::string val; - divide(b, static_cast(e - b), '=', - [&](const char *lhs_data, std::size_t lhs_size, - const char *rhs_data, std::size_t rhs_size) { - key.assign(lhs_data, lhs_size); - val.assign(rhs_data, rhs_size); - }); - - if (!key.empty()) { - auto dec_key = decode_query_component(key); - auto dec_val = decode_query_component(val); - - if (!result.empty()) { result += '&'; } - result += encode_query_component(dec_key); - if (!val.empty() || std::find(b, e, '=') != e) { - result += '='; - result += encode_query_component(dec_val); - } - } - }); - return result; -} - -bool parse_multipart_boundary(const std::string &content_type, - std::string &boundary) { - std::map params; - extract_media_type(content_type, ¶ms); - auto it = params.find("boundary"); - if (it == params.end()) { return false; } - boundary = it->second; - return !boundary.empty(); -} - -void parse_disposition_params(const std::string &s, Params ¶ms) { - std::set cache; - split(s.data(), s.data() + s.size(), ';', [&](const char *b, const char *e) { - std::string kv(b, e); - if (cache.find(kv) != cache.end()) { return; } - cache.insert(kv); - - std::string key; - std::string val; - split(b, e, '=', [&](const char *b2, const char *e2) { - if (key.empty()) { - key.assign(b2, e2); - } else { - val.assign(b2, e2); - } - }); - - if (!key.empty()) { - params.emplace(trim_double_quotes_copy((key)), - trim_double_quotes_copy((val))); - } - }); -} - -#ifdef CPPHTTPLIB_NO_EXCEPTIONS -bool parse_range_header(const std::string &s, Ranges &ranges) { -#else -bool parse_range_header(const std::string &s, Ranges &ranges) try { -#endif - auto is_valid = [](const std::string &str) { - return std::all_of(str.cbegin(), str.cend(), - [](unsigned char c) { return std::isdigit(c); }); - }; - - if (s.size() > 7 && s.compare(0, 6, "bytes=") == 0) { - const auto pos = static_cast(6); - const auto len = static_cast(s.size() - 6); - auto all_valid_ranges = true; - split(&s[pos], &s[pos + len], ',', [&](const char *b, const char *e) { - if (!all_valid_ranges) { return; } - - const auto it = std::find(b, e, '-'); - if (it == e) { - all_valid_ranges = false; - return; - } - - const auto lhs = std::string(b, it); - const auto rhs = std::string(it + 1, e); - if (!is_valid(lhs) || !is_valid(rhs)) { - all_valid_ranges = false; - return; - } - - ssize_t first = -1; - if (!lhs.empty()) { - ssize_t v; - auto res = detail::from_chars(lhs.data(), lhs.data() + lhs.size(), v); - if (res.ec == std::errc{}) { first = v; } - } - - ssize_t last = -1; - if (!rhs.empty()) { - ssize_t v; - auto res = detail::from_chars(rhs.data(), rhs.data() + rhs.size(), v); - if (res.ec == std::errc{}) { last = v; } - } - - if ((first == -1 && last == -1) || - (first != -1 && last != -1 && first > last)) { - all_valid_ranges = false; - return; - } - - ranges.emplace_back(first, last); - }); - return all_valid_ranges && !ranges.empty(); - } - return false; -#ifdef CPPHTTPLIB_NO_EXCEPTIONS -} -#else -} catch (...) { return false; } -#endif - -bool parse_accept_header(const std::string &s, - std::vector &content_types) { - content_types.clear(); - - // Empty string is considered valid (no preference) - if (s.empty()) { return true; } - - // Check for invalid patterns: leading/trailing commas or consecutive commas - if (s.front() == ',' || s.back() == ',' || - s.find(",,") != std::string::npos) { - return false; - } - - struct AcceptEntry { - std::string media_type; - double quality; - int order; - }; - - std::vector entries; - int order = 0; - bool has_invalid_entry = false; - - // Split by comma and parse each entry - split(s.data(), s.data() + s.size(), ',', [&](const char *b, const char *e) { - std::string entry(b, e); - entry = trim_copy(entry); - - if (entry.empty()) { - has_invalid_entry = true; - return; - } - - AcceptEntry accept_entry; - accept_entry.order = order++; - - if (!parse_quality(entry.data(), entry.data() + entry.size(), - accept_entry.media_type, accept_entry.quality)) { - has_invalid_entry = true; - return; - } - - // Remove additional parameters from media type - accept_entry.media_type = extract_media_type(accept_entry.media_type); - - // Basic validation of media type format - if (accept_entry.media_type.empty()) { - has_invalid_entry = true; - return; - } - - // Check for basic media type format (should contain '/' or be '*') - if (accept_entry.media_type != "*" && - accept_entry.media_type.find('/') == std::string::npos) { - has_invalid_entry = true; - return; - } - - entries.push_back(std::move(accept_entry)); - }); - - // Return false if any invalid entry was found - if (has_invalid_entry) { return false; } - - // Sort by quality (descending), then by original order (ascending) - std::sort(entries.begin(), entries.end(), - [](const AcceptEntry &a, const AcceptEntry &b) { - if (a.quality != b.quality) { - return a.quality > b.quality; // Higher quality first - } - return a.order < b.order; // Earlier order first for same quality - }); - - // Extract sorted media types - content_types.reserve(entries.size()); - for (auto &entry : entries) { - content_types.push_back(std::move(entry.media_type)); - } - - return true; -} - -class FormDataParser { -public: - FormDataParser() = default; - - void set_boundary(std::string &&boundary) { - boundary_ = std::move(boundary); - dash_boundary_crlf_ = dash_ + boundary_ + crlf_; - crlf_dash_boundary_ = crlf_ + dash_ + boundary_; - } - - bool is_valid() const { return is_valid_; } - - bool parse(const char *buf, size_t n, const FormDataHeader &header_callback, - const ContentReceiver &content_callback) { - - buf_append(buf, n); - - while (buf_size() > 0) { - switch (state_) { - case 0: { // Initial boundary - auto pos = buf_find(dash_boundary_crlf_); - if (pos == buf_size()) { return true; } - buf_erase(pos + dash_boundary_crlf_.size()); - state_ = 1; - break; - } - case 1: { // New entry - clear_file_info(); - state_ = 2; - break; - } - case 2: { // Headers - auto pos = buf_find(crlf_); - if (pos > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } - while (pos < buf_size()) { - // Empty line - if (pos == 0) { - if (!header_callback(file_)) { - is_valid_ = false; - return false; - } - buf_erase(crlf_.size()); - state_ = 3; - break; - } - - const auto header = buf_head(pos); - - if (!parse_header(header.data(), header.data() + header.size(), - [&](const std::string &, const std::string &) {})) { - is_valid_ = false; - return false; - } - - // Parse and emplace space trimmed headers into a map - if (!parse_header( - header.data(), header.data() + header.size(), - [&](const std::string &key, const std::string &val) { - file_.headers.emplace(key, val); - })) { - is_valid_ = false; - return false; - } - - constexpr const char header_content_type[] = "Content-Type:"; - - if (start_with_case_ignore(header, header_content_type)) { - file_.content_type = - trim_copy(header.substr(str_len(header_content_type))); - } else { - std::string disposition_params; - if (parse_content_disposition(header, disposition_params)) { - Params params; - parse_disposition_params(disposition_params, params); - - auto it = params.find("name"); - if (it != params.end()) { - file_.name = it->second; - } else { - is_valid_ = false; - return false; - } - - it = params.find("filename"); - if (it != params.end()) { file_.filename = it->second; } - - it = params.find("filename*"); - if (it != params.end()) { - // RFC 5987: only UTF-8 encoding is allowed - const auto &val = it->second; - constexpr const char utf8_prefix[] = "UTF-8''"; - constexpr size_t prefix_len = str_len(utf8_prefix); - if (val.size() > prefix_len && - start_with_case_ignore(val, utf8_prefix)) { - file_.filename = decode_path_component( - val.substr(prefix_len)); // override... - } else { - is_valid_ = false; - return false; - } - } - } - } - buf_erase(pos + crlf_.size()); - pos = buf_find(crlf_); - } - if (state_ != 3) { return true; } - break; - } - case 3: { // Body - if (crlf_dash_boundary_.size() > buf_size()) { return true; } - auto pos = buf_find(crlf_dash_boundary_); - if (pos < buf_size()) { - if (!content_callback(buf_data(), pos)) { - is_valid_ = false; - return false; - } - buf_erase(pos + crlf_dash_boundary_.size()); - state_ = 4; - } else { - auto len = buf_size() - crlf_dash_boundary_.size(); - if (len > 0) { - if (!content_callback(buf_data(), len)) { - is_valid_ = false; - return false; - } - buf_erase(len); - } - return true; - } - break; - } - case 4: { // Boundary - if (crlf_.size() > buf_size()) { return true; } - if (buf_start_with(crlf_)) { - buf_erase(crlf_.size()); - state_ = 1; - } else { - if (dash_.size() > buf_size()) { return true; } - if (buf_start_with(dash_)) { - buf_erase(dash_.size()); - is_valid_ = true; - buf_erase(buf_size()); // Remove epilogue - } else { - return true; - } - } - break; - } - } - } - - return true; - } - -private: - void clear_file_info() { - file_.name.clear(); - file_.filename.clear(); - file_.content_type.clear(); - file_.headers.clear(); - } - - bool start_with_case_ignore(const std::string &a, const char *b, - size_t offset = 0) const { - const auto b_len = strlen(b); - if (a.size() < offset + b_len) { return false; } - for (size_t i = 0; i < b_len; i++) { - if (case_ignore::to_lower(a[offset + i]) != case_ignore::to_lower(b[i])) { - return false; - } - } - return true; - } - - // Parses "Content-Disposition: form-data; " without std::regex. - // Returns true if header matches, with the params portion in `params_out`. - bool parse_content_disposition(const std::string &header, - std::string ¶ms_out) const { - constexpr const char prefix[] = "Content-Disposition:"; - constexpr size_t prefix_len = str_len(prefix); - - if (!start_with_case_ignore(header, prefix)) { return false; } - - // Skip whitespace after "Content-Disposition:" - auto pos = prefix_len; - while (pos < header.size() && (header[pos] == ' ' || header[pos] == '\t')) { - pos++; - } - - // Match "form-data;" (case-insensitive) - constexpr const char form_data[] = "form-data;"; - constexpr size_t form_data_len = str_len(form_data); - if (!start_with_case_ignore(header, form_data, pos)) { return false; } - pos += form_data_len; - - // Skip whitespace after "form-data;" - while (pos < header.size() && (header[pos] == ' ' || header[pos] == '\t')) { - pos++; - } - - params_out = header.substr(pos); - return true; - } - - const std::string dash_ = "--"; - const std::string crlf_ = "\r\n"; - std::string boundary_; - std::string dash_boundary_crlf_; - std::string crlf_dash_boundary_; - - size_t state_ = 0; - bool is_valid_ = false; - FormData file_; - - // Buffer - bool start_with(const std::string &a, size_t spos, size_t epos, - const std::string &b) const { - if (epos - spos < b.size()) { return false; } - for (size_t i = 0; i < b.size(); i++) { - if (a[i + spos] != b[i]) { return false; } - } - return true; - } - - size_t buf_size() const { return buf_epos_ - buf_spos_; } - - const char *buf_data() const { return &buf_[buf_spos_]; } - - std::string buf_head(size_t l) const { return buf_.substr(buf_spos_, l); } - - bool buf_start_with(const std::string &s) const { - return start_with(buf_, buf_spos_, buf_epos_, s); - } - - size_t buf_find(const std::string &s) const { - auto c = s.front(); - - size_t off = buf_spos_; - while (off < buf_epos_) { - auto pos = off; - while (true) { - if (pos == buf_epos_) { return buf_size(); } - if (buf_[pos] == c) { break; } - pos++; - } - - auto remaining_size = buf_epos_ - pos; - if (s.size() > remaining_size) { return buf_size(); } - - if (start_with(buf_, pos, buf_epos_, s)) { return pos - buf_spos_; } - - off = pos + 1; - } - - return buf_size(); - } - - void buf_append(const char *data, size_t n) { - auto remaining_size = buf_size(); - if (remaining_size > 0 && buf_spos_ > 0) { - for (size_t i = 0; i < remaining_size; i++) { - buf_[i] = buf_[buf_spos_ + i]; - } - } - buf_spos_ = 0; - buf_epos_ = remaining_size; - - if (remaining_size + n > buf_.size()) { buf_.resize(remaining_size + n); } - - for (size_t i = 0; i < n; i++) { - buf_[buf_epos_ + i] = data[i]; - } - buf_epos_ += n; - } - - void buf_erase(size_t size) { buf_spos_ += size; } - - std::string buf_; - size_t buf_spos_ = 0; - size_t buf_epos_ = 0; -}; - -std::string random_string(size_t length) { - constexpr const char data[] = - "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; - - thread_local auto engine([]() { - // std::random_device might actually be deterministic on some - // platforms, but due to lack of support in the c++ standard library, - // doing better requires either some ugly hacks or breaking portability. - std::random_device seed_gen; - // Request 128 bits of entropy for initialization - std::seed_seq seed_sequence{seed_gen(), seed_gen(), seed_gen(), seed_gen()}; - return std::mt19937(seed_sequence); - }()); - - std::string result; - for (size_t i = 0; i < length; i++) { - result += data[engine() % (sizeof(data) - 1)]; - } - return result; -} - -std::string make_multipart_data_boundary() { - return "--cpp-httplib-multipart-data-" + detail::random_string(16); -} - -bool is_multipart_boundary_chars_valid(const std::string &boundary) { - auto valid = true; - for (size_t i = 0; i < boundary.size(); i++) { - auto c = boundary[i]; - if (!std::isalnum(c) && c != '-' && c != '_') { - valid = false; - break; - } - } - return valid; -} - -template -std::string -serialize_multipart_formdata_item_begin(const T &item, - const std::string &boundary) { - std::string body = "--" + boundary + "\r\n"; - body += "Content-Disposition: form-data; name=\"" + item.name + "\""; - if (!item.filename.empty()) { - body += "; filename=\"" + item.filename + "\""; - } - body += "\r\n"; - if (!item.content_type.empty()) { - body += "Content-Type: " + item.content_type + "\r\n"; - } - body += "\r\n"; - - return body; -} - -std::string serialize_multipart_formdata_item_end() { return "\r\n"; } - -std::string -serialize_multipart_formdata_finish(const std::string &boundary) { - return "--" + boundary + "--\r\n"; -} - -std::string -serialize_multipart_formdata_get_content_type(const std::string &boundary) { - return "multipart/form-data; boundary=" + boundary; -} - -std::string -serialize_multipart_formdata(const UploadFormDataItems &items, - const std::string &boundary, bool finish = true) { - std::string body; - - for (const auto &item : items) { - body += serialize_multipart_formdata_item_begin(item, boundary); - body += item.content + serialize_multipart_formdata_item_end(); - } - - if (finish) { body += serialize_multipart_formdata_finish(boundary); } - - return body; -} - -size_t get_multipart_content_length(const UploadFormDataItems &items, - const std::string &boundary) { - size_t total = 0; - for (const auto &item : items) { - total += serialize_multipart_formdata_item_begin(item, boundary).size(); - total += item.content.size(); - total += serialize_multipart_formdata_item_end().size(); - } - total += serialize_multipart_formdata_finish(boundary).size(); - return total; -} - -struct MultipartSegment { - const char *data; - size_t size; -}; - -// NOTE: items must outlive the returned ContentProvider -// (safe for synchronous use inside Post/Put/Patch) -ContentProvider -make_multipart_content_provider(const UploadFormDataItems &items, - const std::string &boundary) { - // Own the per-item header strings and the finish string - std::vector owned; - owned.reserve(items.size() + 1); - for (const auto &item : items) - owned.push_back(serialize_multipart_formdata_item_begin(item, boundary)); - owned.push_back(serialize_multipart_formdata_finish(boundary)); - - // Flat segment list: [header, content, "\r\n"] * N + [finish] - std::vector segs; - segs.reserve(items.size() * 3 + 1); - static const char crlf[] = "\r\n"; - for (size_t i = 0; i < items.size(); i++) { - segs.push_back({owned[i].data(), owned[i].size()}); - segs.push_back({items[i].content.data(), items[i].content.size()}); - segs.push_back({crlf, 2}); - } - segs.push_back({owned.back().data(), owned.back().size()}); - - struct MultipartState { - std::vector owned; - std::vector segs; - std::vector buf = std::vector(CPPHTTPLIB_SEND_BUFSIZ); - }; - auto state = std::make_shared(); - state->owned = std::move(owned); - // `segs` holds raw pointers into owned strings; std::string move preserves - // the data pointer, so these pointers remain valid after the move above. - state->segs = std::move(segs); - - return [state](size_t offset, size_t length, DataSink &sink) -> bool { - // Buffer multiple small segments into fewer, larger writes to avoid - // excessive TCP packets when there are many form data items (#2410) - auto &buf = state->buf; - auto buf_size = buf.size(); - size_t buf_len = 0; - size_t remaining = length; - - // Find the first segment containing 'offset' - size_t pos = 0; - size_t seg_idx = 0; - for (; seg_idx < state->segs.size(); seg_idx++) { - const auto &seg = state->segs[seg_idx]; - if (seg.size > 0 && offset - pos < seg.size) { break; } - pos += seg.size; - } - - size_t seg_offset = (seg_idx < state->segs.size()) ? offset - pos : 0; - - for (; seg_idx < state->segs.size() && remaining > 0; seg_idx++) { - const auto &seg = state->segs[seg_idx]; - size_t available = seg.size - seg_offset; - size_t to_copy = (std::min)(available, remaining); - const char *src = seg.data + seg_offset; - seg_offset = 0; // only the first segment has a non-zero offset - - while (to_copy > 0) { - size_t space = buf_size - buf_len; - size_t chunk = (std::min)(to_copy, space); - std::memcpy(buf.data() + buf_len, src, chunk); - buf_len += chunk; - src += chunk; - to_copy -= chunk; - remaining -= chunk; - - if (buf_len == buf_size) { - if (!sink.write(buf.data(), buf_len)) { return false; } - buf_len = 0; - } - } - } - - if (buf_len > 0) { return sink.write(buf.data(), buf_len); } - return true; - }; -} - -void coalesce_ranges(Ranges &ranges, size_t content_length) { - if (ranges.size() <= 1) return; - - // Sort ranges by start position - std::sort(ranges.begin(), ranges.end(), - [](const Range &a, const Range &b) { return a.first < b.first; }); - - Ranges coalesced; - coalesced.reserve(ranges.size()); - - for (auto &r : ranges) { - auto first_pos = r.first; - auto last_pos = r.second; - - // Handle special cases like in range_error - if (first_pos == -1 && last_pos == -1) { - first_pos = 0; - last_pos = static_cast(content_length); - } - - if (first_pos == -1) { - first_pos = static_cast(content_length) - last_pos; - last_pos = static_cast(content_length) - 1; - } - - if (last_pos == -1 || last_pos >= static_cast(content_length)) { - last_pos = static_cast(content_length) - 1; - } - - // Skip invalid ranges - if (!(0 <= first_pos && first_pos <= last_pos && - last_pos < static_cast(content_length))) { - continue; - } - - // Coalesce with previous range if overlapping or adjacent (but not - // identical) - if (!coalesced.empty()) { - auto &prev = coalesced.back(); - // Check if current range overlaps or is adjacent to previous range - // but don't coalesce identical ranges (allow duplicates) - if (first_pos <= prev.second + 1 && - !(first_pos == prev.first && last_pos == prev.second)) { - // Extend the previous range - prev.second = (std::max)(prev.second, last_pos); - continue; - } - } - - // Add new range - coalesced.emplace_back(first_pos, last_pos); - } - - ranges = std::move(coalesced); -} - -bool range_error(Request &req, Response &res) { - if (!req.ranges.empty() && 200 <= res.status && res.status < 300) { - ssize_t content_len = static_cast( - res.content_length_ ? res.content_length_ : res.body.size()); - - std::vector> processed_ranges; - size_t overwrapping_count = 0; - - // NOTE: The following Range check is based on '14.2. Range' in RFC 9110 - // 'HTTP Semantics' to avoid potential denial-of-service attacks. - // https://www.rfc-editor.org/rfc/rfc9110#section-14.2 - - // Too many ranges - if (req.ranges.size() > CPPHTTPLIB_RANGE_MAX_COUNT) { return true; } - - for (auto &r : req.ranges) { - auto &first_pos = r.first; - auto &last_pos = r.second; - - if (first_pos == -1 && last_pos == -1) { - first_pos = 0; - last_pos = content_len; - } - - if (first_pos == -1) { - first_pos = content_len - last_pos; - last_pos = content_len - 1; - } - - // NOTE: RFC-9110 '14.1.2. Byte Ranges': - // A client can limit the number of bytes requested without knowing the - // size of the selected representation. If the last-pos value is absent, - // or if the value is greater than or equal to the current length of the - // representation data, the byte range is interpreted as the remainder of - // the representation (i.e., the server replaces the value of last-pos - // with a value that is one less than the current length of the selected - // representation). - // https://www.rfc-editor.org/rfc/rfc9110.html#section-14.1.2-6 - if (last_pos == -1 || last_pos >= content_len) { - last_pos = content_len - 1; - } - - // Range must be within content length - if (!(0 <= first_pos && first_pos <= last_pos && - last_pos <= content_len - 1)) { - return true; - } - - // Request must not have more than two overlapping ranges - for (const auto &processed_range : processed_ranges) { - if (!(last_pos < processed_range.first || - first_pos > processed_range.second)) { - overwrapping_count++; - if (overwrapping_count > 2) { return true; } - break; // Only count once per range - } - } - - processed_ranges.emplace_back(first_pos, last_pos); - } - - // After validation, coalesce overlapping ranges as per RFC 9110 - coalesce_ranges(req.ranges, static_cast(content_len)); - } - - return false; -} - -std::pair -get_range_offset_and_length(Range r, size_t content_length) { - assert(r.first != -1 && r.second != -1); - assert(0 <= r.first && r.first < static_cast(content_length)); - assert(r.first <= r.second && - r.second < static_cast(content_length)); - (void)(content_length); - return std::make_pair(static_cast(r.first), - static_cast(r.second - r.first) + 1); -} - -std::string make_content_range_header_field( - const std::pair &offset_and_length, size_t content_length) { - auto st = offset_and_length.first; - auto ed = st + offset_and_length.second - 1; - - std::string field = "bytes "; - field += std::to_string(st); - field += '-'; - field += std::to_string(ed); - field += '/'; - field += std::to_string(content_length); - return field; -} - -template -bool process_multipart_ranges_data(const Request &req, - const std::string &boundary, - const std::string &content_type, - size_t content_length, SToken stoken, - CToken ctoken, Content content) { - for (size_t i = 0; i < req.ranges.size(); i++) { - ctoken("--"); - stoken(boundary); - ctoken("\r\n"); - if (!content_type.empty()) { - ctoken("Content-Type: "); - stoken(content_type); - ctoken("\r\n"); - } - - auto offset_and_length = - get_range_offset_and_length(req.ranges[i], content_length); - - ctoken("Content-Range: "); - stoken(make_content_range_header_field(offset_and_length, content_length)); - ctoken("\r\n"); - ctoken("\r\n"); - - if (!content(offset_and_length.first, offset_and_length.second)) { - return false; - } - ctoken("\r\n"); - } - - ctoken("--"); - stoken(boundary); - ctoken("--"); - - return true; -} - -void make_multipart_ranges_data(const Request &req, Response &res, - const std::string &boundary, - const std::string &content_type, - size_t content_length, - std::string &data) { - process_multipart_ranges_data( - req, boundary, content_type, content_length, - [&](const std::string &token) { data += token; }, - [&](const std::string &token) { data += token; }, - [&](size_t offset, size_t length) { - assert(offset + length <= content_length); - data += res.body.substr(offset, length); - return true; - }); -} - -size_t get_multipart_ranges_data_length(const Request &req, - const std::string &boundary, - const std::string &content_type, - size_t content_length) { - size_t data_length = 0; - - process_multipart_ranges_data( - req, boundary, content_type, content_length, - [&](const std::string &token) { data_length += token.size(); }, - [&](const std::string &token) { data_length += token.size(); }, - [&](size_t /*offset*/, size_t length) { - data_length += length; - return true; - }); - - return data_length; -} - -template -bool -write_multipart_ranges_data(Stream &strm, const Request &req, Response &res, - const std::string &boundary, - const std::string &content_type, - size_t content_length, const T &is_shutting_down) { - return process_multipart_ranges_data( - req, boundary, content_type, content_length, - [&](const std::string &token) { strm.write(token); }, - [&](const std::string &token) { strm.write(token); }, - [&](size_t offset, size_t length) { - return write_content(strm, res.content_provider_, offset, length, - is_shutting_down); - }); -} - -bool has_framed_body(const Request &req) { - return is_chunked_transfer_encoding(req.headers) || - req.get_header_value_u64("Content-Length") > 0; -} - -bool is_connection_persistent(const Request &req) { - auto conn = req.get_header_value("Connection"); - if (conn == "close") { return false; } - if (req.version == "HTTP/1.0" && conn != "Keep-Alive") { return false; } - return true; -} - -bool expect_content(const Request &req) { - if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH" || - req.method == "DELETE") { - return true; - } - return has_framed_body(req); -} - -#ifdef _WIN32 -class WSInit { -public: - WSInit() { - WSADATA wsaData; - if (WSAStartup(0x0002, &wsaData) == 0) is_valid_ = true; - } - - ~WSInit() { - if (is_valid_) WSACleanup(); - } - - bool is_valid_ = false; -}; - -static WSInit wsinit_; -#endif - -bool parse_www_authenticate(const Response &res, - std::map &auth, - bool is_proxy) { - auto auth_key = is_proxy ? "Proxy-Authenticate" : "WWW-Authenticate"; - if (res.has_header(auth_key)) { - thread_local auto re = - std::regex(R"~((?:(?:,\s*)?(.+?)=(?:"(.*?)"|([^,]*))))~"); - auto s = res.get_header_value(auth_key); - auto pos = s.find(' '); - if (pos != std::string::npos) { - auto type = s.substr(0, pos); - if (type == "Basic") { - return false; - } else if (type == "Digest") { - s = s.substr(pos + 1); - auto beg = std::sregex_iterator(s.begin(), s.end(), re); - for (auto i = beg; i != std::sregex_iterator(); ++i) { - const auto &m = *i; - auto key = s.substr(static_cast(m.position(1)), - static_cast(m.length(1))); - auto val = m.length(2) > 0 - ? s.substr(static_cast(m.position(2)), - static_cast(m.length(2))) - : s.substr(static_cast(m.position(3)), - static_cast(m.length(3))); - auth[std::move(key)] = std::move(val); - } - return true; - } - } - } - return false; -} - -class ContentProviderAdapter { -public: - explicit ContentProviderAdapter( - ContentProviderWithoutLength &&content_provider) - : content_provider_(std::move(content_provider)) {} - - bool operator()(size_t offset, size_t, DataSink &sink) { - return content_provider_(offset, sink); - } - -private: - ContentProviderWithoutLength content_provider_; -}; - -// NOTE: https://www.rfc-editor.org/rfc/rfc9110#section-5 -namespace fields { - -bool is_token_char(char c) { - return std::isalnum(c) || c == '!' || c == '#' || c == '$' || c == '%' || - c == '&' || c == '\'' || c == '*' || c == '+' || c == '-' || - c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~'; -} - -bool is_token(const std::string &s) { - if (s.empty()) { return false; } - for (auto c : s) { - if (!is_token_char(c)) { return false; } - } - return true; -} - -bool is_field_name(const std::string &s) { return is_token(s); } - -bool is_vchar(char c) { return c >= 33 && c <= 126; } - -bool is_obs_text(char c) { return 128 <= static_cast(c); } - -bool is_field_vchar(char c) { return is_vchar(c) || is_obs_text(c); } - -bool is_field_content(const std::string &s) { - if (s.empty()) { return true; } - - if (s.size() == 1) { - return is_field_vchar(s[0]); - } else if (s.size() == 2) { - return is_field_vchar(s[0]) && is_field_vchar(s[1]); - } else { - size_t i = 0; - - if (!is_field_vchar(s[i])) { return false; } - i++; - - while (i < s.size() - 1) { - auto c = s[i++]; - if (c == ' ' || c == '\t' || is_field_vchar(c)) { - } else { - return false; - } - } - - return is_field_vchar(s[i]); - } -} - -bool is_field_value(const std::string &s) { return is_field_content(s); } - -} // namespace fields - -bool perform_websocket_handshake(Stream &strm, const std::string &host, - int port, const std::string &path, - const Headers &headers, - std::string &selected_subprotocol) { - // Validate path and host - if (!fields::is_field_value(path) || !fields::is_field_value(host)) { - return false; - } - - // Validate user-provided headers - for (const auto &h : headers) { - if (!fields::is_field_name(h.first) || !fields::is_field_value(h.second)) { - return false; - } - } - - // Generate random Sec-WebSocket-Key - thread_local std::mt19937 rng(std::random_device{}()); - std::string key_bytes(16, '\0'); - for (size_t i = 0; i < 16; i += 4) { - auto r = rng(); - std::memcpy(&key_bytes[i], &r, (std::min)(size_t(4), size_t(16 - i))); - } - auto client_key = base64_encode(key_bytes); - - // Build upgrade request - std::string req_str = "GET " + path + " HTTP/1.1\r\n"; - req_str += "Host: " + host + ":" + std::to_string(port) + "\r\n"; - req_str += "Upgrade: websocket\r\n"; - req_str += "Connection: Upgrade\r\n"; - req_str += "Sec-WebSocket-Key: " + client_key + "\r\n"; - req_str += "Sec-WebSocket-Version: 13\r\n"; - for (const auto &h : headers) { - req_str += h.first + ": " + h.second + "\r\n"; - } - req_str += "\r\n"; - - if (strm.write(req_str.data(), req_str.size()) < 0) { return false; } - - // Verify 101 response and Sec-WebSocket-Accept header - auto expected_accept = websocket_accept_key(client_key); - return read_websocket_upgrade_response(strm, expected_accept, - selected_subprotocol); -} - -} // namespace detail - -/* - * Group 2: detail namespace - SSL common utilities - */ - -#ifdef CPPHTTPLIB_SSL_ENABLED -namespace detail { - -class SSLSocketStream final : public Stream { -public: - SSLSocketStream( - socket_t sock, tls::session_t session, time_t read_timeout_sec, - time_t read_timeout_usec, time_t write_timeout_sec, - time_t write_timeout_usec, time_t max_timeout_msec = 0, - std::chrono::time_point start_time = - (std::chrono::steady_clock::time_point::min)()); - ~SSLSocketStream() override; - - bool is_readable() const override; - bool wait_readable() const override; - bool wait_writable() const override; - bool is_peer_alive() const override; - ssize_t read(char *ptr, size_t size) override; - ssize_t write(const char *ptr, size_t size) override; - void get_remote_ip_and_port(std::string &ip, int &port) const override; - void get_local_ip_and_port(std::string &ip, int &port) const override; - socket_t socket() const override; - time_t duration() const override; - void set_read_timeout(time_t sec, time_t usec = 0) override; - -private: - socket_t sock_; - tls::session_t session_; - time_t read_timeout_sec_; - time_t read_timeout_usec_; - time_t write_timeout_sec_; - time_t write_timeout_usec_; - time_t max_timeout_msec_; - const std::chrono::time_point start_time_; -}; - -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT -std::string message_digest(const std::string &s, const EVP_MD *algo) { - auto context = std::unique_ptr( - EVP_MD_CTX_new(), EVP_MD_CTX_free); - - unsigned int hash_length = 0; - unsigned char hash[EVP_MAX_MD_SIZE]; - - EVP_DigestInit_ex(context.get(), algo, nullptr); - EVP_DigestUpdate(context.get(), s.c_str(), s.size()); - EVP_DigestFinal_ex(context.get(), hash, &hash_length); - - std::stringstream ss; - for (auto i = 0u; i < hash_length; ++i) { - ss << std::hex << std::setw(2) << std::setfill('0') - << static_cast(hash[i]); - } - - return ss.str(); -} - -std::string MD5(const std::string &s) { - return message_digest(s, EVP_md5()); -} - -std::string SHA_256(const std::string &s) { - return message_digest(s, EVP_sha256()); -} - -std::string SHA_512(const std::string &s) { - return message_digest(s, EVP_sha512()); -} -#elif defined(CPPHTTPLIB_MBEDTLS_SUPPORT) -namespace { -template -std::string hash_to_hex(const unsigned char (&hash)[N]) { - std::stringstream ss; - for (size_t i = 0; i < N; ++i) { - ss << std::hex << std::setw(2) << std::setfill('0') - << static_cast(hash[i]); - } - return ss.str(); -} -} // namespace - -std::string MD5(const std::string &s) { - unsigned char hash[16]; -#ifdef CPPHTTPLIB_MBEDTLS_V3 - mbedtls_md5(reinterpret_cast(s.c_str()), s.size(), - hash); -#else - mbedtls_md5_ret(reinterpret_cast(s.c_str()), s.size(), - hash); -#endif - return hash_to_hex(hash); -} - -std::string SHA_256(const std::string &s) { - unsigned char hash[32]; -#ifdef CPPHTTPLIB_MBEDTLS_V3 - mbedtls_sha256(reinterpret_cast(s.c_str()), s.size(), - hash, 0); -#else - mbedtls_sha256_ret(reinterpret_cast(s.c_str()), - s.size(), hash, 0); -#endif - return hash_to_hex(hash); -} - -std::string SHA_512(const std::string &s) { - unsigned char hash[64]; -#ifdef CPPHTTPLIB_MBEDTLS_V3 - mbedtls_sha512(reinterpret_cast(s.c_str()), s.size(), - hash, 0); -#else - mbedtls_sha512_ret(reinterpret_cast(s.c_str()), - s.size(), hash, 0); -#endif - return hash_to_hex(hash); -} -#elif defined(CPPHTTPLIB_WOLFSSL_SUPPORT) -namespace { -template -std::string hash_to_hex(const unsigned char (&hash)[N]) { - std::stringstream ss; - for (size_t i = 0; i < N; ++i) { - ss << std::hex << std::setw(2) << std::setfill('0') - << static_cast(hash[i]); - } - return ss.str(); -} -} // namespace - -std::string MD5(const std::string &s) { - unsigned char hash[WC_MD5_DIGEST_SIZE]; - wc_Md5Hash(reinterpret_cast(s.c_str()), - static_cast(s.size()), hash); - return hash_to_hex(hash); -} - -std::string SHA_256(const std::string &s) { - unsigned char hash[WC_SHA256_DIGEST_SIZE]; - wc_Sha256Hash(reinterpret_cast(s.c_str()), - static_cast(s.size()), hash); - return hash_to_hex(hash); -} - -std::string SHA_512(const std::string &s) { - unsigned char hash[WC_SHA512_DIGEST_SIZE]; - wc_Sha512Hash(reinterpret_cast(s.c_str()), - static_cast(s.size()), hash); - return hash_to_hex(hash); -} -#endif - -bool is_ip_address(const std::string &host) { - struct in_addr addr4; - struct in6_addr addr6; - return inet_pton(AF_INET, host.c_str(), &addr4) == 1 || - inet_pton(AF_INET6, host.c_str(), &addr6) == 1; -} - -template -bool process_server_socket_ssl( - const std::atomic &svr_sock, tls::session_t session, - socket_t sock, size_t keep_alive_max_count, time_t keep_alive_timeout_sec, - time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, - time_t write_timeout_usec, T callback) { - return process_server_socket_core( - svr_sock, sock, keep_alive_max_count, keep_alive_timeout_sec, - [&](bool close_connection, bool &connection_closed) { - SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec, - write_timeout_sec, write_timeout_usec); - return callback(strm, close_connection, connection_closed); - }); -} - -template -bool process_client_socket_ssl( - tls::session_t session, socket_t sock, time_t read_timeout_sec, - time_t read_timeout_usec, time_t write_timeout_sec, - time_t write_timeout_usec, time_t max_timeout_msec, - std::chrono::time_point start_time, T callback) { - SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec, - write_timeout_sec, write_timeout_usec, max_timeout_msec, - start_time); - return callback(strm); -} - -std::pair make_digest_authentication_header( - const Request &req, const std::map &auth, - size_t cnonce_count, const std::string &cnonce, const std::string &username, - const std::string &password, bool is_proxy = false) { - std::string nc; - { - std::stringstream ss; - ss << std::setfill('0') << std::setw(8) << std::hex << cnonce_count; - nc = ss.str(); - } - - std::string qop; - if (auth.find("qop") != auth.end()) { - qop = auth.at("qop"); - if (qop.find("auth-int") != std::string::npos) { - qop = "auth-int"; - } else if (qop.find("auth") != std::string::npos) { - qop = "auth"; - } else { - qop.clear(); - } - } - - std::string algo = "MD5"; - if (auth.find("algorithm") != auth.end()) { algo = auth.at("algorithm"); } - - std::string response; - { - auto H = algo == "SHA-256" ? detail::SHA_256 - : algo == "SHA-512" ? detail::SHA_512 - : detail::MD5; - - auto A1 = username + ":" + auth.at("realm") + ":" + password; - - auto A2 = req.method + ":" + req.path; - if (qop == "auth-int") { A2 += ":" + H(req.body); } - - if (qop.empty()) { - response = H(H(A1) + ":" + auth.at("nonce") + ":" + H(A2)); - } else { - response = H(H(A1) + ":" + auth.at("nonce") + ":" + nc + ":" + cnonce + - ":" + qop + ":" + H(A2)); - } - } - - auto opaque = (auth.find("opaque") != auth.end()) ? auth.at("opaque") : ""; - - auto field = "Digest username=\"" + username + "\", realm=\"" + - auth.at("realm") + "\", nonce=\"" + auth.at("nonce") + - "\", uri=\"" + req.path + "\", algorithm=" + algo + - (qop.empty() ? ", response=\"" - : ", qop=" + qop + ", nc=" + nc + ", cnonce=\"" + - cnonce + "\", response=\"") + - response + "\"" + - (opaque.empty() ? "" : ", opaque=\"" + opaque + "\""); - - auto key = is_proxy ? "Proxy-Authorization" : "Authorization"; - return std::make_pair(key, field); -} - -bool match_hostname(const std::string &pattern, - const std::string &hostname) { - // Exact match (case-insensitive) - if (detail::case_ignore::equal(hostname, pattern)) { return true; } - - // Split both pattern and hostname into components by '.' - std::vector pattern_components; - if (!pattern.empty()) { - split(pattern.data(), pattern.data() + pattern.size(), '.', - [&](const char *b, const char *e) { - pattern_components.emplace_back(b, e); - }); - } - - std::vector host_components; - if (!hostname.empty()) { - split(hostname.data(), hostname.data() + hostname.size(), '.', - [&](const char *b, const char *e) { - host_components.emplace_back(b, e); - }); - } - - // Component count must match - if (host_components.size() != pattern_components.size()) { return false; } - - // Compare each component with wildcard support - // Supports: "*" (full wildcard), "prefix*" (partial wildcard) - // https://bugs.launchpad.net/ubuntu/+source/firefox-3.0/+bug/376484 - auto itr = pattern_components.begin(); - for (const auto &h : host_components) { - auto &p = *itr; - if (!detail::case_ignore::equal(p, h) && p != "*") { - bool partial_match = false; - if (!p.empty() && p[p.size() - 1] == '*') { - const auto prefix_length = p.size() - 1; - if (prefix_length == 0) { - partial_match = true; - } else if (h.size() >= prefix_length) { - partial_match = - std::equal(p.begin(), - p.begin() + static_cast( - prefix_length), - h.begin(), [](const char ca, const char cb) { - return detail::case_ignore::to_lower(ca) == - detail::case_ignore::to_lower(cb); - }); - } - } - if (!partial_match) { return false; } - } - ++itr; - } - - return true; -} - -#ifdef _WIN32 -// Verify certificate using Windows CertGetCertificateChain API. -// This provides real-time certificate validation with Windows Update -// integration, independent of the TLS backend (OpenSSL or MbedTLS). -bool -verify_cert_with_windows_schannel(const std::vector &der_cert, - const std::string &hostname, - bool verify_hostname, uint64_t &out_error) { - if (der_cert.empty()) { return false; } - - out_error = 0; - - // Create Windows certificate context from DER data - auto cert_context = CertCreateCertificateContext( - X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, der_cert.data(), - static_cast(der_cert.size())); - - if (!cert_context) { - out_error = GetLastError(); - return false; - } - - auto cert_guard = - scope_exit([&] { CertFreeCertificateContext(cert_context); }); - - // Setup chain parameters - CERT_CHAIN_PARA chain_para = {}; - chain_para.cbSize = sizeof(chain_para); - - // Build certificate chain with revocation checking - PCCERT_CHAIN_CONTEXT chain_context = nullptr; - auto chain_result = CertGetCertificateChain( - nullptr, cert_context, nullptr, cert_context->hCertStore, &chain_para, - CERT_CHAIN_CACHE_END_CERT | CERT_CHAIN_REVOCATION_CHECK_END_CERT | - CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT, - nullptr, &chain_context); - - if (!chain_result || !chain_context) { - out_error = GetLastError(); - return false; - } - - auto chain_guard = - scope_exit([&] { CertFreeCertificateChain(chain_context); }); - - // Check if chain has errors - if (chain_context->TrustStatus.dwErrorStatus != CERT_TRUST_NO_ERROR) { - out_error = chain_context->TrustStatus.dwErrorStatus; - return false; - } - - // Verify SSL policy - SSL_EXTRA_CERT_CHAIN_POLICY_PARA extra_policy_para = {}; - extra_policy_para.cbSize = sizeof(extra_policy_para); -#ifdef AUTHTYPE_SERVER - extra_policy_para.dwAuthType = AUTHTYPE_SERVER; -#endif - - std::wstring whost; - if (verify_hostname) { - whost = u8string_to_wstring(hostname.c_str()); - extra_policy_para.pwszServerName = const_cast(whost.c_str()); - } - - CERT_CHAIN_POLICY_PARA policy_para = {}; - policy_para.cbSize = sizeof(policy_para); -#ifdef CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS - policy_para.dwFlags = CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS; -#else - policy_para.dwFlags = 0; -#endif - policy_para.pvExtraPolicyPara = &extra_policy_para; - - CERT_CHAIN_POLICY_STATUS policy_status = {}; - policy_status.cbSize = sizeof(policy_status); - - if (!CertVerifyCertificateChainPolicy(CERT_CHAIN_POLICY_SSL, chain_context, - &policy_para, &policy_status)) { - out_error = GetLastError(); - return false; - } - - if (policy_status.dwError != 0) { - out_error = policy_status.dwError; - return false; - } - - return true; -} -#endif // _WIN32 - -bool setup_client_tls_session(const std::string &host, tls::ctx_t &ctx, - tls::session_t &session, socket_t sock, - bool server_certificate_verification, - const std::string &ca_cert_file_path, - tls::ca_store_t ca_cert_store, - time_t timeout_sec, time_t timeout_usec) { - using namespace tls; - - ctx = create_client_context(); - if (!ctx) { return false; } - - if (server_certificate_verification) { - if (!ca_cert_file_path.empty()) { - load_ca_file(ctx, ca_cert_file_path.c_str()); - } - if (ca_cert_store) { set_ca_store(ctx, ca_cert_store); } - load_system_certs(ctx); - } - - bool is_ip = is_ip_address(host); - -#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT - if (is_ip && server_certificate_verification) { - set_verify_client(ctx, false); - } else { - set_verify_client(ctx, server_certificate_verification); - } -#endif - - session = create_session(ctx, sock); - if (!session) { return false; } - - // RFC 6066: SNI must not be set for IP addresses - if (!is_ip) { set_sni(session, host.c_str()); } - if (server_certificate_verification) { set_hostname(session, host.c_str()); } - - if (!connect_nonblocking(session, sock, timeout_sec, timeout_usec, nullptr)) { - return false; - } - - if (server_certificate_verification) { - if (get_verify_result(session) != 0) { return false; } - } - - return true; -} - -} // namespace detail -#endif // CPPHTTPLIB_SSL_ENABLED - -/* - * Group 3: httplib namespace - Non-SSL public API implementations - */ - -void default_socket_options(socket_t sock) { - set_socket_opt(sock, SOL_SOCKET, -#ifdef SO_REUSEPORT - SO_REUSEPORT, -#else - SO_REUSEADDR, -#endif - 1); -} - -bool set_socket_opt(socket_t sock, int level, int optname, int optval) { - return detail::set_socket_opt_impl(sock, level, optname, &optval, - sizeof(optval)); -} - -std::string get_bearer_token_auth(const Request &req) { - if (req.has_header("Authorization")) { - constexpr auto bearer_header_prefix_len = detail::str_len("Bearer "); - return req.get_header_value("Authorization") - .substr(bearer_header_prefix_len); - } - return ""; -} - -const char *status_message(int status) { - switch (status) { - case StatusCode::Continue_100: return "Continue"; - case StatusCode::SwitchingProtocol_101: return "Switching Protocol"; - case StatusCode::Processing_102: return "Processing"; - case StatusCode::EarlyHints_103: return "Early Hints"; - case StatusCode::OK_200: return "OK"; - case StatusCode::Created_201: return "Created"; - case StatusCode::Accepted_202: return "Accepted"; - case StatusCode::NonAuthoritativeInformation_203: - return "Non-Authoritative Information"; - case StatusCode::NoContent_204: return "No Content"; - case StatusCode::ResetContent_205: return "Reset Content"; - case StatusCode::PartialContent_206: return "Partial Content"; - case StatusCode::MultiStatus_207: return "Multi-Status"; - case StatusCode::AlreadyReported_208: return "Already Reported"; - case StatusCode::IMUsed_226: return "IM Used"; - case StatusCode::MultipleChoices_300: return "Multiple Choices"; - case StatusCode::MovedPermanently_301: return "Moved Permanently"; - case StatusCode::Found_302: return "Found"; - case StatusCode::SeeOther_303: return "See Other"; - case StatusCode::NotModified_304: return "Not Modified"; - case StatusCode::UseProxy_305: return "Use Proxy"; - case StatusCode::unused_306: return "unused"; - case StatusCode::TemporaryRedirect_307: return "Temporary Redirect"; - case StatusCode::PermanentRedirect_308: return "Permanent Redirect"; - case StatusCode::BadRequest_400: return "Bad Request"; - case StatusCode::Unauthorized_401: return "Unauthorized"; - case StatusCode::PaymentRequired_402: return "Payment Required"; - case StatusCode::Forbidden_403: return "Forbidden"; - case StatusCode::NotFound_404: return "Not Found"; - case StatusCode::MethodNotAllowed_405: return "Method Not Allowed"; - case StatusCode::NotAcceptable_406: return "Not Acceptable"; - case StatusCode::ProxyAuthenticationRequired_407: - return "Proxy Authentication Required"; - case StatusCode::RequestTimeout_408: return "Request Timeout"; - case StatusCode::Conflict_409: return "Conflict"; - case StatusCode::Gone_410: return "Gone"; - case StatusCode::LengthRequired_411: return "Length Required"; - case StatusCode::PreconditionFailed_412: return "Precondition Failed"; - case StatusCode::PayloadTooLarge_413: return "Payload Too Large"; - case StatusCode::UriTooLong_414: return "URI Too Long"; - case StatusCode::UnsupportedMediaType_415: return "Unsupported Media Type"; - case StatusCode::RangeNotSatisfiable_416: return "Range Not Satisfiable"; - case StatusCode::ExpectationFailed_417: return "Expectation Failed"; - case StatusCode::ImATeapot_418: return "I'm a teapot"; - case StatusCode::MisdirectedRequest_421: return "Misdirected Request"; - case StatusCode::UnprocessableContent_422: return "Unprocessable Content"; - case StatusCode::Locked_423: return "Locked"; - case StatusCode::FailedDependency_424: return "Failed Dependency"; - case StatusCode::TooEarly_425: return "Too Early"; - case StatusCode::UpgradeRequired_426: return "Upgrade Required"; - case StatusCode::PreconditionRequired_428: return "Precondition Required"; - case StatusCode::TooManyRequests_429: return "Too Many Requests"; - case StatusCode::RequestHeaderFieldsTooLarge_431: - return "Request Header Fields Too Large"; - case StatusCode::UnavailableForLegalReasons_451: - return "Unavailable For Legal Reasons"; - case StatusCode::NotImplemented_501: return "Not Implemented"; - case StatusCode::BadGateway_502: return "Bad Gateway"; - case StatusCode::ServiceUnavailable_503: return "Service Unavailable"; - case StatusCode::GatewayTimeout_504: return "Gateway Timeout"; - case StatusCode::HttpVersionNotSupported_505: - return "HTTP Version Not Supported"; - case StatusCode::VariantAlsoNegotiates_506: return "Variant Also Negotiates"; - case StatusCode::InsufficientStorage_507: return "Insufficient Storage"; - case StatusCode::LoopDetected_508: return "Loop Detected"; - case StatusCode::NotExtended_510: return "Not Extended"; - case StatusCode::NetworkAuthenticationRequired_511: - return "Network Authentication Required"; - - default: - case StatusCode::InternalServerError_500: return "Internal Server Error"; - } -} - -std::string to_string(const Error error) { - switch (error) { - case Error::Success: return "Success (no error)"; - case Error::Unknown: return "Unknown"; - case Error::Connection: return "Could not establish connection"; - case Error::BindIPAddress: return "Failed to bind IP address"; - case Error::Read: return "Failed to read connection"; - case Error::Write: return "Failed to write connection"; - case Error::ExceedRedirectCount: return "Maximum redirect count exceeded"; - case Error::Canceled: return "Connection handling canceled"; - case Error::SSLConnection: return "SSL connection failed"; - case Error::SSLLoadingCerts: return "SSL certificate loading failed"; - case Error::SSLServerVerification: return "SSL server verification failed"; - case Error::SSLServerHostnameVerification: - return "SSL server hostname verification failed"; - case Error::UnsupportedMultipartBoundaryChars: - return "Unsupported HTTP multipart boundary characters"; - case Error::Compression: return "Compression failed"; - case Error::ConnectionTimeout: return "Connection timed out"; - case Error::ProxyConnection: return "Proxy connection failed"; - case Error::ConnectionClosed: return "Connection closed by server"; - case Error::Timeout: return "Read timeout"; - case Error::ResourceExhaustion: return "Resource exhaustion"; - case Error::TooManyFormDataFiles: return "Too many form data files"; - case Error::ExceedMaxPayloadSize: return "Exceeded maximum payload size"; - case Error::ExceedUriMaxLength: return "Exceeded maximum URI length"; - case Error::ExceedMaxSocketDescriptorCount: - return "Exceeded maximum socket descriptor count"; - case Error::InvalidRequestLine: return "Invalid request line"; - case Error::InvalidHTTPMethod: return "Invalid HTTP method"; - case Error::InvalidHTTPVersion: return "Invalid HTTP version"; - case Error::InvalidHeaders: return "Invalid headers"; - case Error::MultipartParsing: return "Multipart parsing failed"; - case Error::OpenFile: return "Failed to open file"; - case Error::Listen: return "Failed to listen on socket"; - case Error::GetSockName: return "Failed to get socket name"; - case Error::UnsupportedAddressFamily: return "Unsupported address family"; - case Error::HTTPParsing: return "HTTP parsing failed"; - case Error::InvalidRangeHeader: return "Invalid Range header"; - default: break; - } - - return "Invalid"; -} - -std::ostream &operator<<(std::ostream &os, const Error &obj) { - os << to_string(obj); - os << " (" << static_cast::type>(obj) << ')'; - return os; -} - -std::string hosted_at(const std::string &hostname) { - std::vector addrs; - hosted_at(hostname, addrs); - if (addrs.empty()) { return std::string(); } - return addrs[0]; -} - -void hosted_at(const std::string &hostname, - std::vector &addrs) { - struct addrinfo hints; - struct addrinfo *result; - - memset(&hints, 0, sizeof(struct addrinfo)); - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - hints.ai_protocol = 0; - - if (detail::getaddrinfo_with_timeout(hostname.c_str(), nullptr, &hints, - &result, 0)) { -#if defined __linux__ && !defined __ANDROID__ - res_init(); -#endif - return; - } - auto se = detail::scope_exit([&] { freeaddrinfo(result); }); - - for (auto rp = result; rp; rp = rp->ai_next) { - const auto &addr = - *reinterpret_cast(rp->ai_addr); - std::string ip; - auto dummy = -1; - if (detail::get_ip_and_port(addr, sizeof(struct sockaddr_storage), ip, - dummy)) { - addrs.emplace_back(std::move(ip)); - } - } -} - -std::string encode_uri_component(const std::string &value) { - std::ostringstream escaped; - escaped.fill('0'); - escaped << std::hex; - - for (auto c : value) { - if (std::isalnum(static_cast(c)) || c == '-' || c == '_' || - c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' || - c == ')') { - escaped << c; - } else { - escaped << std::uppercase; - escaped << '%' << std::setw(2) - << static_cast(static_cast(c)); - escaped << std::nouppercase; - } - } - - return escaped.str(); -} - -std::string encode_uri(const std::string &value) { - std::ostringstream escaped; - escaped.fill('0'); - escaped << std::hex; - - for (auto c : value) { - if (std::isalnum(static_cast(c)) || c == '-' || c == '_' || - c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' || - c == ')' || c == ';' || c == '/' || c == '?' || c == ':' || c == '@' || - c == '&' || c == '=' || c == '+' || c == '$' || c == ',' || c == '#') { - escaped << c; - } else { - escaped << std::uppercase; - escaped << '%' << std::setw(2) - << static_cast(static_cast(c)); - escaped << std::nouppercase; - } - } - - return escaped.str(); -} - -std::string decode_uri_component(const std::string &value) { - std::string result; - - for (size_t i = 0; i < value.size(); i++) { - if (value[i] == '%' && i + 2 < value.size()) { - auto val = 0; - if (detail::from_hex_to_i(value, i + 1, 2, val)) { - result += static_cast(val); - i += 2; - } else { - result += value[i]; - } - } else { - result += value[i]; - } - } - - return result; -} - -std::string decode_uri(const std::string &value) { - std::string result; - - for (size_t i = 0; i < value.size(); i++) { - if (value[i] == '%' && i + 2 < value.size()) { - auto val = 0; - if (detail::from_hex_to_i(value, i + 1, 2, val)) { - result += static_cast(val); - i += 2; - } else { - result += value[i]; - } - } else { - result += value[i]; - } - } - - return result; -} - -std::string encode_path_component(const std::string &component) { - std::string result; - result.reserve(component.size() * 3); - - for (size_t i = 0; i < component.size(); i++) { - auto c = static_cast(component[i]); - - // Unreserved characters per RFC 3986: ALPHA / DIGIT / "-" / "." / "_" / "~" - if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') { - result += static_cast(c); - } - // Path-safe sub-delimiters: "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / - // "," / ";" / "=" - else if (c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' || - c == ')' || c == '*' || c == '+' || c == ',' || c == ';' || - c == '=') { - result += static_cast(c); - } - // Colon is allowed in path segments except first segment - else if (c == ':') { - result += static_cast(c); - } - // @ is allowed in path - else if (c == '@') { - result += static_cast(c); - } else { - result += '%'; - char hex[3]; - snprintf(hex, sizeof(hex), "%02X", c); - result.append(hex, 2); - } - } - return result; -} - -std::string decode_path_component(const std::string &component) { - std::string result; - result.reserve(component.size()); - - for (size_t i = 0; i < component.size(); i++) { - if (component[i] == '%' && i + 1 < component.size()) { - if (component[i + 1] == 'u') { - // Unicode %uXXXX encoding - auto val = 0; - if (detail::from_hex_to_i(component, i + 2, 4, val)) { - // 4 digits Unicode codes: val is 0x0000-0xFFFF (from 4 hex digits), - // so to_utf8 writes at most 3 bytes. buff[4] is safe. - char buff[4]; - size_t len = detail::to_utf8(val, buff); - if (len > 0) { result.append(buff, len); } - i += 5; // 'u0000' - } else { - result += component[i]; - } - } else { - // Standard %XX encoding - auto val = 0; - if (detail::from_hex_to_i(component, i + 1, 2, val)) { - // 2 digits hex codes - result += static_cast(val); - i += 2; // 'XX' - } else { - result += component[i]; - } - } - } else { - result += component[i]; - } - } - return result; -} - -std::string encode_query_component(const std::string &component, - bool space_as_plus) { - std::string result; - result.reserve(component.size() * 3); - - for (size_t i = 0; i < component.size(); i++) { - auto c = static_cast(component[i]); - - // Unreserved characters per RFC 3986 - if (std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~') { - result += static_cast(c); - } - // Space handling - else if (c == ' ') { - if (space_as_plus) { - result += '+'; - } else { - result += "%20"; - } - } - // Plus sign handling - else if (c == '+') { - if (space_as_plus) { - result += "%2B"; - } else { - result += static_cast(c); - } - } - // Query-safe sub-delimiters (excluding & and = which are query delimiters) - else if (c == '!' || c == '$' || c == '\'' || c == '(' || c == ')' || - c == '*' || c == ',' || c == ';') { - result += static_cast(c); - } - // Colon and @ are allowed in query - else if (c == ':' || c == '@') { - result += static_cast(c); - } - // Forward slash is allowed in query values - else if (c == '/') { - result += static_cast(c); - } - // Question mark is allowed in query values (after first ?) - else if (c == '?') { - result += static_cast(c); - } else { - result += '%'; - char hex[3]; - snprintf(hex, sizeof(hex), "%02X", c); - result.append(hex, 2); - } - } - return result; -} - -std::string decode_query_component(const std::string &component, - bool plus_as_space) { - std::string result; - result.reserve(component.size()); - - for (size_t i = 0; i < component.size(); i++) { - if (component[i] == '%' && i + 2 < component.size()) { - std::string hex = component.substr(i + 1, 2); - char *end; - unsigned long value = std::strtoul(hex.c_str(), &end, 16); - if (end == hex.c_str() + 2) { - result += static_cast(value); - i += 2; - } else { - result += component[i]; - } - } else if (component[i] == '+' && plus_as_space) { - result += ' '; // + becomes space in form-urlencoded - } else { - result += component[i]; - } - } - return result; -} - -std::string sanitize_filename(const std::string &filename) { - // Extract basename: find the last path separator (/ or \) - auto pos = filename.find_last_of("/\\"); - auto result = - (pos != std::string::npos) ? filename.substr(pos + 1) : filename; - - // Strip null bytes - result.erase(std::remove(result.begin(), result.end(), '\0'), result.end()); - - // Trim whitespace - { - auto start = result.find_first_not_of(" \t"); - auto end = result.find_last_not_of(" \t"); - result = (start == std::string::npos) - ? "" - : result.substr(start, end - start + 1); - } - - // Reject . and .. - if (result == "." || result == "..") { return ""; } - - return result; -} - -std::string append_query_params(const std::string &path, - const Params ¶ms) { - std::string path_with_query = path; - thread_local const std::regex re("[^?]+\\?.*"); - auto delm = std::regex_match(path, re) ? '&' : '?'; - path_with_query += delm + detail::params_to_query_str(params); - return path_with_query; -} - -// Header utilities -std::pair -make_range_header(const Ranges &ranges) { - std::string field = "bytes="; - auto i = 0; - for (const auto &r : ranges) { - if (i != 0) { field += ", "; } - if (r.first != -1) { field += std::to_string(r.first); } - field += '-'; - if (r.second != -1) { field += std::to_string(r.second); } - i++; - } - return std::make_pair("Range", std::move(field)); -} - -std::pair -make_basic_authentication_header(const std::string &username, - const std::string &password, bool is_proxy) { - auto field = "Basic " + detail::base64_encode(username + ":" + password); - auto key = is_proxy ? "Proxy-Authorization" : "Authorization"; - return std::make_pair(key, std::move(field)); -} - -std::pair -make_bearer_token_authentication_header(const std::string &token, - bool is_proxy = false) { - auto field = "Bearer " + token; - auto key = is_proxy ? "Proxy-Authorization" : "Authorization"; - return std::make_pair(key, std::move(field)); -} - -// Request implementation -size_t Request::get_header_value_u64(const std::string &key, size_t def, - size_t id) const { - return detail::get_header_value_u64(headers, key, def, id); -} - -bool Request::has_header(const std::string &key) const { - return detail::has_header(headers, key); -} - -std::string Request::get_header_value(const std::string &key, - const char *def, size_t id) const { - return detail::get_header_value(headers, key, def, id); -} - -size_t Request::get_header_value_count(const std::string &key) const { - return detail::get_header_value_count(headers, key); -} - -void Request::set_header(const std::string &key, - const std::string &val) { - detail::set_header(headers, key, val); -} - -bool Request::has_trailer(const std::string &key) const { - return trailers.find(key) != trailers.end(); -} - -std::string Request::get_trailer_value(const std::string &key, - size_t id) const { - return detail::get_multimap_value(trailers, key, id); -} - -size_t Request::get_trailer_value_count(const std::string &key) const { - auto r = trailers.equal_range(key); - return static_cast(std::distance(r.first, r.second)); -} - -bool Request::has_param(const std::string &key) const { - return params.find(key) != params.end(); -} - -std::string Request::get_param_value(const std::string &key, - size_t id) const { - return detail::get_multimap_value(params, key, id); -} - -std::vector -Request::get_param_values(const std::string &key) const { - auto rng = params.equal_range(key); - std::vector values; - values.reserve(static_cast(std::distance(rng.first, rng.second))); - for (auto it = rng.first; it != rng.second; ++it) { - values.push_back(it->second); - } - return values; -} - -size_t Request::get_param_value_count(const std::string &key) const { - auto r = params.equal_range(key); - return static_cast(std::distance(r.first, r.second)); -} - -bool Request::is_multipart_form_data() const { - const auto &content_type = get_header_value("Content-Type"); - return detail::extract_media_type(content_type) == "multipart/form-data"; -} - -// Multipart FormData implementation -std::string MultipartFormData::get_field(const std::string &key, - size_t id) const { - auto rng = fields.equal_range(key); - auto it = rng.first; - std::advance(it, static_cast(id)); - if (it != rng.second) { return it->second.content; } - return std::string(); -} - -std::vector -MultipartFormData::get_fields(const std::string &key) const { - std::vector values; - auto rng = fields.equal_range(key); - for (auto it = rng.first; it != rng.second; it++) { - values.push_back(it->second.content); - } - return values; -} - -bool MultipartFormData::has_field(const std::string &key) const { - return fields.find(key) != fields.end(); -} - -size_t MultipartFormData::get_field_count(const std::string &key) const { - auto r = fields.equal_range(key); - return static_cast(std::distance(r.first, r.second)); -} - -FormData MultipartFormData::get_file(const std::string &key, - size_t id) const { - return detail::get_multimap_value(files, key, id); -} - -std::vector -MultipartFormData::get_files(const std::string &key) const { - std::vector values; - auto rng = files.equal_range(key); - for (auto it = rng.first; it != rng.second; it++) { - values.push_back(it->second); - } - return values; -} - -bool MultipartFormData::has_file(const std::string &key) const { - return files.find(key) != files.end(); -} - -size_t MultipartFormData::get_file_count(const std::string &key) const { - auto r = files.equal_range(key); - return static_cast(std::distance(r.first, r.second)); -} - -// Response implementation -size_t Response::get_header_value_u64(const std::string &key, size_t def, - size_t id) const { - return detail::get_header_value_u64(headers, key, def, id); -} - -bool Response::has_header(const std::string &key) const { - return headers.find(key) != headers.end(); -} - -std::string Response::get_header_value(const std::string &key, - const char *def, - size_t id) const { - return detail::get_header_value(headers, key, def, id); -} - -size_t Response::get_header_value_count(const std::string &key) const { - return detail::get_header_value_count(headers, key); -} - -void Response::set_header(const std::string &key, - const std::string &val) { - detail::set_header(headers, key, val); -} -bool Response::has_trailer(const std::string &key) const { - return trailers.find(key) != trailers.end(); -} - -std::string Response::get_trailer_value(const std::string &key, - size_t id) const { - return detail::get_multimap_value(trailers, key, id); -} - -size_t Response::get_trailer_value_count(const std::string &key) const { - auto r = trailers.equal_range(key); - return static_cast(std::distance(r.first, r.second)); -} - -void Response::set_redirect(const std::string &url, int stat) { - if (detail::fields::is_field_value(url)) { - set_header("Location", url); - if (300 <= stat && stat < 400) { - this->status = stat; - } else { - this->status = StatusCode::Found_302; - } - } -} - -void Response::set_content(const char *s, size_t n, - const std::string &content_type) { - body.assign(s, n); - - auto rng = headers.equal_range("Content-Type"); - headers.erase(rng.first, rng.second); - set_header("Content-Type", content_type); -} - -void Response::set_content(const std::string &s, - const std::string &content_type) { - set_content(s.data(), s.size(), content_type); -} - -void Response::set_content(std::string &&s, - const std::string &content_type) { - body = std::move(s); - - auto rng = headers.equal_range("Content-Type"); - headers.erase(rng.first, rng.second); - set_header("Content-Type", content_type); -} - -void Response::set_content_provider( - size_t in_length, const std::string &content_type, ContentProvider provider, - ContentProviderResourceReleaser resource_releaser) { - set_header("Content-Type", content_type); - content_length_ = in_length; - if (in_length > 0) { content_provider_ = std::move(provider); } - content_provider_resource_releaser_ = std::move(resource_releaser); - is_chunked_content_provider_ = false; -} - -void Response::set_content_provider( - const std::string &content_type, ContentProviderWithoutLength provider, - ContentProviderResourceReleaser resource_releaser) { - set_header("Content-Type", content_type); - content_length_ = 0; - content_provider_ = detail::ContentProviderAdapter(std::move(provider)); - content_provider_resource_releaser_ = std::move(resource_releaser); - is_chunked_content_provider_ = false; -} - -void Response::set_chunked_content_provider( - const std::string &content_type, ContentProviderWithoutLength provider, - ContentProviderResourceReleaser resource_releaser) { - set_header("Content-Type", content_type); - content_length_ = 0; - content_provider_ = detail::ContentProviderAdapter(std::move(provider)); - content_provider_resource_releaser_ = std::move(resource_releaser); - is_chunked_content_provider_ = true; -} - -void Response::set_file_content(const std::string &path, - const std::string &content_type) { - file_content_path_ = path; - file_content_content_type_ = content_type; -} - -void Response::set_file_content(const std::string &path) { - file_content_path_ = path; -} - -// Result implementation -size_t Result::get_request_header_value_u64(const std::string &key, - size_t def, - size_t id) const { - return detail::get_header_value_u64(request_headers_, key, def, id); -} - -bool Result::has_request_header(const std::string &key) const { - return request_headers_.find(key) != request_headers_.end(); -} - -std::string Result::get_request_header_value(const std::string &key, - const char *def, - size_t id) const { - return detail::get_header_value(request_headers_, key, def, id); -} - -size_t -Result::get_request_header_value_count(const std::string &key) const { - auto r = request_headers_.equal_range(key); - return static_cast(std::distance(r.first, r.second)); -} - -// Stream implementation -ssize_t Stream::write(const char *ptr) { - return write(ptr, strlen(ptr)); -} - -ssize_t Stream::write(const std::string &s) { - return write(s.data(), s.size()); -} - -// BodyReader implementation -ssize_t detail::BodyReader::read(char *buf, size_t len) { - if (!stream) { - last_error = Error::Connection; - return -1; - } - if (eof) { return 0; } - - if (!chunked) { - // Content-Length based reading - if (has_content_length && bytes_read >= content_length) { - eof = true; - return 0; - } - - auto to_read = len; - if (has_content_length) { - auto remaining = content_length - bytes_read; - to_read = (std::min)(len, remaining); - } - auto n = stream->read(buf, to_read); - - if (n < 0) { - last_error = stream->get_error(); - if (last_error == Error::Success) { last_error = Error::Read; } - eof = true; - return n; - } - if (n == 0) { - // Unexpected EOF before content_length - last_error = stream->get_error(); - if (last_error == Error::Success) { last_error = Error::Read; } - eof = true; - return 0; - } - - bytes_read += static_cast(n); - if (has_content_length && bytes_read >= content_length) { eof = true; } - if (payload_max_length > 0 && bytes_read > payload_max_length) { - last_error = Error::ExceedMaxPayloadSize; - eof = true; - return -1; - } - return n; - } - - // Chunked transfer encoding: delegate to shared decoder instance. - if (!chunked_decoder) { chunked_decoder.reset(new ChunkedDecoder(*stream)); } - - size_t chunk_offset = 0; - size_t chunk_total = 0; - auto n = chunked_decoder->read_payload(buf, len, chunk_offset, chunk_total); - if (n < 0) { - last_error = stream->get_error(); - if (last_error == Error::Success) { last_error = Error::Read; } - eof = true; - return n; - } - - if (n == 0) { - // Final chunk observed. Leave trailer parsing to the caller (StreamHandle). - eof = true; - return 0; - } - - bytes_read += static_cast(n); - if (payload_max_length > 0 && bytes_read > payload_max_length) { - last_error = Error::ExceedMaxPayloadSize; - eof = true; - return -1; - } - return n; -} - -// ThreadPool implementation -ThreadPool::ThreadPool(size_t n, size_t max_n, size_t mqr) - : base_thread_count_(n), max_queued_requests_(mqr), idle_thread_count_(0), - shutdown_(false) { -#ifndef CPPHTTPLIB_NO_EXCEPTIONS - if (max_n != 0 && max_n < n) { - std::string msg = "max_threads must be >= base_threads"; - throw std::invalid_argument(msg); - } -#endif - max_thread_count_ = max_n == 0 ? n : max_n; - threads_.reserve(base_thread_count_); -#ifndef CPPHTTPLIB_NO_EXCEPTIONS - try { -#endif - for (size_t i = 0; i < base_thread_count_; i++) { - threads_.emplace_back(std::thread([this]() { worker(false); })); - } -#ifndef CPPHTTPLIB_NO_EXCEPTIONS - } catch (...) { - // If thread creation fails partway (e.g., pthread_create returns EAGAIN), - // signal the workers we already spawned to exit and join them so the - // vector destructor does not see joinable threads (which would call - // std::terminate). Then rethrow so the caller learns of the failure. - { - std::unique_lock lock(mutex_); - shutdown_ = true; - } - cond_.notify_all(); - for (auto &t : threads_) { - if (t.joinable()) { t.join(); } - } - throw; - } -#endif -} - -bool ThreadPool::enqueue(std::function fn) { - { - std::unique_lock lock(mutex_); - if (shutdown_) { return false; } - if (max_queued_requests_ > 0 && jobs_.size() >= max_queued_requests_) { - return false; - } - jobs_.push_back(std::move(fn)); - - // Spawn a dynamic thread if no idle threads and under max - if (idle_thread_count_ == 0 && - threads_.size() + dynamic_threads_.size() < max_thread_count_) { - cleanup_finished_threads(); - dynamic_threads_.emplace_back(std::thread([this]() { worker(true); })); - } - } - - cond_.notify_one(); - return true; -} - -void ThreadPool::shutdown() { - { - std::unique_lock lock(mutex_); - shutdown_ = true; - } - - cond_.notify_all(); - - for (auto &t : threads_) { - if (t.joinable()) { t.join(); } - } - - // Move dynamic_threads_ to a local list under the lock to avoid racing - // with worker threads that call move_to_finished() concurrently. - std::list remaining_dynamic; - { - std::unique_lock lock(mutex_); - remaining_dynamic = std::move(dynamic_threads_); - } - for (auto &t : remaining_dynamic) { - if (t.joinable()) { t.join(); } - } - - std::unique_lock lock(mutex_); - cleanup_finished_threads(); -} - -void ThreadPool::move_to_finished(std::thread::id id) { - // Must be called with mutex_ held - for (auto it = dynamic_threads_.begin(); it != dynamic_threads_.end(); ++it) { - if (it->get_id() == id) { - finished_threads_.push_back(std::move(*it)); - dynamic_threads_.erase(it); - return; - } - } -} - -void ThreadPool::cleanup_finished_threads() { - // Must be called with mutex_ held - for (auto &t : finished_threads_) { - if (t.joinable()) { t.join(); } - } - finished_threads_.clear(); -} - -void ThreadPool::worker(bool is_dynamic) { - for (;;) { - std::function fn; - { - std::unique_lock lock(mutex_); - idle_thread_count_++; - - if (is_dynamic) { - auto has_work = cond_.wait_for( - lock, std::chrono::seconds(CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT), - [&] { return !jobs_.empty() || shutdown_; }); - if (!has_work) { - // Timed out with no work - exit this dynamic thread - idle_thread_count_--; - move_to_finished(std::this_thread::get_id()); - break; - } - } else { - cond_.wait(lock, [&] { return !jobs_.empty() || shutdown_; }); - } - - idle_thread_count_--; - - if (shutdown_ && jobs_.empty()) { break; } - - fn = std::move(jobs_.front()); - jobs_.pop_front(); - } - - assert(true == static_cast(fn)); - fn(); - } - -#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && !defined(OPENSSL_IS_BORINGSSL) && \ - !defined(LIBRESSL_VERSION_NUMBER) - OPENSSL_thread_stop(); -#endif -} - -/* - * Group 1 (continued): detail namespace - Stream implementations - */ - -namespace detail { - -void calc_actual_timeout(time_t max_timeout_msec, time_t duration_msec, - time_t timeout_sec, time_t timeout_usec, - time_t &actual_timeout_sec, - time_t &actual_timeout_usec) { - auto timeout_msec = (timeout_sec * 1000) + (timeout_usec / 1000); - - auto actual_timeout_msec = - (std::min)(max_timeout_msec - duration_msec, timeout_msec); - - if (actual_timeout_msec < 0) { actual_timeout_msec = 0; } - - actual_timeout_sec = actual_timeout_msec / 1000; - actual_timeout_usec = (actual_timeout_msec % 1000) * 1000; -} - -// Socket stream implementation -SocketStream::SocketStream( - socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, - time_t write_timeout_sec, time_t write_timeout_usec, - time_t max_timeout_msec, - std::chrono::time_point start_time) - : sock_(sock), read_timeout_sec_(read_timeout_sec), - read_timeout_usec_(read_timeout_usec), - write_timeout_sec_(write_timeout_sec), - write_timeout_usec_(write_timeout_usec), - max_timeout_msec_(max_timeout_msec), start_time_(start_time), - read_buff_(read_buff_size_, 0) {} - -SocketStream::~SocketStream() = default; - -bool SocketStream::is_readable() const { - return read_buff_off_ < read_buff_content_size_; -} - -bool SocketStream::wait_readable() const { - if (max_timeout_msec_ <= 0) { - return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0; - } - - time_t read_timeout_sec; - time_t read_timeout_usec; - calc_actual_timeout(max_timeout_msec_, duration(), read_timeout_sec_, - read_timeout_usec_, read_timeout_sec, read_timeout_usec); - - return select_read(sock_, read_timeout_sec, read_timeout_usec) > 0; -} - -bool SocketStream::wait_writable() const { - return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0; -} - -bool SocketStream::is_peer_alive() const { - return detail::is_socket_alive(sock_); -} - -ssize_t SocketStream::read(char *ptr, size_t size) { -#ifdef _WIN32 - size = - (std::min)(size, static_cast((std::numeric_limits::max)())); -#else - size = (std::min)(size, - static_cast((std::numeric_limits::max)())); -#endif - - if (read_buff_off_ < read_buff_content_size_) { - auto remaining_size = read_buff_content_size_ - read_buff_off_; - if (size <= remaining_size) { - memcpy(ptr, read_buff_.data() + read_buff_off_, size); - read_buff_off_ += size; - return static_cast(size); - } else { - memcpy(ptr, read_buff_.data() + read_buff_off_, remaining_size); - read_buff_off_ += remaining_size; - return static_cast(remaining_size); - } - } - - if (!wait_readable()) { - error_ = Error::Timeout; - return -1; - } - - read_buff_off_ = 0; - read_buff_content_size_ = 0; - - if (size < read_buff_size_) { - auto n = read_socket(sock_, read_buff_.data(), read_buff_size_, - CPPHTTPLIB_RECV_FLAGS); - if (n <= 0) { - if (n == 0) { - error_ = Error::ConnectionClosed; - } else { - error_ = Error::Read; - } - return n; - } else if (n <= static_cast(size)) { - memcpy(ptr, read_buff_.data(), static_cast(n)); - return n; - } else { - memcpy(ptr, read_buff_.data(), size); - read_buff_off_ = size; - read_buff_content_size_ = static_cast(n); - return static_cast(size); - } - } else { - auto n = read_socket(sock_, ptr, size, CPPHTTPLIB_RECV_FLAGS); - if (n <= 0) { - if (n == 0) { - error_ = Error::ConnectionClosed; - } else { - error_ = Error::Read; - } - } - return n; - } -} - -ssize_t SocketStream::write(const char *ptr, size_t size) { - if (!wait_writable()) { return -1; } - -#if defined(_WIN32) && !defined(_WIN64) - size = - (std::min)(size, static_cast((std::numeric_limits::max)())); -#endif - - return send_socket(sock_, ptr, size, CPPHTTPLIB_SEND_FLAGS); -} - -void SocketStream::get_remote_ip_and_port(std::string &ip, - int &port) const { - return detail::get_remote_ip_and_port(sock_, ip, port); -} - -void SocketStream::get_local_ip_and_port(std::string &ip, - int &port) const { - return detail::get_local_ip_and_port(sock_, ip, port); -} - -socket_t SocketStream::socket() const { return sock_; } - -time_t SocketStream::duration() const { - return std::chrono::duration_cast( - std::chrono::steady_clock::now() - start_time_) - .count(); -} - -void SocketStream::set_read_timeout(time_t sec, time_t usec) { - read_timeout_sec_ = sec; - read_timeout_usec_ = usec; -} - -// Buffer stream implementation -bool BufferStream::is_readable() const { return true; } - -bool BufferStream::wait_readable() const { return true; } - -bool BufferStream::wait_writable() const { return true; } - -ssize_t BufferStream::read(char *ptr, size_t size) { -#if defined(_MSC_VER) && _MSC_VER < 1910 - auto len_read = buffer._Copy_s(ptr, size, size, position); -#else - auto len_read = buffer.copy(ptr, size, position); -#endif - position += static_cast(len_read); - return static_cast(len_read); -} - -ssize_t BufferStream::write(const char *ptr, size_t size) { - buffer.append(ptr, size); - return static_cast(size); -} - -void BufferStream::get_remote_ip_and_port(std::string & /*ip*/, - int & /*port*/) const {} - -void BufferStream::get_local_ip_and_port(std::string & /*ip*/, - int & /*port*/) const {} - -socket_t BufferStream::socket() const { return 0; } - -time_t BufferStream::duration() const { return 0; } - -const std::string &BufferStream::get_buffer() const { return buffer; } - -PathParamsMatcher::PathParamsMatcher(const std::string &pattern) - : MatcherBase(pattern) { - constexpr const char marker[] = "/:"; - - // One past the last ending position of a path param substring - std::size_t last_param_end = 0; - -#ifndef CPPHTTPLIB_NO_EXCEPTIONS - // Needed to ensure that parameter names are unique during matcher - // construction - // If exceptions are disabled, only last duplicate path - // parameter will be set - std::unordered_set param_name_set; -#endif - - while (true) { - const auto marker_pos = pattern.find( - marker, last_param_end == 0 ? last_param_end : last_param_end - 1); - if (marker_pos == std::string::npos) { break; } - - static_fragments_.push_back( - pattern.substr(last_param_end, marker_pos - last_param_end + 1)); - - const auto param_name_start = marker_pos + str_len(marker); - - auto sep_pos = pattern.find(separator, param_name_start); - if (sep_pos == std::string::npos) { sep_pos = pattern.length(); } - - auto param_name = - pattern.substr(param_name_start, sep_pos - param_name_start); - -#ifndef CPPHTTPLIB_NO_EXCEPTIONS - if (param_name_set.find(param_name) != param_name_set.cend()) { - std::string msg = "Encountered path parameter '" + param_name + - "' multiple times in route pattern '" + pattern + "'."; - throw std::invalid_argument(msg); - } -#endif - - param_names_.push_back(std::move(param_name)); - - last_param_end = sep_pos + 1; - } - - if (last_param_end < pattern.length()) { - static_fragments_.push_back(pattern.substr(last_param_end)); - } -} - -bool PathParamsMatcher::match(Request &request) const { - request.matches = std::smatch(); - request.path_params.clear(); - request.path_params.reserve(param_names_.size()); - - // One past the position at which the path matched the pattern last time - std::size_t starting_pos = 0; - for (size_t i = 0; i < static_fragments_.size(); ++i) { - const auto &fragment = static_fragments_[i]; - - if (starting_pos + fragment.length() > request.path.length()) { - return false; - } - - // Avoid unnecessary allocation by using strncmp instead of substr + - // comparison - if (std::strncmp(request.path.c_str() + starting_pos, fragment.c_str(), - fragment.length()) != 0) { - return false; - } - - starting_pos += fragment.length(); - - // Should only happen when we have a static fragment after a param - // Example: '/users/:id/subscriptions' - // The 'subscriptions' fragment here does not have a corresponding param - if (i >= param_names_.size()) { continue; } - - auto sep_pos = request.path.find(separator, starting_pos); - if (sep_pos == std::string::npos) { sep_pos = request.path.length(); } - - const auto ¶m_name = param_names_[i]; - - request.path_params.emplace( - param_name, request.path.substr(starting_pos, sep_pos - starting_pos)); - - // Mark everything up to '/' as matched - starting_pos = sep_pos + 1; - } - // Returns false if the path is longer than the pattern - return starting_pos >= request.path.length(); -} - -bool RegexMatcher::match(Request &request) const { - request.path_params.clear(); - return std::regex_match(request.path, request.matches, regex_); -} - -// Enclose IPv6 address in brackets if needed -std::string prepare_host_string(const std::string &host) { - // Enclose IPv6 address in brackets (but not if already enclosed) - if (host.find(':') == std::string::npos || - (!host.empty() && host[0] == '[')) { - // IPv4, hostname, or already bracketed IPv6 - return host; - } else { - // IPv6 address without brackets - return "[" + host + "]"; - } -} - -std::string make_host_and_port_string(const std::string &host, int port, - bool is_ssl) { - auto result = prepare_host_string(host); - - // Append port if not default - if ((!is_ssl && port == 80) || (is_ssl && port == 443)) { - ; // do nothing - } else { - result += ":" + std::to_string(port); - } - - return result; -} - -// Create "host:port" string always including port number (for CONNECT method) -std::string -make_host_and_port_string_always_port(const std::string &host, int port) { - return prepare_host_string(host) + ":" + std::to_string(port); -} - -bool parse_no_proxy_entry(const std::string &token, NoProxyEntry &out); -NormalizedTarget normalize_target(const std::string &host); -bool ip_in_cidr(const IPBytes &ip, const IPBytes &net, int prefix_bits); -bool host_matches_no_proxy(const NormalizedTarget &target, - const std::vector &entries); - -bool ip_in_cidr(const IPBytes &ip, const IPBytes &net, int prefix_bits) { - if (prefix_bits < 0 || prefix_bits > 128) { return false; } - if (prefix_bits == 0) { return true; } - int full_bytes = prefix_bits / 8; - int rem_bits = prefix_bits % 8; - if (full_bytes > 0 && std::memcmp(ip.data(), net.data(), - static_cast(full_bytes)) != 0) { - return false; - } - if (rem_bits == 0) { return true; } - auto i = static_cast(full_bytes); - auto mask = static_cast(0xFFu << (8 - rem_bits)); - return (ip[i] & mask) == (net[i] & mask); -} - -bool parse_no_proxy_entry(const std::string &token, NoProxyEntry &out) { - if (token.empty()) { return false; } - - if (token == "*") { - out.kind = NoProxyKind::Wildcard; - return true; - } - - auto slash = token.find('/'); - std::string addr_part = - (slash == std::string::npos) ? token : token.substr(0, slash); - std::string prefix_part = - (slash == std::string::npos) ? std::string() : token.substr(slash + 1); - - // A bare slash or trailing-slash CIDR like "10.0.0.0/" is malformed; - // don't silently treat it as a /32 (or /128). - if (slash != std::string::npos && prefix_part.empty()) { return false; } - - // Accept the bracketed IPv6 form ("[::1]", "[fe80::]/10") as well as the - // bare form. Brackets have no meaning for IPv4, so skip the IPv4 attempt - // when brackets are present. - bool bracketed = addr_part.size() >= 2 && addr_part.front() == '[' && - addr_part.back() == ']'; - if (bracketed) { addr_part = addr_part.substr(1, addr_part.size() - 2); } - - if (!bracketed) { - struct in_addr v4; - if (inet_pton(AF_INET, addr_part.c_str(), &v4) == 1) { - int prefix = 32; - if (!prefix_part.empty()) { - auto r = from_chars(prefix_part.data(), - prefix_part.data() + prefix_part.size(), prefix); - if (r.ec != std::errc{} || - r.ptr != prefix_part.data() + prefix_part.size()) { - return false; - } - if (prefix < 0 || prefix > 32) { return false; } - } - out.kind = NoProxyKind::IPv4Cidr; - std::memcpy(out.net.data(), &v4, sizeof(v4)); - out.prefix_bits = prefix; - return true; - } - } - - struct in6_addr v6; - if (inet_pton(AF_INET6, addr_part.c_str(), &v6) == 1) { - int prefix = 128; - if (!prefix_part.empty()) { - auto r = from_chars(prefix_part.data(), - prefix_part.data() + prefix_part.size(), prefix); - if (r.ec != std::errc{} || - r.ptr != prefix_part.data() + prefix_part.size()) { - return false; - } - if (prefix < 0 || prefix > 128) { return false; } - } - out.kind = NoProxyKind::IPv6Cidr; - std::memcpy(out.net.data(), &v6, sizeof(v6)); - out.prefix_bits = prefix; - return true; - } - - // Bracketed entries can only be IPv6. If the IPv6 parse above failed, - // the entry is malformed — don't fall through to the hostname branch. - if (bracketed) { return false; } - - // A '/' on a non-IP token means a CIDR prefix without an address. Reject. - if (slash != std::string::npos) { return false; } - // Port-specific entries (host:port) are not supported. - if (token.find(':') != std::string::npos) { return false; } - - std::string hostname = case_ignore::to_lower(token); - while (!hostname.empty() && hostname.front() == '.') { - hostname.erase(hostname.begin()); - } - while (!hostname.empty() && hostname.back() == '.') { - hostname.pop_back(); - } - if (hostname.empty()) { return false; } - - out.kind = NoProxyKind::HostnameSuffix; - out.hostname_pattern = std::move(hostname); - return true; -} - -NormalizedTarget normalize_target(const std::string &host) { - NormalizedTarget t; - std::string h = host; - - if (h.size() >= 2 && h.front() == '[' && h.back() == ']') { - h = h.substr(1, h.size() - 2); - } - - // Strip a single trailing dot so "example.com." canonicalizes to - // "example.com". - if (!h.empty() && h.back() == '.') { h.pop_back(); } - - t.hostname = case_ignore::to_lower(h); - - if (!t.hostname.empty()) { - struct in_addr v4; - struct in6_addr v6; - if (inet_pton(AF_INET, t.hostname.c_str(), &v4) == 1) { - t.is_ipv4 = true; - std::memcpy(t.ip.data(), &v4, sizeof(v4)); - } else if (inet_pton(AF_INET6, t.hostname.c_str(), &v6) == 1) { - t.is_ipv6 = true; - std::memcpy(t.ip.data(), &v6, sizeof(v6)); - } - } - return t; -} - -bool host_matches_no_proxy(const NormalizedTarget &target, - const std::vector &entries) { - if (target.hostname.empty()) { return false; } - for (const auto &e : entries) { - switch (e.kind) { - case NoProxyKind::Wildcard: return true; - case NoProxyKind::IPv4Cidr: - if (target.is_ipv4 && ip_in_cidr(target.ip, e.net, e.prefix_bits)) { - return true; - } - break; - case NoProxyKind::IPv6Cidr: - if (target.is_ipv6 && ip_in_cidr(target.ip, e.net, e.prefix_bits)) { - return true; - } - break; - case NoProxyKind::HostnameSuffix: - if (target.is_ipv4 || target.is_ipv6) { break; } - if (target.hostname == e.hostname_pattern) { return true; } - // Dot-boundary suffix match: prevents "evilexample.com" from matching - // an entry of "example.com". - if (target.hostname.size() > e.hostname_pattern.size() + 1) { - auto offset = target.hostname.size() - e.hostname_pattern.size(); - if (target.hostname[offset - 1] == '.' && - target.hostname.compare(offset, e.hostname_pattern.size(), - e.hostname_pattern) == 0) { - return true; - } - } - break; - } - } - return false; -} - -template -bool check_and_write_headers(Stream &strm, Headers &headers, - T header_writer, Error &error) { - for (const auto &h : headers) { - if (!detail::fields::is_field_name(h.first) || - !detail::fields::is_field_value(h.second)) { - error = Error::InvalidHeaders; - return false; - } - } - if (header_writer(strm, headers) <= 0) { - error = Error::Write; - return false; - } - return true; -} - -} // namespace detail - -/* - * Group 2 (continued): detail namespace - SSLSocketStream implementation - */ - -#ifdef CPPHTTPLIB_SSL_ENABLED -namespace detail { - -// SSL socket stream implementation -SSLSocketStream::SSLSocketStream( - socket_t sock, tls::session_t session, time_t read_timeout_sec, - time_t read_timeout_usec, time_t write_timeout_sec, - time_t write_timeout_usec, time_t max_timeout_msec, - std::chrono::time_point start_time) - : sock_(sock), session_(session), read_timeout_sec_(read_timeout_sec), - read_timeout_usec_(read_timeout_usec), - write_timeout_sec_(write_timeout_sec), - write_timeout_usec_(write_timeout_usec), - max_timeout_msec_(max_timeout_msec), start_time_(start_time) { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - // Clear AUTO_RETRY for proper non-blocking I/O timeout handling - // Note: create_session() also clears this, but SSLClient currently - // uses ssl_new() which does not. Until full TLS API migration is complete, - // we need to ensure AUTO_RETRY is cleared here regardless of how the - // SSL session was created. - SSL_clear_mode(static_cast(session), SSL_MODE_AUTO_RETRY); -#endif -} - -SSLSocketStream::~SSLSocketStream() = default; - -bool SSLSocketStream::is_readable() const { - return tls::pending(session_) > 0; -} - -bool SSLSocketStream::wait_readable() const { - if (max_timeout_msec_ <= 0) { - return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0; - } - - time_t read_timeout_sec; - time_t read_timeout_usec; - calc_actual_timeout(max_timeout_msec_, duration(), read_timeout_sec_, - read_timeout_usec_, read_timeout_sec, read_timeout_usec); - - return select_read(sock_, read_timeout_sec, read_timeout_usec) > 0; -} - -bool SSLSocketStream::wait_writable() const { - return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0 && - !tls::is_peer_closed(session_, sock_); -} - -bool SSLSocketStream::is_peer_alive() const { - return !tls::is_peer_closed(session_, sock_); -} - -ssize_t SSLSocketStream::read(char *ptr, size_t size) { - if (tls::pending(session_) > 0) { - tls::TlsError err; - auto ret = tls::read(session_, ptr, size, err); - if (ret == 0 || err.code == tls::ErrorCode::PeerClosed) { - error_ = Error::ConnectionClosed; - } - return ret; - } else if (wait_readable()) { - tls::TlsError err; - auto ret = tls::read(session_, ptr, size, err); - if (ret < 0) { - auto n = 1000; -#ifdef _WIN32 - while (--n >= 0 && (err.code == tls::ErrorCode::WantRead || - (err.code == tls::ErrorCode::SyscallError && - WSAGetLastError() == WSAETIMEDOUT))) { -#else - while (--n >= 0 && err.code == tls::ErrorCode::WantRead) { -#endif - if (tls::pending(session_) > 0) { - return tls::read(session_, ptr, size, err); - } else if (wait_readable()) { - std::this_thread::sleep_for(std::chrono::microseconds{10}); - ret = tls::read(session_, ptr, size, err); - if (ret >= 0) { return ret; } - } else { - break; - } - } - assert(ret < 0); - } else if (ret == 0 || err.code == tls::ErrorCode::PeerClosed) { - error_ = Error::ConnectionClosed; - } - return ret; - } else { - error_ = Error::Timeout; - return -1; - } -} - -ssize_t SSLSocketStream::write(const char *ptr, size_t size) { - if (wait_writable()) { - auto handle_size = - std::min(size, (std::numeric_limits::max)()); - - tls::TlsError err; - auto ret = tls::write(session_, ptr, handle_size, err); - if (ret < 0) { - auto n = 1000; -#ifdef _WIN32 - while (--n >= 0 && (err.code == tls::ErrorCode::WantWrite || - (err.code == tls::ErrorCode::SyscallError && - WSAGetLastError() == WSAETIMEDOUT))) { -#else - while (--n >= 0 && err.code == tls::ErrorCode::WantWrite) { -#endif - if (wait_writable()) { - std::this_thread::sleep_for(std::chrono::microseconds{10}); - ret = tls::write(session_, ptr, handle_size, err); - if (ret >= 0) { return ret; } - } else { - break; - } - } - assert(ret < 0); - } - return ret; - } - return -1; -} - -void SSLSocketStream::get_remote_ip_and_port(std::string &ip, - int &port) const { - detail::get_remote_ip_and_port(sock_, ip, port); -} - -void SSLSocketStream::get_local_ip_and_port(std::string &ip, - int &port) const { - detail::get_local_ip_and_port(sock_, ip, port); -} - -socket_t SSLSocketStream::socket() const { return sock_; } - -time_t SSLSocketStream::duration() const { - return std::chrono::duration_cast( - std::chrono::steady_clock::now() - start_time_) - .count(); -} - -void SSLSocketStream::set_read_timeout(time_t sec, time_t usec) { - read_timeout_sec_ = sec; - read_timeout_usec_ = usec; -} - -} // namespace detail -#endif // CPPHTTPLIB_SSL_ENABLED - -/* - * Group 4: Server implementation - */ - -// HTTP server implementation -Server::Server() - : new_task_queue([] { - return new ThreadPool(CPPHTTPLIB_THREAD_POOL_COUNT, - CPPHTTPLIB_THREAD_POOL_MAX_COUNT); - }) { -#ifndef _WIN32 - signal(SIGPIPE, SIG_IGN); -#endif -} - -Server::~Server() = default; - -std::unique_ptr -Server::make_matcher(const std::string &pattern) { - if (pattern.find("/:") != std::string::npos) { - return detail::make_unique(pattern); - } else { - return detail::make_unique(pattern); - } -} - -Server &Server::Get(const std::string &pattern, Handler handler) { - return add_handler(get_handlers_, pattern, std::move(handler)); -} - -Server &Server::Post(const std::string &pattern, Handler handler) { - return add_handler(post_handlers_, pattern, std::move(handler)); -} - -Server &Server::Post(const std::string &pattern, - HandlerWithContentReader handler) { - return add_handler(post_handlers_for_content_reader_, pattern, - std::move(handler)); -} - -Server &Server::Put(const std::string &pattern, Handler handler) { - return add_handler(put_handlers_, pattern, std::move(handler)); -} - -Server &Server::Put(const std::string &pattern, - HandlerWithContentReader handler) { - return add_handler(put_handlers_for_content_reader_, pattern, - std::move(handler)); -} - -Server &Server::Patch(const std::string &pattern, Handler handler) { - return add_handler(patch_handlers_, pattern, std::move(handler)); -} - -Server &Server::Patch(const std::string &pattern, - HandlerWithContentReader handler) { - return add_handler(patch_handlers_for_content_reader_, pattern, - std::move(handler)); -} - -Server &Server::Delete(const std::string &pattern, Handler handler) { - return add_handler(delete_handlers_, pattern, std::move(handler)); -} - -Server &Server::Delete(const std::string &pattern, - HandlerWithContentReader handler) { - return add_handler(delete_handlers_for_content_reader_, pattern, - std::move(handler)); -} - -Server &Server::Options(const std::string &pattern, Handler handler) { - return add_handler(options_handlers_, pattern, std::move(handler)); -} - -Server &Server::WebSocket(const std::string &pattern, - WebSocketHandler handler) { - websocket_handlers_.push_back( - {make_matcher(pattern), std::move(handler), nullptr}); - return *this; -} - -Server &Server::WebSocket(const std::string &pattern, - WebSocketHandler handler, - SubProtocolSelector sub_protocol_selector) { - websocket_handlers_.push_back({make_matcher(pattern), std::move(handler), - std::move(sub_protocol_selector)}); - return *this; -} - -bool Server::set_base_dir(const std::string &dir, - const std::string &mount_point) { - return set_mount_point(mount_point, dir); -} - -bool Server::set_mount_point(const std::string &mount_point, - const std::string &dir, Headers headers) { - detail::FileStat stat(dir); - if (stat.is_dir()) { - std::string mnt = !mount_point.empty() ? mount_point : "/"; - if (!mnt.empty() && mnt[0] == '/') { - std::string resolved_base; - if (detail::canonicalize_path(dir.c_str(), resolved_base)) { -#if defined(_WIN32) - if (resolved_base.back() != '\\' && resolved_base.back() != '/') { - resolved_base += '\\'; - } -#else - if (resolved_base.back() != '/') { resolved_base += '/'; } -#endif - } - base_dirs_.push_back( - {std::move(mnt), dir, std::move(resolved_base), std::move(headers)}); - return true; - } - } - return false; -} - -bool Server::remove_mount_point(const std::string &mount_point) { - for (auto it = base_dirs_.begin(); it != base_dirs_.end(); ++it) { - if (it->mount_point == mount_point) { - base_dirs_.erase(it); - return true; - } - } - return false; -} - -Server & -Server::set_file_extension_and_mimetype_mapping(const std::string &ext, - const std::string &mime) { - file_extension_and_mimetype_map_[ext] = mime; - return *this; -} - -Server &Server::set_default_file_mimetype(const std::string &mime) { - default_file_mimetype_ = mime; - return *this; -} - -Server &Server::set_file_request_handler(Handler handler) { - file_request_handler_ = std::move(handler); - return *this; -} - -Server &Server::set_error_handler_core(HandlerWithResponse handler, - std::true_type) { - error_handler_ = std::move(handler); - return *this; -} - -Server &Server::set_error_handler_core(Handler handler, - std::false_type) { - error_handler_ = [handler](const Request &req, Response &res) { - handler(req, res); - return HandlerResponse::Handled; - }; - return *this; -} - -Server &Server::set_exception_handler(ExceptionHandler handler) { - exception_handler_ = std::move(handler); - return *this; -} - -Server &Server::set_pre_routing_handler(HandlerWithResponse handler) { - pre_routing_handler_ = std::move(handler); - return *this; -} - -Server &Server::set_post_routing_handler(Handler handler) { - post_routing_handler_ = std::move(handler); - return *this; -} - -Server &Server::set_pre_request_handler(HandlerWithResponse handler) { - pre_request_handler_ = std::move(handler); - return *this; -} - -Server &Server::set_logger(Logger logger) { - logger_ = std::move(logger); - return *this; -} - -Server &Server::set_error_logger(ErrorLogger error_logger) { - error_logger_ = std::move(error_logger); - return *this; -} - -Server &Server::set_pre_compression_logger(Logger logger) { - pre_compression_logger_ = std::move(logger); - return *this; -} - -Server & -Server::set_expect_100_continue_handler(Expect100ContinueHandler handler) { - expect_100_continue_handler_ = std::move(handler); - return *this; -} - -Server &Server::set_address_family(int family) { - address_family_ = family; - return *this; -} - -Server &Server::set_tcp_nodelay(bool on) { - tcp_nodelay_ = on; - return *this; -} - -Server &Server::set_ipv6_v6only(bool on) { - ipv6_v6only_ = on; - return *this; -} - -Server &Server::set_socket_options(SocketOptions socket_options) { - socket_options_ = std::move(socket_options); - return *this; -} - -Server &Server::set_default_headers(Headers headers) { - default_headers_ = std::move(headers); - return *this; -} - -Server &Server::set_header_writer( - std::function const &writer) { - header_writer_ = writer; - return *this; -} - -Server & -Server::set_trusted_proxies(const std::vector &proxies) { - trusted_proxies_ = proxies; - return *this; -} - -Server &Server::set_keep_alive_max_count(size_t count) { - keep_alive_max_count_ = count; - return *this; -} - -Server &Server::set_keep_alive_timeout(time_t sec) { - keep_alive_timeout_sec_ = sec; - return *this; -} - -template -Server &Server::set_keep_alive_timeout( - const std::chrono::duration &duration) { - detail::duration_to_sec_and_usec(duration, [&](time_t sec, time_t /*usec*/) { - set_keep_alive_timeout(sec); - }); - return *this; -} - -Server &Server::set_read_timeout(time_t sec, time_t usec) { - read_timeout_sec_ = sec; - read_timeout_usec_ = usec; - return *this; -} - -Server &Server::set_write_timeout(time_t sec, time_t usec) { - write_timeout_sec_ = sec; - write_timeout_usec_ = usec; - return *this; -} - -Server &Server::set_idle_interval(time_t sec, time_t usec) { - idle_interval_sec_ = sec; - idle_interval_usec_ = usec; - return *this; -} - -Server &Server::set_payload_max_length(size_t length) { - payload_max_length_ = length; - return *this; -} - -Server &Server::set_websocket_max_missed_pongs(int count) { - websocket_max_missed_pongs_ = count; - return *this; -} - -Server &Server::set_websocket_ping_interval(time_t sec) { - websocket_ping_interval_sec_ = sec; - return *this; -} - -template -Server &Server::set_websocket_ping_interval( - const std::chrono::duration &duration) { - detail::duration_to_sec_and_usec(duration, [&](time_t sec, time_t /*usec*/) { - set_websocket_ping_interval(sec); - }); - return *this; -} - -bool Server::bind_to_port(const std::string &host, int port, - int socket_flags) { - auto ret = bind_internal(host, port, socket_flags); - if (ret == -1) { is_decommissioned = true; } - return ret >= 0; -} -int Server::bind_to_any_port(const std::string &host, int socket_flags) { - auto ret = bind_internal(host, 0, socket_flags); - if (ret == -1) { is_decommissioned = true; } - return ret; -} - -bool Server::listen_after_bind() { return listen_internal(); } - -bool Server::listen(const std::string &host, int port, - int socket_flags) { - return bind_to_port(host, port, socket_flags) && listen_internal(); -} - -bool Server::is_running() const { return is_running_; } - -void Server::wait_until_ready() const { - while (!is_running_ && !is_decommissioned) { - std::this_thread::sleep_for(std::chrono::milliseconds{1}); - } -} - -void Server::stop() noexcept { - if (is_running_) { - assert(svr_sock_ != INVALID_SOCKET); - std::atomic sock(svr_sock_.exchange(INVALID_SOCKET)); - detail::shutdown_socket(sock); - detail::close_socket(sock); - } - is_decommissioned = false; -} - -void Server::decommission() { is_decommissioned = true; } - -bool Server::parse_request_line(const char *s, Request &req) const { - auto len = strlen(s); - if (len < 2 || s[len - 2] != '\r' || s[len - 1] != '\n') { return false; } - len -= 2; - - { - size_t count = 0; - - detail::split(s, s + len, ' ', [&](const char *b, const char *e) { - switch (count) { - case 0: req.method = std::string(b, e); break; - case 1: req.target = std::string(b, e); break; - case 2: req.version = std::string(b, e); break; - default: break; - } - count++; - }); - - if (count != 3) { return false; } - } - - thread_local const std::set methods{ - "GET", "HEAD", "POST", "PUT", "DELETE", - "CONNECT", "OPTIONS", "TRACE", "PATCH", "PRI"}; - - if (methods.find(req.method) == methods.end()) { - output_error_log(Error::InvalidHTTPMethod, &req); - return false; - } - - if (req.version != "HTTP/1.1" && req.version != "HTTP/1.0") { - output_error_log(Error::InvalidHTTPVersion, &req); - return false; - } - - { - // Skip URL fragment - for (size_t i = 0; i < req.target.size(); i++) { - if (req.target[i] == '#') { - req.target.erase(i); - break; - } - } - - detail::divide(req.target, '?', - [&](const char *lhs_data, std::size_t lhs_size, - const char *rhs_data, std::size_t rhs_size) { - req.path = - decode_path_component(std::string(lhs_data, lhs_size)); - detail::parse_query_text(rhs_data, rhs_size, req.params); - }); - } - - return true; -} - -bool Server::write_response(Stream &strm, bool close_connection, - Request &req, Response &res) { - // NOTE: `req.ranges` should be empty, otherwise it will be applied - // incorrectly to the error content. - req.ranges.clear(); - return write_response_core(strm, close_connection, req, res, false); -} - -bool Server::write_response_with_content(Stream &strm, - bool close_connection, - const Request &req, - Response &res) { - return write_response_core(strm, close_connection, req, res, true); -} - -bool Server::write_response_core(Stream &strm, bool close_connection, - const Request &req, Response &res, - bool need_apply_ranges) { - assert(res.status != -1); - - if (400 <= res.status && error_handler_ && - error_handler_(req, res) == HandlerResponse::Handled) { - need_apply_ranges = true; - } - - std::string content_type; - std::string boundary; - if (need_apply_ranges) { apply_ranges(req, res, content_type, boundary); } - - // Prepare additional headers - if (close_connection || req.get_header_value("Connection") == "close" || - 400 <= res.status) { // Don't leave connections open after errors - res.set_header("Connection", "close"); - } else { - std::string s = "timeout="; - s += std::to_string(keep_alive_timeout_sec_); - s += ", max="; - s += std::to_string(keep_alive_max_count_); - res.set_header("Keep-Alive", s); - } - - if ((!res.body.empty() || res.content_length_ > 0 || res.content_provider_) && - !res.has_header("Content-Type")) { - res.set_header("Content-Type", "text/plain"); - } - - if (res.body.empty() && !res.content_length_ && !res.content_provider_ && - !res.has_header("Content-Length")) { - res.set_header("Content-Length", "0"); - } - - if (req.method == "HEAD" && !res.has_header("Accept-Ranges")) { - res.set_header("Accept-Ranges", "bytes"); - } - - if (post_routing_handler_) { post_routing_handler_(req, res); } - - // Response line and headers - detail::BufferStream bstrm; - if (!detail::write_response_line(bstrm, res.status)) { return false; } - if (header_writer_(bstrm, res.headers) <= 0) { return false; } - - // Combine small body with headers to reduce write syscalls - if (req.method != "HEAD" && !res.body.empty() && !res.content_provider_) { - bstrm.write(res.body.data(), res.body.size()); - } - - // Log before writing to avoid race condition with client-side code that - // accesses logger-captured data immediately after receiving the response. - output_log(req, res); - - // Flush buffer - auto &data = bstrm.get_buffer(); - if (!detail::write_data(strm, data.data(), data.size())) { return false; } - - // Streaming body - auto ret = true; - if (req.method != "HEAD" && res.content_provider_) { - if (write_content_with_provider(strm, req, res, boundary, content_type)) { - res.content_provider_success_ = true; - } else { - ret = false; - } - } - - return ret; -} - -bool -Server::write_content_with_provider(Stream &strm, const Request &req, - Response &res, const std::string &boundary, - const std::string &content_type) { - auto is_shutting_down = [this]() { - return this->svr_sock_ == INVALID_SOCKET; - }; - - if (res.content_length_ > 0) { - if (req.ranges.empty()) { - return detail::write_content(strm, res.content_provider_, 0, - res.content_length_, is_shutting_down); - } else if (req.ranges.size() == 1) { - auto offset_and_length = detail::get_range_offset_and_length( - req.ranges[0], res.content_length_); - - return detail::write_content(strm, res.content_provider_, - offset_and_length.first, - offset_and_length.second, is_shutting_down); - } else { - return detail::write_multipart_ranges_data( - strm, req, res, boundary, content_type, res.content_length_, - is_shutting_down); - } - } else { - if (res.is_chunked_content_provider_) { - auto type = detail::encoding_type(req, res); - - auto compressor = detail::make_compressor(type); - if (!compressor) { - compressor = detail::make_unique(); - } - - return detail::write_content_chunked(strm, res.content_provider_, - is_shutting_down, *compressor); - } else { - return detail::write_content_without_length(strm, res.content_provider_, - is_shutting_down); - } - } -} - -bool Server::read_content(Stream &strm, Request &req, Response &res) { - FormFields::iterator cur_field; - FormFiles::iterator cur_file; - auto is_text_field = false; - size_t count = 0; - if (read_content_core( - strm, req, res, - // Regular - [&](const char *buf, size_t n) { - // Prevent arithmetic overflow when checking sizes. - // Avoid computing (req.body.size() + n) directly because - // adding two unsigned `size_t` values can wrap around and - // produce a small result instead of indicating overflow. - // Instead, check using subtraction: ensure `n` does not - // exceed the remaining capacity `max_size() - size()`. - if (req.body.size() >= req.body.max_size() || - n > req.body.max_size() - req.body.size()) { - return false; - } - - // Limit decompressed body size to payload_max_length_ to protect - // against "zip bomb" attacks where a small compressed payload - // decompresses to a massive size. - if (payload_max_length_ > 0 && - (req.body.size() >= payload_max_length_ || - n > payload_max_length_ - req.body.size())) { - return false; - } - - req.body.append(buf, n); - return true; - }, - // Multipart FormData - [&](const FormData &file) { - if (count++ == CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT) { - output_error_log(Error::TooManyFormDataFiles, &req); - return false; - } - - if (file.filename.empty()) { - cur_field = req.form.fields.emplace( - file.name, FormField{file.name, file.content, file.headers}); - is_text_field = true; - } else { - cur_file = req.form.files.emplace(file.name, file); - is_text_field = false; - } - return true; - }, - [&](const char *buf, size_t n) { - if (is_text_field) { - auto &content = cur_field->second.content; - if (content.size() + n > content.max_size()) { return false; } - content.append(buf, n); - } else { - auto &content = cur_file->second.content; - if (content.size() + n > content.max_size()) { return false; } - content.append(buf, n); - } - return true; - })) { - const auto &content_type = req.get_header_value("Content-Type"); - if (detail::extract_media_type(content_type) == - "application/x-www-form-urlencoded") { - if (req.body.size() > CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH) { - res.status = StatusCode::PayloadTooLarge_413; // NOTE: should be 414? - output_error_log(Error::ExceedMaxPayloadSize, &req); - return false; - } - detail::parse_query_text(req.body, req.params); - } - return true; - } - return false; -} - -bool Server::read_content_with_content_receiver( - Stream &strm, Request &req, Response &res, ContentReceiver receiver, - FormDataHeader multipart_header, ContentReceiver multipart_receiver) { - return read_content_core(strm, req, res, std::move(receiver), - std::move(multipart_header), - std::move(multipart_receiver)); -} - -bool Server::read_content_core( - Stream &strm, Request &req, Response &res, ContentReceiver receiver, - FormDataHeader multipart_header, ContentReceiver multipart_receiver) const { - detail::FormDataParser multipart_form_data_parser; - ContentReceiverWithProgress out; - - if (req.is_multipart_form_data()) { - const auto &content_type = req.get_header_value("Content-Type"); - std::string boundary; - if (!detail::parse_multipart_boundary(content_type, boundary)) { - res.status = StatusCode::BadRequest_400; - output_error_log(Error::MultipartParsing, &req); - return false; - } - - multipart_form_data_parser.set_boundary(std::move(boundary)); - out = [&](const char *buf, size_t n, size_t /*off*/, size_t /*len*/) { - return multipart_form_data_parser.parse(buf, n, multipart_header, - multipart_receiver); - }; - } else { - out = [receiver](const char *buf, size_t n, size_t /*off*/, - size_t /*len*/) { return receiver(buf, n); }; - } - - // RFC 9112 §6: no Transfer-Encoding and no Content-Length means no body. - // For non-SSL builds we still scan non-persistent connections for stray - // body bytes so the payload limit is enforced (413). On keep-alive, - // pending bytes may be the next request (issue #2450), so skip. -#if !defined(CPPHTTPLIB_SSL_ENABLED) - if (!req.has_header("Content-Length") && - !detail::is_chunked_transfer_encoding(req.headers)) { - if (!detail::is_connection_persistent(req) && payload_max_length_ > 0 && - payload_max_length_ < (std::numeric_limits::max)()) { - auto has_data = strm.is_readable(); - if (!has_data) { - auto s = strm.socket(); - if (s != INVALID_SOCKET) { - has_data = detail::select_read(s, 0, 0) > 0; - } - } - if (has_data) { - auto result = - detail::read_content_without_length(strm, payload_max_length_, out); - if (result == detail::ReadContentResult::PayloadTooLarge) { - res.status = StatusCode::PayloadTooLarge_413; - return false; - } else if (result != detail::ReadContentResult::Success) { - return false; - } - return true; - } - } - return true; - } -#else - if (!req.has_header("Content-Length") && - !detail::is_chunked_transfer_encoding(req.headers)) { - return true; - } -#endif - - if (!detail::read_content(strm, req, payload_max_length_, res.status, nullptr, - out, true)) { - return false; - } - - req.body_consumed_ = true; - - if (req.is_multipart_form_data()) { - if (!multipart_form_data_parser.is_valid()) { - res.status = StatusCode::BadRequest_400; - output_error_log(Error::MultipartParsing, &req); - return false; - } - } - - return true; -} - -bool Server::handle_file_request(Request &req, Response &res) { - for (const auto &entry : base_dirs_) { - // Prefix match - if (!req.path.compare(0, entry.mount_point.size(), entry.mount_point)) { - std::string sub_path = "/" + req.path.substr(entry.mount_point.size()); - if (detail::is_valid_path(sub_path)) { - auto path = entry.base_dir + sub_path; - if (path.back() == '/') { path += "index.html"; } - - // Defense-in-depth: is_valid_path blocks ".." traversal in the URL, - // but symlinks/junctions can still escape the base directory. - if (!entry.resolved_base_dir.empty()) { - std::string resolved_path; - if (detail::canonicalize_path(path.c_str(), resolved_path) && - !detail::is_path_within_base(resolved_path, - entry.resolved_base_dir)) { - res.status = StatusCode::Forbidden_403; - return true; - } - } - - detail::FileStat stat(path); - - if (stat.is_dir()) { - res.set_redirect(sub_path + "/", StatusCode::MovedPermanently_301); - return true; - } - - if (stat.is_file()) { - for (const auto &kv : entry.headers) { - res.set_header(kv.first, kv.second); - } - - auto etag = detail::compute_etag(stat); - if (!etag.empty()) { res.set_header("ETag", etag); } - - auto mtime = stat.mtime(); - - auto last_modified = detail::file_mtime_to_http_date(mtime); - if (!last_modified.empty()) { - res.set_header("Last-Modified", last_modified); - } - - if (check_if_not_modified(req, res, etag, mtime)) { return true; } - - check_if_range(req, etag, mtime); - - auto mm = std::make_shared(path.c_str()); - if (!mm->is_open()) { - output_error_log(Error::OpenFile, &req); - return false; - } - - res.set_content_provider( - mm->size(), - detail::find_content_type(path, file_extension_and_mimetype_map_, - default_file_mimetype_), - [mm](size_t offset, size_t length, DataSink &sink) -> bool { - sink.write(mm->data() + offset, length); - return true; - }); - - if (req.method != "HEAD" && file_request_handler_) { - file_request_handler_(req, res); - } - - return true; - } else { - output_error_log(Error::OpenFile, &req); - } - } - } - } - return false; -} - -bool Server::check_if_not_modified(const Request &req, Response &res, - const std::string &etag, - time_t mtime) const { - // Handle conditional GET: - // 1. If-None-Match takes precedence (RFC 9110 Section 13.1.2) - // 2. If-Modified-Since is checked only when If-None-Match is absent - if (req.has_header("If-None-Match")) { - if (!etag.empty()) { - auto val = req.get_header_value("If-None-Match"); - - // NOTE: We use exact string matching here. This works correctly - // because our server always generates weak ETags (W/"..."), and - // clients typically send back the same ETag they received. - // RFC 9110 Section 8.8.3.2 allows weak comparison for - // If-None-Match, where W/"x" and "x" would match, but this - // simplified implementation requires exact matches. - auto ret = detail::split_find(val.data(), val.data() + val.size(), ',', - [&](const char *b, const char *e) { - auto seg_len = static_cast(e - b); - return (seg_len == 1 && *b == '*') || - (seg_len == etag.size() && - std::equal(b, e, etag.begin())); - }); - - if (ret) { - res.status = StatusCode::NotModified_304; - return true; - } - } - } else if (req.has_header("If-Modified-Since")) { - auto val = req.get_header_value("If-Modified-Since"); - auto t = detail::parse_http_date(val); - - if (t != static_cast(-1) && mtime <= t) { - res.status = StatusCode::NotModified_304; - return true; - } - } - return false; -} - -bool Server::check_if_range(Request &req, const std::string &etag, - time_t mtime) const { - // Handle If-Range for partial content requests (RFC 9110 - // Section 13.1.5). If-Range is only evaluated when Range header is - // present. If the validator matches, serve partial content; otherwise - // serve full content. - if (!req.ranges.empty() && req.has_header("If-Range")) { - auto val = req.get_header_value("If-Range"); - - auto is_valid_range = [&]() { - if (detail::is_strong_etag(val)) { - // RFC 9110 Section 13.1.5: If-Range requires strong ETag - // comparison. - return (!etag.empty() && val == etag); - } else if (detail::is_weak_etag(val)) { - // Weak ETags are not valid for If-Range (RFC 9110 Section 13.1.5) - return false; - } else { - // HTTP-date comparison - auto t = detail::parse_http_date(val); - return (t != static_cast(-1) && mtime <= t); - } - }; - - if (!is_valid_range()) { - // Validator doesn't match: ignore Range and serve full content - req.ranges.clear(); - return false; - } - } - - return true; -} - -socket_t -Server::create_server_socket(const std::string &host, int port, - int socket_flags, - SocketOptions socket_options) const { - return detail::create_socket( - host, std::string(), port, address_family_, socket_flags, tcp_nodelay_, - ipv6_v6only_, std::move(socket_options), - [&](socket_t sock, struct addrinfo &ai, bool & /*quit*/) -> bool { - if (::bind(sock, ai.ai_addr, static_cast(ai.ai_addrlen))) { - output_error_log(Error::BindIPAddress, nullptr); - return false; - } - if (::listen(sock, CPPHTTPLIB_LISTEN_BACKLOG)) { - output_error_log(Error::Listen, nullptr); - return false; - } - return true; - }); -} - -int Server::bind_internal(const std::string &host, int port, - int socket_flags) { - if (is_decommissioned) { return -1; } - - if (!is_valid()) { return -1; } - - svr_sock_ = create_server_socket(host, port, socket_flags, socket_options_); - if (svr_sock_ == INVALID_SOCKET) { return -1; } - - if (port == 0) { - struct sockaddr_storage addr; - socklen_t addr_len = sizeof(addr); - if (getsockname(svr_sock_, reinterpret_cast(&addr), - &addr_len) == -1) { - output_error_log(Error::GetSockName, nullptr); - return -1; - } - if (addr.ss_family == AF_INET) { - return ntohs(reinterpret_cast(&addr)->sin_port); - } else if (addr.ss_family == AF_INET6) { - return ntohs(reinterpret_cast(&addr)->sin6_port); - } else { - output_error_log(Error::UnsupportedAddressFamily, nullptr); - return -1; - } - } else { - return port; - } -} - -bool Server::listen_internal() { - if (is_decommissioned) { return false; } - - auto ret = true; - is_running_ = true; - auto se = detail::scope_exit([&]() { is_running_ = false; }); - - { - std::unique_ptr task_queue(new_task_queue()); - - while (svr_sock_ != INVALID_SOCKET) { -#ifndef _WIN32 - if (idle_interval_sec_ > 0 || idle_interval_usec_ > 0) { -#endif - auto val = detail::select_read(svr_sock_, idle_interval_sec_, - idle_interval_usec_); - if (val == 0) { // Timeout - task_queue->on_idle(); - continue; - } -#ifndef _WIN32 - } -#endif - -#if defined _WIN32 - // sockets connected via WASAccept inherit flags NO_HANDLE_INHERIT, - // OVERLAPPED - socket_t sock = WSAAccept(svr_sock_, nullptr, nullptr, nullptr, 0); -#elif defined SOCK_CLOEXEC - socket_t sock = accept4(svr_sock_, nullptr, nullptr, SOCK_CLOEXEC); -#else - socket_t sock = accept(svr_sock_, nullptr, nullptr); -#endif - - if (sock == INVALID_SOCKET) { - if (errno == EMFILE) { - // The per-process limit of open file descriptors has been reached. - // Try to accept new connections after a short sleep. - std::this_thread::sleep_for(std::chrono::microseconds{1}); - continue; - } else if (errno == EINTR || errno == EAGAIN) { - continue; - } - if (svr_sock_ != INVALID_SOCKET) { - detail::close_socket(svr_sock_); - ret = false; - output_error_log(Error::Connection, nullptr); - } else { - ; // The server socket was closed by user. - } - break; - } - - detail::set_socket_opt_time(sock, SOL_SOCKET, SO_RCVTIMEO, - read_timeout_sec_, read_timeout_usec_); - detail::set_socket_opt_time(sock, SOL_SOCKET, SO_SNDTIMEO, - write_timeout_sec_, write_timeout_usec_); - - if (tcp_nodelay_) { set_socket_opt(sock, IPPROTO_TCP, TCP_NODELAY, 1); } - - if (!task_queue->enqueue( - [this, sock]() { process_and_close_socket(sock); })) { - output_error_log(Error::ResourceExhaustion, nullptr); - detail::shutdown_socket(sock); - detail::close_socket(sock); - } - } - - task_queue->shutdown(); - } - - is_decommissioned = !ret; - return ret; -} - -bool Server::routing(Request &req, Response &res, Stream &strm) { - if (pre_routing_handler_ && - pre_routing_handler_(req, res) == HandlerResponse::Handled) { - return true; - } - - // File handler - if ((req.method == "GET" || req.method == "HEAD") && - handle_file_request(req, res)) { - return true; - } - - if (detail::expect_content(req)) { - // Content reader handler - { - // Track whether the ContentReader was aborted due to the decompressed - // payload exceeding `payload_max_length_`. - // The user handler runs after the lambda returns, so we must restore the - // 413 status if the handler overwrites it. - bool content_reader_payload_too_large = false; - - ContentReader reader( - [&](ContentReceiver receiver) { - auto result = read_content_with_content_receiver( - strm, req, res, std::move(receiver), nullptr, nullptr); - if (!result) { - output_error_log(Error::Read, &req); - if (res.status == StatusCode::PayloadTooLarge_413) { - content_reader_payload_too_large = true; - } - } - return result; - }, - [&](FormDataHeader header, ContentReceiver receiver) { - auto result = read_content_with_content_receiver( - strm, req, res, nullptr, std::move(header), - std::move(receiver)); - if (!result) { - output_error_log(Error::Read, &req); - if (res.status == StatusCode::PayloadTooLarge_413) { - content_reader_payload_too_large = true; - } - } - return result; - }); - - bool dispatched = false; - if (req.method == "POST") { - dispatched = dispatch_request_for_content_reader( - req, res, std::move(reader), post_handlers_for_content_reader_); - } else if (req.method == "PUT") { - dispatched = dispatch_request_for_content_reader( - req, res, std::move(reader), put_handlers_for_content_reader_); - } else if (req.method == "PATCH") { - dispatched = dispatch_request_for_content_reader( - req, res, std::move(reader), patch_handlers_for_content_reader_); - } else if (req.method == "DELETE") { - dispatched = dispatch_request_for_content_reader( - req, res, std::move(reader), delete_handlers_for_content_reader_); - } - - if (dispatched) { - if (content_reader_payload_too_large) { - // Enforce the limit: override any status the handler may have set - // and return false so the error path sends a plain 413 response. - res.status = StatusCode::PayloadTooLarge_413; - res.body.clear(); - res.content_length_ = 0; - res.content_provider_ = nullptr; - return false; - } - return true; - } - } - - // Read content into `req.body` - if (!read_content(strm, req, res)) { - output_error_log(Error::Read, &req); - return false; - } - } - - // Regular handler - if (req.method == "GET" || req.method == "HEAD") { - return dispatch_request(req, res, get_handlers_); - } else if (req.method == "POST") { - return dispatch_request(req, res, post_handlers_); - } else if (req.method == "PUT") { - return dispatch_request(req, res, put_handlers_); - } else if (req.method == "DELETE") { - return dispatch_request(req, res, delete_handlers_); - } else if (req.method == "OPTIONS") { - return dispatch_request(req, res, options_handlers_); - } else if (req.method == "PATCH") { - return dispatch_request(req, res, patch_handlers_); - } - - res.status = StatusCode::BadRequest_400; - return false; -} - -bool Server::dispatch_request(Request &req, Response &res, - const Handlers &handlers) const { - for (const auto &x : handlers) { - const auto &matcher = x.first; - const auto &handler = x.second; - - if (matcher->match(req)) { - req.matched_route = matcher->pattern(); - if (!pre_request_handler_ || - pre_request_handler_(req, res) != HandlerResponse::Handled) { - handler(req, res); - } - return true; - } - } - return false; -} - -void Server::apply_ranges(const Request &req, Response &res, - std::string &content_type, - std::string &boundary) const { - if (req.ranges.size() > 1 && res.status == StatusCode::PartialContent_206) { - auto it = res.headers.find("Content-Type"); - if (it != res.headers.end()) { - content_type = it->second; - res.headers.erase(it); - } - - boundary = detail::make_multipart_data_boundary(); - - res.set_header("Content-Type", - "multipart/byteranges; boundary=" + boundary); - } - - auto type = detail::encoding_type(req, res); - - if (res.body.empty()) { - if (res.content_length_ > 0) { - size_t length = 0; - if (req.ranges.empty() || res.status != StatusCode::PartialContent_206) { - length = res.content_length_; - } else if (req.ranges.size() == 1) { - auto offset_and_length = detail::get_range_offset_and_length( - req.ranges[0], res.content_length_); - - length = offset_and_length.second; - - auto content_range = detail::make_content_range_header_field( - offset_and_length, res.content_length_); - res.set_header("Content-Range", content_range); - } else { - length = detail::get_multipart_ranges_data_length( - req, boundary, content_type, res.content_length_); - } - res.set_header("Content-Length", std::to_string(length)); - } else { - if (res.content_provider_) { - if (res.is_chunked_content_provider_) { - res.set_header("Transfer-Encoding", "chunked"); - if (type != detail::EncodingType::None) { - res.set_header("Content-Encoding", detail::encoding_name(type)); - res.set_header("Vary", "Accept-Encoding"); - } - } - } - } - } else { - if (req.ranges.empty() || res.status != StatusCode::PartialContent_206) { - ; - } else if (req.ranges.size() == 1) { - auto offset_and_length = - detail::get_range_offset_and_length(req.ranges[0], res.body.size()); - auto offset = offset_and_length.first; - auto length = offset_and_length.second; - - auto content_range = detail::make_content_range_header_field( - offset_and_length, res.body.size()); - res.set_header("Content-Range", content_range); - - assert(offset + length <= res.body.size()); - res.body = res.body.substr(offset, length); - } else { - std::string data; - detail::make_multipart_ranges_data(req, res, boundary, content_type, - res.body.size(), data); - res.body.swap(data); - } - - if (type != detail::EncodingType::None) { - output_pre_compression_log(req, res); - - if (auto compressor = detail::make_compressor(type)) { - std::string compressed; - if (compressor->compress(res.body.data(), res.body.size(), true, - [&](const char *data, size_t data_len) { - compressed.append(data, data_len); - return true; - })) { - res.body.swap(compressed); - res.set_header("Content-Encoding", detail::encoding_name(type)); - res.set_header("Vary", "Accept-Encoding"); - } - } - } - - auto length = std::to_string(res.body.size()); - res.set_header("Content-Length", length); - } -} - -bool Server::dispatch_request_for_content_reader( - Request &req, Response &res, ContentReader content_reader, - const HandlersForContentReader &handlers) const { - for (const auto &x : handlers) { - const auto &matcher = x.first; - const auto &handler = x.second; - - if (matcher->match(req)) { - req.matched_route = matcher->pattern(); - if (!pre_request_handler_ || - pre_request_handler_(req, res) != HandlerResponse::Handled) { - handler(req, res, content_reader); - } - return true; - } - } - return false; -} - -std::string -get_client_ip(const std::string &x_forwarded_for, - const std::vector &trusted_proxies) { - // X-Forwarded-For is a comma-separated list per RFC 7239 - std::vector ip_list; - detail::split(x_forwarded_for.data(), - x_forwarded_for.data() + x_forwarded_for.size(), ',', - [&](const char *b, const char *e) { - auto r = detail::trim(b, e, 0, static_cast(e - b)); - ip_list.emplace_back(std::string(b + r.first, b + r.second)); - }); - - // A malformed X-Forwarded-For (empty, comma-only, whitespace-only) yields - // no segments. Signal "no client IP derived" with an empty string so the - // caller can fall back to the connection-level remote address. - if (ip_list.empty()) { return std::string(); } - - for (size_t i = 0; i < ip_list.size(); ++i) { - auto ip = ip_list[i]; - - auto is_trusted_proxy = - std::any_of(trusted_proxies.begin(), trusted_proxies.end(), - [&](const std::string &proxy) { return ip == proxy; }); - - if (is_trusted_proxy) { - if (i == 0) { - // If the trusted proxy is the first IP, there's no preceding client IP - return ip; - } else { - // Return the IP immediately before the trusted proxy - return ip_list[i - 1]; - } - } - } - - // If no trusted proxy is found, return the first IP in the list - return ip_list.front(); -} - -bool -Server::process_request(Stream &strm, const std::string &remote_addr, - int remote_port, const std::string &local_addr, - int local_port, bool close_connection, - bool &connection_closed, - const std::function &setup_request, - bool *websocket_upgraded) { - std::array buf{}; - - detail::stream_line_reader line_reader(strm, buf.data(), buf.size()); - - // Connection has been closed on client - if (!line_reader.getline()) { return false; } - - Request req; - req.start_time_ = std::chrono::steady_clock::now(); - req.remote_addr = remote_addr; - req.remote_port = remote_port; - req.local_addr = local_addr; - req.local_port = local_port; - - Response res; - res.version = "HTTP/1.1"; - res.headers = default_headers_; - - // Request line and headers - if (!parse_request_line(line_reader.ptr(), req)) { - res.status = StatusCode::BadRequest_400; - output_error_log(Error::InvalidRequestLine, &req); - return write_response(strm, close_connection, req, res); - } - - // Request headers - if (!detail::read_headers(strm, req.headers)) { - res.status = StatusCode::BadRequest_400; - output_error_log(Error::InvalidHeaders, &req); - return write_response(strm, close_connection, req, res); - } - - // RFC 9112 §6.3: Reject requests with both a non-zero Content-Length and - // any Transfer-Encoding to prevent request smuggling. Content-Length: 0 is - // tolerated for compatibility with existing clients. - if (req.get_header_value_u64("Content-Length") > 0 && - req.has_header("Transfer-Encoding")) { - connection_closed = true; - res.status = StatusCode::BadRequest_400; - return write_response(strm, close_connection, req, res); - } - - // Check if the request URI doesn't exceed the limit - if (req.target.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) { - connection_closed = true; - res.status = StatusCode::UriTooLong_414; - output_error_log(Error::ExceedUriMaxLength, &req); - return write_response(strm, close_connection, req, res); - } - - if (req.get_header_value("Connection") == "close") { - connection_closed = true; - } - - if (req.version == "HTTP/1.0" && - req.get_header_value("Connection") != "Keep-Alive") { - connection_closed = true; - } - - if (!trusted_proxies_.empty() && req.has_header("X-Forwarded-For")) { - auto x_forwarded_for = req.get_header_value("X-Forwarded-For"); - auto derived = get_client_ip(x_forwarded_for, trusted_proxies_); - req.remote_addr = derived.empty() ? remote_addr : derived; - } else { - req.remote_addr = remote_addr; - } - req.remote_port = remote_port; - - req.local_addr = local_addr; - req.local_port = local_port; - - if (req.has_header("Accept")) { - const auto &accept_header = req.get_header_value("Accept"); - if (!detail::parse_accept_header(accept_header, req.accept_content_types)) { - connection_closed = true; - res.status = StatusCode::BadRequest_400; - output_error_log(Error::HTTPParsing, &req); - return write_response(strm, close_connection, req, res); - } - } - - if (req.has_header("Range")) { - const auto &range_header_value = req.get_header_value("Range"); - if (!detail::parse_range_header(range_header_value, req.ranges)) { - connection_closed = true; - res.status = StatusCode::RangeNotSatisfiable_416; - output_error_log(Error::InvalidRangeHeader, &req); - return write_response(strm, close_connection, req, res); - } - } - - if (setup_request) { setup_request(req); } - - if (req.get_header_value("Expect") == "100-continue") { - int status = StatusCode::Continue_100; - if (expect_100_continue_handler_) { - status = expect_100_continue_handler_(req, res); - } - switch (status) { - case StatusCode::Continue_100: - case StatusCode::ExpectationFailed_417: - detail::write_response_line(strm, status); - strm.write("\r\n"); - break; - default: - connection_closed = true; - return write_response(strm, true, req, res); - } - } - - // Setup `is_connection_closed` method - auto sock = strm.socket(); - req.is_connection_closed = [sock]() { - return !detail::is_socket_alive(sock); - }; - - // WebSocket upgrade - // Check pre_routing_handler_ before upgrading so that authentication - // and other middleware can reject the request with an HTTP response - // (e.g., 401) before the protocol switches. - if (detail::is_websocket_upgrade(req)) { - if (pre_routing_handler_ && - pre_routing_handler_(req, res) == HandlerResponse::Handled) { - if (res.status == -1) { res.status = StatusCode::OK_200; } - return write_response(strm, close_connection, req, res); - } - // Find matching WebSocket handler - for (const auto &entry : websocket_handlers_) { - if (entry.matcher->match(req)) { - // Compute accept key - auto client_key = req.get_header_value("Sec-WebSocket-Key"); - auto accept_key = detail::websocket_accept_key(client_key); - - // Negotiate subprotocol - std::string selected_subprotocol; - if (entry.sub_protocol_selector) { - auto protocol_header = req.get_header_value("Sec-WebSocket-Protocol"); - if (!protocol_header.empty()) { - std::vector protocols; - std::istringstream iss(protocol_header); - std::string token; - while (std::getline(iss, token, ',')) { - // Trim whitespace - auto start = token.find_first_not_of(' '); - auto end = token.find_last_not_of(' '); - if (start != std::string::npos) { - protocols.push_back(token.substr(start, end - start + 1)); - } - } - selected_subprotocol = entry.sub_protocol_selector(protocols); - } - } - - // Send 101 Switching Protocols - std::string handshake_response = "HTTP/1.1 101 Switching Protocols\r\n" - "Upgrade: websocket\r\n" - "Connection: Upgrade\r\n" - "Sec-WebSocket-Accept: " + - accept_key + "\r\n"; - if (!selected_subprotocol.empty()) { - if (!detail::fields::is_field_value(selected_subprotocol)) { - return false; - } - handshake_response += - "Sec-WebSocket-Protocol: " + selected_subprotocol + "\r\n"; - } - handshake_response += "\r\n"; - if (strm.write(handshake_response.data(), handshake_response.size()) < - 0) { - return false; - } - - connection_closed = true; - if (websocket_upgraded) { *websocket_upgraded = true; } - - { - // Use WebSocket-specific read timeout instead of HTTP timeout - strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND, 0); - ws::WebSocket ws(strm, req, true, websocket_ping_interval_sec_, - websocket_max_missed_pongs_); - entry.handler(req, ws); - } - return true; - } - } - // No matching handler - fall through to 404 - } - - // Routing - auto routed = false; -#ifdef CPPHTTPLIB_NO_EXCEPTIONS - routed = routing(req, res, strm); -#else - try { - routed = routing(req, res, strm); - } catch (std::exception &) { - if (exception_handler_) { - auto ep = std::current_exception(); - exception_handler_(req, res, ep); - routed = true; - } else { - res.status = StatusCode::InternalServerError_500; - } - } catch (...) { - if (exception_handler_) { - auto ep = std::current_exception(); - exception_handler_(req, res, ep); - routed = true; - } else { - res.status = StatusCode::InternalServerError_500; - } - } -#endif - auto ret = false; - if (routed) { - if (res.status == -1) { - res.status = req.ranges.empty() ? StatusCode::OK_200 - : StatusCode::PartialContent_206; - } - - // Serve file content by using a content provider - auto file_open_error = false; - if (!res.file_content_path_.empty()) { - const auto &path = res.file_content_path_; - auto mm = std::make_shared(path.c_str()); - if (!mm->is_open()) { - res.body.clear(); - res.content_length_ = 0; - res.content_provider_ = nullptr; - res.status = StatusCode::NotFound_404; - output_error_log(Error::OpenFile, &req); - file_open_error = true; - } else { - auto content_type = res.file_content_content_type_; - if (content_type.empty()) { - content_type = detail::find_content_type( - path, file_extension_and_mimetype_map_, default_file_mimetype_); - } - - res.set_content_provider( - mm->size(), content_type, - [mm](size_t offset, size_t length, DataSink &sink) -> bool { - sink.write(mm->data() + offset, length); - return true; - }); - } - } - - if (file_open_error) { - ret = write_response(strm, close_connection, req, res); - } else if (detail::range_error(req, res)) { - res.body.clear(); - res.content_length_ = 0; - res.content_provider_ = nullptr; - res.status = StatusCode::RangeNotSatisfiable_416; - ret = write_response(strm, close_connection, req, res); - } else { - ret = write_response_with_content(strm, close_connection, req, res); - } - } else { - if (res.status == -1) { res.status = StatusCode::NotFound_404; } - ret = write_response(strm, close_connection, req, res); - } - - // Drain any unconsumed framed body to prevent request smuggling on - // keep-alive. Without framing there is no body to drain — reading would - // consume the next request (issue #2450). - if (!req.body_consumed_ && detail::has_framed_body(req)) { - int dummy_status; - if (!detail::read_content( - strm, req, payload_max_length_, dummy_status, nullptr, - [](const char *, size_t, size_t, size_t) { return true; }, false)) { - connection_closed = true; - } - } - - return ret; -} - -bool Server::is_valid() const { return true; } - -bool Server::process_and_close_socket(socket_t sock) { - std::string remote_addr; - int remote_port = 0; - detail::get_remote_ip_and_port(sock, remote_addr, remote_port); - - std::string local_addr; - int local_port = 0; - detail::get_local_ip_and_port(sock, local_addr, local_port); - - bool websocket_upgraded = false; - auto ret = detail::process_server_socket( - svr_sock_, sock, keep_alive_max_count_, keep_alive_timeout_sec_, - read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, - write_timeout_usec_, - [&](Stream &strm, bool close_connection, bool &connection_closed) { - return process_request(strm, remote_addr, remote_port, local_addr, - local_port, close_connection, connection_closed, - nullptr, &websocket_upgraded); - }); - - detail::shutdown_socket(sock); - detail::close_socket(sock); - return ret; -} - -void Server::output_log(const Request &req, const Response &res) const { - if (logger_) { - std::lock_guard guard(logger_mutex_); - logger_(req, res); - } -} - -void Server::output_pre_compression_log(const Request &req, - const Response &res) const { - if (pre_compression_logger_) { - std::lock_guard guard(logger_mutex_); - pre_compression_logger_(req, res); - } -} - -void Server::output_error_log(const Error &err, - const Request *req) const { - if (error_logger_) { - std::lock_guard guard(logger_mutex_); - error_logger_(err, req); - } -} - -/* - * Group 5: ClientImpl and Client (Universal) implementation - */ -// HTTP client implementation -ClientImpl::ClientImpl(const std::string &host) - : ClientImpl(host, 80, std::string(), std::string()) {} - -ClientImpl::ClientImpl(const std::string &host, int port) - : ClientImpl(host, port, std::string(), std::string()) {} - -ClientImpl::ClientImpl(const std::string &host, int port, - const std::string &client_cert_path, - const std::string &client_key_path) - : host_(detail::escape_abstract_namespace_unix_domain(host)), port_(port), - client_cert_path_(client_cert_path), client_key_path_(client_key_path) {} - -ClientImpl::~ClientImpl() { - // Wait until all the requests in flight are handled. - size_t retry_count = 10; - while (retry_count-- > 0) { - { - std::lock_guard guard(socket_mutex_); - if (socket_requests_in_flight_ == 0) { break; } - } - std::this_thread::sleep_for(std::chrono::milliseconds{1}); - } - - std::lock_guard guard(socket_mutex_); - shutdown_socket(socket_); - close_socket(socket_); -} - -bool ClientImpl::is_valid() const { return true; } - -void ClientImpl::copy_settings(const ClientImpl &rhs) { - client_cert_path_ = rhs.client_cert_path_; - client_key_path_ = rhs.client_key_path_; - connection_timeout_sec_ = rhs.connection_timeout_sec_; - read_timeout_sec_ = rhs.read_timeout_sec_; - read_timeout_usec_ = rhs.read_timeout_usec_; - write_timeout_sec_ = rhs.write_timeout_sec_; - write_timeout_usec_ = rhs.write_timeout_usec_; - max_timeout_msec_ = rhs.max_timeout_msec_; - basic_auth_username_ = rhs.basic_auth_username_; - basic_auth_password_ = rhs.basic_auth_password_; - bearer_token_auth_token_ = rhs.bearer_token_auth_token_; - keep_alive_ = rhs.keep_alive_; - follow_location_ = rhs.follow_location_; - path_encode_ = rhs.path_encode_; - address_family_ = rhs.address_family_; - tcp_nodelay_ = rhs.tcp_nodelay_; - ipv6_v6only_ = rhs.ipv6_v6only_; - socket_options_ = rhs.socket_options_; - compress_ = rhs.compress_; - decompress_ = rhs.decompress_; - payload_max_length_ = rhs.payload_max_length_; - has_payload_max_length_ = rhs.has_payload_max_length_; - interface_ = rhs.interface_; - proxy_host_ = rhs.proxy_host_; - proxy_port_ = rhs.proxy_port_; - proxy_basic_auth_username_ = rhs.proxy_basic_auth_username_; - proxy_basic_auth_password_ = rhs.proxy_basic_auth_password_; - proxy_bearer_token_auth_token_ = rhs.proxy_bearer_token_auth_token_; - no_proxy_entries_ = rhs.no_proxy_entries_; - logger_ = rhs.logger_; - error_logger_ = rhs.error_logger_; - -#ifdef CPPHTTPLIB_SSL_ENABLED - digest_auth_username_ = rhs.digest_auth_username_; - digest_auth_password_ = rhs.digest_auth_password_; - proxy_digest_auth_username_ = rhs.proxy_digest_auth_username_; - proxy_digest_auth_password_ = rhs.proxy_digest_auth_password_; - ca_cert_file_path_ = rhs.ca_cert_file_path_; - ca_cert_dir_path_ = rhs.ca_cert_dir_path_; - server_certificate_verification_ = rhs.server_certificate_verification_; - server_hostname_verification_ = rhs.server_hostname_verification_; -#endif -} - -bool -ClientImpl::is_proxy_enabled_for_host(const std::string &host) const { - if (proxy_host_.empty() || proxy_port_ == -1) { return false; } - if (no_proxy_entries_.empty()) { return true; } - // host_ is const so its normalized form is invariant; cache it. The - // cross-host path (setup_redirect_client passing next_host) re-normalizes. - if (host == host_) { - if (!host_normalized_valid_) { - host_normalized_ = detail::normalize_target(host_); - host_normalized_valid_ = true; - } - return !detail::host_matches_no_proxy(host_normalized_, no_proxy_entries_); - } - auto target = detail::normalize_target(host); - return !detail::host_matches_no_proxy(target, no_proxy_entries_); -} - -socket_t ClientImpl::create_client_socket(Error &error) const { - if (is_proxy_enabled_for_host(host_)) { - return detail::create_client_socket( - proxy_host_, std::string(), proxy_port_, address_family_, tcp_nodelay_, - ipv6_v6only_, socket_options_, connection_timeout_sec_, - connection_timeout_usec_, read_timeout_sec_, read_timeout_usec_, - write_timeout_sec_, write_timeout_usec_, interface_, error); - } - - // Check is custom IP specified for host_ - std::string ip; - auto it = addr_map_.find(host_); - if (it != addr_map_.end()) { ip = it->second; } - - return detail::create_client_socket( - host_, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_, - socket_options_, connection_timeout_sec_, connection_timeout_usec_, - read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, - write_timeout_usec_, interface_, error); -} - -bool ClientImpl::create_and_connect_socket(Socket &socket, - Error &error) { - auto sock = create_client_socket(error); - if (sock == INVALID_SOCKET) { return false; } - socket.sock = sock; - return true; -} - -bool ClientImpl::ensure_socket_connection(Socket &socket, Error &error) { - return create_and_connect_socket(socket, error); -} - -bool ClientImpl::setup_proxy_connection( - Socket & /*socket*/, - std::chrono::time_point /*start_time*/, - Response & /*res*/, bool & /*success*/, Error & /*error*/) { - return true; -} - -void ClientImpl::shutdown_ssl(Socket & /*socket*/, - bool /*shutdown_gracefully*/) { - // If there are any requests in flight from threads other than us, then it's - // a thread-unsafe race because individual ssl* objects are not thread-safe. - assert(socket_requests_in_flight_ == 0 || - socket_requests_are_from_thread_ == std::this_thread::get_id()); -} - -void ClientImpl::shutdown_socket(Socket &socket) const { - if (socket.sock == INVALID_SOCKET) { return; } - detail::shutdown_socket(socket.sock); -} - -void ClientImpl::close_socket(Socket &socket) { - // If there are requests in flight in another thread, usually closing - // the socket will be fine and they will simply receive an error when - // using the closed socket, but it is still a bug since rarely the OS - // may reassign the socket id to be used for a new socket, and then - // suddenly they will be operating on a live socket that is different - // than the one they intended! - assert(socket_requests_in_flight_ == 0 || - socket_requests_are_from_thread_ == std::this_thread::get_id()); - - // It is also a bug if this happens while SSL is still active -#ifdef CPPHTTPLIB_SSL_ENABLED - assert(socket.ssl == nullptr); -#endif - - if (socket.sock == INVALID_SOCKET) { return; } - detail::close_socket(socket.sock); - socket.sock = INVALID_SOCKET; -} - -void ClientImpl::disconnect(bool gracefully) { - shutdown_ssl(socket_, gracefully); - shutdown_socket(socket_); - close_socket(socket_); -} - -bool ClientImpl::read_response_line(Stream &strm, const Request &req, - Response &res, - bool skip_100_continue) const { - std::array buf{}; - - detail::stream_line_reader line_reader(strm, buf.data(), buf.size()); - - if (!line_reader.getline()) { return false; } - -#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR - thread_local const std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r?\n"); -#else - thread_local const std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r\n"); -#endif - - std::cmatch m; - if (!std::regex_match(line_reader.ptr(), m, re)) { - return req.method == "CONNECT"; - } - res.version = std::string(m[1]); - res.status = std::stoi(std::string(m[2])); - res.reason = std::string(m[3]); - - // Ignore '100 Continue' (only when not using Expect: 100-continue explicitly) - while (skip_100_continue && res.status == StatusCode::Continue_100) { - if (!line_reader.getline()) { return false; } // CRLF - if (!line_reader.getline()) { return false; } // next response line - - if (!std::regex_match(line_reader.ptr(), m, re)) { return false; } - res.version = std::string(m[1]); - res.status = std::stoi(std::string(m[2])); - res.reason = std::string(m[3]); - } - - return true; -} - -bool ClientImpl::send(Request &req, Response &res, Error &error) { - std::lock_guard request_mutex_guard(request_mutex_); - auto ret = send_(req, res, error); - if (error == Error::SSLPeerCouldBeClosed_) { - assert(!ret); - ret = send_(req, res, error); - // If still failing with SSLPeerCouldBeClosed_, convert to Read error - if (error == Error::SSLPeerCouldBeClosed_) { error = Error::Read; } - } - return ret; -} - -bool ClientImpl::send_(Request &req, Response &res, Error &error) { - { - std::lock_guard guard(socket_mutex_); - - // Set this to false immediately - if it ever gets set to true by the end - // of the request, we know another thread instructed us to close the - // socket. - socket_should_be_closed_when_request_is_done_ = false; - - auto is_alive = false; - if (socket_.is_open()) { - is_alive = detail::is_socket_alive(socket_.sock); - -#ifdef CPPHTTPLIB_SSL_ENABLED - if (is_alive && is_ssl()) { - if (tls::is_peer_closed(socket_.ssl, socket_.sock)) { - is_alive = false; - } - } -#endif - - if (!is_alive) { - // Peer seems gone — non-graceful shutdown to avoid SIGPIPE. - disconnect(/*gracefully=*/false); - } - } - - if (!is_alive) { - if (!ensure_socket_connection(socket_, error)) { - output_error_log(error, &req); - return false; - } - - { - auto success = true; - if (!setup_proxy_connection(socket_, req.start_time_, res, success, - error)) { - if (!success) { output_error_log(error, &req); } - return success; - } - } - } - - // Mark the current socket as being in use so that it cannot be closed by - // anyone else while this request is ongoing, even though we will be - // releasing the mutex. - if (socket_requests_in_flight_ > 1) { - assert(socket_requests_are_from_thread_ == std::this_thread::get_id()); - } - socket_requests_in_flight_ += 1; - socket_requests_are_from_thread_ = std::this_thread::get_id(); - } - - for (const auto &header : default_headers_) { - if (req.headers.find(header.first) == req.headers.end()) { - req.headers.insert(header); - } - } - - auto ret = false; - auto close_connection = !keep_alive_; - - auto se = detail::scope_exit([&]() { - // Briefly lock mutex in order to mark that a request is no longer ongoing - std::lock_guard guard(socket_mutex_); - socket_requests_in_flight_ -= 1; - if (socket_requests_in_flight_ <= 0) { - assert(socket_requests_in_flight_ == 0); - socket_requests_are_from_thread_ = std::thread::id(); - } - - if (socket_should_be_closed_when_request_is_done_ || close_connection || - !ret) { - disconnect(/*gracefully=*/true); - } - }); - - ret = process_socket(socket_, req.start_time_, [&](Stream &strm) { - return handle_request(strm, req, res, close_connection, error); - }); - - if (!ret) { - if (error == Error::Success) { - error = Error::Unknown; - output_error_log(error, &req); - } - } - - return ret; -} - -Result ClientImpl::send(const Request &req) { - auto req2 = req; - return send_(std::move(req2)); -} - -Result ClientImpl::send_(Request &&req) { - auto res = detail::make_unique(); - auto error = Error::Success; - auto ret = send(req, *res, error); -#ifdef CPPHTTPLIB_SSL_ENABLED - return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers), - last_ssl_error_, last_backend_error_}; -#else - return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers)}; -#endif -} - -void ClientImpl::prepare_default_headers(Request &r, bool for_stream, - const std::string &ct) { - (void)for_stream; - for (const auto &header : default_headers_) { - if (!r.has_header(header.first)) { r.headers.insert(header); } - } - - if (!r.has_header("Host")) { - if (address_family_ == AF_UNIX) { - r.headers.emplace("Host", "localhost"); - } else { - r.headers.emplace( - "Host", detail::make_host_and_port_string(host_, port_, is_ssl())); - } - } - - if (!r.has_header("Accept")) { r.headers.emplace("Accept", "*/*"); } - - if (!r.content_receiver) { - if (!r.has_header("Accept-Encoding")) { - std::string accept_encoding; -#ifdef CPPHTTPLIB_BROTLI_SUPPORT - accept_encoding = "br"; -#endif -#ifdef CPPHTTPLIB_ZLIB_SUPPORT - if (!accept_encoding.empty()) { accept_encoding += ", "; } - accept_encoding += "gzip, deflate"; -#endif -#ifdef CPPHTTPLIB_ZSTD_SUPPORT - if (!accept_encoding.empty()) { accept_encoding += ", "; } - accept_encoding += "zstd"; -#endif - r.set_header("Accept-Encoding", accept_encoding); - } - -#ifndef CPPHTTPLIB_NO_DEFAULT_USER_AGENT - if (!r.has_header("User-Agent")) { - auto agent = std::string("cpp-httplib/") + CPPHTTPLIB_VERSION; - r.set_header("User-Agent", agent); - } -#endif - } - - if (!r.body.empty()) { - if (!ct.empty() && !r.has_header("Content-Type")) { - r.headers.emplace("Content-Type", ct); - } - if (!r.has_header("Content-Length")) { - r.headers.emplace("Content-Length", std::to_string(r.body.size())); - } - } -} - -ClientImpl::StreamHandle -ClientImpl::open_stream(const std::string &method, const std::string &path, - const Params ¶ms, const Headers &headers, - const std::string &body, - const std::string &content_type) { - StreamHandle handle; - handle.response = detail::make_unique(); - handle.error = Error::Success; - - auto query_path = params.empty() ? path : append_query_params(path, params); - handle.connection_ = detail::make_unique(); - - { - std::lock_guard guard(socket_mutex_); - - auto is_alive = false; - if (socket_.is_open()) { - is_alive = detail::is_socket_alive(socket_.sock); -#ifdef CPPHTTPLIB_SSL_ENABLED - if (is_alive && is_ssl()) { - if (tls::is_peer_closed(socket_.ssl, socket_.sock)) { - is_alive = false; - } - } -#endif - if (!is_alive) { disconnect(/*gracefully=*/false); } - } - - if (!is_alive) { - if (!ensure_socket_connection(socket_, handle.error)) { - handle.response.reset(); - return handle; - } - - { - auto success = true; - auto start_time = std::chrono::steady_clock::now(); - if (!setup_proxy_connection(socket_, start_time, *handle.response, - success, handle.error)) { - if (!success) { handle.response.reset(); } - return handle; - } - } - } - - transfer_socket_ownership_to_handle(handle); - } - -#ifdef CPPHTTPLIB_SSL_ENABLED - if (is_ssl() && handle.connection_->session) { - handle.socket_stream_ = detail::make_unique( - handle.connection_->sock, handle.connection_->session, - read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, - write_timeout_usec_); - } else { - handle.socket_stream_ = detail::make_unique( - handle.connection_->sock, read_timeout_sec_, read_timeout_usec_, - write_timeout_sec_, write_timeout_usec_); - } -#else - handle.socket_stream_ = detail::make_unique( - handle.connection_->sock, read_timeout_sec_, read_timeout_usec_, - write_timeout_sec_, write_timeout_usec_); -#endif - handle.stream_ = handle.socket_stream_.get(); - - Request req; - req.method = method; - req.path = query_path; - req.headers = headers; - req.body = body; - - prepare_default_headers(req, true, content_type); - - auto &strm = *handle.stream_; - if (detail::write_request_line(strm, req.method, req.path) < 0) { - handle.error = Error::Write; - handle.response.reset(); - return handle; - } - - if (!detail::check_and_write_headers(strm, req.headers, header_writer_, - handle.error)) { - handle.response.reset(); - return handle; - } - - if (!body.empty()) { - if (strm.write(body.data(), body.size()) < 0) { - handle.error = Error::Write; - handle.response.reset(); - return handle; - } - } - - if (!read_response_line(strm, req, *handle.response) || - !detail::read_headers(strm, handle.response->headers)) { - handle.error = Error::Read; - handle.response.reset(); - return handle; - } - - handle.body_reader_.stream = handle.stream_; - handle.body_reader_.payload_max_length = payload_max_length_; - - if (handle.response->has_header("Content-Length")) { - bool is_invalid = false; - auto content_length = detail::get_header_value_u64( - handle.response->headers, "Content-Length", 0, 0, is_invalid); - if (is_invalid) { - handle.error = Error::Read; - handle.response.reset(); - return handle; - } - handle.body_reader_.has_content_length = true; - handle.body_reader_.content_length = content_length; - } - - auto transfer_encoding = - handle.response->get_header_value("Transfer-Encoding"); - handle.body_reader_.chunked = (transfer_encoding == "chunked"); - - auto content_encoding = handle.response->get_header_value("Content-Encoding"); - if (!content_encoding.empty()) { - handle.decompressor_ = detail::create_decompressor(content_encoding); - } - - return handle; -} - -ssize_t ClientImpl::StreamHandle::read(char *buf, size_t len) { - if (!is_valid() || !response) { return -1; } - - if (decompressor_) { return read_with_decompression(buf, len); } - auto n = detail::read_body_content(stream_, body_reader_, buf, len); - - if (n <= 0 && body_reader_.chunked && !trailers_parsed_ && stream_) { - trailers_parsed_ = true; - if (body_reader_.chunked_decoder) { - if (!body_reader_.chunked_decoder->parse_trailers_into( - response->trailers, response->headers)) { - return n; - } - } else { - detail::ChunkedDecoder dec(*stream_); - if (!dec.parse_trailers_into(response->trailers, response->headers)) { - return n; - } - } - } - - return n; -} - -ssize_t ClientImpl::StreamHandle::read_with_decompression(char *buf, - size_t len) { - if (decompress_offset_ < decompress_buffer_.size()) { - auto available = decompress_buffer_.size() - decompress_offset_; - auto to_copy = (std::min)(len, available); - std::memcpy(buf, decompress_buffer_.data() + decompress_offset_, to_copy); - decompress_offset_ += to_copy; - decompressed_bytes_read_ += to_copy; - return static_cast(to_copy); - } - - decompress_buffer_.clear(); - decompress_offset_ = 0; - - constexpr size_t kDecompressionBufferSize = 8192; - char compressed_buf[kDecompressionBufferSize]; - - while (true) { - auto n = detail::read_body_content(stream_, body_reader_, compressed_buf, - sizeof(compressed_buf)); - - if (n <= 0) { return n; } - - bool decompress_ok = decompressor_->decompress( - compressed_buf, static_cast(n), - [this](const char *data, size_t data_len) { - decompress_buffer_.append(data, data_len); - auto limit = body_reader_.payload_max_length; - if (decompressed_bytes_read_ + decompress_buffer_.size() > limit) { - return false; - } - return true; - }); - - if (!decompress_ok) { - body_reader_.last_error = Error::Read; - return -1; - } - - if (!decompress_buffer_.empty()) { break; } - } - - auto to_copy = (std::min)(len, decompress_buffer_.size()); - std::memcpy(buf, decompress_buffer_.data(), to_copy); - decompress_offset_ = to_copy; - decompressed_bytes_read_ += to_copy; - return static_cast(to_copy); -} - -void ClientImpl::StreamHandle::parse_trailers_if_needed() { - if (!response || !stream_ || !body_reader_.chunked || trailers_parsed_) { - return; - } - - trailers_parsed_ = true; - - const auto bufsiz = 128; - char line_buf[bufsiz]; - detail::stream_line_reader line_reader(*stream_, line_buf, bufsiz); - - if (!line_reader.getline()) { return; } - - if (!detail::parse_trailers(line_reader, response->trailers, - response->headers)) { - return; - } -} - -namespace detail { - -ChunkedDecoder::ChunkedDecoder(Stream &s) : strm(s) {} - -ssize_t ChunkedDecoder::read_payload(char *buf, size_t len, - size_t &out_chunk_offset, - size_t &out_chunk_total) { - if (finished) { return 0; } - - if (chunk_remaining == 0) { - stream_line_reader lr(strm, line_buf, sizeof(line_buf)); - if (!lr.getline()) { return -1; } - - // RFC 9112 §7.1: chunk-size = 1*HEXDIG - const char *p = lr.ptr(); - int v = 0; - if (!is_hex(*p, v)) { return -1; } - - size_t chunk_len = 0; - constexpr size_t chunk_len_max = (std::numeric_limits::max)(); - for (; is_hex(*p, v); ++p) { - if (chunk_len > (chunk_len_max >> 4)) { return -1; } - chunk_len = (chunk_len << 4) | static_cast(v); - } - - while (is_space_or_tab(*p)) { - ++p; - } - if (*p != '\0' && *p != ';' && *p != '\r' && *p != '\n') { return -1; } - - if (chunk_len == 0) { - chunk_remaining = 0; - finished = true; - out_chunk_offset = 0; - out_chunk_total = 0; - return 0; - } - - chunk_remaining = chunk_len; - last_chunk_total = chunk_remaining; - last_chunk_offset = 0; - } - - auto to_read = (std::min)(chunk_remaining, len); - auto n = strm.read(buf, to_read); - if (n <= 0) { return -1; } - - auto offset_before = last_chunk_offset; - last_chunk_offset += static_cast(n); - chunk_remaining -= static_cast(n); - - out_chunk_offset = offset_before; - out_chunk_total = last_chunk_total; - - if (chunk_remaining == 0) { - stream_line_reader lr(strm, line_buf, sizeof(line_buf)); - if (!lr.getline()) { return -1; } - if (std::strcmp(lr.ptr(), "\r\n") != 0) { return -1; } - } - - return n; -} - -bool ChunkedDecoder::parse_trailers_into(Headers &dest, - const Headers &src_headers) { - stream_line_reader lr(strm, line_buf, sizeof(line_buf)); - if (!lr.getline()) { return false; } - return parse_trailers(lr, dest, src_headers); -} - -} // namespace detail - -void -ClientImpl::transfer_socket_ownership_to_handle(StreamHandle &handle) { - handle.connection_->sock = socket_.sock; -#ifdef CPPHTTPLIB_SSL_ENABLED - handle.connection_->session = socket_.ssl; - socket_.ssl = nullptr; -#endif - socket_.sock = INVALID_SOCKET; -} - -bool ClientImpl::handle_request(Stream &strm, Request &req, - Response &res, bool close_connection, - Error &error) { - if (req.path.empty()) { - error = Error::Connection; - output_error_log(error, &req); - return false; - } - - auto req_save = req; - - bool ret; - - if (!is_ssl() && is_proxy_enabled_for_host(host_)) { - auto req2 = req; - req2.path = "http://" + - detail::make_host_and_port_string(host_, port_, false) + - req.path; - ret = process_request(strm, req2, res, close_connection, error); - req = std::move(req2); - req.path = req_save.path; - } else { - ret = process_request(strm, req, res, close_connection, error); - } - - if (!ret) { return false; } - - if (res.get_header_value("Connection") == "close" || - (res.version == "HTTP/1.0" && res.reason != "Connection established")) { - // NOTE: this requires a not-entirely-obvious chain of calls to be correct - // for this to be safe. - - // This is safe to call because handle_request is only called by send_ - // which locks the request mutex during the process. It would be a bug - // to call it from a different thread since it's a thread-safety issue - // to do these things to the socket if another thread is using the socket. - std::lock_guard guard(socket_mutex_); - disconnect(/*gracefully=*/true); - } - - if (300 < res.status && res.status < 400 && follow_location_) { - req = std::move(req_save); - ret = redirect(req, res, error); - } - -#ifdef CPPHTTPLIB_SSL_ENABLED - if ((res.status == StatusCode::Unauthorized_401 || - res.status == StatusCode::ProxyAuthenticationRequired_407) && - req.authorization_count_ < 5) { - auto is_proxy = res.status == StatusCode::ProxyAuthenticationRequired_407; - - // Only retry when the 407 actually came from a proxy hop: plain HTTP - // through an enabled proxy. HTTPS via CONNECT tunnels the 407 from the - // origin (#2457); direct/bypassed origins have no proxy hop at all. - if (is_proxy && !(!is_ssl() && is_proxy_enabled_for_host(host_))) { - return ret; - } - - const auto &username = - is_proxy ? proxy_digest_auth_username_ : digest_auth_username_; - const auto &password = - is_proxy ? proxy_digest_auth_password_ : digest_auth_password_; - - if (!username.empty() && !password.empty()) { - std::map auth; - if (detail::parse_www_authenticate(res, auth, is_proxy)) { - Request new_req = req; - new_req.authorization_count_ += 1; - new_req.headers.erase(is_proxy ? "Proxy-Authorization" - : "Authorization"); - new_req.headers.insert(detail::make_digest_authentication_header( - req, auth, new_req.authorization_count_, detail::random_string(10), - username, password, is_proxy)); - - Response new_res; - - ret = send(new_req, new_res, error); - if (ret) { res = std::move(new_res); } - } - } - } -#endif - - return ret; -} - -bool ClientImpl::redirect(Request &req, Response &res, Error &error) { - if (req.redirect_count_ == 0) { - error = Error::ExceedRedirectCount; - output_error_log(error, &req); - return false; - } - - auto location = res.get_header_value("location"); - if (location.empty()) { return false; } - - detail::UrlComponents uc; - if (!detail::parse_url(location, uc)) { return false; } - - // Only follow http/https redirects - if (!uc.scheme.empty() && uc.scheme != "http" && uc.scheme != "https") { - return false; - } - - auto scheme = is_ssl() ? "https" : "http"; - - auto next_scheme = std::move(uc.scheme); - auto next_host = std::move(uc.host); - auto port_str = std::move(uc.port); - auto next_path = std::move(uc.path); - auto next_query = std::move(uc.query); - - auto next_port = port_; - if (!port_str.empty()) { - if (!detail::parse_port(port_str, next_port)) { return false; } - } else if (!next_scheme.empty()) { - next_port = next_scheme == "https" ? 443 : 80; - } - - if (next_scheme.empty()) { next_scheme = scheme; } - if (next_host.empty()) { next_host = host_; } - if (next_path.empty()) { next_path = "/"; } - - auto path = decode_path_component(next_path) + next_query; - - // Same host redirect - use current client - if (next_scheme == scheme && next_host == host_ && next_port == port_) { - return detail::redirect(*this, req, res, path, location, error); - } - - // Cross-host/scheme redirect - create new client with robust setup - return create_redirect_client(next_scheme, next_host, next_port, req, res, - path, location, error); -} - -// New method for robust redirect client creation -bool ClientImpl::create_redirect_client( - const std::string &scheme, const std::string &host, int port, Request &req, - Response &res, const std::string &path, const std::string &location, - Error &error) { - // Determine if we need SSL - auto need_ssl = (scheme == "https"); - - // Clean up request headers that are host/client specific - // Remove headers that should not be carried over to new host - auto headers_to_remove = - std::vector{"Host", "Proxy-Authorization", "Authorization"}; - - for (const auto &header_name : headers_to_remove) { - auto it = req.headers.find(header_name); - while (it != req.headers.end()) { - it = req.headers.erase(it); - it = req.headers.find(header_name); - } - } - - // Create appropriate client type and handle redirect - if (need_ssl) { -#ifdef CPPHTTPLIB_SSL_ENABLED - // Create SSL client for HTTPS redirect - SSLClient redirect_client(host, port); - - // Setup basic client configuration first - setup_redirect_client(redirect_client); - - redirect_client.enable_server_certificate_verification( - server_certificate_verification_); - redirect_client.enable_server_hostname_verification( - server_hostname_verification_); - - // Transfer CA certificate to redirect client - if (!ca_cert_pem_.empty()) { - redirect_client.load_ca_cert_store(ca_cert_pem_.c_str(), - ca_cert_pem_.size()); - } - if (!ca_cert_file_path_.empty()) { - redirect_client.set_ca_cert_path(ca_cert_file_path_, ca_cert_dir_path_); - } - - // Client certificates are set through constructor for SSLClient - // NOTE: SSLClient constructor already takes client_cert_path and - // client_key_path so we need to create it properly if client certs are - // needed - - // Execute the redirect - return detail::redirect(redirect_client, req, res, path, location, error); -#else - // SSL not supported - set appropriate error - error = Error::SSLConnection; - output_error_log(error, &req); - return false; -#endif - } else { - // HTTP redirect - ClientImpl redirect_client(host, port); - - // Setup client with robust configuration - setup_redirect_client(redirect_client); - - // Execute the redirect - return detail::redirect(redirect_client, req, res, path, location, error); - } -} - -// New method for robust client setup (based on basic_manual_redirect.cpp -// logic) -template -void ClientImpl::setup_redirect_client(ClientType &client) { - // Copy basic settings first - client.set_connection_timeout(connection_timeout_sec_); - client.set_read_timeout(read_timeout_sec_, read_timeout_usec_); - client.set_write_timeout(write_timeout_sec_, write_timeout_usec_); - client.set_keep_alive(keep_alive_); - client.set_follow_location( - true); // Enable redirects to handle multi-step redirects - client.set_path_encode(path_encode_); - client.set_compress(compress_); - client.set_decompress(decompress_); - - // NOTE: Authentication credentials (basic auth, bearer token, digest auth) - // are intentionally NOT copied to the redirect client. Per RFC 9110 Section - // 15.4, credentials must not be forwarded when redirecting to a different - // host. This function is only called for cross-host redirects; same-host - // redirects are handled directly in ClientImpl::redirect(). - - // Copy the proxy configuration unconditionally; the per-target bypass is - // re-evaluated at send time, so a later hop to a non-bypassed host can - // still use the proxy. - client.no_proxy_entries_ = no_proxy_entries_; - if (!proxy_host_.empty() && proxy_port_ != -1) { - client.set_proxy(proxy_host_, proxy_port_); - - if (!proxy_basic_auth_username_.empty()) { - client.set_proxy_basic_auth(proxy_basic_auth_username_, - proxy_basic_auth_password_); - } - if (!proxy_bearer_token_auth_token_.empty()) { - client.set_proxy_bearer_token_auth(proxy_bearer_token_auth_token_); - } -#ifdef CPPHTTPLIB_SSL_ENABLED - if (!proxy_digest_auth_username_.empty()) { - client.set_proxy_digest_auth(proxy_digest_auth_username_, - proxy_digest_auth_password_); - } -#endif - } - - // Copy network and socket settings - client.set_address_family(address_family_); - client.set_tcp_nodelay(tcp_nodelay_); - client.set_ipv6_v6only(ipv6_v6only_); - if (socket_options_) { client.set_socket_options(socket_options_); } - if (!interface_.empty()) { client.set_interface(interface_); } - - // Copy logging and headers - if (logger_) { client.set_logger(logger_); } - if (error_logger_) { client.set_error_logger(error_logger_); } - - // NOTE: DO NOT copy default_headers_ as they may contain stale Host headers - // Each new client should generate its own headers based on its target host -} - -bool ClientImpl::write_content_with_provider(Stream &strm, - const Request &req, - Error &error) const { - auto is_shutting_down = []() { return false; }; - - if (req.is_chunked_content_provider_) { - auto compressor = compress_ ? detail::create_compressor().first - : std::unique_ptr(); - if (!compressor) { - compressor = detail::make_unique(); - } - - return detail::write_content_chunked(strm, req.content_provider_, - is_shutting_down, *compressor, error); - } else { - return detail::write_content_with_progress( - strm, req.content_provider_, 0, req.content_length_, is_shutting_down, - req.upload_progress, error); - } -} - -bool ClientImpl::write_request(Stream &strm, Request &req, - bool close_connection, Error &error, - bool skip_body) { - // Prepare additional headers - if (close_connection) { - if (!req.has_header("Connection")) { - req.set_header("Connection", "close"); - } - } - - std::string ct_for_defaults; - if (!req.has_header("Content-Type") && !req.body.empty()) { - ct_for_defaults = "text/plain"; - } - prepare_default_headers(req, false, ct_for_defaults); - - if (req.body.empty()) { - if (req.content_provider_) { - if (!req.is_chunked_content_provider_) { - if (!req.has_header("Content-Length")) { - auto length = std::to_string(req.content_length_); - req.set_header("Content-Length", length); - } - } - } else { - if (req.method == "POST" || req.method == "PUT" || - req.method == "PATCH") { - req.set_header("Content-Length", "0"); - } - } - } - - if (!basic_auth_password_.empty() || !basic_auth_username_.empty()) { - if (!req.has_header("Authorization")) { - req.headers.insert(make_basic_authentication_header( - basic_auth_username_, basic_auth_password_, false)); - } - } - - if (!bearer_token_auth_token_.empty()) { - if (!req.has_header("Authorization")) { - req.headers.insert(make_bearer_token_authentication_header( - bearer_token_auth_token_, false)); - } - } - - // Proxy-Authorization is only sent when the proxy is actually used for - // this target — otherwise NO_PROXY-matched requests would leak proxy - // credentials directly to the destination server. - if (is_proxy_enabled_for_host(host_)) { - if (!proxy_basic_auth_username_.empty() && - !proxy_basic_auth_password_.empty() && - !req.has_header("Proxy-Authorization")) { - req.headers.insert(make_basic_authentication_header( - proxy_basic_auth_username_, proxy_basic_auth_password_, true)); - } - if (!proxy_bearer_token_auth_token_.empty() && - !req.has_header("Proxy-Authorization")) { - req.headers.insert(make_bearer_token_authentication_header( - proxy_bearer_token_auth_token_, true)); - } - } - - // Request line and headers - { - detail::BufferStream bstrm; - - // Extract path and query from req.path - std::string path_part, query_part; - auto query_pos = req.path.find('?'); - if (query_pos != std::string::npos) { - path_part = req.path.substr(0, query_pos); - query_part = req.path.substr(query_pos + 1); - } else { - path_part = req.path; - query_part = ""; - } - - // Encode path part. If the original `req.path` already contained a - // query component, preserve its raw query string (including parameter - // order) instead of reparsing and reassembling it which may reorder - // parameters due to container ordering (e.g. `Params` uses - // `std::multimap`). When there is no query in `req.path`, fall back to - // building a query from `req.params` so existing callers that pass - // `Params` continue to work. - auto path_with_query = - path_encode_ ? detail::encode_path(path_part) : path_part; - - if (!query_part.empty()) { - // Normalize the query string (decode then re-encode) while preserving - // the original parameter order. - auto normalized = detail::normalize_query_string(query_part); - if (!normalized.empty()) { path_with_query += '?' + normalized; } - - // Still populate req.params for handlers/users who read them. - detail::parse_query_text(query_part, req.params); - } else { - // No query in path; parse any query_part (empty) and append params - // from `req.params` when present (preserves prior behavior for - // callers who provide Params separately). - detail::parse_query_text(query_part, req.params); - if (!req.params.empty()) { - path_with_query = append_query_params(path_with_query, req.params); - } - } - - // Write request line and headers - detail::write_request_line(bstrm, req.method, path_with_query); - if (!detail::check_and_write_headers(bstrm, req.headers, header_writer_, - error)) { - output_error_log(error, &req); - return false; - } - - // Flush buffer - auto &data = bstrm.get_buffer(); - if (!detail::write_data(strm, data.data(), data.size())) { - error = Error::Write; - output_error_log(error, &req); - return false; - } - } - - // After sending request line and headers, wait briefly for an early server - // response (e.g. 4xx) and avoid sending a potentially large request body - // unnecessarily. This workaround is only enabled on Windows because Unix - // platforms surface write errors (EPIPE) earlier; on Windows kernel send - // buffering can accept large writes even when the peer already responded. - // Check the stream first (which covers SSL via `is_readable()`), then - // fall back to select on the socket. Only perform the wait for very large - // request bodies to avoid interfering with normal small requests and - // reduce side-effects. Poll briefly (up to 50ms as default) for an early - // response. Skip this check when using Expect: 100-continue, as the protocol - // handles early responses properly. -#if defined(_WIN32) - if (!skip_body && - req.body.size() > CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_THRESHOLD && - req.path.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) { - auto start = std::chrono::high_resolution_clock::now(); - - for (;;) { - // Prefer socket-level readiness to avoid SSL_pending() false-positives - // from SSL internals. If the underlying socket is readable, assume an - // early response may be present. - auto sock = strm.socket(); - if (sock != INVALID_SOCKET && detail::select_read(sock, 0, 0) > 0) { - return false; - } - - // Fallback to stream-level check for non-socket streams or when the - // socket isn't reporting readable. Avoid using `is_readable()` for - // SSL, since `SSL_pending()` may report buffered records that do not - // indicate a complete application-level response yet. - if (!is_ssl() && strm.is_readable()) { return false; } - - auto now = std::chrono::high_resolution_clock::now(); - auto elapsed = - std::chrono::duration_cast(now - start) - .count(); - if (elapsed >= CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND) { - break; - } - - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - } -#endif - - // Body - if (skip_body) { return true; } - - return write_request_body(strm, req, error); -} - -bool ClientImpl::write_request_body(Stream &strm, Request &req, - Error &error) { - if (req.body.empty()) { - return write_content_with_provider(strm, req, error); - } - - if (req.upload_progress) { - auto body_size = req.body.size(); - size_t written = 0; - auto data = req.body.data(); - - while (written < body_size) { - size_t to_write = (std::min)(CPPHTTPLIB_SEND_BUFSIZ, body_size - written); - if (!detail::write_data(strm, data + written, to_write)) { - error = Error::Write; - output_error_log(error, &req); - return false; - } - written += to_write; - - if (!req.upload_progress(written, body_size)) { - error = Error::Canceled; - output_error_log(error, &req); - return false; - } - } - } else { - if (!detail::write_data(strm, req.body.data(), req.body.size())) { - error = Error::Write; - output_error_log(error, &req); - return false; - } - } - - return true; -} - -std::unique_ptr -ClientImpl::send_with_content_provider_and_receiver( - Request &req, const char *body, size_t content_length, - ContentProvider content_provider, - ContentProviderWithoutLength content_provider_without_length, - const std::string &content_type, ContentReceiver content_receiver, - Error &error) { - if (!content_type.empty()) { req.set_header("Content-Type", content_type); } - - auto enc = compress_ - ? detail::create_compressor() - : std::pair, const char *>( - nullptr, nullptr); - - if (enc.second) { req.set_header("Content-Encoding", enc.second); } - - if (enc.first && !content_provider_without_length) { - auto &compressor = enc.first; - - if (content_provider) { - auto ok = true; - size_t offset = 0; - DataSink data_sink; - - data_sink.write = [&](const char *data, size_t data_len) -> bool { - if (ok) { - auto last = offset + data_len == content_length; - - auto ret = compressor->compress( - data, data_len, last, - [&](const char *compressed_data, size_t compressed_data_len) { - req.body.append(compressed_data, compressed_data_len); - return true; - }); - - if (ret) { - offset += data_len; - } else { - ok = false; - } - } - return ok; - }; - - while (ok && offset < content_length) { - if (!content_provider(offset, content_length - offset, data_sink)) { - error = Error::Canceled; - output_error_log(error, &req); - return nullptr; - } - } - } else { - if (!compressor->compress(body, content_length, true, - [&](const char *data, size_t data_len) { - req.body.append(data, data_len); - return true; - })) { - error = Error::Compression; - output_error_log(error, &req); - return nullptr; - } - } - } else { - if (content_provider) { - req.content_length_ = content_length; - req.content_provider_ = std::move(content_provider); - req.is_chunked_content_provider_ = false; - } else if (content_provider_without_length) { - req.content_length_ = 0; - req.content_provider_ = detail::ContentProviderAdapter( - std::move(content_provider_without_length)); - req.is_chunked_content_provider_ = true; - req.set_header("Transfer-Encoding", "chunked"); - } else { - req.body.assign(body, content_length); - } - } - - if (content_receiver) { - req.content_receiver = - [content_receiver](const char *data, size_t data_length, - size_t /*offset*/, size_t /*total_length*/) { - return content_receiver(data, data_length); - }; - } - - auto res = detail::make_unique(); - return send(req, *res, error) ? std::move(res) : nullptr; -} - -Result ClientImpl::send_with_content_provider_and_receiver( - const std::string &method, const std::string &path, const Headers &headers, - const char *body, size_t content_length, ContentProvider content_provider, - ContentProviderWithoutLength content_provider_without_length, - const std::string &content_type, ContentReceiver content_receiver, - UploadProgress progress) { - Request req; - req.method = method; - req.headers = headers; - req.path = path; - req.upload_progress = std::move(progress); - if (max_timeout_msec_ > 0) { - req.start_time_ = std::chrono::steady_clock::now(); - } - - auto error = Error::Success; - - auto res = send_with_content_provider_and_receiver( - req, body, content_length, std::move(content_provider), - std::move(content_provider_without_length), content_type, - std::move(content_receiver), error); - -#ifdef CPPHTTPLIB_SSL_ENABLED - return Result{std::move(res), error, std::move(req.headers), last_ssl_error_, - last_backend_error_}; -#else - return Result{std::move(res), error, std::move(req.headers)}; -#endif -} - -void ClientImpl::output_log(const Request &req, - const Response &res) const { - if (logger_) { - std::lock_guard guard(logger_mutex_); - logger_(req, res); - } -} - -void ClientImpl::output_error_log(const Error &err, - const Request *req) const { - if (error_logger_) { - std::lock_guard guard(logger_mutex_); - error_logger_(err, req); - } -} - -bool ClientImpl::process_request(Stream &strm, Request &req, - Response &res, bool close_connection, - Error &error) { - // Auto-add Expect: 100-continue for large bodies - if (CPPHTTPLIB_EXPECT_100_THRESHOLD > 0 && !req.has_header("Expect")) { - auto body_size = req.body.empty() ? req.content_length_ : req.body.size(); - if (body_size >= CPPHTTPLIB_EXPECT_100_THRESHOLD) { - req.set_header("Expect", "100-continue"); - } - } - - // Check for Expect: 100-continue - auto expect_100_continue = req.get_header_value("Expect") == "100-continue"; - - // Send request (skip body if using Expect: 100-continue) - auto write_request_success = - write_request(strm, req, close_connection, error, expect_100_continue); - -#ifdef CPPHTTPLIB_SSL_ENABLED - if (is_ssl() && !expect_100_continue) { - auto is_proxy_enabled = is_proxy_enabled_for_host(host_); - if (!is_proxy_enabled) { - if (tls::is_peer_closed(socket_.ssl, socket_.sock)) { - error = Error::SSLPeerCouldBeClosed_; - output_error_log(error, &req); - return false; - } - } - } -#endif - - // Handle Expect: 100-continue. - // - // Wait for an interim/early response by attempting to read the status line - // under a short timeout, instead of trusting raw socket readability. Over - // TLS, post-handshake records (e.g. session tickets) make the socket - // readable without any HTTP response being available; relying on - // `select_read` there caused the body to be withheld forever and the - // request to fail with `Read` (#2458). If no status line arrives within the - // timeout, send the body anyway (matching curl's behavior). - auto status_line_read = false; - if (expect_100_continue && write_request_success) { - if (CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND > 0) { - time_t sec = CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND / 1000; - time_t usec = (CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND % 1000) * 1000; - strm.set_read_timeout(sec, usec); - status_line_read = read_response_line(strm, req, res, false); - strm.set_read_timeout(read_timeout_sec_, read_timeout_usec_); - } - - if (!status_line_read) { - // No interim response within the timeout: send the body and handle the - // response as usual. - if (!write_request_body(strm, req, error)) { return false; } - expect_100_continue = false; // Switch to normal response handling - } - } - - // Receive response and headers - // When using Expect: 100-continue, don't auto-skip `100 Continue` response - if ((!status_line_read && - !read_response_line(strm, req, res, !expect_100_continue)) || - !detail::read_headers(strm, res.headers)) { - if (write_request_success) { error = Error::Read; } - output_error_log(error, &req); - return false; - } - - if (!write_request_success) { return false; } - - // Handle Expect: 100-continue response - if (expect_100_continue) { - if (res.status == StatusCode::Continue_100) { - // Server accepted, send the body - if (!write_request_body(strm, req, error)) { return false; } - - // Read the actual response - res.headers.clear(); - res.body.clear(); - if (!read_response_line(strm, req, res) || - !detail::read_headers(strm, res.headers)) { - error = Error::Read; - output_error_log(error, &req); - return false; - } - } - // If not 100 Continue, server returned an error; proceed with that response - } - - // Body - if ((res.status != StatusCode::NoContent_204) && req.method != "HEAD" && - req.method != "CONNECT") { - auto redirect = 300 < res.status && res.status < 400 && - res.status != StatusCode::NotModified_304 && - follow_location_; - - if (req.response_handler && !redirect) { - if (!req.response_handler(res)) { - error = Error::Canceled; - output_error_log(error, &req); - return false; - } - } - - auto out = - req.content_receiver - ? static_cast( - [&](const char *buf, size_t n, size_t off, size_t len) { - if (redirect) { return true; } - auto ret = req.content_receiver(buf, n, off, len); - if (!ret) { - error = Error::Canceled; - output_error_log(error, &req); - } - return ret; - }) - : static_cast( - [&](const char *buf, size_t n, size_t /*off*/, - size_t /*len*/) { - assert(res.body.size() + n <= res.body.max_size()); - if (payload_max_length_ > 0 && - (res.body.size() >= payload_max_length_ || - n > payload_max_length_ - res.body.size())) { - return false; - } - res.body.append(buf, n); - return true; - }); - - auto progress = [&](size_t current, size_t total) { - if (!req.download_progress || redirect) { return true; } - auto ret = req.download_progress(current, total); - if (!ret) { - error = Error::Canceled; - output_error_log(error, &req); - } - return ret; - }; - - if (res.has_header("Content-Length")) { - if (!req.content_receiver) { - auto len = res.get_header_value_u64("Content-Length"); - if (len > res.body.max_size()) { - error = Error::Read; - output_error_log(error, &req); - return false; - } - // Cap the reservation by payload_max_length_ to avoid OOM when a - // hostile or malformed server sends an enormous Content-Length. - // The actual body read below is bounded by payload_max_length_, - // so reserving more than that is never useful. - auto reserve_len = static_cast(len); - if (payload_max_length_ > 0 && reserve_len > payload_max_length_) { - reserve_len = payload_max_length_; - } - res.body.reserve(reserve_len); - } - } - - if (res.status != StatusCode::NotModified_304) { - int dummy_status; - auto max_length = (!has_payload_max_length_ && req.content_receiver) - ? (std::numeric_limits::max)() - : payload_max_length_; - if (!detail::read_content(strm, res, max_length, dummy_status, - std::move(progress), std::move(out), - decompress_)) { - if (error != Error::Canceled) { error = Error::Read; } - output_error_log(error, &req); - return false; - } - } - } - - // Log - output_log(req, res); - - return true; -} - -ContentProviderWithoutLength ClientImpl::get_multipart_content_provider( - const std::string &boundary, const UploadFormDataItems &items, - const FormDataProviderItems &provider_items) const { - size_t cur_item = 0; - size_t cur_start = 0; - // cur_item and cur_start are copied to within the std::function and - // maintain state between successive calls - return [&, cur_item, cur_start](size_t offset, - DataSink &sink) mutable -> bool { - if (!offset && !items.empty()) { - sink.os << detail::serialize_multipart_formdata(items, boundary, false); - return true; - } else if (cur_item < provider_items.size()) { - if (!cur_start) { - const auto &begin = detail::serialize_multipart_formdata_item_begin( - provider_items[cur_item], boundary); - offset += begin.size(); - cur_start = offset; - sink.os << begin; - } - - DataSink cur_sink; - auto has_data = true; - cur_sink.write = sink.write; - cur_sink.done = [&]() { has_data = false; }; - - if (!provider_items[cur_item].provider(offset - cur_start, cur_sink)) { - return false; - } - - if (!has_data) { - sink.os << detail::serialize_multipart_formdata_item_end(); - cur_item++; - cur_start = 0; - } - return true; - } else { - sink.os << detail::serialize_multipart_formdata_finish(boundary); - sink.done(); - return true; - } - }; -} - -bool ClientImpl::process_socket( - const Socket &socket, - std::chrono::time_point start_time, - std::function callback) { - return detail::process_client_socket( - socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, - write_timeout_usec_, max_timeout_msec_, start_time, std::move(callback)); -} - -bool ClientImpl::is_ssl() const { return false; } - -Result ClientImpl::Get(const std::string &path, - DownloadProgress progress) { - return Get(path, Headers(), std::move(progress)); -} - -Result ClientImpl::Get(const std::string &path, const Params ¶ms, - const Headers &headers, - DownloadProgress progress) { - if (params.empty()) { return Get(path, headers); } - - std::string path_with_query = append_query_params(path, params); - return Get(path_with_query, headers, std::move(progress)); -} - -Result ClientImpl::Get(const std::string &path, const Headers &headers, - DownloadProgress progress) { - Request req; - req.method = "GET"; - req.path = path; - req.headers = headers; - req.download_progress = std::move(progress); - if (max_timeout_msec_ > 0) { - req.start_time_ = std::chrono::steady_clock::now(); - } - - return send_(std::move(req)); -} - -Result ClientImpl::Get(const std::string &path, - ContentReceiver content_receiver, - DownloadProgress progress) { - return Get(path, Headers(), nullptr, std::move(content_receiver), - std::move(progress)); -} - -Result ClientImpl::Get(const std::string &path, const Headers &headers, - ContentReceiver content_receiver, - DownloadProgress progress) { - return Get(path, headers, nullptr, std::move(content_receiver), - std::move(progress)); -} - -Result ClientImpl::Get(const std::string &path, - ResponseHandler response_handler, - ContentReceiver content_receiver, - DownloadProgress progress) { - return Get(path, Headers(), std::move(response_handler), - std::move(content_receiver), std::move(progress)); -} - -Result ClientImpl::Get(const std::string &path, const Headers &headers, - ResponseHandler response_handler, - ContentReceiver content_receiver, - DownloadProgress progress) { - Request req; - req.method = "GET"; - req.path = path; - req.headers = headers; - req.response_handler = std::move(response_handler); - req.content_receiver = - [content_receiver](const char *data, size_t data_length, - size_t /*offset*/, size_t /*total_length*/) { - return content_receiver(data, data_length); - }; - req.download_progress = std::move(progress); - if (max_timeout_msec_ > 0) { - req.start_time_ = std::chrono::steady_clock::now(); - } - - return send_(std::move(req)); -} - -Result ClientImpl::Get(const std::string &path, const Params ¶ms, - const Headers &headers, - ContentReceiver content_receiver, - DownloadProgress progress) { - return Get(path, params, headers, nullptr, std::move(content_receiver), - std::move(progress)); -} - -Result ClientImpl::Get(const std::string &path, const Params ¶ms, - const Headers &headers, - ResponseHandler response_handler, - ContentReceiver content_receiver, - DownloadProgress progress) { - if (params.empty()) { - return Get(path, headers, std::move(response_handler), - std::move(content_receiver), std::move(progress)); - } - - std::string path_with_query = append_query_params(path, params); - return Get(path_with_query, headers, std::move(response_handler), - std::move(content_receiver), std::move(progress)); -} - -Result ClientImpl::Head(const std::string &path) { - return Head(path, Headers()); -} - -Result ClientImpl::Head(const std::string &path, - const Headers &headers) { - Request req; - req.method = "HEAD"; - req.headers = headers; - req.path = path; - if (max_timeout_msec_ > 0) { - req.start_time_ = std::chrono::steady_clock::now(); - } - - return send_(std::move(req)); -} - -Result ClientImpl::Post(const std::string &path) { - return Post(path, std::string(), std::string()); -} - -Result ClientImpl::Post(const std::string &path, - const Headers &headers) { - return Post(path, headers, nullptr, 0, std::string()); -} - -Result ClientImpl::Post(const std::string &path, const char *body, - size_t content_length, - const std::string &content_type, - UploadProgress progress) { - return Post(path, Headers(), body, content_length, content_type, progress); -} - -Result ClientImpl::Post(const std::string &path, const std::string &body, - const std::string &content_type, - UploadProgress progress) { - return Post(path, Headers(), body, content_type, progress); -} - -Result ClientImpl::Post(const std::string &path, const Params ¶ms) { - return Post(path, Headers(), params); -} - -Result ClientImpl::Post(const std::string &path, size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - UploadProgress progress) { - return Post(path, Headers(), content_length, std::move(content_provider), - content_type, progress); -} - -Result ClientImpl::Post(const std::string &path, size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - UploadProgress progress) { - return Post(path, Headers(), content_length, std::move(content_provider), - content_type, std::move(content_receiver), progress); -} - -Result ClientImpl::Post(const std::string &path, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - UploadProgress progress) { - return Post(path, Headers(), std::move(content_provider), content_type, - progress); -} - -Result ClientImpl::Post(const std::string &path, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - UploadProgress progress) { - return Post(path, Headers(), std::move(content_provider), content_type, - std::move(content_receiver), progress); -} - -Result ClientImpl::Post(const std::string &path, const Headers &headers, - const Params ¶ms) { - auto query = detail::params_to_query_str(params); - return Post(path, headers, query, "application/x-www-form-urlencoded"); -} - -Result ClientImpl::Post(const std::string &path, - const UploadFormDataItems &items, - UploadProgress progress) { - return Post(path, Headers(), items, progress); -} - -Result ClientImpl::Post(const std::string &path, const Headers &headers, - const UploadFormDataItems &items, - UploadProgress progress) { - const auto &boundary = detail::make_multipart_data_boundary(); - const auto &content_type = - detail::serialize_multipart_formdata_get_content_type(boundary); - auto content_length = detail::get_multipart_content_length(items, boundary); - return Post(path, headers, content_length, - detail::make_multipart_content_provider(items, boundary), - content_type, progress); -} - -Result ClientImpl::Post(const std::string &path, const Headers &headers, - const UploadFormDataItems &items, - const std::string &boundary, - UploadProgress progress) { - if (!detail::is_multipart_boundary_chars_valid(boundary)) { - return Result{nullptr, Error::UnsupportedMultipartBoundaryChars}; - } - - const auto &content_type = - detail::serialize_multipart_formdata_get_content_type(boundary); - auto content_length = detail::get_multipart_content_length(items, boundary); - return Post(path, headers, content_length, - detail::make_multipart_content_provider(items, boundary), - content_type, progress); -} - -Result ClientImpl::Post(const std::string &path, const Headers &headers, - const char *body, size_t content_length, - const std::string &content_type, - UploadProgress progress) { - return send_with_content_provider_and_receiver( - "POST", path, headers, body, content_length, nullptr, nullptr, - content_type, nullptr, progress); -} - -Result ClientImpl::Post(const std::string &path, const Headers &headers, - const std::string &body, - const std::string &content_type, - UploadProgress progress) { - return send_with_content_provider_and_receiver( - "POST", path, headers, body.data(), body.size(), nullptr, nullptr, - content_type, nullptr, progress); -} - -Result ClientImpl::Post(const std::string &path, const Headers &headers, - size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - UploadProgress progress) { - return send_with_content_provider_and_receiver( - "POST", path, headers, nullptr, content_length, - std::move(content_provider), nullptr, content_type, nullptr, progress); -} - -Result ClientImpl::Post(const std::string &path, const Headers &headers, - size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - DownloadProgress progress) { - return send_with_content_provider_and_receiver( - "POST", path, headers, nullptr, content_length, - std::move(content_provider), nullptr, content_type, - std::move(content_receiver), std::move(progress)); -} - -Result ClientImpl::Post(const std::string &path, const Headers &headers, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - UploadProgress progress) { - return send_with_content_provider_and_receiver( - "POST", path, headers, nullptr, 0, nullptr, std::move(content_provider), - content_type, nullptr, progress); -} - -Result ClientImpl::Post(const std::string &path, const Headers &headers, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - DownloadProgress progress) { - return send_with_content_provider_and_receiver( - "POST", path, headers, nullptr, 0, nullptr, std::move(content_provider), - content_type, std::move(content_receiver), std::move(progress)); -} - -Result ClientImpl::Post(const std::string &path, const Headers &headers, - const UploadFormDataItems &items, - const FormDataProviderItems &provider_items, - UploadProgress progress) { - const auto &boundary = detail::make_multipart_data_boundary(); - const auto &content_type = - detail::serialize_multipart_formdata_get_content_type(boundary); - return send_with_content_provider_and_receiver( - "POST", path, headers, nullptr, 0, nullptr, - get_multipart_content_provider(boundary, items, provider_items), - content_type, nullptr, progress); -} - -Result ClientImpl::Post(const std::string &path, const Headers &headers, - const std::string &body, - const std::string &content_type, - ContentReceiver content_receiver, - DownloadProgress progress) { - Request req; - req.method = "POST"; - req.path = path; - req.headers = headers; - req.body = body; - req.content_receiver = - [content_receiver](const char *data, size_t data_length, - size_t /*offset*/, size_t /*total_length*/) { - return content_receiver(data, data_length); - }; - req.download_progress = std::move(progress); - - if (max_timeout_msec_ > 0) { - req.start_time_ = std::chrono::steady_clock::now(); - } - - if (!content_type.empty()) { req.set_header("Content-Type", content_type); } - - return send_(std::move(req)); -} - -Result ClientImpl::Put(const std::string &path) { - return Put(path, std::string(), std::string()); -} - -Result ClientImpl::Put(const std::string &path, const Headers &headers) { - return Put(path, headers, nullptr, 0, std::string()); -} - -Result ClientImpl::Put(const std::string &path, const char *body, - size_t content_length, - const std::string &content_type, - UploadProgress progress) { - return Put(path, Headers(), body, content_length, content_type, progress); -} - -Result ClientImpl::Put(const std::string &path, const std::string &body, - const std::string &content_type, - UploadProgress progress) { - return Put(path, Headers(), body, content_type, progress); -} - -Result ClientImpl::Put(const std::string &path, const Params ¶ms) { - return Put(path, Headers(), params); -} - -Result ClientImpl::Put(const std::string &path, size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - UploadProgress progress) { - return Put(path, Headers(), content_length, std::move(content_provider), - content_type, progress); -} - -Result ClientImpl::Put(const std::string &path, size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - UploadProgress progress) { - return Put(path, Headers(), content_length, std::move(content_provider), - content_type, std::move(content_receiver), progress); -} - -Result ClientImpl::Put(const std::string &path, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - UploadProgress progress) { - return Put(path, Headers(), std::move(content_provider), content_type, - progress); -} - -Result ClientImpl::Put(const std::string &path, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - UploadProgress progress) { - return Put(path, Headers(), std::move(content_provider), content_type, - std::move(content_receiver), progress); -} - -Result ClientImpl::Put(const std::string &path, const Headers &headers, - const Params ¶ms) { - auto query = detail::params_to_query_str(params); - return Put(path, headers, query, "application/x-www-form-urlencoded"); -} - -Result ClientImpl::Put(const std::string &path, - const UploadFormDataItems &items, - UploadProgress progress) { - return Put(path, Headers(), items, progress); -} - -Result ClientImpl::Put(const std::string &path, const Headers &headers, - const UploadFormDataItems &items, - UploadProgress progress) { - const auto &boundary = detail::make_multipart_data_boundary(); - const auto &content_type = - detail::serialize_multipart_formdata_get_content_type(boundary); - auto content_length = detail::get_multipart_content_length(items, boundary); - return Put(path, headers, content_length, - detail::make_multipart_content_provider(items, boundary), - content_type, progress); -} - -Result ClientImpl::Put(const std::string &path, const Headers &headers, - const UploadFormDataItems &items, - const std::string &boundary, - UploadProgress progress) { - if (!detail::is_multipart_boundary_chars_valid(boundary)) { - return Result{nullptr, Error::UnsupportedMultipartBoundaryChars}; - } - - const auto &content_type = - detail::serialize_multipart_formdata_get_content_type(boundary); - auto content_length = detail::get_multipart_content_length(items, boundary); - return Put(path, headers, content_length, - detail::make_multipart_content_provider(items, boundary), - content_type, progress); -} - -Result ClientImpl::Put(const std::string &path, const Headers &headers, - const char *body, size_t content_length, - const std::string &content_type, - UploadProgress progress) { - return send_with_content_provider_and_receiver( - "PUT", path, headers, body, content_length, nullptr, nullptr, - content_type, nullptr, progress); -} - -Result ClientImpl::Put(const std::string &path, const Headers &headers, - const std::string &body, - const std::string &content_type, - UploadProgress progress) { - return send_with_content_provider_and_receiver( - "PUT", path, headers, body.data(), body.size(), nullptr, nullptr, - content_type, nullptr, progress); -} - -Result ClientImpl::Put(const std::string &path, const Headers &headers, - size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - UploadProgress progress) { - return send_with_content_provider_and_receiver( - "PUT", path, headers, nullptr, content_length, - std::move(content_provider), nullptr, content_type, nullptr, progress); -} - -Result ClientImpl::Put(const std::string &path, const Headers &headers, - size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - UploadProgress progress) { - return send_with_content_provider_and_receiver( - "PUT", path, headers, nullptr, content_length, - std::move(content_provider), nullptr, content_type, - std::move(content_receiver), progress); -} - -Result ClientImpl::Put(const std::string &path, const Headers &headers, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - UploadProgress progress) { - return send_with_content_provider_and_receiver( - "PUT", path, headers, nullptr, 0, nullptr, std::move(content_provider), - content_type, nullptr, progress); -} - -Result ClientImpl::Put(const std::string &path, const Headers &headers, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - UploadProgress progress) { - return send_with_content_provider_and_receiver( - "PUT", path, headers, nullptr, 0, nullptr, std::move(content_provider), - content_type, std::move(content_receiver), progress); -} - -Result ClientImpl::Put(const std::string &path, const Headers &headers, - const UploadFormDataItems &items, - const FormDataProviderItems &provider_items, - UploadProgress progress) { - const auto &boundary = detail::make_multipart_data_boundary(); - const auto &content_type = - detail::serialize_multipart_formdata_get_content_type(boundary); - return send_with_content_provider_and_receiver( - "PUT", path, headers, nullptr, 0, nullptr, - get_multipart_content_provider(boundary, items, provider_items), - content_type, nullptr, progress); -} - -Result ClientImpl::Put(const std::string &path, const Headers &headers, - const std::string &body, - const std::string &content_type, - ContentReceiver content_receiver, - DownloadProgress progress) { - Request req; - req.method = "PUT"; - req.path = path; - req.headers = headers; - req.body = body; - req.content_receiver = - [content_receiver](const char *data, size_t data_length, - size_t /*offset*/, size_t /*total_length*/) { - return content_receiver(data, data_length); - }; - req.download_progress = std::move(progress); - - if (max_timeout_msec_ > 0) { - req.start_time_ = std::chrono::steady_clock::now(); - } - - if (!content_type.empty()) { req.set_header("Content-Type", content_type); } - - return send_(std::move(req)); -} - -Result ClientImpl::Patch(const std::string &path) { - return Patch(path, std::string(), std::string()); -} - -Result ClientImpl::Patch(const std::string &path, const Headers &headers, - UploadProgress progress) { - return Patch(path, headers, nullptr, 0, std::string(), progress); -} - -Result ClientImpl::Patch(const std::string &path, const char *body, - size_t content_length, - const std::string &content_type, - UploadProgress progress) { - return Patch(path, Headers(), body, content_length, content_type, progress); -} - -Result ClientImpl::Patch(const std::string &path, - const std::string &body, - const std::string &content_type, - UploadProgress progress) { - return Patch(path, Headers(), body, content_type, progress); -} - -Result ClientImpl::Patch(const std::string &path, const Params ¶ms) { - return Patch(path, Headers(), params); -} - -Result ClientImpl::Patch(const std::string &path, size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - UploadProgress progress) { - return Patch(path, Headers(), content_length, std::move(content_provider), - content_type, progress); -} - -Result ClientImpl::Patch(const std::string &path, size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - UploadProgress progress) { - return Patch(path, Headers(), content_length, std::move(content_provider), - content_type, std::move(content_receiver), progress); -} - -Result ClientImpl::Patch(const std::string &path, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - UploadProgress progress) { - return Patch(path, Headers(), std::move(content_provider), content_type, - progress); -} - -Result ClientImpl::Patch(const std::string &path, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - UploadProgress progress) { - return Patch(path, Headers(), std::move(content_provider), content_type, - std::move(content_receiver), progress); -} - -Result ClientImpl::Patch(const std::string &path, const Headers &headers, - const Params ¶ms) { - auto query = detail::params_to_query_str(params); - return Patch(path, headers, query, "application/x-www-form-urlencoded"); -} - -Result ClientImpl::Patch(const std::string &path, - const UploadFormDataItems &items, - UploadProgress progress) { - return Patch(path, Headers(), items, progress); -} - -Result ClientImpl::Patch(const std::string &path, const Headers &headers, - const UploadFormDataItems &items, - UploadProgress progress) { - const auto &boundary = detail::make_multipart_data_boundary(); - const auto &content_type = - detail::serialize_multipart_formdata_get_content_type(boundary); - auto content_length = detail::get_multipart_content_length(items, boundary); - return Patch(path, headers, content_length, - detail::make_multipart_content_provider(items, boundary), - content_type, progress); -} - -Result ClientImpl::Patch(const std::string &path, const Headers &headers, - const UploadFormDataItems &items, - const std::string &boundary, - UploadProgress progress) { - if (!detail::is_multipart_boundary_chars_valid(boundary)) { - return Result{nullptr, Error::UnsupportedMultipartBoundaryChars}; - } - - const auto &content_type = - detail::serialize_multipart_formdata_get_content_type(boundary); - auto content_length = detail::get_multipart_content_length(items, boundary); - return Patch(path, headers, content_length, - detail::make_multipart_content_provider(items, boundary), - content_type, progress); -} - -Result ClientImpl::Patch(const std::string &path, const Headers &headers, - const char *body, size_t content_length, - const std::string &content_type, - UploadProgress progress) { - return send_with_content_provider_and_receiver( - "PATCH", path, headers, body, content_length, nullptr, nullptr, - content_type, nullptr, progress); -} - -Result ClientImpl::Patch(const std::string &path, const Headers &headers, - const std::string &body, - const std::string &content_type, - UploadProgress progress) { - return send_with_content_provider_and_receiver( - "PATCH", path, headers, body.data(), body.size(), nullptr, nullptr, - content_type, nullptr, progress); -} - -Result ClientImpl::Patch(const std::string &path, const Headers &headers, - size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - UploadProgress progress) { - return send_with_content_provider_and_receiver( - "PATCH", path, headers, nullptr, content_length, - std::move(content_provider), nullptr, content_type, nullptr, progress); -} - -Result ClientImpl::Patch(const std::string &path, const Headers &headers, - size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - UploadProgress progress) { - return send_with_content_provider_and_receiver( - "PATCH", path, headers, nullptr, content_length, - std::move(content_provider), nullptr, content_type, - std::move(content_receiver), progress); -} - -Result ClientImpl::Patch(const std::string &path, const Headers &headers, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - UploadProgress progress) { - return send_with_content_provider_and_receiver( - "PATCH", path, headers, nullptr, 0, nullptr, std::move(content_provider), - content_type, nullptr, progress); -} - -Result ClientImpl::Patch(const std::string &path, const Headers &headers, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - UploadProgress progress) { - return send_with_content_provider_and_receiver( - "PATCH", path, headers, nullptr, 0, nullptr, std::move(content_provider), - content_type, std::move(content_receiver), progress); -} - -Result ClientImpl::Patch(const std::string &path, const Headers &headers, - const UploadFormDataItems &items, - const FormDataProviderItems &provider_items, - UploadProgress progress) { - const auto &boundary = detail::make_multipart_data_boundary(); - const auto &content_type = - detail::serialize_multipart_formdata_get_content_type(boundary); - return send_with_content_provider_and_receiver( - "PATCH", path, headers, nullptr, 0, nullptr, - get_multipart_content_provider(boundary, items, provider_items), - content_type, nullptr, progress); -} - -Result ClientImpl::Patch(const std::string &path, const Headers &headers, - const std::string &body, - const std::string &content_type, - ContentReceiver content_receiver, - DownloadProgress progress) { - Request req; - req.method = "PATCH"; - req.path = path; - req.headers = headers; - req.body = body; - req.content_receiver = - [content_receiver](const char *data, size_t data_length, - size_t /*offset*/, size_t /*total_length*/) { - return content_receiver(data, data_length); - }; - req.download_progress = std::move(progress); - - if (max_timeout_msec_ > 0) { - req.start_time_ = std::chrono::steady_clock::now(); - } - - if (!content_type.empty()) { req.set_header("Content-Type", content_type); } - - return send_(std::move(req)); -} - -Result ClientImpl::Delete(const std::string &path, - DownloadProgress progress) { - return Delete(path, Headers(), std::string(), std::string(), progress); -} - -Result ClientImpl::Delete(const std::string &path, - const Headers &headers, - DownloadProgress progress) { - return Delete(path, headers, std::string(), std::string(), progress); -} - -Result ClientImpl::Delete(const std::string &path, const char *body, - size_t content_length, - const std::string &content_type, - DownloadProgress progress) { - return Delete(path, Headers(), body, content_length, content_type, progress); -} - -Result ClientImpl::Delete(const std::string &path, - const std::string &body, - const std::string &content_type, - DownloadProgress progress) { - return Delete(path, Headers(), body.data(), body.size(), content_type, - progress); -} - -Result ClientImpl::Delete(const std::string &path, - const Headers &headers, - const std::string &body, - const std::string &content_type, - DownloadProgress progress) { - return Delete(path, headers, body.data(), body.size(), content_type, - progress); -} - -Result ClientImpl::Delete(const std::string &path, const Params ¶ms, - DownloadProgress progress) { - return Delete(path, Headers(), params, progress); -} - -Result ClientImpl::Delete(const std::string &path, - const Headers &headers, const Params ¶ms, - DownloadProgress progress) { - auto query = detail::params_to_query_str(params); - return Delete(path, headers, query, "application/x-www-form-urlencoded", - progress); -} - -Result ClientImpl::Delete(const std::string &path, - const Headers &headers, const char *body, - size_t content_length, - const std::string &content_type, - DownloadProgress progress) { - Request req; - req.method = "DELETE"; - req.headers = headers; - req.path = path; - req.download_progress = std::move(progress); - if (max_timeout_msec_ > 0) { - req.start_time_ = std::chrono::steady_clock::now(); - } - - if (!content_type.empty()) { req.set_header("Content-Type", content_type); } - req.body.assign(body, content_length); - - return send_(std::move(req)); -} - -Result ClientImpl::Options(const std::string &path) { - return Options(path, Headers()); -} - -Result ClientImpl::Options(const std::string &path, - const Headers &headers) { - Request req; - req.method = "OPTIONS"; - req.headers = headers; - req.path = path; - if (max_timeout_msec_ > 0) { - req.start_time_ = std::chrono::steady_clock::now(); - } - - return send_(std::move(req)); -} - -void ClientImpl::stop() { - std::lock_guard guard(socket_mutex_); - - // If there is anything ongoing right now, the ONLY thread-safe thing we can - // do is to shutdown_socket, so that threads using this socket suddenly - // discover they can't read/write any more and error out. Everything else - // (closing the socket, shutting ssl down) is unsafe because these actions - // are not thread-safe. - if (socket_requests_in_flight_ > 0) { - shutdown_socket(socket_); - - // Aside from that, we set a flag for the socket to be closed when we're - // done. - socket_should_be_closed_when_request_is_done_ = true; - return; - } - - disconnect(/*gracefully=*/true); -} - -std::string ClientImpl::host() const { return host_; } - -int ClientImpl::port() const { return port_; } - -size_t ClientImpl::is_socket_open() const { - std::lock_guard guard(socket_mutex_); - return socket_.is_open(); -} - -socket_t ClientImpl::socket() const { return socket_.sock; } - -void ClientImpl::set_connection_timeout(time_t sec, time_t usec) { - connection_timeout_sec_ = sec; - connection_timeout_usec_ = usec; -} - -void ClientImpl::set_read_timeout(time_t sec, time_t usec) { - read_timeout_sec_ = sec; - read_timeout_usec_ = usec; -} - -void ClientImpl::set_write_timeout(time_t sec, time_t usec) { - write_timeout_sec_ = sec; - write_timeout_usec_ = usec; -} - -void ClientImpl::set_max_timeout(time_t msec) { - max_timeout_msec_ = msec; -} - -void ClientImpl::set_basic_auth(const std::string &username, - const std::string &password) { - basic_auth_username_ = username; - basic_auth_password_ = password; -} - -void ClientImpl::set_bearer_token_auth(const std::string &token) { - bearer_token_auth_token_ = token; -} - -void ClientImpl::set_keep_alive(bool on) { keep_alive_ = on; } - -void ClientImpl::set_follow_location(bool on) { follow_location_ = on; } - -void ClientImpl::set_path_encode(bool on) { path_encode_ = on; } - -void -ClientImpl::set_hostname_addr_map(std::map addr_map) { - addr_map_ = std::move(addr_map); -} - -void ClientImpl::set_default_headers(Headers headers) { - default_headers_ = std::move(headers); -} - -void ClientImpl::set_header_writer( - std::function const &writer) { - header_writer_ = writer; -} - -void ClientImpl::set_address_family(int family) { - address_family_ = family; -} - -void ClientImpl::set_tcp_nodelay(bool on) { tcp_nodelay_ = on; } - -void ClientImpl::set_ipv6_v6only(bool on) { ipv6_v6only_ = on; } - -void ClientImpl::set_socket_options(SocketOptions socket_options) { - socket_options_ = std::move(socket_options); -} - -void ClientImpl::set_compress(bool on) { compress_ = on; } - -void ClientImpl::set_decompress(bool on) { decompress_ = on; } - -void ClientImpl::set_payload_max_length(size_t length) { - payload_max_length_ = length; - has_payload_max_length_ = true; -} - -void ClientImpl::set_interface(const std::string &intf) { - interface_ = intf; -} - -void ClientImpl::set_proxy(const std::string &host, int port) { - proxy_host_ = host; - proxy_port_ = port; - std::lock_guard guard(socket_mutex_); - disconnect(/*gracefully=*/true); -} - -void ClientImpl::set_proxy_basic_auth(const std::string &username, - const std::string &password) { - proxy_basic_auth_username_ = username; - proxy_basic_auth_password_ = password; -} - -void ClientImpl::set_proxy_bearer_token_auth(const std::string &token) { - proxy_bearer_token_auth_token_ = token; -} - -void ClientImpl::set_no_proxy(const std::vector &patterns) { - std::vector parsed; - parsed.reserve(patterns.size()); - for (const auto &p : patterns) { - auto trimmed = detail::trim_copy(p); - if (trimmed.empty()) { continue; } - detail::NoProxyEntry entry; - if (detail::parse_no_proxy_entry(trimmed, entry)) { - parsed.push_back(std::move(entry)); - } - } - no_proxy_entries_ = std::move(parsed); - std::lock_guard guard(socket_mutex_); - disconnect(/*gracefully=*/true); -} - -#ifdef CPPHTTPLIB_SSL_ENABLED -void ClientImpl::set_digest_auth(const std::string &username, - const std::string &password) { - digest_auth_username_ = username; - digest_auth_password_ = password; -} - -void ClientImpl::set_ca_cert_path(const std::string &ca_cert_file_path, - const std::string &ca_cert_dir_path) { - ca_cert_file_path_ = ca_cert_file_path; - ca_cert_dir_path_ = ca_cert_dir_path; -} - -void ClientImpl::set_proxy_digest_auth(const std::string &username, - const std::string &password) { - proxy_digest_auth_username_ = username; - proxy_digest_auth_password_ = password; -} - -void ClientImpl::enable_server_certificate_verification(bool enabled) { - server_certificate_verification_ = enabled; -} - -void ClientImpl::enable_server_hostname_verification(bool enabled) { - server_hostname_verification_ = enabled; -} -#endif - -void ClientImpl::set_logger(Logger logger) { - logger_ = std::move(logger); -} - -void ClientImpl::set_error_logger(ErrorLogger error_logger) { - error_logger_ = std::move(error_logger); -} - -/* - * SSL/TLS Common Implementation - */ - -ClientConnection::~ClientConnection() { -#ifdef CPPHTTPLIB_SSL_ENABLED - if (session) { - tls::shutdown(session, true); - tls::free_session(session); - session = nullptr; - } -#endif - - if (sock != INVALID_SOCKET) { - detail::close_socket(sock); - sock = INVALID_SOCKET; - } -} - -// Universal client implementation -Client::Client(const std::string &scheme_host_port) - : Client(scheme_host_port, std::string(), std::string()) {} - -Client::Client(const std::string &scheme_host_port, - const std::string &client_cert_path, - const std::string &client_key_path) { - detail::UrlComponents uc; - if (detail::parse_url(scheme_host_port, uc) && !uc.host.empty()) { - auto &scheme = uc.scheme; - -#ifdef CPPHTTPLIB_SSL_ENABLED - if (!scheme.empty() && (scheme != "http" && scheme != "https")) { -#else - if (!scheme.empty() && scheme != "http") { -#endif -#ifndef CPPHTTPLIB_NO_EXCEPTIONS - std::string msg = "'" + scheme + "' scheme is not supported."; - throw std::invalid_argument(msg); -#endif - return; - } - - auto is_ssl = scheme == "https"; - - auto host = std::move(uc.host); - - auto port = is_ssl ? 443 : 80; - if (!uc.port.empty() && !detail::parse_port(uc.port, port)) { return; } - - if (is_ssl) { -#ifdef CPPHTTPLIB_SSL_ENABLED - cli_ = detail::make_unique(host, port, client_cert_path, - client_key_path); - is_ssl_ = is_ssl; -#endif - } else { - cli_ = detail::make_unique(host, port, client_cert_path, - client_key_path); - } - } else { - // NOTE: Update TEST(UniversalClientImplTest, Ipv6LiteralAddress) - // if port param below changes. - cli_ = detail::make_unique(scheme_host_port, 80, - client_cert_path, client_key_path); - } -} - -Client::Client(const std::string &host, int port) - : Client(host, port, std::string(), std::string()) {} - -Client::Client(const std::string &host, int port, - const std::string &client_cert_path, - const std::string &client_key_path) - : cli_(detail::make_unique(host, port, client_cert_path, - client_key_path)) {} - -Client::~Client() = default; - -bool Client::is_valid() const { - return cli_ != nullptr && cli_->is_valid(); -} - -Result Client::Get(const std::string &path, DownloadProgress progress) { - return cli_->Get(path, std::move(progress)); -} -Result Client::Get(const std::string &path, const Headers &headers, - DownloadProgress progress) { - return cli_->Get(path, headers, std::move(progress)); -} -Result Client::Get(const std::string &path, - ContentReceiver content_receiver, - DownloadProgress progress) { - return cli_->Get(path, std::move(content_receiver), std::move(progress)); -} -Result Client::Get(const std::string &path, const Headers &headers, - ContentReceiver content_receiver, - DownloadProgress progress) { - return cli_->Get(path, headers, std::move(content_receiver), - std::move(progress)); -} -Result Client::Get(const std::string &path, - ResponseHandler response_handler, - ContentReceiver content_receiver, - DownloadProgress progress) { - return cli_->Get(path, std::move(response_handler), - std::move(content_receiver), std::move(progress)); -} -Result Client::Get(const std::string &path, const Headers &headers, - ResponseHandler response_handler, - ContentReceiver content_receiver, - DownloadProgress progress) { - return cli_->Get(path, headers, std::move(response_handler), - std::move(content_receiver), std::move(progress)); -} -Result Client::Get(const std::string &path, const Params ¶ms, - const Headers &headers, DownloadProgress progress) { - return cli_->Get(path, params, headers, std::move(progress)); -} -Result Client::Get(const std::string &path, const Params ¶ms, - const Headers &headers, - ContentReceiver content_receiver, - DownloadProgress progress) { - return cli_->Get(path, params, headers, std::move(content_receiver), - std::move(progress)); -} -Result Client::Get(const std::string &path, const Params ¶ms, - const Headers &headers, - ResponseHandler response_handler, - ContentReceiver content_receiver, - DownloadProgress progress) { - return cli_->Get(path, params, headers, std::move(response_handler), - std::move(content_receiver), std::move(progress)); -} - -Result Client::Head(const std::string &path) { return cli_->Head(path); } -Result Client::Head(const std::string &path, const Headers &headers) { - return cli_->Head(path, headers); -} - -Result Client::Post(const std::string &path) { return cli_->Post(path); } -Result Client::Post(const std::string &path, const Headers &headers) { - return cli_->Post(path, headers); -} -Result Client::Post(const std::string &path, const char *body, - size_t content_length, - const std::string &content_type, - UploadProgress progress) { - return cli_->Post(path, body, content_length, content_type, progress); -} -Result Client::Post(const std::string &path, const Headers &headers, - const char *body, size_t content_length, - const std::string &content_type, - UploadProgress progress) { - return cli_->Post(path, headers, body, content_length, content_type, - progress); -} -Result Client::Post(const std::string &path, const std::string &body, - const std::string &content_type, - UploadProgress progress) { - return cli_->Post(path, body, content_type, progress); -} -Result Client::Post(const std::string &path, const Headers &headers, - const std::string &body, - const std::string &content_type, - UploadProgress progress) { - return cli_->Post(path, headers, body, content_type, progress); -} -Result Client::Post(const std::string &path, size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - UploadProgress progress) { - return cli_->Post(path, content_length, std::move(content_provider), - content_type, progress); -} -Result Client::Post(const std::string &path, size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - UploadProgress progress) { - return cli_->Post(path, content_length, std::move(content_provider), - content_type, std::move(content_receiver), progress); -} -Result Client::Post(const std::string &path, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - UploadProgress progress) { - return cli_->Post(path, std::move(content_provider), content_type, progress); -} -Result Client::Post(const std::string &path, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - UploadProgress progress) { - return cli_->Post(path, std::move(content_provider), content_type, - std::move(content_receiver), progress); -} -Result Client::Post(const std::string &path, const Headers &headers, - size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - UploadProgress progress) { - return cli_->Post(path, headers, content_length, std::move(content_provider), - content_type, progress); -} -Result Client::Post(const std::string &path, const Headers &headers, - size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - DownloadProgress progress) { - return cli_->Post(path, headers, content_length, std::move(content_provider), - content_type, std::move(content_receiver), progress); -} -Result Client::Post(const std::string &path, const Headers &headers, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - UploadProgress progress) { - return cli_->Post(path, headers, std::move(content_provider), content_type, - progress); -} -Result Client::Post(const std::string &path, const Headers &headers, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - DownloadProgress progress) { - return cli_->Post(path, headers, std::move(content_provider), content_type, - std::move(content_receiver), progress); -} -Result Client::Post(const std::string &path, const Params ¶ms) { - return cli_->Post(path, params); -} -Result Client::Post(const std::string &path, const Headers &headers, - const Params ¶ms) { - return cli_->Post(path, headers, params); -} -Result Client::Post(const std::string &path, - const UploadFormDataItems &items, - UploadProgress progress) { - return cli_->Post(path, items, progress); -} -Result Client::Post(const std::string &path, const Headers &headers, - const UploadFormDataItems &items, - UploadProgress progress) { - return cli_->Post(path, headers, items, progress); -} -Result Client::Post(const std::string &path, const Headers &headers, - const UploadFormDataItems &items, - const std::string &boundary, - UploadProgress progress) { - return cli_->Post(path, headers, items, boundary, progress); -} -Result Client::Post(const std::string &path, const Headers &headers, - const UploadFormDataItems &items, - const FormDataProviderItems &provider_items, - UploadProgress progress) { - return cli_->Post(path, headers, items, provider_items, progress); -} -Result Client::Post(const std::string &path, const Headers &headers, - const std::string &body, - const std::string &content_type, - ContentReceiver content_receiver, - DownloadProgress progress) { - return cli_->Post(path, headers, body, content_type, - std::move(content_receiver), progress); -} - -Result Client::Put(const std::string &path) { return cli_->Put(path); } -Result Client::Put(const std::string &path, const Headers &headers) { - return cli_->Put(path, headers); -} -Result Client::Put(const std::string &path, const char *body, - size_t content_length, - const std::string &content_type, - UploadProgress progress) { - return cli_->Put(path, body, content_length, content_type, progress); -} -Result Client::Put(const std::string &path, const Headers &headers, - const char *body, size_t content_length, - const std::string &content_type, - UploadProgress progress) { - return cli_->Put(path, headers, body, content_length, content_type, progress); -} -Result Client::Put(const std::string &path, const std::string &body, - const std::string &content_type, - UploadProgress progress) { - return cli_->Put(path, body, content_type, progress); -} -Result Client::Put(const std::string &path, const Headers &headers, - const std::string &body, - const std::string &content_type, - UploadProgress progress) { - return cli_->Put(path, headers, body, content_type, progress); -} -Result Client::Put(const std::string &path, size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - UploadProgress progress) { - return cli_->Put(path, content_length, std::move(content_provider), - content_type, progress); -} -Result Client::Put(const std::string &path, size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - UploadProgress progress) { - return cli_->Put(path, content_length, std::move(content_provider), - content_type, std::move(content_receiver), progress); -} -Result Client::Put(const std::string &path, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - UploadProgress progress) { - return cli_->Put(path, std::move(content_provider), content_type, progress); -} -Result Client::Put(const std::string &path, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - UploadProgress progress) { - return cli_->Put(path, std::move(content_provider), content_type, - std::move(content_receiver), progress); -} -Result Client::Put(const std::string &path, const Headers &headers, - size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - UploadProgress progress) { - return cli_->Put(path, headers, content_length, std::move(content_provider), - content_type, progress); -} -Result Client::Put(const std::string &path, const Headers &headers, - size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - UploadProgress progress) { - return cli_->Put(path, headers, content_length, std::move(content_provider), - content_type, std::move(content_receiver), progress); -} -Result Client::Put(const std::string &path, const Headers &headers, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - UploadProgress progress) { - return cli_->Put(path, headers, std::move(content_provider), content_type, - progress); -} -Result Client::Put(const std::string &path, const Headers &headers, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - UploadProgress progress) { - return cli_->Put(path, headers, std::move(content_provider), content_type, - std::move(content_receiver), progress); -} -Result Client::Put(const std::string &path, const Params ¶ms) { - return cli_->Put(path, params); -} -Result Client::Put(const std::string &path, const Headers &headers, - const Params ¶ms) { - return cli_->Put(path, headers, params); -} -Result Client::Put(const std::string &path, - const UploadFormDataItems &items, - UploadProgress progress) { - return cli_->Put(path, items, progress); -} -Result Client::Put(const std::string &path, const Headers &headers, - const UploadFormDataItems &items, - UploadProgress progress) { - return cli_->Put(path, headers, items, progress); -} -Result Client::Put(const std::string &path, const Headers &headers, - const UploadFormDataItems &items, - const std::string &boundary, - UploadProgress progress) { - return cli_->Put(path, headers, items, boundary, progress); -} -Result Client::Put(const std::string &path, const Headers &headers, - const UploadFormDataItems &items, - const FormDataProviderItems &provider_items, - UploadProgress progress) { - return cli_->Put(path, headers, items, provider_items, progress); -} -Result Client::Put(const std::string &path, const Headers &headers, - const std::string &body, - const std::string &content_type, - ContentReceiver content_receiver, - DownloadProgress progress) { - return cli_->Put(path, headers, body, content_type, content_receiver, - progress); -} - -Result Client::Patch(const std::string &path) { - return cli_->Patch(path); -} -Result Client::Patch(const std::string &path, const Headers &headers) { - return cli_->Patch(path, headers); -} -Result Client::Patch(const std::string &path, const char *body, - size_t content_length, - const std::string &content_type, - UploadProgress progress) { - return cli_->Patch(path, body, content_length, content_type, progress); -} -Result Client::Patch(const std::string &path, const Headers &headers, - const char *body, size_t content_length, - const std::string &content_type, - UploadProgress progress) { - return cli_->Patch(path, headers, body, content_length, content_type, - progress); -} -Result Client::Patch(const std::string &path, const std::string &body, - const std::string &content_type, - UploadProgress progress) { - return cli_->Patch(path, body, content_type, progress); -} -Result Client::Patch(const std::string &path, const Headers &headers, - const std::string &body, - const std::string &content_type, - UploadProgress progress) { - return cli_->Patch(path, headers, body, content_type, progress); -} -Result Client::Patch(const std::string &path, size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - UploadProgress progress) { - return cli_->Patch(path, content_length, std::move(content_provider), - content_type, progress); -} -Result Client::Patch(const std::string &path, size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - UploadProgress progress) { - return cli_->Patch(path, content_length, std::move(content_provider), - content_type, std::move(content_receiver), progress); -} -Result Client::Patch(const std::string &path, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - UploadProgress progress) { - return cli_->Patch(path, std::move(content_provider), content_type, progress); -} -Result Client::Patch(const std::string &path, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - UploadProgress progress) { - return cli_->Patch(path, std::move(content_provider), content_type, - std::move(content_receiver), progress); -} -Result Client::Patch(const std::string &path, const Headers &headers, - size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - UploadProgress progress) { - return cli_->Patch(path, headers, content_length, std::move(content_provider), - content_type, progress); -} -Result Client::Patch(const std::string &path, const Headers &headers, - size_t content_length, - ContentProvider content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - UploadProgress progress) { - return cli_->Patch(path, headers, content_length, std::move(content_provider), - content_type, std::move(content_receiver), progress); -} -Result Client::Patch(const std::string &path, const Headers &headers, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - UploadProgress progress) { - return cli_->Patch(path, headers, std::move(content_provider), content_type, - progress); -} -Result Client::Patch(const std::string &path, const Headers &headers, - ContentProviderWithoutLength content_provider, - const std::string &content_type, - ContentReceiver content_receiver, - UploadProgress progress) { - return cli_->Patch(path, headers, std::move(content_provider), content_type, - std::move(content_receiver), progress); -} -Result Client::Patch(const std::string &path, const Params ¶ms) { - return cli_->Patch(path, params); -} -Result Client::Patch(const std::string &path, const Headers &headers, - const Params ¶ms) { - return cli_->Patch(path, headers, params); -} -Result Client::Patch(const std::string &path, - const UploadFormDataItems &items, - UploadProgress progress) { - return cli_->Patch(path, items, progress); -} -Result Client::Patch(const std::string &path, const Headers &headers, - const UploadFormDataItems &items, - UploadProgress progress) { - return cli_->Patch(path, headers, items, progress); -} -Result Client::Patch(const std::string &path, const Headers &headers, - const UploadFormDataItems &items, - const std::string &boundary, - UploadProgress progress) { - return cli_->Patch(path, headers, items, boundary, progress); -} -Result Client::Patch(const std::string &path, const Headers &headers, - const UploadFormDataItems &items, - const FormDataProviderItems &provider_items, - UploadProgress progress) { - return cli_->Patch(path, headers, items, provider_items, progress); -} -Result Client::Patch(const std::string &path, const Headers &headers, - const std::string &body, - const std::string &content_type, - ContentReceiver content_receiver, - DownloadProgress progress) { - return cli_->Patch(path, headers, body, content_type, content_receiver, - progress); -} - -Result Client::Delete(const std::string &path, - DownloadProgress progress) { - return cli_->Delete(path, progress); -} -Result Client::Delete(const std::string &path, const Headers &headers, - DownloadProgress progress) { - return cli_->Delete(path, headers, progress); -} -Result Client::Delete(const std::string &path, const char *body, - size_t content_length, - const std::string &content_type, - DownloadProgress progress) { - return cli_->Delete(path, body, content_length, content_type, progress); -} -Result Client::Delete(const std::string &path, const Headers &headers, - const char *body, size_t content_length, - const std::string &content_type, - DownloadProgress progress) { - return cli_->Delete(path, headers, body, content_length, content_type, - progress); -} -Result Client::Delete(const std::string &path, const std::string &body, - const std::string &content_type, - DownloadProgress progress) { - return cli_->Delete(path, body, content_type, progress); -} -Result Client::Delete(const std::string &path, const Headers &headers, - const std::string &body, - const std::string &content_type, - DownloadProgress progress) { - return cli_->Delete(path, headers, body, content_type, progress); -} -Result Client::Delete(const std::string &path, const Params ¶ms, - DownloadProgress progress) { - return cli_->Delete(path, params, progress); -} -Result Client::Delete(const std::string &path, const Headers &headers, - const Params ¶ms, DownloadProgress progress) { - return cli_->Delete(path, headers, params, progress); -} - -Result Client::Options(const std::string &path) { - return cli_->Options(path); -} -Result Client::Options(const std::string &path, const Headers &headers) { - return cli_->Options(path, headers); -} - -ClientImpl::StreamHandle -Client::open_stream(const std::string &method, const std::string &path, - const Params ¶ms, const Headers &headers, - const std::string &body, const std::string &content_type) { - return cli_->open_stream(method, path, params, headers, body, content_type); -} - -bool Client::send(Request &req, Response &res, Error &error) { - return cli_->send(req, res, error); -} - -Result Client::send(const Request &req) { return cli_->send(req); } - -void Client::stop() { cli_->stop(); } - -std::string Client::host() const { return cli_->host(); } - -int Client::port() const { return cli_->port(); } - -size_t Client::is_socket_open() const { return cli_->is_socket_open(); } - -socket_t Client::socket() const { return cli_->socket(); } - -void -Client::set_hostname_addr_map(std::map addr_map) { - cli_->set_hostname_addr_map(std::move(addr_map)); -} - -void Client::set_default_headers(Headers headers) { - cli_->set_default_headers(std::move(headers)); -} - -void Client::set_header_writer( - std::function const &writer) { - cli_->set_header_writer(writer); -} - -void Client::set_address_family(int family) { - cli_->set_address_family(family); -} - -void Client::set_tcp_nodelay(bool on) { cli_->set_tcp_nodelay(on); } - -void Client::set_socket_options(SocketOptions socket_options) { - cli_->set_socket_options(std::move(socket_options)); -} - -void Client::set_connection_timeout(time_t sec, time_t usec) { - cli_->set_connection_timeout(sec, usec); -} - -void Client::set_read_timeout(time_t sec, time_t usec) { - cli_->set_read_timeout(sec, usec); -} - -void Client::set_write_timeout(time_t sec, time_t usec) { - cli_->set_write_timeout(sec, usec); -} - -void Client::set_basic_auth(const std::string &username, - const std::string &password) { - cli_->set_basic_auth(username, password); -} -void Client::set_bearer_token_auth(const std::string &token) { - cli_->set_bearer_token_auth(token); -} - -void Client::set_keep_alive(bool on) { cli_->set_keep_alive(on); } -void Client::set_follow_location(bool on) { - cli_->set_follow_location(on); -} - -void Client::set_path_encode(bool on) { cli_->set_path_encode(on); } - -void Client::set_compress(bool on) { cli_->set_compress(on); } - -void Client::set_decompress(bool on) { cli_->set_decompress(on); } - -void Client::set_payload_max_length(size_t length) { - cli_->set_payload_max_length(length); -} - -void Client::set_interface(const std::string &intf) { - cli_->set_interface(intf); -} - -void Client::set_proxy(const std::string &host, int port) { - cli_->set_proxy(host, port); -} -void Client::set_proxy_basic_auth(const std::string &username, - const std::string &password) { - cli_->set_proxy_basic_auth(username, password); -} -void Client::set_proxy_bearer_token_auth(const std::string &token) { - cli_->set_proxy_bearer_token_auth(token); -} -void Client::set_no_proxy(const std::vector &patterns) { - cli_->set_no_proxy(patterns); -} - -void Client::set_logger(Logger logger) { - cli_->set_logger(std::move(logger)); -} - -void Client::set_error_logger(ErrorLogger error_logger) { - cli_->set_error_logger(std::move(error_logger)); -} - -/* - * Group 6: SSL Server and Client implementation - */ - -#ifdef CPPHTTPLIB_SSL_ENABLED - -// SSL HTTP server implementation -SSLServer::SSLServer(const char *cert_path, const char *private_key_path, - const char *client_ca_cert_file_path, - const char *client_ca_cert_dir_path, - const char *private_key_password) { - using namespace tls; - - ctx_ = create_server_context(); - if (!ctx_) { return; } - - // Load server certificate and private key - if (!set_server_cert_file(ctx_, cert_path, private_key_path, - private_key_password)) { - last_ssl_error_ = static_cast(get_error()); - free_context(ctx_); - ctx_ = nullptr; - return; - } - - // Load client CA certificates for client authentication - if (client_ca_cert_file_path || client_ca_cert_dir_path) { - if (!set_client_ca_file(ctx_, client_ca_cert_file_path, - client_ca_cert_dir_path)) { - last_ssl_error_ = static_cast(get_error()); - free_context(ctx_); - ctx_ = nullptr; - return; - } - // Enable client certificate verification - set_verify_client(ctx_, true); - } -} - -SSLServer::SSLServer(const PemMemory &pem) { - using namespace tls; - ctx_ = create_server_context(); - if (ctx_) { - if (!set_server_cert_pem(ctx_, pem.cert_pem, pem.key_pem, - pem.private_key_password)) { - last_ssl_error_ = static_cast(get_error()); - free_context(ctx_); - ctx_ = nullptr; - } else if (pem.client_ca_pem && pem.client_ca_pem_len > 0) { - if (!load_ca_pem(ctx_, pem.client_ca_pem, pem.client_ca_pem_len)) { - last_ssl_error_ = static_cast(get_error()); - free_context(ctx_); - ctx_ = nullptr; - } else { - set_verify_client(ctx_, true); - } - } - } -} - -SSLServer::SSLServer(const tls::ContextSetupCallback &setup_callback) { - using namespace tls; - ctx_ = create_server_context(); - if (ctx_) { - if (!setup_callback(ctx_)) { - free_context(ctx_); - ctx_ = nullptr; - } - } -} - -SSLServer::~SSLServer() { - if (ctx_) { tls::free_context(ctx_); } -} - -bool SSLServer::is_valid() const { return ctx_ != nullptr; } - -bool SSLServer::process_and_close_socket(socket_t sock) { - using namespace tls; - - // Create TLS session with mutex protection - session_t session = nullptr; - { - std::lock_guard guard(ctx_mutex_); - session = create_session(static_cast(ctx_), sock); - } - - if (!session) { - last_ssl_error_ = static_cast(get_error()); - detail::shutdown_socket(sock); - detail::close_socket(sock); - return false; - } - - // Use scope_exit to ensure cleanup on all paths (including exceptions) - bool handshake_done = false; - bool ret = false; - bool websocket_upgraded = false; - auto cleanup = detail::scope_exit([&] { - if (handshake_done) { shutdown(session, !websocket_upgraded && ret); } - free_session(session); - detail::shutdown_socket(sock); - detail::close_socket(sock); - }); - - // Perform TLS accept handshake with timeout - TlsError tls_err; - if (!accept_nonblocking(session, sock, read_timeout_sec_, read_timeout_usec_, - &tls_err)) { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - // Map TlsError to legacy ssl_error for backward compatibility - if (tls_err.code == ErrorCode::WantRead) { - last_ssl_error_ = SSL_ERROR_WANT_READ; - } else if (tls_err.code == ErrorCode::WantWrite) { - last_ssl_error_ = SSL_ERROR_WANT_WRITE; - } else { - last_ssl_error_ = SSL_ERROR_SSL; - } -#else - last_ssl_error_ = static_cast(get_error()); -#endif - return false; - } - - handshake_done = true; - - std::string remote_addr; - int remote_port = 0; - detail::get_remote_ip_and_port(sock, remote_addr, remote_port); - - std::string local_addr; - int local_port = 0; - detail::get_local_ip_and_port(sock, local_addr, local_port); - - ret = detail::process_server_socket_ssl( - svr_sock_, session, sock, keep_alive_max_count_, keep_alive_timeout_sec_, - read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, - write_timeout_usec_, - [&](Stream &strm, bool close_connection, bool &connection_closed) { - return process_request( - strm, remote_addr, remote_port, local_addr, local_port, - close_connection, connection_closed, - [&](Request &req) { req.ssl = session; }, &websocket_upgraded); - }); - - return ret; -} - -bool SSLServer::update_certs_pem(const char *cert_pem, - const char *key_pem, - const char *client_ca_pem, - const char *password) { - if (!ctx_) { return false; } - std::lock_guard guard(ctx_mutex_); - if (!tls::update_server_cert(ctx_, cert_pem, key_pem, password)) { - return false; - } - if (client_ca_pem) { - return tls::update_server_client_ca(ctx_, client_ca_pem); - } - return true; -} - -// SSL HTTP client implementation -SSLClient::~SSLClient() { - if (ctx_) { tls::free_context(ctx_); } - // Make sure to shut down SSL since shutdown_ssl will resolve to the - // base function rather than the derived function once we get to the - // base class destructor, and won't free the SSL (causing a leak). - shutdown_ssl_impl(socket_, true); -} - -bool SSLClient::is_valid() const { return ctx_ != nullptr; } - -void SSLClient::shutdown_ssl(Socket &socket, bool shutdown_gracefully) { - shutdown_ssl_impl(socket, shutdown_gracefully); -} - -void SSLClient::shutdown_ssl_impl(Socket &socket, - bool shutdown_gracefully) { - if (socket.sock == INVALID_SOCKET) { - assert(socket.ssl == nullptr); - return; - } - if (socket.ssl) { - tls::shutdown(socket.ssl, shutdown_gracefully); - { - std::lock_guard guard(ctx_mutex_); - tls::free_session(socket.ssl); - } - socket.ssl = nullptr; - } - assert(socket.ssl == nullptr); -} - -bool SSLClient::process_socket( - const Socket &socket, - std::chrono::time_point start_time, - std::function callback) { - assert(socket.ssl); - return detail::process_client_socket_ssl( - socket.ssl, socket.sock, read_timeout_sec_, read_timeout_usec_, - write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, start_time, - std::move(callback)); -} - -bool SSLClient::is_ssl() const { return true; } - -bool SSLClient::create_and_connect_socket(Socket &socket, Error &error) { - if (!is_valid()) { - error = Error::SSLConnection; - return false; - } - return ClientImpl::create_and_connect_socket(socket, error); -} - -bool SSLClient::setup_proxy_connection( - Socket &socket, - std::chrono::time_point start_time, - Response &res, bool &success, Error &error) { - if (!is_proxy_enabled_for_host(host_)) { return true; } - - if (!connect_with_proxy(socket, start_time, res, success, error)) { - return false; - } - - if (!initialize_ssl(socket, error)) { - success = false; - return false; - } - - return true; -} - -// Assumes that socket_mutex_ is locked and that there are no requests in -// flight -bool SSLClient::connect_with_proxy( - Socket &socket, - std::chrono::time_point start_time, - Response &res, bool &success, Error &error) { - success = true; - Response proxy_res; - if (!detail::process_client_socket( - socket.sock, read_timeout_sec_, read_timeout_usec_, - write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, - start_time, [&](Stream &strm) { - Request req2; - req2.method = "CONNECT"; - req2.path = - detail::make_host_and_port_string_always_port(host_, port_); - if (max_timeout_msec_ > 0) { - req2.start_time_ = std::chrono::steady_clock::now(); - } - return process_request(strm, req2, proxy_res, false, error); - })) { - // Thread-safe to close everything because we are assuming there are no - // requests in flight - shutdown_ssl(socket, true); - shutdown_socket(socket); - close_socket(socket); - success = false; - return false; - } - - if (proxy_res.status == StatusCode::ProxyAuthenticationRequired_407) { - if (!proxy_digest_auth_username_.empty() && - !proxy_digest_auth_password_.empty()) { - std::map auth; - if (detail::parse_www_authenticate(proxy_res, auth, true)) { - // Close the current socket and create a new one for the authenticated - // request - shutdown_ssl(socket, true); - shutdown_socket(socket); - close_socket(socket); - - // Create a new socket for the authenticated CONNECT request - if (!ensure_socket_connection(socket, error)) { - success = false; - output_error_log(error, nullptr); - return false; - } - - proxy_res = Response(); - if (!detail::process_client_socket( - socket.sock, read_timeout_sec_, read_timeout_usec_, - write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, - start_time, [&](Stream &strm) { - Request req3; - req3.method = "CONNECT"; - req3.path = detail::make_host_and_port_string_always_port( - host_, port_); - req3.headers.insert(detail::make_digest_authentication_header( - req3, auth, 1, detail::random_string(10), - proxy_digest_auth_username_, proxy_digest_auth_password_, - true)); - if (max_timeout_msec_ > 0) { - req3.start_time_ = std::chrono::steady_clock::now(); - } - return process_request(strm, req3, proxy_res, false, error); - })) { - // Thread-safe to close everything because we are assuming there are - // no requests in flight - shutdown_ssl(socket, true); - shutdown_socket(socket); - close_socket(socket); - success = false; - return false; - } - } - } - } - - // If status code is not 200, proxy request is failed. - // Set error to ProxyConnection and return proxy response - // as the response of the request - if (proxy_res.status != StatusCode::OK_200) { - error = Error::ProxyConnection; - output_error_log(error, nullptr); - res = std::move(proxy_res); - // Thread-safe to close everything because we are assuming there are - // no requests in flight - shutdown_ssl(socket, true); - shutdown_socket(socket); - close_socket(socket); - return false; - } - - return true; -} - -bool SSLClient::ensure_socket_connection(Socket &socket, Error &error) { - if (!ClientImpl::ensure_socket_connection(socket, error)) { return false; } - - if (is_proxy_enabled_for_host(host_)) { return true; } - - if (!initialize_ssl(socket, error)) { - shutdown_socket(socket); - close_socket(socket); - return false; - } - - return true; -} - -// SSL HTTP client implementation -SSLClient::SSLClient(const std::string &host) - : SSLClient(host, 443, std::string(), std::string()) {} - -SSLClient::SSLClient(const std::string &host, int port) - : SSLClient(host, port, std::string(), std::string()) {} - -void SSLClient::init_ctx() { - ctx_ = tls::create_client_context(); - if (ctx_) { tls::set_min_version(ctx_, tls::Version::TLS1_2); } -} - -void SSLClient::reset_ctx_on_error() { - last_backend_error_ = tls::get_error(); - tls::free_context(ctx_); - ctx_ = nullptr; -} - -SSLClient::SSLClient(const std::string &host, int port, - const std::string &client_cert_path, - const std::string &client_key_path, - const std::string &private_key_password) - : ClientImpl(host, port, client_cert_path, client_key_path) { - init_ctx(); - if (!ctx_) { return; } - - if (!client_cert_path.empty() && !client_key_path.empty()) { - const char *password = - private_key_password.empty() ? nullptr : private_key_password.c_str(); - if (!tls::set_client_cert_file(ctx_, client_cert_path.c_str(), - client_key_path.c_str(), password)) { - reset_ctx_on_error(); - } - } -} - -SSLClient::SSLClient(const std::string &host, int port, - const PemMemory &pem) - : ClientImpl(host, port) { - init_ctx(); - if (!ctx_) { return; } - - if (pem.cert_pem && pem.key_pem) { - if (!tls::set_client_cert_pem(ctx_, pem.cert_pem, pem.key_pem, - pem.private_key_password)) { - reset_ctx_on_error(); - } - } -} - -void SSLClient::set_ca_cert_store(tls::ca_store_t ca_cert_store) { - if (ca_cert_store && ctx_) { - // set_ca_store takes ownership of ca_cert_store - tls::set_ca_store(ctx_, ca_cert_store); - } else if (ca_cert_store) { - tls::free_ca_store(ca_cert_store); - } -} - -void -SSLClient::set_server_certificate_verifier(tls::VerifyCallback verifier) { - if (!ctx_) { return; } - tls::set_verify_callback(ctx_, verifier); -} - -void SSLClient::set_session_verifier( - std::function verifier) { - session_verifier_ = std::move(verifier); -} - -#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE -void SSLClient::enable_windows_certificate_verification(bool enabled) { - enable_windows_cert_verification_ = enabled; -} -#endif - -void SSLClient::load_ca_cert_store(const char *ca_cert, - std::size_t size) { - if (ctx_ && ca_cert && size > 0) { - ca_cert_pem_.assign(ca_cert, size); // Store for redirect transfer - tls::load_ca_pem(ctx_, ca_cert, size); - } -} - -bool SSLClient::load_certs() { - auto ret = true; - - std::call_once(initialize_cert_, [&]() { - std::lock_guard guard(ctx_mutex_); - - if (!ca_cert_file_path_.empty()) { - if (!tls::load_ca_file(ctx_, ca_cert_file_path_.c_str())) { - last_backend_error_ = tls::get_error(); - ret = false; - } - } else if (!ca_cert_dir_path_.empty()) { - if (!tls::load_ca_dir(ctx_, ca_cert_dir_path_.c_str())) { - last_backend_error_ = tls::get_error(); - ret = false; - } - } else if (ca_cert_pem_.empty()) { - if (!tls::load_system_certs(ctx_)) { - last_backend_error_ = tls::get_error(); - } - } - }); - - return ret; -} - -bool SSLClient::initialize_ssl(Socket &socket, Error &error) { - using namespace tls; - - // Load CA certificates if server verification is enabled - if (server_certificate_verification_) { - if (!load_certs()) { - error = Error::SSLLoadingCerts; - output_error_log(error, nullptr); - return false; - } - } - - bool is_ip = detail::is_ip_address(host_); - -#if defined(CPPHTTPLIB_MBEDTLS_SUPPORT) || defined(CPPHTTPLIB_WOLFSSL_SUPPORT) - // MbedTLS/wolfSSL need explicit verification mode (OpenSSL uses - // SSL_VERIFY_NONE by default and performs all verification post-handshake). - // For IP addresses with verification enabled, use OPTIONAL mode since - // these backends require hostname for strict verification. - if (is_ip && server_certificate_verification_) { - set_verify_client(ctx_, false); - } else { - set_verify_client(ctx_, server_certificate_verification_); - } -#endif - - // Create TLS session - session_t session = nullptr; - { - std::lock_guard guard(ctx_mutex_); - session = create_session(ctx_, socket.sock); - } - - if (!session) { - error = Error::SSLConnection; - last_backend_error_ = get_error(); - return false; - } - - // Use scope_exit to ensure session is freed on error paths - bool success = false; - auto session_guard = detail::scope_exit([&] { - if (!success) { free_session(session); } - }); - - // Set SNI extension (skip for IP addresses per RFC 6066). - // On MbedTLS, set_sni also enables hostname verification internally. - // On OpenSSL, set_sni only sets SNI; verification is done post-handshake. - if (!is_ip) { - if (!set_sni(session, host_.c_str())) { - error = Error::SSLConnection; - last_backend_error_ = get_error(); - return false; - } - } - - // Perform non-blocking TLS handshake with timeout - TlsError tls_err; - if (!connect_nonblocking(session, socket.sock, connection_timeout_sec_, - connection_timeout_usec_, &tls_err)) { - last_ssl_error_ = static_cast(tls_err.code); - last_backend_error_ = tls_err.backend_code; - if (tls_err.code == ErrorCode::CertVerifyFailed) { - error = Error::SSLServerVerification; - } else if (tls_err.code == ErrorCode::HostnameMismatch) { - error = Error::SSLServerHostnameVerification; - } else { - error = Error::SSLConnection; - } - output_error_log(error, nullptr); - return false; - } - - // Post-handshake session verifier callback - auto verification_status = SSLVerifierResponse::NoDecisionMade; - if (session_verifier_) { verification_status = session_verifier_(session); } - - if (verification_status == SSLVerifierResponse::CertificateRejected) { - last_backend_error_ = get_error(); - error = Error::SSLServerVerification; - output_error_log(error, nullptr); - return false; - } - - // Default server certificate verification - if (verification_status == SSLVerifierResponse::NoDecisionMade && - server_certificate_verification_) { - verify_result_ = tls::get_verify_result(session); - if (verify_result_ != 0) { - last_backend_error_ = static_cast(verify_result_); - error = Error::SSLServerVerification; - output_error_log(error, nullptr); - return false; - } - - auto server_cert = get_peer_cert(session); - if (!server_cert) { - last_backend_error_ = get_error(); - error = Error::SSLServerVerification; - output_error_log(error, nullptr); - return false; - } - auto cert_guard = detail::scope_exit([&] { free_cert(server_cert); }); - - // Hostname verification (post-handshake for all cases). - // On OpenSSL, verification is always post-handshake (SSL_VERIFY_NONE). - // On MbedTLS, set_sni already enabled hostname verification during - // handshake for non-IP hosts, but this check is still needed for IP - // addresses where SNI is not set. - if (server_hostname_verification_) { - if (!verify_hostname(server_cert, host_.c_str())) { - last_backend_error_ = hostname_mismatch_code(); - error = Error::SSLServerHostnameVerification; - output_error_log(error, nullptr); - return false; - } - } - -#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE - // Additional Windows Schannel verification. - // This provides real-time certificate validation with Windows Update - // integration, working with both OpenSSL and MbedTLS backends. - // Skip when a custom CA cert is specified, as the Windows certificate - // store would not know about user-provided CA certificates. - if (enable_windows_cert_verification_ && ca_cert_file_path_.empty() && - ca_cert_dir_path_.empty() && ca_cert_pem_.empty()) { - std::vector der; - if (get_cert_der(server_cert, der)) { - uint64_t wincrypt_error = 0; - if (!detail::verify_cert_with_windows_schannel( - der, host_, server_hostname_verification_, wincrypt_error)) { - last_backend_error_ = wincrypt_error; - error = Error::SSLServerVerification; - output_error_log(error, nullptr); - return false; - } - } - } -#endif - } - - success = true; - socket.ssl = session; - return true; -} - -void Client::set_digest_auth(const std::string &username, - const std::string &password) { - cli_->set_digest_auth(username, password); -} - -void Client::set_proxy_digest_auth(const std::string &username, - const std::string &password) { - cli_->set_proxy_digest_auth(username, password); -} - -void Client::enable_server_certificate_verification(bool enabled) { - cli_->enable_server_certificate_verification(enabled); -} - -void Client::enable_server_hostname_verification(bool enabled) { - cli_->enable_server_hostname_verification(enabled); -} - -#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE -void Client::enable_windows_certificate_verification(bool enabled) { - if (is_ssl_) { - static_cast(*cli_).enable_windows_certificate_verification( - enabled); - } -} -#endif - -void Client::set_ca_cert_path(const std::string &ca_cert_file_path, - const std::string &ca_cert_dir_path) { - cli_->set_ca_cert_path(ca_cert_file_path, ca_cert_dir_path); -} - -void Client::set_ca_cert_store(tls::ca_store_t ca_cert_store) { - if (is_ssl_) { - static_cast(*cli_).set_ca_cert_store(ca_cert_store); - } else if (ca_cert_store) { - tls::free_ca_store(ca_cert_store); - } -} - -void Client::load_ca_cert_store(const char *ca_cert, std::size_t size) { - set_ca_cert_store(tls::create_ca_store(ca_cert, size)); -} - -void -Client::set_server_certificate_verifier(tls::VerifyCallback verifier) { - if (is_ssl_) { - static_cast(*cli_).set_server_certificate_verifier( - std::move(verifier)); - } -} - -void Client::set_session_verifier( - std::function verifier) { - if (is_ssl_) { - static_cast(*cli_).set_session_verifier(std::move(verifier)); - } -} - -tls::ctx_t Client::tls_context() const { - if (is_ssl_) { return static_cast(*cli_).tls_context(); } - return nullptr; -} - -#endif // CPPHTTPLIB_SSL_ENABLED - -/* - * Group 7: TLS abstraction layer - Common API - */ - -#ifdef CPPHTTPLIB_SSL_ENABLED - -namespace tls { - -// Helper for PeerCert construction -PeerCert get_peer_cert_from_session(const_session_t session) { - return PeerCert(get_peer_cert(session)); -} - -namespace impl { - -VerifyCallback &get_verify_callback() { - static thread_local VerifyCallback callback; - return callback; -} - -VerifyCallback &get_mbedtls_verify_callback() { - static thread_local VerifyCallback callback; - return callback; -} - -// Check if a string is an IPv4 address -bool is_ipv4_address(const std::string &str) { - int dots = 0; - for (char c : str) { - if (c == '.') { - dots++; - } else if (!isdigit(static_cast(c))) { - return false; - } - } - return dots == 3; -} - -// Parse IPv4 address string to bytes -bool parse_ipv4(const std::string &str, unsigned char *out) { - const char *p = str.c_str(); - for (int i = 0; i < 4; i++) { - if (i > 0) { - if (*p != '.') { return false; } - p++; - } - int val = 0; - int digits = 0; - while (*p >= '0' && *p <= '9') { - val = val * 10 + (*p - '0'); - if (val > 255) { return false; } - p++; - digits++; - } - if (digits == 0) { return false; } - // Reject leading zeros (e.g., "01.002.03.04") to prevent ambiguity - if (digits > 1 && *(p - digits) == '0') { return false; } - out[i] = static_cast(val); - } - return *p == '\0'; -} - -#ifdef _WIN32 -// Enumerate Windows system certificates and call callback with DER data -template -bool enumerate_windows_system_certs(Callback cb) { - bool loaded = false; - static const wchar_t *store_names[] = {L"ROOT", L"CA"}; - for (auto store_name : store_names) { - HCERTSTORE hStore = CertOpenSystemStoreW(0, store_name); - if (hStore) { - PCCERT_CONTEXT pContext = nullptr; - while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) != - nullptr) { - if (cb(pContext->pbCertEncoded, pContext->cbCertEncoded)) { - loaded = true; - } - } - CertCloseStore(hStore, 0); - } - } - return loaded; -} -#endif - -#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -// Enumerate macOS Keychain certificates and call callback with DER data -template -bool enumerate_macos_keychain_certs(Callback cb) { - bool loaded = false; - const SecTrustSettingsDomain domains[] = { - kSecTrustSettingsDomainSystem, - kSecTrustSettingsDomainAdmin, - kSecTrustSettingsDomainUser, - }; - for (auto domain : domains) { - CFArrayRef certs = nullptr; - OSStatus status = SecTrustSettingsCopyCertificates(domain, &certs); - if (status != errSecSuccess || !certs) { - if (certs) CFRelease(certs); - continue; - } - CFIndex count = CFArrayGetCount(certs); - for (CFIndex i = 0; i < count; i++) { - SecCertificateRef cert = - (SecCertificateRef)CFArrayGetValueAtIndex(certs, i); - CFDataRef data = SecCertificateCopyData(cert); - if (data) { - if (cb(CFDataGetBytePtr(data), - static_cast(CFDataGetLength(data)))) { - loaded = true; - } - CFRelease(data); - } - } - CFRelease(certs); - } - return loaded; -} -#endif - -#if !defined(_WIN32) && !(defined(__APPLE__) && \ - defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN)) -// Common CA certificate file paths on Linux/Unix -const char **system_ca_paths() { - static const char *paths[] = { - "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu - "/etc/pki/tls/certs/ca-bundle.crt", // RHEL/CentOS - "/etc/ssl/ca-bundle.pem", // OpenSUSE - "/etc/pki/tls/cacert.pem", // OpenELEC - "/etc/ssl/cert.pem", // Alpine, FreeBSD - nullptr}; - return paths; -} - -// Common CA certificate directory paths on Linux/Unix -const char **system_ca_dirs() { - static const char *dirs[] = {"/etc/ssl/certs", // Debian/Ubuntu - "/etc/pki/tls/certs", // RHEL/CentOS - "/usr/share/ca-certificates", // Other - nullptr}; - return dirs; -} -#endif - -} // namespace impl - -bool set_client_ca_file(ctx_t ctx, const char *ca_file, - const char *ca_dir) { - if (!ctx) { return false; } - - bool success = true; - if (ca_file && *ca_file) { - if (!load_ca_file(ctx, ca_file)) { success = false; } - } - if (ca_dir && *ca_dir) { - if (!load_ca_dir(ctx, ca_dir)) { success = false; } - } - -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - // Set CA list for client certificate request (CertificateRequest message) - if (ca_file && *ca_file) { - auto list = SSL_load_client_CA_file(ca_file); - if (list) { SSL_CTX_set_client_CA_list(static_cast(ctx), list); } - } -#endif - - return success; -} - -bool set_server_cert_pem(ctx_t ctx, const char *cert, const char *key, - const char *password) { - return set_client_cert_pem(ctx, cert, key, password); -} - -bool set_server_cert_file(ctx_t ctx, const char *cert_path, - const char *key_path, const char *password) { - return set_client_cert_file(ctx, cert_path, key_path, password); -} - -// PeerCert implementation -PeerCert::PeerCert() = default; - -PeerCert::PeerCert(cert_t cert) : cert_(cert) {} - -PeerCert::PeerCert(PeerCert &&other) noexcept : cert_(other.cert_) { - other.cert_ = nullptr; -} - -PeerCert &PeerCert::operator=(PeerCert &&other) noexcept { - if (this != &other) { - if (cert_) { free_cert(cert_); } - cert_ = other.cert_; - other.cert_ = nullptr; - } - return *this; -} - -PeerCert::~PeerCert() { - if (cert_) { free_cert(cert_); } -} - -PeerCert::operator bool() const { return cert_ != nullptr; } - -std::string PeerCert::subject_cn() const { - return cert_ ? get_cert_subject_cn(cert_) : std::string(); -} - -std::string PeerCert::issuer_name() const { - return cert_ ? get_cert_issuer_name(cert_) : std::string(); -} - -bool PeerCert::check_hostname(const char *hostname) const { - return cert_ ? verify_hostname(cert_, hostname) : false; -} - -std::vector PeerCert::sans() const { - std::vector result; - if (cert_) { get_cert_sans(cert_, result); } - return result; -} - -bool PeerCert::validity(time_t ¬_before, time_t ¬_after) const { - return cert_ ? get_cert_validity(cert_, not_before, not_after) : false; -} - -std::string PeerCert::serial() const { - return cert_ ? get_cert_serial(cert_) : std::string(); -} - -// VerifyContext method implementations -std::string VerifyContext::subject_cn() const { - return cert ? get_cert_subject_cn(cert) : std::string(); -} - -std::string VerifyContext::issuer_name() const { - return cert ? get_cert_issuer_name(cert) : std::string(); -} - -bool VerifyContext::check_hostname(const char *hostname) const { - return cert ? verify_hostname(cert, hostname) : false; -} - -std::vector VerifyContext::sans() const { - std::vector result; - if (cert) { get_cert_sans(cert, result); } - return result; -} - -bool VerifyContext::validity(time_t ¬_before, - time_t ¬_after) const { - return cert ? get_cert_validity(cert, not_before, not_after) : false; -} - -std::string VerifyContext::serial() const { - return cert ? get_cert_serial(cert) : std::string(); -} - -// TlsError static method implementation -std::string TlsError::verify_error_to_string(long error_code) { - return verify_error_string(error_code); -} - -} // namespace tls - -// Request::peer_cert() implementation -tls::PeerCert Request::peer_cert() const { - return tls::get_peer_cert_from_session(ssl); -} - -// Request::sni() implementation -std::string Request::sni() const { - if (!ssl) { return std::string(); } - const char *s = tls::get_sni(ssl); - return s ? std::string(s) : std::string(); -} - -#endif // CPPHTTPLIB_SSL_ENABLED - -/* - * Group 8: TLS abstraction layer - OpenSSL backend - */ - -/* - * OpenSSL Backend Implementation - */ - -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT -namespace tls { - -namespace impl { - -// Helper to map OpenSSL SSL_get_error to ErrorCode -ErrorCode map_ssl_error(int ssl_error, int &out_errno) { - switch (ssl_error) { - case SSL_ERROR_NONE: return ErrorCode::Success; - case SSL_ERROR_WANT_READ: return ErrorCode::WantRead; - case SSL_ERROR_WANT_WRITE: return ErrorCode::WantWrite; - case SSL_ERROR_ZERO_RETURN: return ErrorCode::PeerClosed; - case SSL_ERROR_SYSCALL: out_errno = errno; return ErrorCode::SyscallError; - case SSL_ERROR_SSL: - default: return ErrorCode::Fatal; - } -} - -// Helper: Create client CA list from PEM string -// Returns a new STACK_OF(X509_NAME)* or nullptr on failure -// Caller takes ownership of returned list -STACK_OF(X509_NAME) * - create_client_ca_list_from_pem(const char *ca_pem) { - if (!ca_pem) { return nullptr; } - - auto ca_list = sk_X509_NAME_new_null(); - if (!ca_list) { return nullptr; } - - BIO *bio = BIO_new_mem_buf(ca_pem, -1); - if (!bio) { - sk_X509_NAME_pop_free(ca_list, X509_NAME_free); - return nullptr; - } - - X509 *cert = nullptr; - while ((cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) != - nullptr) { - const X509_NAME *name = X509_get_subject_name(cert); - if (name) { - sk_X509_NAME_push(ca_list, X509_NAME_dup(const_cast(name))); - } - X509_free(cert); - } - BIO_free(bio); - - return ca_list; -} - -// OpenSSL verify callback wrapper -int openssl_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) { - auto &callback = get_verify_callback(); - if (!callback) { return preverify_ok; } - - // Get SSL object from X509_STORE_CTX - auto ssl = static_cast( - X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx())); - if (!ssl) { return preverify_ok; } - - // Get current certificate and depth - auto cert = X509_STORE_CTX_get_current_cert(ctx); - int depth = X509_STORE_CTX_get_error_depth(ctx); - int error = X509_STORE_CTX_get_error(ctx); - - // Build context - VerifyContext verify_ctx; - verify_ctx.session = static_cast(ssl); - verify_ctx.cert = static_cast(cert); - verify_ctx.depth = depth; - verify_ctx.preverify_ok = (preverify_ok != 0); - verify_ctx.error_code = error; - verify_ctx.error_string = - (error != X509_V_OK) ? X509_verify_cert_error_string(error) : nullptr; - - return callback(verify_ctx) ? 1 : 0; -} - -} // namespace impl - -ctx_t create_client_context() { - SSL_CTX *ctx = SSL_CTX_new(TLS_client_method()); - if (ctx) { - // Disable auto-retry to properly handle non-blocking I/O - SSL_CTX_clear_mode(ctx, SSL_MODE_AUTO_RETRY); - // Set minimum TLS version - SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); - } - return static_cast(ctx); -} - -void free_context(ctx_t ctx) { - if (ctx) { SSL_CTX_free(static_cast(ctx)); } -} - -bool set_min_version(ctx_t ctx, Version version) { - if (!ctx) return false; - return SSL_CTX_set_min_proto_version(static_cast(ctx), - static_cast(version)) == 1; -} - -bool load_ca_pem(ctx_t ctx, const char *pem, size_t len) { - if (!ctx || !pem || len == 0) return false; - - auto ssl_ctx = static_cast(ctx); - auto store = SSL_CTX_get_cert_store(ssl_ctx); - if (!store) return false; - - auto bio = BIO_new_mem_buf(pem, static_cast(len)); - if (!bio) return false; - - bool ok = true; - X509 *cert = nullptr; - while ((cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) != - nullptr) { - if (X509_STORE_add_cert(store, cert) != 1) { - // Ignore duplicate errors - auto err = ERR_peek_last_error(); - if (ERR_GET_REASON(err) != X509_R_CERT_ALREADY_IN_HASH_TABLE) { - ok = false; - } - } - X509_free(cert); - if (!ok) break; - } - BIO_free(bio); - - // Clear any "no more certificates" errors - ERR_clear_error(); - return ok; -} - -bool load_ca_file(ctx_t ctx, const char *file_path) { - if (!ctx || !file_path) return false; - return SSL_CTX_load_verify_locations(static_cast(ctx), file_path, - nullptr) == 1; -} - -bool load_ca_dir(ctx_t ctx, const char *dir_path) { - if (!ctx || !dir_path) return false; - return SSL_CTX_load_verify_locations(static_cast(ctx), nullptr, - dir_path) == 1; -} - -bool load_system_certs(ctx_t ctx) { - if (!ctx) return false; - auto ssl_ctx = static_cast(ctx); - -#ifdef _WIN32 - // Windows: Load from system certificate store (ROOT and CA) - auto store = SSL_CTX_get_cert_store(ssl_ctx); - if (!store) return false; - - bool loaded_any = false; - static const wchar_t *store_names[] = {L"ROOT", L"CA"}; - for (auto store_name : store_names) { - auto hStore = CertOpenSystemStoreW(NULL, store_name); - if (!hStore) continue; - - PCCERT_CONTEXT pContext = nullptr; - while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) != - nullptr) { - const unsigned char *data = pContext->pbCertEncoded; - auto x509 = d2i_X509(nullptr, &data, pContext->cbCertEncoded); - if (x509) { - if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; } - X509_free(x509); - } - } - CertCloseStore(hStore, 0); - } - return loaded_any; - -#elif defined(__APPLE__) -#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN - // macOS: Load from Keychain - auto store = SSL_CTX_get_cert_store(ssl_ctx); - if (!store) return false; - - bool loaded_any = false; - const SecTrustSettingsDomain domains[] = { - kSecTrustSettingsDomainSystem, - kSecTrustSettingsDomainAdmin, - kSecTrustSettingsDomainUser, - }; - for (auto domain : domains) { - CFArrayRef certs = nullptr; - if (SecTrustSettingsCopyCertificates(domain, &certs) != errSecSuccess || - !certs) { - if (certs) CFRelease(certs); - continue; - } - auto count = CFArrayGetCount(certs); - for (CFIndex i = 0; i < count; i++) { - auto cert = reinterpret_cast( - const_cast(CFArrayGetValueAtIndex(certs, i))); - CFDataRef der = SecCertificateCopyData(cert); - if (der) { - const unsigned char *data = CFDataGetBytePtr(der); - auto x509 = d2i_X509(nullptr, &data, CFDataGetLength(der)); - if (x509) { - if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; } - X509_free(x509); - } - CFRelease(der); - } - } - CFRelease(certs); - } - return loaded_any || SSL_CTX_set_default_verify_paths(ssl_ctx) == 1; -#else - return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1; -#endif - -#else - // Other Unix: use default verify paths - return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1; -#endif -} - -bool set_client_cert_pem(ctx_t ctx, const char *cert, const char *key, - const char *password) { - if (!ctx || !cert || !key) return false; - - auto ssl_ctx = static_cast(ctx); - - // Load certificate - auto cert_bio = BIO_new_mem_buf(cert, -1); - if (!cert_bio) return false; - - auto x509 = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr); - BIO_free(cert_bio); - if (!x509) return false; - - auto cert_ok = SSL_CTX_use_certificate(ssl_ctx, x509) == 1; - X509_free(x509); - if (!cert_ok) return false; - - // Load private key - auto key_bio = BIO_new_mem_buf(key, -1); - if (!key_bio) return false; - - auto pkey = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, - password ? const_cast(password) - : nullptr); - BIO_free(key_bio); - if (!pkey) return false; - - auto key_ok = SSL_CTX_use_PrivateKey(ssl_ctx, pkey) == 1; - EVP_PKEY_free(pkey); - - return key_ok && SSL_CTX_check_private_key(ssl_ctx) == 1; -} - -bool set_client_cert_file(ctx_t ctx, const char *cert_path, - const char *key_path, const char *password) { - if (!ctx || !cert_path || !key_path) return false; - - auto ssl_ctx = static_cast(ctx); - - if (password && password[0] != '\0') { - SSL_CTX_set_default_passwd_cb_userdata( - ssl_ctx, reinterpret_cast(const_cast(password))); - } - - return SSL_CTX_use_certificate_chain_file(ssl_ctx, cert_path) == 1 && - SSL_CTX_use_PrivateKey_file(ssl_ctx, key_path, SSL_FILETYPE_PEM) == 1; -} - -ctx_t create_server_context() { - SSL_CTX *ctx = SSL_CTX_new(TLS_server_method()); - if (ctx) { - SSL_CTX_set_options(ctx, SSL_OP_NO_COMPRESSION | - SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); - SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); - } - return static_cast(ctx); -} - -void set_verify_client(ctx_t ctx, bool require) { - if (!ctx) return; - SSL_CTX_set_verify(static_cast(ctx), - require - ? (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT) - : SSL_VERIFY_NONE, - nullptr); -} - -session_t create_session(ctx_t ctx, socket_t sock) { - if (!ctx || sock == INVALID_SOCKET) return nullptr; - - auto ssl_ctx = static_cast(ctx); - SSL *ssl = SSL_new(ssl_ctx); - if (!ssl) return nullptr; - - // Disable auto-retry for proper non-blocking I/O handling - SSL_clear_mode(ssl, SSL_MODE_AUTO_RETRY); - - auto bio = BIO_new_socket(static_cast(sock), BIO_NOCLOSE); - if (!bio) { - SSL_free(ssl); - return nullptr; - } - - SSL_set_bio(ssl, bio, bio); - return static_cast(ssl); -} - -void free_session(session_t session) { - if (session) { SSL_free(static_cast(session)); } -} - -bool set_sni(session_t session, const char *hostname) { - if (!session || !hostname) return false; - - auto ssl = static_cast(session); - - // Set SNI (Server Name Indication) only - does not enable verification -#if defined(OPENSSL_IS_BORINGSSL) - return SSL_set_tlsext_host_name(ssl, hostname) == 1; -#else - // Direct call instead of macro to suppress -Wold-style-cast warning - return SSL_ctrl(ssl, SSL_CTRL_SET_TLSEXT_HOSTNAME, TLSEXT_NAMETYPE_host_name, - static_cast(const_cast(hostname))) == 1; -#endif -} - -bool set_hostname(session_t session, const char *hostname) { - if (!session || !hostname) return false; - - auto ssl = static_cast(session); - - // Set SNI (Server Name Indication) - if (!set_sni(session, hostname)) { return false; } - - // Enable hostname verification - auto param = SSL_get0_param(ssl); - if (!param) return false; - - X509_VERIFY_PARAM_set_hostflags(param, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS); - if (X509_VERIFY_PARAM_set1_host(param, hostname, 0) != 1) { return false; } - - SSL_set_verify(ssl, SSL_VERIFY_PEER, nullptr); - return true; -} - -TlsError connect(session_t session) { - if (!session) { return TlsError(); } - - auto ssl = static_cast(session); - auto ret = SSL_connect(ssl); - - TlsError err; - if (ret == 1) { - err.code = ErrorCode::Success; - } else { - auto ssl_err = SSL_get_error(ssl, ret); - err.code = impl::map_ssl_error(ssl_err, err.sys_errno); - err.backend_code = ERR_get_error(); - } - return err; -} - -TlsError accept(session_t session) { - if (!session) { return TlsError(); } - - auto ssl = static_cast(session); - auto ret = SSL_accept(ssl); - - TlsError err; - if (ret == 1) { - err.code = ErrorCode::Success; - } else { - auto ssl_err = SSL_get_error(ssl, ret); - err.code = impl::map_ssl_error(ssl_err, err.sys_errno); - err.backend_code = ERR_get_error(); - } - return err; -} - -bool connect_nonblocking(session_t session, socket_t sock, - time_t timeout_sec, time_t timeout_usec, - TlsError *err) { - if (!session) { - if (err) { err->code = ErrorCode::Fatal; } - return false; - } - - auto ssl = static_cast(session); - auto bio = SSL_get_rbio(ssl); - - // Set non-blocking mode for handshake - detail::set_nonblocking(sock, true); - if (bio) { BIO_set_nbio(bio, 1); } - - auto cleanup = detail::scope_exit([&]() { - // Restore blocking mode after handshake - if (bio) { BIO_set_nbio(bio, 0); } - detail::set_nonblocking(sock, false); - }); - - auto res = 0; - while ((res = SSL_connect(ssl)) != 1) { - auto ssl_err = SSL_get_error(ssl, res); - switch (ssl_err) { - case SSL_ERROR_WANT_READ: - if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) { - continue; - } - break; - case SSL_ERROR_WANT_WRITE: - if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) { - continue; - } - break; - default: break; - } - if (err) { - err->code = impl::map_ssl_error(ssl_err, err->sys_errno); - err->backend_code = ERR_get_error(); - } - return false; - } - if (err) { err->code = ErrorCode::Success; } - return true; -} - -bool accept_nonblocking(session_t session, socket_t sock, - time_t timeout_sec, time_t timeout_usec, - TlsError *err) { - if (!session) { - if (err) { err->code = ErrorCode::Fatal; } - return false; - } - - auto ssl = static_cast(session); - auto bio = SSL_get_rbio(ssl); - - // Set non-blocking mode for handshake - detail::set_nonblocking(sock, true); - if (bio) { BIO_set_nbio(bio, 1); } - - auto cleanup = detail::scope_exit([&]() { - // Restore blocking mode after handshake - if (bio) { BIO_set_nbio(bio, 0); } - detail::set_nonblocking(sock, false); - }); - - auto res = 0; - while ((res = SSL_accept(ssl)) != 1) { - auto ssl_err = SSL_get_error(ssl, res); - switch (ssl_err) { - case SSL_ERROR_WANT_READ: - if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) { - continue; - } - break; - case SSL_ERROR_WANT_WRITE: - if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) { - continue; - } - break; - default: break; - } - if (err) { - err->code = impl::map_ssl_error(ssl_err, err->sys_errno); - err->backend_code = ERR_get_error(); - } - return false; - } - if (err) { err->code = ErrorCode::Success; } - return true; -} - -ssize_t read(session_t session, void *buf, size_t len, TlsError &err) { - if (!session || !buf) { - err.code = ErrorCode::Fatal; - return -1; - } - - auto ssl = static_cast(session); - constexpr auto max_len = - static_cast((std::numeric_limits::max)()); - if (len > max_len) { len = max_len; } - auto ret = SSL_read(ssl, buf, static_cast(len)); - - if (ret > 0) { - err.code = ErrorCode::Success; - return ret; - } - - auto ssl_err = SSL_get_error(ssl, ret); - err.code = impl::map_ssl_error(ssl_err, err.sys_errno); - if (err.code == ErrorCode::PeerClosed) { - return 0; - } // Gracefully handle the peer closed state. - if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); } - return -1; -} - -ssize_t write(session_t session, const void *buf, size_t len, - TlsError &err) { - if (!session || !buf) { - err.code = ErrorCode::Fatal; - return -1; - } - - auto ssl = static_cast(session); - auto ret = SSL_write(ssl, buf, static_cast(len)); - - if (ret > 0) { - err.code = ErrorCode::Success; - return ret; - } - - auto ssl_err = SSL_get_error(ssl, ret); - err.code = impl::map_ssl_error(ssl_err, err.sys_errno); - if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); } - return -1; -} - -int pending(const_session_t session) { - if (!session) return 0; - return SSL_pending(static_cast(const_cast(session))); -} - -void shutdown(session_t session, bool graceful) { - if (!session) return; - - auto ssl = static_cast(session); - if (graceful) { - // First call sends close_notify - if (SSL_shutdown(ssl) == 0) { - // Second call waits for peer's close_notify - SSL_shutdown(ssl); - } - } -} - -bool is_peer_closed(session_t session, socket_t sock) { - if (!session) return true; - - // Temporarily set socket to non-blocking to avoid blocking on SSL_peek - detail::set_nonblocking(sock, true); - auto se = detail::scope_exit([&]() { detail::set_nonblocking(sock, false); }); - - auto ssl = static_cast(session); - char buf; - auto ret = SSL_peek(ssl, &buf, 1); - if (ret > 0) return false; - - auto err = SSL_get_error(ssl, ret); - return err == SSL_ERROR_ZERO_RETURN; -} - -cert_t get_peer_cert(const_session_t session) { - if (!session) return nullptr; - return static_cast(SSL_get1_peer_certificate( - static_cast(const_cast(session)))); -} - -void free_cert(cert_t cert) { - if (cert) { X509_free(static_cast(cert)); } -} - -bool verify_hostname(cert_t cert, const char *hostname) { - if (!cert || !hostname) return false; - - auto x509 = static_cast(cert); - - // Use X509_check_ip_asc for IP addresses, X509_check_host for DNS names - if (detail::is_ip_address(hostname)) { - return X509_check_ip_asc(x509, hostname, 0) == 1; - } - return X509_check_host(x509, hostname, strlen(hostname), 0, nullptr) == 1; -} - -uint64_t hostname_mismatch_code() { - return static_cast(X509_V_ERR_HOSTNAME_MISMATCH); -} - -long get_verify_result(const_session_t session) { - if (!session) return X509_V_ERR_UNSPECIFIED; - return SSL_get_verify_result(static_cast(const_cast(session))); -} - -std::string get_cert_subject_cn(cert_t cert) { - if (!cert) return ""; - auto x509 = static_cast(cert); - auto subject_name = X509_get_subject_name(x509); - if (!subject_name) return ""; - - char buf[256]; - auto len = - X509_NAME_get_text_by_NID(subject_name, NID_commonName, buf, sizeof(buf)); - if (len < 0) return ""; - return std::string(buf, static_cast(len)); -} - -std::string get_cert_issuer_name(cert_t cert) { - if (!cert) return ""; - auto x509 = static_cast(cert); - auto issuer_name = X509_get_issuer_name(x509); - if (!issuer_name) return ""; - - char buf[256]; - X509_NAME_oneline(issuer_name, buf, sizeof(buf)); - return std::string(buf); -} - -bool get_cert_sans(cert_t cert, std::vector &sans) { - sans.clear(); - if (!cert) return false; - auto x509 = static_cast(cert); - - auto names = static_cast( - X509_get_ext_d2i(x509, NID_subject_alt_name, nullptr, nullptr)); - if (!names) return true; // No SANs is valid - - auto count = sk_GENERAL_NAME_num(names); - for (decltype(count) i = 0; i < count; i++) { - auto gen = sk_GENERAL_NAME_value(names, i); - if (!gen) continue; - - SanEntry entry; - switch (gen->type) { - case GEN_DNS: - entry.type = SanType::DNS; - if (gen->d.dNSName) { - entry.value = std::string( - reinterpret_cast( - ASN1_STRING_get0_data(gen->d.dNSName)), - static_cast(ASN1_STRING_length(gen->d.dNSName))); - } - break; - case GEN_IPADD: - entry.type = SanType::IP; - if (gen->d.iPAddress) { - auto data = ASN1_STRING_get0_data(gen->d.iPAddress); - auto len = ASN1_STRING_length(gen->d.iPAddress); - if (len == 4) { - // IPv4 - char buf[INET_ADDRSTRLEN]; - inet_ntop(AF_INET, data, buf, sizeof(buf)); - entry.value = buf; - } else if (len == 16) { - // IPv6 - char buf[INET6_ADDRSTRLEN]; - inet_ntop(AF_INET6, data, buf, sizeof(buf)); - entry.value = buf; - } - } - break; - case GEN_EMAIL: - entry.type = SanType::EMAIL; - if (gen->d.rfc822Name) { - entry.value = std::string( - reinterpret_cast( - ASN1_STRING_get0_data(gen->d.rfc822Name)), - static_cast(ASN1_STRING_length(gen->d.rfc822Name))); - } - break; - case GEN_URI: - entry.type = SanType::URI; - if (gen->d.uniformResourceIdentifier) { - entry.value = std::string( - reinterpret_cast( - ASN1_STRING_get0_data(gen->d.uniformResourceIdentifier)), - static_cast( - ASN1_STRING_length(gen->d.uniformResourceIdentifier))); - } - break; - default: entry.type = SanType::OTHER; break; - } - - if (!entry.value.empty()) { sans.push_back(std::move(entry)); } - } - - GENERAL_NAMES_free(names); - return true; -} - -bool get_cert_validity(cert_t cert, time_t ¬_before, - time_t ¬_after) { - if (!cert) return false; - auto x509 = static_cast(cert); - - auto nb = X509_get0_notBefore(x509); - auto na = X509_get0_notAfter(x509); - if (!nb || !na) return false; - - ASN1_TIME *epoch = ASN1_TIME_new(); - if (!epoch) return false; - auto se = detail::scope_exit([&] { ASN1_TIME_free(epoch); }); - - if (!ASN1_TIME_set(epoch, 0)) return false; - - int pday, psec; - - if (!ASN1_TIME_diff(&pday, &psec, epoch, nb)) return false; - not_before = 86400 * (time_t)pday + psec; - - if (!ASN1_TIME_diff(&pday, &psec, epoch, na)) return false; - not_after = 86400 * (time_t)pday + psec; - - return true; -} - -std::string get_cert_serial(cert_t cert) { - if (!cert) return ""; - auto x509 = static_cast(cert); - - auto serial = X509_get_serialNumber(x509); - if (!serial) return ""; - - auto bn = ASN1_INTEGER_to_BN(serial, nullptr); - if (!bn) return ""; - - auto hex = BN_bn2hex(bn); - BN_free(bn); - if (!hex) return ""; - - std::string result(hex); - OPENSSL_free(hex); - return result; -} - -bool get_cert_der(cert_t cert, std::vector &der) { - if (!cert) return false; - auto x509 = static_cast(cert); - auto len = i2d_X509(x509, nullptr); - if (len < 0) return false; - der.resize(static_cast(len)); - auto p = der.data(); - i2d_X509(x509, &p); - return true; -} - -const char *get_sni(const_session_t session) { - if (!session) return nullptr; - auto ssl = static_cast(const_cast(session)); - return SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); -} - -uint64_t peek_error() { return ERR_peek_last_error(); } - -uint64_t get_error() { return ERR_get_error(); } - -std::string error_string(uint64_t code) { - char buf[256]; - ERR_error_string_n(static_cast(code), buf, sizeof(buf)); - return std::string(buf); -} - -ca_store_t create_ca_store(const char *pem, size_t len) { - auto mem = BIO_new_mem_buf(pem, static_cast(len)); - if (!mem) { return nullptr; } - auto mem_guard = detail::scope_exit([&] { BIO_free_all(mem); }); - - auto inf = PEM_X509_INFO_read_bio(mem, nullptr, nullptr, nullptr); - if (!inf) { return nullptr; } - - auto store = X509_STORE_new(); - if (store) { - for (auto i = 0; i < static_cast(sk_X509_INFO_num(inf)); i++) { - auto itmp = sk_X509_INFO_value(inf, i); - if (!itmp) { continue; } - if (itmp->x509) { X509_STORE_add_cert(store, itmp->x509); } - if (itmp->crl) { X509_STORE_add_crl(store, itmp->crl); } - } - } - - sk_X509_INFO_pop_free(inf, X509_INFO_free); - return static_cast(store); -} - -void free_ca_store(ca_store_t store) { - if (store) { X509_STORE_free(static_cast(store)); } -} - -bool set_ca_store(ctx_t ctx, ca_store_t store) { - if (!ctx || !store) { return false; } - auto ssl_ctx = static_cast(ctx); - auto x509_store = static_cast(store); - - // Check if same store is already set - if (SSL_CTX_get_cert_store(ssl_ctx) == x509_store) { return true; } - - // SSL_CTX_set_cert_store takes ownership and frees the old store - SSL_CTX_set_cert_store(ssl_ctx, x509_store); - return true; -} - -size_t get_ca_certs(ctx_t ctx, std::vector &certs) { - certs.clear(); - if (!ctx) { return 0; } - auto ssl_ctx = static_cast(ctx); - - auto store = SSL_CTX_get_cert_store(ssl_ctx); - if (!store) { return 0; } - - auto objs = X509_STORE_get0_objects(store); - if (!objs) { return 0; } - - auto count = sk_X509_OBJECT_num(objs); - for (decltype(count) i = 0; i < count; i++) { - auto obj = sk_X509_OBJECT_value(objs, i); - if (!obj) { continue; } - if (X509_OBJECT_get_type(obj) == X509_LU_X509) { - auto x509 = X509_OBJECT_get0_X509(obj); - if (x509) { - // Increment reference count so caller can free it - X509_up_ref(x509); - certs.push_back(static_cast(x509)); - } - } - } - return certs.size(); -} - -std::vector get_ca_names(ctx_t ctx) { - std::vector names; - if (!ctx) { return names; } - auto ssl_ctx = static_cast(ctx); - - auto store = SSL_CTX_get_cert_store(ssl_ctx); - if (!store) { return names; } - - auto objs = X509_STORE_get0_objects(store); - if (!objs) { return names; } - - auto count = sk_X509_OBJECT_num(objs); - for (decltype(count) i = 0; i < count; i++) { - auto obj = sk_X509_OBJECT_value(objs, i); - if (!obj) { continue; } - if (X509_OBJECT_get_type(obj) == X509_LU_X509) { - auto x509 = X509_OBJECT_get0_X509(obj); - if (x509) { - auto subject = X509_get_subject_name(x509); - if (subject) { - char buf[512]; - X509_NAME_oneline(subject, buf, sizeof(buf)); - names.push_back(buf); - } - } - } - } - return names; -} - -bool update_server_cert(ctx_t ctx, const char *cert_pem, - const char *key_pem, const char *password) { - if (!ctx || !cert_pem || !key_pem) { return false; } - auto ssl_ctx = static_cast(ctx); - - // Load certificate from PEM - auto cert_bio = BIO_new_mem_buf(cert_pem, -1); - if (!cert_bio) { return false; } - auto cert = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr); - BIO_free(cert_bio); - if (!cert) { return false; } - - // Load private key from PEM - auto key_bio = BIO_new_mem_buf(key_pem, -1); - if (!key_bio) { - X509_free(cert); - return false; - } - auto key = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, - password ? const_cast(password) - : nullptr); - BIO_free(key_bio); - if (!key) { - X509_free(cert); - return false; - } - - // Update certificate and key - auto ret = SSL_CTX_use_certificate(ssl_ctx, cert) == 1 && - SSL_CTX_use_PrivateKey(ssl_ctx, key) == 1; - - X509_free(cert); - EVP_PKEY_free(key); - return ret; -} - -bool update_server_client_ca(ctx_t ctx, const char *ca_pem) { - if (!ctx || !ca_pem) { return false; } - auto ssl_ctx = static_cast(ctx); - - // Create new X509_STORE from PEM - auto store = create_ca_store(ca_pem, strlen(ca_pem)); - if (!store) { return false; } - - // SSL_CTX_set_cert_store takes ownership - SSL_CTX_set_cert_store(ssl_ctx, static_cast(store)); - - // Set client CA list for client certificate request - auto ca_list = impl::create_client_ca_list_from_pem(ca_pem); - if (ca_list) { - // SSL_CTX_set_client_CA_list takes ownership of ca_list - SSL_CTX_set_client_CA_list(ssl_ctx, ca_list); - } - - return true; -} - -bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { - if (!ctx) { return false; } - auto ssl_ctx = static_cast(ctx); - - impl::get_verify_callback() = std::move(callback); - - if (impl::get_verify_callback()) { - SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, impl::openssl_verify_callback); - } else { - SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, nullptr); - } - return true; -} - -long get_verify_error(const_session_t session) { - if (!session) { return -1; } - auto ssl = static_cast(const_cast(session)); - return SSL_get_verify_result(ssl); -} - -std::string verify_error_string(long error_code) { - if (error_code == X509_V_OK) { return ""; } - const char *str = X509_verify_cert_error_string(static_cast(error_code)); - return str ? str : "unknown error"; -} - -} // namespace tls - -bool SSLClient::verify_host(X509 *server_cert) const { - /* Quote from RFC2818 section 3.1 "Server Identity" - - If a subjectAltName extension of type dNSName is present, that MUST - be used as the identity. Otherwise, the (most specific) Common Name - field in the Subject field of the certificate MUST be used. Although - the use of the Common Name is existing practice, it is deprecated and - Certification Authorities are encouraged to use the dNSName instead. - - Matching is performed using the matching rules specified by - [RFC2459]. If more than one identity of a given type is present in - the certificate (e.g., more than one dNSName name, a match in any one - of the set is considered acceptable.) Names may contain the wildcard - character * which is considered to match any single domain name - component or component fragment. E.g., *.a.com matches foo.a.com but - not bar.foo.a.com. f*.com matches foo.com but not bar.com. - - In some cases, the URI is specified as an IP address rather than a - hostname. In this case, the iPAddress subjectAltName must be present - in the certificate and must exactly match the IP in the URI. - - */ - return verify_host_with_subject_alt_name(server_cert) || - verify_host_with_common_name(server_cert); -} - -bool -SSLClient::verify_host_with_subject_alt_name(X509 *server_cert) const { - auto ret = false; - - auto type = GEN_DNS; - - struct in6_addr addr6 = {}; - struct in_addr addr = {}; - size_t addr_len = 0; - -#ifndef __MINGW32__ - if (inet_pton(AF_INET6, host_.c_str(), &addr6)) { - type = GEN_IPADD; - addr_len = sizeof(struct in6_addr); - } else if (inet_pton(AF_INET, host_.c_str(), &addr)) { - type = GEN_IPADD; - addr_len = sizeof(struct in_addr); - } -#endif - - auto alt_names = static_cast( - X509_get_ext_d2i(server_cert, NID_subject_alt_name, nullptr, nullptr)); - - if (alt_names) { - auto dsn_matched = false; - auto ip_matched = false; - - auto count = sk_GENERAL_NAME_num(alt_names); - - for (decltype(count) i = 0; i < count && !dsn_matched; i++) { - auto val = sk_GENERAL_NAME_value(alt_names, i); - if (!val || val->type != type) { continue; } - - auto name = - reinterpret_cast(ASN1_STRING_get0_data(val->d.ia5)); - if (name == nullptr) { continue; } - - auto name_len = static_cast(ASN1_STRING_length(val->d.ia5)); - - switch (type) { - case GEN_DNS: - dsn_matched = - detail::match_hostname(std::string(name, name_len), host_); - break; - - case GEN_IPADD: - if (!memcmp(&addr6, name, addr_len) || !memcmp(&addr, name, addr_len)) { - ip_matched = true; - } - break; - } - } - - if (dsn_matched || ip_matched) { ret = true; } - } - - GENERAL_NAMES_free(const_cast( - reinterpret_cast(alt_names))); - return ret; -} - -bool SSLClient::verify_host_with_common_name(X509 *server_cert) const { - const auto subject_name = X509_get_subject_name(server_cert); - - if (subject_name != nullptr) { - char name[BUFSIZ]; - auto name_len = X509_NAME_get_text_by_NID(subject_name, NID_commonName, - name, sizeof(name)); - - if (name_len != -1) { - return detail::match_hostname( - std::string(name, static_cast(name_len)), host_); - } - } - - return false; -} - -#endif // CPPHTTPLIB_OPENSSL_SUPPORT - -/* - * Group 9: TLS abstraction layer - Mbed TLS backend - */ - -/* - * Mbed TLS Backend Implementation - */ - -#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT -namespace tls { - -namespace impl { - -// Mbed TLS session wrapper -struct MbedTlsSession { - mbedtls_ssl_context ssl; - socket_t sock = INVALID_SOCKET; - std::string hostname; // For client: set via set_sni - std::string sni_hostname; // For server: received from client via SNI callback - - MbedTlsSession() { mbedtls_ssl_init(&ssl); } - - ~MbedTlsSession() { mbedtls_ssl_free(&ssl); } - - MbedTlsSession(const MbedTlsSession &) = delete; - MbedTlsSession &operator=(const MbedTlsSession &) = delete; -}; - -// Thread-local error code accessor for Mbed TLS (since it doesn't have an error -// queue) -int &mbedtls_last_error() { - static thread_local int err = 0; - return err; -} - -// Helper to map Mbed TLS error to ErrorCode -ErrorCode map_mbedtls_error(int ret, int &out_errno) { - if (ret == 0) { return ErrorCode::Success; } - if (ret == MBEDTLS_ERR_SSL_WANT_READ) { return ErrorCode::WantRead; } - if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) { return ErrorCode::WantWrite; } - if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) { - return ErrorCode::PeerClosed; - } - if (ret == MBEDTLS_ERR_NET_CONN_RESET || ret == MBEDTLS_ERR_NET_SEND_FAILED || - ret == MBEDTLS_ERR_NET_RECV_FAILED) { - out_errno = errno; - return ErrorCode::SyscallError; - } - if (ret == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) { - return ErrorCode::CertVerifyFailed; - } - return ErrorCode::Fatal; -} - -// BIO-like send callback for Mbed TLS -int mbedtls_net_send_cb(void *ctx, const unsigned char *buf, - size_t len) { - auto sock = *static_cast(ctx); -#ifdef _WIN32 - auto ret = - send(sock, reinterpret_cast(buf), static_cast(len), 0); - if (ret == SOCKET_ERROR) { - int err = WSAGetLastError(); - if (err == WSAEWOULDBLOCK) { return MBEDTLS_ERR_SSL_WANT_WRITE; } - return MBEDTLS_ERR_NET_SEND_FAILED; - } -#else - auto ret = send(sock, buf, len, 0); - if (ret < 0) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - return MBEDTLS_ERR_SSL_WANT_WRITE; - } - return MBEDTLS_ERR_NET_SEND_FAILED; - } -#endif - return static_cast(ret); -} - -// BIO-like recv callback for Mbed TLS -int mbedtls_net_recv_cb(void *ctx, unsigned char *buf, size_t len) { - auto sock = *static_cast(ctx); -#ifdef _WIN32 - auto ret = - recv(sock, reinterpret_cast(buf), static_cast(len), 0); - if (ret == SOCKET_ERROR) { - int err = WSAGetLastError(); - if (err == WSAEWOULDBLOCK) { return MBEDTLS_ERR_SSL_WANT_READ; } - return MBEDTLS_ERR_NET_RECV_FAILED; - } -#else - auto ret = recv(sock, buf, len, 0); - if (ret < 0) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { - return MBEDTLS_ERR_SSL_WANT_READ; - } - return MBEDTLS_ERR_NET_RECV_FAILED; - } -#endif - if (ret == 0) { return MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY; } - return static_cast(ret); -} - -// MbedTlsContext constructor/destructor implementations -MbedTlsContext::MbedTlsContext() { - mbedtls_ssl_config_init(&conf); - mbedtls_entropy_init(&entropy); - mbedtls_ctr_drbg_init(&ctr_drbg); - mbedtls_x509_crt_init(&ca_chain); - mbedtls_x509_crt_init(&own_cert); - mbedtls_pk_init(&own_key); -} - -MbedTlsContext::~MbedTlsContext() { - mbedtls_pk_free(&own_key); - mbedtls_x509_crt_free(&own_cert); - mbedtls_x509_crt_free(&ca_chain); - mbedtls_ctr_drbg_free(&ctr_drbg); - mbedtls_entropy_free(&entropy); - mbedtls_ssl_config_free(&conf); -} - -// Thread-local storage for SNI captured during handshake -// This is needed because the SNI callback doesn't have a way to pass -// session-specific data before the session is fully set up -std::string &mbedpending_sni() { - static thread_local std::string sni; - return sni; -} - -// SNI callback for Mbed TLS server to capture client's SNI hostname -int mbedtls_sni_callback(void *p_ctx, mbedtls_ssl_context *ssl, - const unsigned char *name, size_t name_len) { - (void)p_ctx; - (void)ssl; - - // Store SNI name in thread-local storage - // It will be retrieved and stored in the session after handshake - if (name && name_len > 0) { - mbedpending_sni().assign(reinterpret_cast(name), name_len); - } else { - mbedpending_sni().clear(); - } - return 0; // Accept any SNI -} - -int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt, - int cert_depth, uint32_t *flags); - -// MbedTLS verify callback wrapper -int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt, - int cert_depth, uint32_t *flags) { - auto &callback = get_verify_callback(); - if (!callback) { return 0; } // Continue with default verification - - // data points to the MbedTlsSession - auto *session = static_cast(data); - - // Build context - VerifyContext verify_ctx; - verify_ctx.session = static_cast(session); - verify_ctx.cert = static_cast(crt); - verify_ctx.depth = cert_depth; - verify_ctx.preverify_ok = (*flags == 0); - verify_ctx.error_code = static_cast(*flags); - - // Convert Mbed TLS flags to error string - static thread_local char error_buf[256]; - if (*flags != 0) { - mbedtls_x509_crt_verify_info(error_buf, sizeof(error_buf), "", *flags); - verify_ctx.error_string = error_buf; - } else { - verify_ctx.error_string = nullptr; - } - - bool accepted = callback(verify_ctx); - - if (accepted) { - *flags = 0; // Clear all error flags - return 0; - } - return MBEDTLS_ERR_X509_CERT_VERIFY_FAILED; -} - -} // namespace impl - -ctx_t create_client_context() { - auto ctx = new (std::nothrow) impl::MbedTlsContext(); - if (!ctx) { return nullptr; } - - ctx->is_server = false; - - // Seed the random number generator - const char *pers = "httplib_client"; - int ret = mbedtls_ctr_drbg_seed( - &ctx->ctr_drbg, mbedtls_entropy_func, &ctx->entropy, - reinterpret_cast(pers), strlen(pers)); - if (ret != 0) { - impl::mbedtls_last_error() = ret; - delete ctx; - return nullptr; - } - - // Set up SSL config for client - ret = mbedtls_ssl_config_defaults(&ctx->conf, MBEDTLS_SSL_IS_CLIENT, - MBEDTLS_SSL_TRANSPORT_STREAM, - MBEDTLS_SSL_PRESET_DEFAULT); - if (ret != 0) { - impl::mbedtls_last_error() = ret; - delete ctx; - return nullptr; - } - - // Set random number generator - mbedtls_ssl_conf_rng(&ctx->conf, mbedtls_ctr_drbg_random, &ctx->ctr_drbg); - - // Default: verify peer certificate - mbedtls_ssl_conf_authmode(&ctx->conf, MBEDTLS_SSL_VERIFY_REQUIRED); - - // Set minimum TLS version to 1.2 -#ifdef CPPHTTPLIB_MBEDTLS_V3 - mbedtls_ssl_conf_min_tls_version(&ctx->conf, MBEDTLS_SSL_VERSION_TLS1_2); -#else - mbedtls_ssl_conf_min_version(&ctx->conf, MBEDTLS_SSL_MAJOR_VERSION_3, - MBEDTLS_SSL_MINOR_VERSION_3); -#endif - - return static_cast(ctx); -} - -ctx_t create_server_context() { - auto ctx = new (std::nothrow) impl::MbedTlsContext(); - if (!ctx) { return nullptr; } - - ctx->is_server = true; - - // Seed the random number generator - const char *pers = "httplib_server"; - int ret = mbedtls_ctr_drbg_seed( - &ctx->ctr_drbg, mbedtls_entropy_func, &ctx->entropy, - reinterpret_cast(pers), strlen(pers)); - if (ret != 0) { - impl::mbedtls_last_error() = ret; - delete ctx; - return nullptr; - } - - // Set up SSL config for server - ret = mbedtls_ssl_config_defaults(&ctx->conf, MBEDTLS_SSL_IS_SERVER, - MBEDTLS_SSL_TRANSPORT_STREAM, - MBEDTLS_SSL_PRESET_DEFAULT); - if (ret != 0) { - impl::mbedtls_last_error() = ret; - delete ctx; - return nullptr; - } - - // Set random number generator - mbedtls_ssl_conf_rng(&ctx->conf, mbedtls_ctr_drbg_random, &ctx->ctr_drbg); - - // Default: don't verify client - mbedtls_ssl_conf_authmode(&ctx->conf, MBEDTLS_SSL_VERIFY_NONE); - - // Set minimum TLS version to 1.2 -#ifdef CPPHTTPLIB_MBEDTLS_V3 - mbedtls_ssl_conf_min_tls_version(&ctx->conf, MBEDTLS_SSL_VERSION_TLS1_2); -#else - mbedtls_ssl_conf_min_version(&ctx->conf, MBEDTLS_SSL_MAJOR_VERSION_3, - MBEDTLS_SSL_MINOR_VERSION_3); -#endif - - // Set SNI callback to capture client's SNI hostname - mbedtls_ssl_conf_sni(&ctx->conf, impl::mbedtls_sni_callback, nullptr); - - return static_cast(ctx); -} - -void free_context(ctx_t ctx) { - if (ctx) { delete static_cast(ctx); } -} - -bool set_min_version(ctx_t ctx, Version version) { - if (!ctx) { return false; } - auto mctx = static_cast(ctx); - -#ifdef CPPHTTPLIB_MBEDTLS_V3 - // Mbed TLS 3.x uses mbedtls_ssl_protocol_version enum - mbedtls_ssl_protocol_version min_ver = MBEDTLS_SSL_VERSION_TLS1_2; - if (version >= Version::TLS1_3) { -#if defined(MBEDTLS_SSL_PROTO_TLS1_3) - min_ver = MBEDTLS_SSL_VERSION_TLS1_3; -#endif - } - mbedtls_ssl_conf_min_tls_version(&mctx->conf, min_ver); -#else - // Mbed TLS 2.x uses major/minor version numbers - int major = MBEDTLS_SSL_MAJOR_VERSION_3; - int minor = MBEDTLS_SSL_MINOR_VERSION_3; // TLS 1.2 - if (version >= Version::TLS1_3) { -#if defined(MBEDTLS_SSL_PROTO_TLS1_3) - minor = MBEDTLS_SSL_MINOR_VERSION_4; // TLS 1.3 -#else - minor = MBEDTLS_SSL_MINOR_VERSION_3; // Fall back to TLS 1.2 -#endif - } - mbedtls_ssl_conf_min_version(&mctx->conf, major, minor); -#endif - return true; -} - -bool load_ca_pem(ctx_t ctx, const char *pem, size_t len) { - if (!ctx || !pem) { return false; } - auto mctx = static_cast(ctx); - - // mbedtls_x509_crt_parse expects null-terminated string for PEM - // Add null terminator if not present - std::string pem_str(pem, len); - int ret = mbedtls_x509_crt_parse( - &mctx->ca_chain, reinterpret_cast(pem_str.c_str()), - pem_str.size() + 1); - if (ret != 0) { - impl::mbedtls_last_error() = ret; - return false; - } - - mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr); - return true; -} - -bool load_ca_file(ctx_t ctx, const char *file_path) { - if (!ctx || !file_path) { return false; } - auto mctx = static_cast(ctx); - - int ret = mbedtls_x509_crt_parse_file(&mctx->ca_chain, file_path); - if (ret != 0) { - impl::mbedtls_last_error() = ret; - return false; - } - - mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr); - return true; -} - -bool load_ca_dir(ctx_t ctx, const char *dir_path) { - if (!ctx || !dir_path) { return false; } - auto mctx = static_cast(ctx); - - int ret = mbedtls_x509_crt_parse_path(&mctx->ca_chain, dir_path); - if (ret < 0) { // Returns number of certs on success, negative on error - impl::mbedtls_last_error() = ret; - return false; - } - - mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr); - return true; -} - -bool load_system_certs(ctx_t ctx) { - if (!ctx) { return false; } - auto mctx = static_cast(ctx); - bool loaded = false; - -#ifdef _WIN32 - loaded = impl::enumerate_windows_system_certs( - [&](const unsigned char *data, size_t len) { - return mbedtls_x509_crt_parse_der(&mctx->ca_chain, data, len) == 0; - }); -#elif defined(__APPLE__) && defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) - loaded = impl::enumerate_macos_keychain_certs( - [&](const unsigned char *data, size_t len) { - return mbedtls_x509_crt_parse_der(&mctx->ca_chain, data, len) == 0; - }); -#else - for (auto path = impl::system_ca_paths(); *path; ++path) { - if (mbedtls_x509_crt_parse_file(&mctx->ca_chain, *path) >= 0) { - loaded = true; - break; - } - } - - if (!loaded) { - for (auto dir = impl::system_ca_dirs(); *dir; ++dir) { - if (mbedtls_x509_crt_parse_path(&mctx->ca_chain, *dir) >= 0) { - loaded = true; - break; - } - } - } -#endif - - if (loaded) { - mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr); - } - return loaded; -} - -bool set_client_cert_pem(ctx_t ctx, const char *cert, const char *key, - const char *password) { - if (!ctx || !cert || !key) { return false; } - auto mctx = static_cast(ctx); - - // Parse certificate - std::string cert_str(cert); - int ret = mbedtls_x509_crt_parse( - &mctx->own_cert, - reinterpret_cast(cert_str.c_str()), - cert_str.size() + 1); - if (ret != 0) { - impl::mbedtls_last_error() = ret; - return false; - } - - // Parse private key - std::string key_str(key); - const unsigned char *pwd = - password ? reinterpret_cast(password) : nullptr; - size_t pwd_len = password ? strlen(password) : 0; - -#ifdef CPPHTTPLIB_MBEDTLS_V3 - ret = mbedtls_pk_parse_key( - &mctx->own_key, reinterpret_cast(key_str.c_str()), - key_str.size() + 1, pwd, pwd_len, mbedtls_ctr_drbg_random, - &mctx->ctr_drbg); -#else - ret = mbedtls_pk_parse_key( - &mctx->own_key, reinterpret_cast(key_str.c_str()), - key_str.size() + 1, pwd, pwd_len); -#endif - if (ret != 0) { - impl::mbedtls_last_error() = ret; - return false; - } - - // Verify that the certificate and private key match -#ifdef CPPHTTPLIB_MBEDTLS_V3 - ret = mbedtls_pk_check_pair(&mctx->own_cert.pk, &mctx->own_key, - mbedtls_ctr_drbg_random, &mctx->ctr_drbg); -#else - ret = mbedtls_pk_check_pair(&mctx->own_cert.pk, &mctx->own_key); -#endif - if (ret != 0) { - impl::mbedtls_last_error() = ret; - return false; - } - - ret = mbedtls_ssl_conf_own_cert(&mctx->conf, &mctx->own_cert, &mctx->own_key); - if (ret != 0) { - impl::mbedtls_last_error() = ret; - return false; - } - - return true; -} - -bool set_client_cert_file(ctx_t ctx, const char *cert_path, - const char *key_path, const char *password) { - if (!ctx || !cert_path || !key_path) { return false; } - auto mctx = static_cast(ctx); - - // Parse certificate file - int ret = mbedtls_x509_crt_parse_file(&mctx->own_cert, cert_path); - if (ret != 0) { - impl::mbedtls_last_error() = ret; - return false; - } - - // Parse private key file -#ifdef CPPHTTPLIB_MBEDTLS_V3 - ret = mbedtls_pk_parse_keyfile(&mctx->own_key, key_path, password, - mbedtls_ctr_drbg_random, &mctx->ctr_drbg); -#else - ret = mbedtls_pk_parse_keyfile(&mctx->own_key, key_path, password); -#endif - if (ret != 0) { - impl::mbedtls_last_error() = ret; - return false; - } - - // Verify that the certificate and private key match -#ifdef CPPHTTPLIB_MBEDTLS_V3 - ret = mbedtls_pk_check_pair(&mctx->own_cert.pk, &mctx->own_key, - mbedtls_ctr_drbg_random, &mctx->ctr_drbg); -#else - ret = mbedtls_pk_check_pair(&mctx->own_cert.pk, &mctx->own_key); -#endif - if (ret != 0) { - impl::mbedtls_last_error() = ret; - return false; - } - - ret = mbedtls_ssl_conf_own_cert(&mctx->conf, &mctx->own_cert, &mctx->own_key); - if (ret != 0) { - impl::mbedtls_last_error() = ret; - return false; - } - - return true; -} - -void set_verify_client(ctx_t ctx, bool require) { - if (!ctx) { return; } - auto mctx = static_cast(ctx); - mctx->verify_client = require; - if (require) { - mbedtls_ssl_conf_authmode(&mctx->conf, MBEDTLS_SSL_VERIFY_REQUIRED); - } else { - // If a verify callback is set, use OPTIONAL mode to ensure the callback - // is called (matching OpenSSL behavior). Otherwise use NONE. - mbedtls_ssl_conf_authmode(&mctx->conf, mctx->has_verify_callback - ? MBEDTLS_SSL_VERIFY_OPTIONAL - : MBEDTLS_SSL_VERIFY_NONE); - } -} - -session_t create_session(ctx_t ctx, socket_t sock) { - if (!ctx || sock == INVALID_SOCKET) { return nullptr; } - auto mctx = static_cast(ctx); - - auto session = new (std::nothrow) impl::MbedTlsSession(); - if (!session) { return nullptr; } - - session->sock = sock; - - int ret = mbedtls_ssl_setup(&session->ssl, &mctx->conf); - if (ret != 0) { - impl::mbedtls_last_error() = ret; - delete session; - return nullptr; - } - - // Set BIO callbacks - mbedtls_ssl_set_bio(&session->ssl, &session->sock, impl::mbedtls_net_send_cb, - impl::mbedtls_net_recv_cb, nullptr); - - // Set per-session verify callback with session pointer if callback is - // registered - if (mctx->has_verify_callback) { - mbedtls_ssl_set_verify(&session->ssl, impl::mbedtls_verify_callback, - session); - } - - return static_cast(session); -} - -void free_session(session_t session) { - if (session) { delete static_cast(session); } -} - -bool set_sni(session_t session, const char *hostname) { - if (!session || !hostname) { return false; } - auto msession = static_cast(session); - - int ret = mbedtls_ssl_set_hostname(&msession->ssl, hostname); - if (ret != 0) { - impl::mbedtls_last_error() = ret; - return false; - } - - msession->hostname = hostname; - return true; -} - -bool set_hostname(session_t session, const char *hostname) { - // In Mbed TLS, set_hostname also sets up hostname verification - return set_sni(session, hostname); -} - -TlsError connect(session_t session) { - TlsError err; - if (!session) { - err.code = ErrorCode::Fatal; - return err; - } - - auto msession = static_cast(session); - int ret = mbedtls_ssl_handshake(&msession->ssl); - - if (ret == 0) { - err.code = ErrorCode::Success; - } else { - err.code = impl::map_mbedtls_error(ret, err.sys_errno); - err.backend_code = static_cast(-ret); - impl::mbedtls_last_error() = ret; - } - - return err; -} - -TlsError accept(session_t session) { - // Same as connect for Mbed TLS - handshake works for both client and server - auto result = connect(session); - - // After successful handshake, capture SNI from thread-local storage - if (result.code == ErrorCode::Success && session) { - auto msession = static_cast(session); - msession->sni_hostname = std::move(impl::mbedpending_sni()); - impl::mbedpending_sni().clear(); - } - - return result; -} - -bool connect_nonblocking(session_t session, socket_t sock, - time_t timeout_sec, time_t timeout_usec, - TlsError *err) { - if (!session) { - if (err) { err->code = ErrorCode::Fatal; } - return false; - } - - auto msession = static_cast(session); - - // Set socket to non-blocking mode - detail::set_nonblocking(sock, true); - auto cleanup = - detail::scope_exit([&]() { detail::set_nonblocking(sock, false); }); - - int ret; - while ((ret = mbedtls_ssl_handshake(&msession->ssl)) != 0) { - if (ret == MBEDTLS_ERR_SSL_WANT_READ) { - if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) { - continue; - } - } else if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) { - if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) { - continue; - } - } - - // TlsError or timeout - if (err) { - err->code = impl::map_mbedtls_error(ret, err->sys_errno); - err->backend_code = static_cast(-ret); - } - impl::mbedtls_last_error() = ret; - return false; - } - - if (err) { err->code = ErrorCode::Success; } - return true; -} - -bool accept_nonblocking(session_t session, socket_t sock, - time_t timeout_sec, time_t timeout_usec, - TlsError *err) { - // Same implementation as connect for Mbed TLS - bool result = - connect_nonblocking(session, sock, timeout_sec, timeout_usec, err); - - // After successful handshake, capture SNI from thread-local storage - if (result && session) { - auto msession = static_cast(session); - msession->sni_hostname = std::move(impl::mbedpending_sni()); - impl::mbedpending_sni().clear(); - } - - return result; -} - -ssize_t read(session_t session, void *buf, size_t len, TlsError &err) { - if (!session || !buf) { - err.code = ErrorCode::Fatal; - return -1; - } - - auto msession = static_cast(session); - int ret = - mbedtls_ssl_read(&msession->ssl, static_cast(buf), len); - - if (ret > 0) { - err.code = ErrorCode::Success; - return static_cast(ret); - } - - if (ret == 0) { - err.code = ErrorCode::PeerClosed; - return 0; - } - - err.code = impl::map_mbedtls_error(ret, err.sys_errno); - err.backend_code = static_cast(-ret); - impl::mbedtls_last_error() = ret; - // mbedTLS signals a clean close_notify via a negative error code rather - // than 0; surface it as a clean EOF the way OpenSSL/wolfSSL do. - if (err.code == ErrorCode::PeerClosed) { return 0; } - return -1; -} - -ssize_t write(session_t session, const void *buf, size_t len, - TlsError &err) { - if (!session || !buf) { - err.code = ErrorCode::Fatal; - return -1; - } - - auto msession = static_cast(session); - int ret = mbedtls_ssl_write(&msession->ssl, - static_cast(buf), len); - - if (ret > 0) { - err.code = ErrorCode::Success; - return static_cast(ret); - } - - if (ret == 0) { - err.code = ErrorCode::PeerClosed; - return 0; - } - - err.code = impl::map_mbedtls_error(ret, err.sys_errno); - err.backend_code = static_cast(-ret); - impl::mbedtls_last_error() = ret; - return -1; -} - -int pending(const_session_t session) { - if (!session) { return 0; } - auto msession = - static_cast(const_cast(session)); - return static_cast(mbedtls_ssl_get_bytes_avail(&msession->ssl)); -} - -void shutdown(session_t session, bool graceful) { - if (!session) { return; } - auto msession = static_cast(session); - - if (graceful) { - // Try to send close_notify, but don't block forever - int ret; - int attempts = 0; - while ((ret = mbedtls_ssl_close_notify(&msession->ssl)) != 0 && - attempts < 3) { - if (ret != MBEDTLS_ERR_SSL_WANT_READ && - ret != MBEDTLS_ERR_SSL_WANT_WRITE) { - break; - } - attempts++; - } - } -} - -bool is_peer_closed(session_t session, socket_t sock) { - if (!session || sock == INVALID_SOCKET) { return true; } - auto msession = static_cast(session); - - // Check if there's already decrypted data available in the TLS buffer - // If so, the connection is definitely alive - if (mbedtls_ssl_get_bytes_avail(&msession->ssl) > 0) { return false; } - - // Set socket to non-blocking to avoid blocking on read - detail::set_nonblocking(sock, true); - auto cleanup = - detail::scope_exit([&]() { detail::set_nonblocking(sock, false); }); - - // Try a 1-byte read to check connection status - // Note: This will consume the byte if data is available, but for the - // purpose of checking if peer is closed, this should be acceptable - // since we're only called when we expect the connection might be closing - unsigned char buf; - int ret = mbedtls_ssl_read(&msession->ssl, &buf, 1); - - // If we got data or WANT_READ (would block), connection is alive - if (ret > 0 || ret == MBEDTLS_ERR_SSL_WANT_READ) { return false; } - - // If we get a peer close notify or a connection reset, the peer is closed - return ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY || - ret == MBEDTLS_ERR_NET_CONN_RESET || ret == 0; -} - -cert_t get_peer_cert(const_session_t session) { - if (!session) { return nullptr; } - auto msession = - static_cast(const_cast(session)); - - // Mbed TLS returns a pointer to the internal peer cert chain. - // WARNING: This pointer is only valid while the session is active. - // Do not use the certificate after calling free_session(). - const mbedtls_x509_crt *cert = mbedtls_ssl_get_peer_cert(&msession->ssl); - return const_cast(cert); -} - -void free_cert(cert_t cert) { - // Mbed TLS: peer certificate is owned by the SSL context. - // No-op here, but callers should still call this for cross-backend - // portability. - (void)cert; -} - -bool verify_hostname(cert_t cert, const char *hostname) { - if (!cert || !hostname) { return false; } - auto mcert = static_cast(cert); - std::string host_str(hostname); - - // Check if hostname is an IP address - bool is_ip = impl::is_ipv4_address(host_str); - unsigned char ip_bytes[4]; - if (is_ip) { impl::parse_ipv4(host_str, ip_bytes); } - - // Check Subject Alternative Names (SAN) - // In Mbed TLS 3.x, subject_alt_names contains raw values without ASN.1 tags - // - DNS names: raw string bytes - // - IP addresses: raw IP bytes (4 for IPv4, 16 for IPv6) - const mbedtls_x509_sequence *san = &mcert->subject_alt_names; - while (san != nullptr && san->buf.p != nullptr && san->buf.len > 0) { - const unsigned char *p = san->buf.p; - size_t len = san->buf.len; - - if (is_ip) { - // Check if this SAN is an IPv4 address (4 bytes) - if (len == 4 && memcmp(p, ip_bytes, 4) == 0) { return true; } - // Check if this SAN is an IPv6 address (16 bytes) - skip for now - } else { - // Check if this SAN is a DNS name (printable ASCII string) - bool is_dns = len > 0; - for (size_t i = 0; i < len && is_dns; i++) { - if (p[i] < 32 || p[i] > 126) { is_dns = false; } - } - if (is_dns) { - std::string san_name(reinterpret_cast(p), len); - if (detail::match_hostname(san_name, host_str)) { return true; } - } - } - san = san->next; - } - - // Fallback: Check Common Name (CN) in subject - char cn[256]; - int ret = mbedtls_x509_dn_gets(cn, sizeof(cn), &mcert->subject); - if (ret > 0) { - std::string cn_str(cn); - - // Look for "CN=" in the DN string - size_t cn_pos = cn_str.find("CN="); - if (cn_pos != std::string::npos) { - size_t start = cn_pos + 3; - size_t end = cn_str.find(',', start); - std::string cn_value = - cn_str.substr(start, end == std::string::npos ? end : end - start); - - if (detail::match_hostname(cn_value, host_str)) { return true; } - } - } - - return false; -} - -uint64_t hostname_mismatch_code() { - return static_cast(MBEDTLS_X509_BADCERT_CN_MISMATCH); -} - -long get_verify_result(const_session_t session) { - if (!session) { return -1; } - auto msession = - static_cast(const_cast(session)); - uint32_t flags = mbedtls_ssl_get_verify_result(&msession->ssl); - // Return 0 (X509_V_OK equivalent) if verification passed - return flags == 0 ? 0 : static_cast(flags); -} - -std::string get_cert_subject_cn(cert_t cert) { - if (!cert) return ""; - auto x509 = static_cast(cert); - - // Find the CN in the subject - const mbedtls_x509_name *name = &x509->subject; - while (name != nullptr) { - if (MBEDTLS_OID_CMP(MBEDTLS_OID_AT_CN, &name->oid) == 0) { - return std::string(reinterpret_cast(name->val.p), - name->val.len); - } - name = name->next; - } - return ""; -} - -std::string get_cert_issuer_name(cert_t cert) { - if (!cert) return ""; - auto x509 = static_cast(cert); - - // Build a human-readable issuer name string - char buf[512]; - int ret = mbedtls_x509_dn_gets(buf, sizeof(buf), &x509->issuer); - if (ret < 0) return ""; - return std::string(buf); -} - -bool get_cert_sans(cert_t cert, std::vector &sans) { - sans.clear(); - if (!cert) return false; - auto x509 = static_cast(cert); - - // Parse the Subject Alternative Name extension - const mbedtls_x509_sequence *cur = &x509->subject_alt_names; - while (cur != nullptr) { - if (cur->buf.len > 0) { - // Mbed TLS stores SAN as ASN.1 sequences - // The tag byte indicates the type - const unsigned char *p = cur->buf.p; - size_t len = cur->buf.len; - - // First byte is the tag - unsigned char tag = *p; - p++; - len--; - - // Parse length (simple single-byte length assumed) - if (len > 0 && *p < 0x80) { - size_t value_len = *p; - p++; - len--; - - if (value_len <= len) { - SanEntry entry; - // ASN.1 context tags for GeneralName - switch (tag & 0x1F) { - case 2: // dNSName - entry.type = SanType::DNS; - entry.value = - std::string(reinterpret_cast(p), value_len); - break; - case 7: // iPAddress - entry.type = SanType::IP; - if (value_len == 4) { - // IPv4 - char buf[16]; - snprintf(buf, sizeof(buf), "%d.%d.%d.%d", p[0], p[1], p[2], p[3]); - entry.value = buf; - } else if (value_len == 16) { - // IPv6 - char buf[64]; - snprintf(buf, sizeof(buf), - "%02x%02x:%02x%02x:%02x%02x:%02x%02x:" - "%02x%02x:%02x%02x:%02x%02x:%02x%02x", - p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8], - p[9], p[10], p[11], p[12], p[13], p[14], p[15]); - entry.value = buf; - } - break; - case 1: // rfc822Name (email) - entry.type = SanType::EMAIL; - entry.value = - std::string(reinterpret_cast(p), value_len); - break; - case 6: // uniformResourceIdentifier - entry.type = SanType::URI; - entry.value = - std::string(reinterpret_cast(p), value_len); - break; - default: entry.type = SanType::OTHER; break; - } - - if (!entry.value.empty()) { sans.push_back(std::move(entry)); } - } - } - } - cur = cur->next; - } - return true; -} - -bool get_cert_validity(cert_t cert, time_t ¬_before, - time_t ¬_after) { - if (!cert) return false; - auto x509 = static_cast(cert); - - // Convert mbedtls_x509_time to time_t - auto to_time_t = [](const mbedtls_x509_time &t) -> time_t { - struct tm tm_time = {}; - tm_time.tm_year = t.year - 1900; - tm_time.tm_mon = t.mon - 1; - tm_time.tm_mday = t.day; - tm_time.tm_hour = t.hour; - tm_time.tm_min = t.min; - tm_time.tm_sec = t.sec; -#ifdef _WIN32 - return _mkgmtime(&tm_time); -#else - return timegm(&tm_time); -#endif - }; - - not_before = to_time_t(x509->valid_from); - not_after = to_time_t(x509->valid_to); - return true; -} - -std::string get_cert_serial(cert_t cert) { - if (!cert) return ""; - auto x509 = static_cast(cert); - - // Convert serial number to hex string - std::string result; - result.reserve(x509->serial.len * 2); - for (size_t i = 0; i < x509->serial.len; i++) { - char hex[3]; - snprintf(hex, sizeof(hex), "%02X", x509->serial.p[i]); - result += hex; - } - return result; -} - -bool get_cert_der(cert_t cert, std::vector &der) { - if (!cert) return false; - auto crt = static_cast(cert); - if (!crt->raw.p || crt->raw.len == 0) return false; - der.assign(crt->raw.p, crt->raw.p + crt->raw.len); - return true; -} - -const char *get_sni(const_session_t session) { - if (!session) return nullptr; - auto msession = static_cast(session); - - // For server: return SNI received from client during handshake - if (!msession->sni_hostname.empty()) { - return msession->sni_hostname.c_str(); - } - - // For client: return the hostname set via set_sni - if (!msession->hostname.empty()) { return msession->hostname.c_str(); } - - return nullptr; -} - -uint64_t peek_error() { - // Mbed TLS doesn't have an error queue, return the last error - return static_cast(-impl::mbedtls_last_error()); -} - -uint64_t get_error() { - // Mbed TLS doesn't have an error queue, return and clear the last error - uint64_t err = static_cast(-impl::mbedtls_last_error()); - impl::mbedtls_last_error() = 0; - return err; -} - -std::string error_string(uint64_t code) { - char buf[256]; - mbedtls_strerror(-static_cast(code), buf, sizeof(buf)); - return std::string(buf); -} - -ca_store_t create_ca_store(const char *pem, size_t len) { - auto *ca_chain = new (std::nothrow) mbedtls_x509_crt; - if (!ca_chain) { return nullptr; } - - mbedtls_x509_crt_init(ca_chain); - - // mbedtls_x509_crt_parse expects null-terminated PEM - int ret = mbedtls_x509_crt_parse(ca_chain, - reinterpret_cast(pem), - len + 1); // +1 for null terminator - if (ret != 0) { - // Try without +1 in case PEM is already null-terminated - ret = mbedtls_x509_crt_parse( - ca_chain, reinterpret_cast(pem), len); - if (ret != 0) { - mbedtls_x509_crt_free(ca_chain); - delete ca_chain; - return nullptr; - } - } - - return static_cast(ca_chain); -} - -void free_ca_store(ca_store_t store) { - if (store) { - auto *ca_chain = static_cast(store); - mbedtls_x509_crt_free(ca_chain); - delete ca_chain; - } -} - -bool set_ca_store(ctx_t ctx, ca_store_t store) { - if (!ctx || !store) { return false; } - auto *mbed_ctx = static_cast(ctx); - auto *ca_chain = static_cast(store); - - // Free existing CA chain - mbedtls_x509_crt_free(&mbed_ctx->ca_chain); - mbedtls_x509_crt_init(&mbed_ctx->ca_chain); - - // Copy the CA chain (deep copy) - // Parse from the raw data of the source cert - mbedtls_x509_crt *src = ca_chain; - while (src != nullptr) { - int ret = mbedtls_x509_crt_parse_der(&mbed_ctx->ca_chain, src->raw.p, - src->raw.len); - if (ret != 0) { return false; } - src = src->next; - } - - // Update the SSL config to use the new CA chain - mbedtls_ssl_conf_ca_chain(&mbed_ctx->conf, &mbed_ctx->ca_chain, nullptr); - return true; -} - -size_t get_ca_certs(ctx_t ctx, std::vector &certs) { - certs.clear(); - if (!ctx) { return 0; } - auto *mbed_ctx = static_cast(ctx); - - // Iterate through the CA chain - mbedtls_x509_crt *cert = &mbed_ctx->ca_chain; - while (cert != nullptr && cert->raw.len > 0) { - // Create a copy of the certificate for the caller - auto *copy = new mbedtls_x509_crt; - mbedtls_x509_crt_init(copy); - int ret = mbedtls_x509_crt_parse_der(copy, cert->raw.p, cert->raw.len); - if (ret == 0) { - certs.push_back(static_cast(copy)); - } else { - mbedtls_x509_crt_free(copy); - delete copy; - } - cert = cert->next; - } - return certs.size(); -} - -std::vector get_ca_names(ctx_t ctx) { - std::vector names; - if (!ctx) { return names; } - auto *mbed_ctx = static_cast(ctx); - - // Iterate through the CA chain - mbedtls_x509_crt *cert = &mbed_ctx->ca_chain; - while (cert != nullptr && cert->raw.len > 0) { - char buf[512]; - int ret = mbedtls_x509_dn_gets(buf, sizeof(buf), &cert->subject); - if (ret > 0) { names.push_back(buf); } - cert = cert->next; - } - return names; -} - -bool update_server_cert(ctx_t ctx, const char *cert_pem, - const char *key_pem, const char *password) { - if (!ctx || !cert_pem || !key_pem) { return false; } - auto *mbed_ctx = static_cast(ctx); - - // Free existing certificate and key - mbedtls_x509_crt_free(&mbed_ctx->own_cert); - mbedtls_pk_free(&mbed_ctx->own_key); - mbedtls_x509_crt_init(&mbed_ctx->own_cert); - mbedtls_pk_init(&mbed_ctx->own_key); - - // Parse certificate PEM - int ret = mbedtls_x509_crt_parse( - &mbed_ctx->own_cert, reinterpret_cast(cert_pem), - strlen(cert_pem) + 1); - if (ret != 0) { - impl::mbedtls_last_error() = ret; - return false; - } - - // Parse private key PEM -#ifdef CPPHTTPLIB_MBEDTLS_V3 - ret = mbedtls_pk_parse_key( - &mbed_ctx->own_key, reinterpret_cast(key_pem), - strlen(key_pem) + 1, - password ? reinterpret_cast(password) : nullptr, - password ? strlen(password) : 0, mbedtls_ctr_drbg_random, - &mbed_ctx->ctr_drbg); -#else - ret = mbedtls_pk_parse_key( - &mbed_ctx->own_key, reinterpret_cast(key_pem), - strlen(key_pem) + 1, - password ? reinterpret_cast(password) : nullptr, - password ? strlen(password) : 0); -#endif - if (ret != 0) { - impl::mbedtls_last_error() = ret; - return false; - } - - // Configure SSL to use the new certificate and key - ret = mbedtls_ssl_conf_own_cert(&mbed_ctx->conf, &mbed_ctx->own_cert, - &mbed_ctx->own_key); - if (ret != 0) { - impl::mbedtls_last_error() = ret; - return false; - } - - return true; -} - -bool update_server_client_ca(ctx_t ctx, const char *ca_pem) { - if (!ctx || !ca_pem) { return false; } - auto *mbed_ctx = static_cast(ctx); - - // Free existing CA chain - mbedtls_x509_crt_free(&mbed_ctx->ca_chain); - mbedtls_x509_crt_init(&mbed_ctx->ca_chain); - - // Parse CA PEM - int ret = mbedtls_x509_crt_parse( - &mbed_ctx->ca_chain, reinterpret_cast(ca_pem), - strlen(ca_pem) + 1); - if (ret != 0) { - impl::mbedtls_last_error() = ret; - return false; - } - - // Update SSL config to use new CA chain - mbedtls_ssl_conf_ca_chain(&mbed_ctx->conf, &mbed_ctx->ca_chain, nullptr); - return true; -} - -bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { - if (!ctx) { return false; } - auto *mbed_ctx = static_cast(ctx); - - impl::get_verify_callback() = std::move(callback); - mbed_ctx->has_verify_callback = - static_cast(impl::get_verify_callback()); - - if (mbed_ctx->has_verify_callback) { - // Set OPTIONAL mode to ensure callback is called even when verification - // is disabled (matching OpenSSL behavior where SSL_VERIFY_PEER is set) - mbedtls_ssl_conf_authmode(&mbed_ctx->conf, MBEDTLS_SSL_VERIFY_OPTIONAL); - mbedtls_ssl_conf_verify(&mbed_ctx->conf, impl::mbedtls_verify_callback, - nullptr); - } else { - mbedtls_ssl_conf_verify(&mbed_ctx->conf, nullptr, nullptr); - } - return true; -} - -long get_verify_error(const_session_t session) { - if (!session) { return -1; } - auto *msession = - static_cast(const_cast(session)); - return static_cast(mbedtls_ssl_get_verify_result(&msession->ssl)); -} - -std::string verify_error_string(long error_code) { - if (error_code == 0) { return ""; } - char buf[256]; - mbedtls_x509_crt_verify_info(buf, sizeof(buf), "", - static_cast(error_code)); - // Remove trailing newline if present - std::string result(buf); - while (!result.empty() && (result.back() == '\n' || result.back() == ' ')) { - result.pop_back(); - } - return result; -} - -} // namespace tls - -#endif // CPPHTTPLIB_MBEDTLS_SUPPORT - -/* - * Group 10: TLS abstraction layer - wolfSSL backend - */ - -/* - * wolfSSL Backend Implementation - */ - -#ifdef CPPHTTPLIB_WOLFSSL_SUPPORT -namespace tls { - -namespace impl { - -// wolfSSL session wrapper -struct WolfSSLSession { - WOLFSSL *ssl = nullptr; - socket_t sock = INVALID_SOCKET; - std::string hostname; // For client: set via set_sni - std::string sni_hostname; // For server: received from client via SNI callback - - WolfSSLSession() = default; - - ~WolfSSLSession() { - if (ssl) { wolfSSL_free(ssl); } - } - - WolfSSLSession(const WolfSSLSession &) = delete; - WolfSSLSession &operator=(const WolfSSLSession &) = delete; -}; - -// Thread-local error code accessor for wolfSSL -uint64_t &wolfssl_last_error() { - static thread_local uint64_t err = 0; - return err; -} - -// Helper to map wolfSSL error to ErrorCode. -// ssl_error is the value from wolfSSL_get_error(). -// raw_ret is the raw return value from the wolfSSL call (for low-level error). -ErrorCode map_wolfssl_error(WOLFSSL *ssl, int ssl_error, - int &out_errno) { - switch (ssl_error) { - case SSL_ERROR_NONE: return ErrorCode::Success; - case SSL_ERROR_WANT_READ: return ErrorCode::WantRead; - case SSL_ERROR_WANT_WRITE: return ErrorCode::WantWrite; - case SSL_ERROR_ZERO_RETURN: return ErrorCode::PeerClosed; - case SSL_ERROR_SYSCALL: out_errno = errno; return ErrorCode::SyscallError; - default: - if (ssl) { - // wolfSSL stores the low-level error code as a negative value. - // DOMAIN_NAME_MISMATCH (-322) indicates hostname verification failure. - int low_err = ssl_error; // wolfSSL_get_error returns the low-level code - if (low_err == DOMAIN_NAME_MISMATCH) { - return ErrorCode::HostnameMismatch; - } - // Check verify result to distinguish cert verification from generic SSL - // errors. - long vr = wolfSSL_get_verify_result(ssl); - if (vr != 0) { return ErrorCode::CertVerifyFailed; } - } - return ErrorCode::Fatal; - } -} - -// WolfSSLContext constructor/destructor implementations -WolfSSLContext::WolfSSLContext() { wolfSSL_Init(); } - -WolfSSLContext::~WolfSSLContext() { - if (ctx) { wolfSSL_CTX_free(ctx); } -} - -// Thread-local storage for SNI captured during handshake -std::string &wolfssl_pending_sni() { - static thread_local std::string sni; - return sni; -} - -// SNI callback for wolfSSL server to capture client's SNI hostname -int wolfssl_sni_callback(WOLFSSL *ssl, int *ret, void *exArg) { - (void)ret; - (void)exArg; - - void *name_data = nullptr; - unsigned short name_len = - wolfSSL_SNI_GetRequest(ssl, WOLFSSL_SNI_HOST_NAME, &name_data); - - if (name_data && name_len > 0) { - wolfssl_pending_sni().assign(static_cast(name_data), - name_len); - } else { - wolfssl_pending_sni().clear(); - } - return 0; // Continue regardless -} - -// wolfSSL verify callback wrapper -int wolfssl_verify_callback(int preverify_ok, - WOLFSSL_X509_STORE_CTX *x509_ctx) { - auto &callback = get_verify_callback(); - if (!callback) { return preverify_ok; } - - WOLFSSL_X509 *cert = wolfSSL_X509_STORE_CTX_get_current_cert(x509_ctx); - int depth = wolfSSL_X509_STORE_CTX_get_error_depth(x509_ctx); - int err = wolfSSL_X509_STORE_CTX_get_error(x509_ctx); - - // Get the WOLFSSL object from the X509_STORE_CTX - WOLFSSL *ssl = static_cast(wolfSSL_X509_STORE_CTX_get_ex_data( - x509_ctx, wolfSSL_get_ex_data_X509_STORE_CTX_idx())); - - VerifyContext verify_ctx; - verify_ctx.session = static_cast(ssl); - verify_ctx.cert = static_cast(cert); - verify_ctx.depth = depth; - verify_ctx.preverify_ok = (preverify_ok != 0); - verify_ctx.error_code = static_cast(err); - - if (err != 0) { - verify_ctx.error_string = wolfSSL_X509_verify_cert_error_string(err); - } else { - verify_ctx.error_string = nullptr; - } - - bool accepted = callback(verify_ctx); - return accepted ? 1 : 0; -} - -void set_wolfssl_password_cb(WOLFSSL_CTX *ctx, const char *password) { - wolfSSL_CTX_set_default_passwd_cb_userdata(ctx, const_cast(password)); - wolfSSL_CTX_set_default_passwd_cb( - ctx, [](char *buf, int size, int /*rwflag*/, void *userdata) -> int { - auto *pwd = static_cast(userdata); - if (!pwd) return 0; - auto len = static_cast(strlen(pwd)); - if (len > size) len = size; - memcpy(buf, pwd, static_cast(len)); - return len; - }); -} - -} // namespace impl - -ctx_t create_client_context() { - auto ctx = new (std::nothrow) impl::WolfSSLContext(); - if (!ctx) { return nullptr; } - - ctx->is_server = false; - - WOLFSSL_METHOD *method = wolfTLSv1_2_client_method(); - if (!method) { - delete ctx; - return nullptr; - } - - ctx->ctx = wolfSSL_CTX_new(method); - if (!ctx->ctx) { - delete ctx; - return nullptr; - } - - // Default: verify peer certificate - wolfSSL_CTX_set_verify(ctx->ctx, SSL_VERIFY_PEER, nullptr); - - return static_cast(ctx); -} - -ctx_t create_server_context() { - auto ctx = new (std::nothrow) impl::WolfSSLContext(); - if (!ctx) { return nullptr; } - - ctx->is_server = true; - - WOLFSSL_METHOD *method = wolfTLSv1_2_server_method(); - if (!method) { - delete ctx; - return nullptr; - } - - ctx->ctx = wolfSSL_CTX_new(method); - if (!ctx->ctx) { - delete ctx; - return nullptr; - } - - // Default: don't verify client - wolfSSL_CTX_set_verify(ctx->ctx, SSL_VERIFY_NONE, nullptr); - - // Enable SNI on server - wolfSSL_CTX_SNI_SetOptions(ctx->ctx, WOLFSSL_SNI_HOST_NAME, - WOLFSSL_SNI_CONTINUE_ON_MISMATCH); - wolfSSL_CTX_set_servername_callback(ctx->ctx, impl::wolfssl_sni_callback); - - return static_cast(ctx); -} - -void free_context(ctx_t ctx) { - if (ctx) { delete static_cast(ctx); } -} - -bool set_min_version(ctx_t ctx, Version version) { - if (!ctx) { return false; } - auto wctx = static_cast(ctx); - - int min_ver = WOLFSSL_TLSV1_2; - if (version >= Version::TLS1_3) { min_ver = WOLFSSL_TLSV1_3; } - - return wolfSSL_CTX_SetMinVersion(wctx->ctx, min_ver) == WOLFSSL_SUCCESS; -} - -bool load_ca_pem(ctx_t ctx, const char *pem, size_t len) { - if (!ctx || !pem) { return false; } - auto wctx = static_cast(ctx); - - int ret = wolfSSL_CTX_load_verify_buffer( - wctx->ctx, reinterpret_cast(pem), - static_cast(len), SSL_FILETYPE_PEM); - if (ret != SSL_SUCCESS) { - impl::wolfssl_last_error() = - static_cast(wolfSSL_ERR_peek_last_error()); - return false; - } - wctx->ca_pem_data_.append(pem, len); - return true; -} - -bool load_ca_file(ctx_t ctx, const char *file_path) { - if (!ctx || !file_path) { return false; } - auto wctx = static_cast(ctx); - - int ret = wolfSSL_CTX_load_verify_locations(wctx->ctx, file_path, nullptr); - if (ret != SSL_SUCCESS) { - impl::wolfssl_last_error() = - static_cast(wolfSSL_ERR_peek_last_error()); - return false; - } - return true; -} - -bool load_ca_dir(ctx_t ctx, const char *dir_path) { - if (!ctx || !dir_path) { return false; } - auto wctx = static_cast(ctx); - - int ret = wolfSSL_CTX_load_verify_locations(wctx->ctx, nullptr, dir_path); - // wolfSSL may fail if the directory doesn't contain properly hashed certs. - // Unlike OpenSSL which lazily loads certs from directories, wolfSSL scans - // immediately. Return true even on failure since the CA file may have - // already been loaded, matching OpenSSL's lenient behavior. - (void)ret; - return true; -} - -bool load_system_certs(ctx_t ctx) { - if (!ctx) { return false; } - auto wctx = static_cast(ctx); - bool loaded = false; - -#ifdef _WIN32 - loaded = impl::enumerate_windows_system_certs( - [&](const unsigned char *data, size_t len) { - return wolfSSL_CTX_load_verify_buffer(wctx->ctx, data, - static_cast(len), - SSL_FILETYPE_ASN1) == SSL_SUCCESS; - }); -#elif defined(__APPLE__) && defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) - loaded = impl::enumerate_macos_keychain_certs( - [&](const unsigned char *data, size_t len) { - return wolfSSL_CTX_load_verify_buffer(wctx->ctx, data, - static_cast(len), - SSL_FILETYPE_ASN1) == SSL_SUCCESS; - }); -#else - for (auto path = impl::system_ca_paths(); *path; ++path) { - if (wolfSSL_CTX_load_verify_locations(wctx->ctx, *path, nullptr) == - SSL_SUCCESS) { - loaded = true; - break; - } - } - - if (!loaded) { - for (auto dir = impl::system_ca_dirs(); *dir; ++dir) { - if (wolfSSL_CTX_load_verify_locations(wctx->ctx, nullptr, *dir) == - SSL_SUCCESS) { - loaded = true; - break; - } - } - } -#endif - - return loaded; -} - -bool set_client_cert_pem(ctx_t ctx, const char *cert, const char *key, - const char *password) { - if (!ctx || !cert || !key) { return false; } - auto wctx = static_cast(ctx); - - // Load certificate - int ret = wolfSSL_CTX_use_certificate_buffer( - wctx->ctx, reinterpret_cast(cert), - static_cast(strlen(cert)), SSL_FILETYPE_PEM); - if (ret != SSL_SUCCESS) { - impl::wolfssl_last_error() = - static_cast(wolfSSL_ERR_peek_last_error()); - return false; - } - - // Set password callback if password is provided - if (password) { impl::set_wolfssl_password_cb(wctx->ctx, password); } - - // Load private key - ret = wolfSSL_CTX_use_PrivateKey_buffer( - wctx->ctx, reinterpret_cast(key), - static_cast(strlen(key)), SSL_FILETYPE_PEM); - if (ret != SSL_SUCCESS) { - impl::wolfssl_last_error() = - static_cast(wolfSSL_ERR_peek_last_error()); - return false; - } - - // Verify that the certificate and private key match - return wolfSSL_CTX_check_private_key(wctx->ctx) == SSL_SUCCESS; -} - -bool set_client_cert_file(ctx_t ctx, const char *cert_path, - const char *key_path, const char *password) { - if (!ctx || !cert_path || !key_path) { return false; } - auto wctx = static_cast(ctx); - - // Load certificate file - int ret = - wolfSSL_CTX_use_certificate_file(wctx->ctx, cert_path, SSL_FILETYPE_PEM); - if (ret != SSL_SUCCESS) { - impl::wolfssl_last_error() = - static_cast(wolfSSL_ERR_peek_last_error()); - return false; - } - - // Set password callback if password is provided - if (password) { impl::set_wolfssl_password_cb(wctx->ctx, password); } - - // Load private key file - ret = wolfSSL_CTX_use_PrivateKey_file(wctx->ctx, key_path, SSL_FILETYPE_PEM); - if (ret != SSL_SUCCESS) { - impl::wolfssl_last_error() = - static_cast(wolfSSL_ERR_peek_last_error()); - return false; - } - - // Verify that the certificate and private key match - return wolfSSL_CTX_check_private_key(wctx->ctx) == SSL_SUCCESS; -} - -void set_verify_client(ctx_t ctx, bool require) { - if (!ctx) { return; } - auto wctx = static_cast(ctx); - wctx->verify_client = require; - if (require) { - wolfSSL_CTX_set_verify( - wctx->ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, - wctx->has_verify_callback ? impl::wolfssl_verify_callback : nullptr); - } else { - if (wctx->has_verify_callback) { - wolfSSL_CTX_set_verify(wctx->ctx, SSL_VERIFY_PEER, - impl::wolfssl_verify_callback); - } else { - wolfSSL_CTX_set_verify(wctx->ctx, SSL_VERIFY_NONE, nullptr); - } - } -} - -session_t create_session(ctx_t ctx, socket_t sock) { - if (!ctx || sock == INVALID_SOCKET) { return nullptr; } - auto wctx = static_cast(ctx); - - auto session = new (std::nothrow) impl::WolfSSLSession(); - if (!session) { return nullptr; } - - session->sock = sock; - session->ssl = wolfSSL_new(wctx->ctx); - if (!session->ssl) { - impl::wolfssl_last_error() = - static_cast(wolfSSL_ERR_peek_last_error()); - delete session; - return nullptr; - } - - wolfSSL_set_fd(session->ssl, static_cast(sock)); - - return static_cast(session); -} - -void free_session(session_t session) { - if (session) { delete static_cast(session); } -} - -bool set_sni(session_t session, const char *hostname) { - if (!session || !hostname) { return false; } - auto wsession = static_cast(session); - - int ret = wolfSSL_UseSNI(wsession->ssl, WOLFSSL_SNI_HOST_NAME, hostname, - static_cast(strlen(hostname))); - if (ret != WOLFSSL_SUCCESS) { - impl::wolfssl_last_error() = - static_cast(wolfSSL_ERR_peek_last_error()); - return false; - } - - // Also set hostname for verification - wolfSSL_check_domain_name(wsession->ssl, hostname); - - wsession->hostname = hostname; - return true; -} - -bool set_hostname(session_t session, const char *hostname) { - // In wolfSSL, set_hostname also sets up hostname verification - return set_sni(session, hostname); -} - -TlsError connect(session_t session) { - TlsError err; - if (!session) { - err.code = ErrorCode::Fatal; - return err; - } - - auto wsession = static_cast(session); - int ret = wolfSSL_connect(wsession->ssl); - - if (ret == SSL_SUCCESS) { - err.code = ErrorCode::Success; - } else { - int ssl_error = wolfSSL_get_error(wsession->ssl, ret); - err.code = impl::map_wolfssl_error(wsession->ssl, ssl_error, err.sys_errno); - err.backend_code = static_cast(ssl_error); - impl::wolfssl_last_error() = err.backend_code; - } - - return err; -} - -TlsError accept(session_t session) { - TlsError err; - if (!session) { - err.code = ErrorCode::Fatal; - return err; - } - - auto wsession = static_cast(session); - int ret = wolfSSL_accept(wsession->ssl); - - if (ret == SSL_SUCCESS) { - err.code = ErrorCode::Success; - // Capture SNI from thread-local storage after successful handshake - wsession->sni_hostname = std::move(impl::wolfssl_pending_sni()); - impl::wolfssl_pending_sni().clear(); - } else { - int ssl_error = wolfSSL_get_error(wsession->ssl, ret); - err.code = impl::map_wolfssl_error(wsession->ssl, ssl_error, err.sys_errno); - err.backend_code = static_cast(ssl_error); - impl::wolfssl_last_error() = err.backend_code; - } - - return err; -} - -bool connect_nonblocking(session_t session, socket_t sock, - time_t timeout_sec, time_t timeout_usec, - TlsError *err) { - if (!session) { - if (err) { err->code = ErrorCode::Fatal; } - return false; - } - - auto wsession = static_cast(session); - - // Set socket to non-blocking mode - detail::set_nonblocking(sock, true); - auto cleanup = - detail::scope_exit([&]() { detail::set_nonblocking(sock, false); }); - - int ret; - while ((ret = wolfSSL_connect(wsession->ssl)) != SSL_SUCCESS) { - int ssl_error = wolfSSL_get_error(wsession->ssl, ret); - if (ssl_error == SSL_ERROR_WANT_READ) { - if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) { - continue; - } - } else if (ssl_error == SSL_ERROR_WANT_WRITE) { - if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) { - continue; - } - } - - // Error or timeout - if (err) { - err->code = - impl::map_wolfssl_error(wsession->ssl, ssl_error, err->sys_errno); - err->backend_code = static_cast(ssl_error); - } - impl::wolfssl_last_error() = static_cast(ssl_error); - return false; - } - - if (err) { err->code = ErrorCode::Success; } - return true; -} - -bool accept_nonblocking(session_t session, socket_t sock, - time_t timeout_sec, time_t timeout_usec, - TlsError *err) { - if (!session) { - if (err) { err->code = ErrorCode::Fatal; } - return false; - } - - auto wsession = static_cast(session); - - // Set socket to non-blocking mode - detail::set_nonblocking(sock, true); - auto cleanup = - detail::scope_exit([&]() { detail::set_nonblocking(sock, false); }); - - int ret; - while ((ret = wolfSSL_accept(wsession->ssl)) != SSL_SUCCESS) { - int ssl_error = wolfSSL_get_error(wsession->ssl, ret); - if (ssl_error == SSL_ERROR_WANT_READ) { - if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) { - continue; - } - } else if (ssl_error == SSL_ERROR_WANT_WRITE) { - if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) { - continue; - } - } - - // Error or timeout - if (err) { - err->code = - impl::map_wolfssl_error(wsession->ssl, ssl_error, err->sys_errno); - err->backend_code = static_cast(ssl_error); - } - impl::wolfssl_last_error() = static_cast(ssl_error); - return false; - } - - if (err) { err->code = ErrorCode::Success; } - - // Capture SNI from thread-local storage after successful handshake - wsession->sni_hostname = std::move(impl::wolfssl_pending_sni()); - impl::wolfssl_pending_sni().clear(); - - return true; -} - -ssize_t read(session_t session, void *buf, size_t len, TlsError &err) { - if (!session || !buf) { - err.code = ErrorCode::Fatal; - return -1; - } - - auto wsession = static_cast(session); - int ret = wolfSSL_read(wsession->ssl, buf, static_cast(len)); - - if (ret > 0) { - err.code = ErrorCode::Success; - return static_cast(ret); - } - - if (ret == 0) { - err.code = ErrorCode::PeerClosed; - return 0; - } - - int ssl_error = wolfSSL_get_error(wsession->ssl, ret); - err.code = impl::map_wolfssl_error(wsession->ssl, ssl_error, err.sys_errno); - err.backend_code = static_cast(ssl_error); - impl::wolfssl_last_error() = err.backend_code; - return -1; -} - -ssize_t write(session_t session, const void *buf, size_t len, - TlsError &err) { - if (!session || !buf) { - err.code = ErrorCode::Fatal; - return -1; - } - - auto wsession = static_cast(session); - int ret = wolfSSL_write(wsession->ssl, buf, static_cast(len)); - - if (ret > 0) { - err.code = ErrorCode::Success; - return static_cast(ret); - } - - // wolfSSL_write returns 0 when the peer has sent a close_notify. - // Treat this as an error (return -1) so callers don't spin in a - // write loop adding zero to the offset. - if (ret == 0) { - err.code = ErrorCode::PeerClosed; - return -1; - } - - int ssl_error = wolfSSL_get_error(wsession->ssl, ret); - err.code = impl::map_wolfssl_error(wsession->ssl, ssl_error, err.sys_errno); - err.backend_code = static_cast(ssl_error); - impl::wolfssl_last_error() = err.backend_code; - return -1; -} - -int pending(const_session_t session) { - if (!session) { return 0; } - auto wsession = - static_cast(const_cast(session)); - return wolfSSL_pending(wsession->ssl); -} - -void shutdown(session_t session, bool graceful) { - if (!session) { return; } - auto wsession = static_cast(session); - - if (graceful) { - int ret; - int attempts = 0; - while ((ret = wolfSSL_shutdown(wsession->ssl)) != SSL_SUCCESS && - attempts < 3) { - int ssl_error = wolfSSL_get_error(wsession->ssl, ret); - if (ssl_error != SSL_ERROR_WANT_READ && - ssl_error != SSL_ERROR_WANT_WRITE) { - break; - } - attempts++; - } - } else { - wolfSSL_shutdown(wsession->ssl); - } -} - -bool is_peer_closed(session_t session, socket_t sock) { - if (!session || sock == INVALID_SOCKET) { return true; } - auto wsession = static_cast(session); - - // Check if there's already decrypted data available - if (wolfSSL_pending(wsession->ssl) > 0) { return false; } - - // Set socket to non-blocking to avoid blocking on read - detail::set_nonblocking(sock, true); - auto cleanup = - detail::scope_exit([&]() { detail::set_nonblocking(sock, false); }); - - // Peek 1 byte to check connection status without consuming data - unsigned char buf; - int ret = wolfSSL_peek(wsession->ssl, &buf, 1); - - // If we got data or WANT_READ (would block), connection is alive - if (ret > 0) { return false; } - - int ssl_error = wolfSSL_get_error(wsession->ssl, ret); - if (ssl_error == SSL_ERROR_WANT_READ) { return false; } - - return ssl_error == SSL_ERROR_ZERO_RETURN || ssl_error == SSL_ERROR_SYSCALL || - ret == 0; -} - -cert_t get_peer_cert(const_session_t session) { - if (!session) { return nullptr; } - auto wsession = - static_cast(const_cast(session)); - - WOLFSSL_X509 *cert = wolfSSL_get_peer_certificate(wsession->ssl); - return static_cast(cert); -} - -void free_cert(cert_t cert) { - if (cert) { wolfSSL_X509_free(static_cast(cert)); } -} - -bool verify_hostname(cert_t cert, const char *hostname) { - if (!cert || !hostname) { return false; } - auto x509 = static_cast(cert); - std::string host_str(hostname); - - // Check if hostname is an IP address - bool is_ip = impl::is_ipv4_address(host_str); - unsigned char ip_bytes[4]; - if (is_ip) { impl::parse_ipv4(host_str, ip_bytes); } - - // Check Subject Alternative Names - auto *san_names = static_cast( - wolfSSL_X509_get_ext_d2i(x509, NID_subject_alt_name, nullptr, nullptr)); - - if (san_names) { - int san_count = wolfSSL_sk_num(san_names); - for (int i = 0; i < san_count; i++) { - auto *names = - static_cast(wolfSSL_sk_value(san_names, i)); - if (!names) continue; - - if (!is_ip && names->type == WOLFSSL_GEN_DNS) { - // DNS name - unsigned char *dns_name = nullptr; - int dns_len = wolfSSL_ASN1_STRING_to_UTF8(&dns_name, names->d.dNSName); - if (dns_name && dns_len > 0) { - std::string san_name(reinterpret_cast(dns_name), - static_cast(dns_len)); - XFREE(dns_name, nullptr, DYNAMIC_TYPE_OPENSSL); - if (detail::match_hostname(san_name, host_str)) { - wolfSSL_sk_free(san_names); - return true; - } - } - } else if (is_ip && names->type == WOLFSSL_GEN_IPADD) { - // IP address - unsigned char *ip_data = wolfSSL_ASN1_STRING_data(names->d.iPAddress); - int ip_len = wolfSSL_ASN1_STRING_length(names->d.iPAddress); - if (ip_data && ip_len == 4 && memcmp(ip_data, ip_bytes, 4) == 0) { - wolfSSL_sk_free(san_names); - return true; - } - } - } - wolfSSL_sk_free(san_names); - } - - // Fallback: Check Common Name (CN) in subject - WOLFSSL_X509_NAME *subject = wolfSSL_X509_get_subject_name(x509); - if (subject) { - char cn[256] = {}; - int cn_len = wolfSSL_X509_NAME_get_text_by_NID(subject, NID_commonName, cn, - sizeof(cn)); - if (cn_len > 0) { - std::string cn_str(cn, static_cast(cn_len)); - if (detail::match_hostname(cn_str, host_str)) { return true; } - } - } - - return false; -} - -uint64_t hostname_mismatch_code() { - return static_cast(DOMAIN_NAME_MISMATCH); -} - -long get_verify_result(const_session_t session) { - if (!session) { return -1; } - auto wsession = - static_cast(const_cast(session)); - long result = wolfSSL_get_verify_result(wsession->ssl); - return result; -} - -std::string get_cert_subject_cn(cert_t cert) { - if (!cert) return ""; - auto x509 = static_cast(cert); - - WOLFSSL_X509_NAME *subject = wolfSSL_X509_get_subject_name(x509); - if (!subject) return ""; - - char cn[256] = {}; - int cn_len = wolfSSL_X509_NAME_get_text_by_NID(subject, NID_commonName, cn, - sizeof(cn)); - if (cn_len <= 0) return ""; - return std::string(cn, static_cast(cn_len)); -} - -std::string get_cert_issuer_name(cert_t cert) { - if (!cert) return ""; - auto x509 = static_cast(cert); - - WOLFSSL_X509_NAME *issuer = wolfSSL_X509_get_issuer_name(x509); - if (!issuer) return ""; - - char *name_str = wolfSSL_X509_NAME_oneline(issuer, nullptr, 0); - if (!name_str) return ""; - - std::string result(name_str); - XFREE(name_str, nullptr, DYNAMIC_TYPE_OPENSSL); - return result; -} - -bool get_cert_sans(cert_t cert, std::vector &sans) { - sans.clear(); - if (!cert) return false; - auto x509 = static_cast(cert); - - auto *san_names = static_cast( - wolfSSL_X509_get_ext_d2i(x509, NID_subject_alt_name, nullptr, nullptr)); - if (!san_names) return true; // No SANs is not an error - - int count = wolfSSL_sk_num(san_names); - for (int i = 0; i < count; i++) { - auto *name = - static_cast(wolfSSL_sk_value(san_names, i)); - if (!name) continue; - - SanEntry entry; - switch (name->type) { - case WOLFSSL_GEN_DNS: { - entry.type = SanType::DNS; - unsigned char *dns_name = nullptr; - int dns_len = wolfSSL_ASN1_STRING_to_UTF8(&dns_name, name->d.dNSName); - if (dns_name && dns_len > 0) { - entry.value = std::string(reinterpret_cast(dns_name), - static_cast(dns_len)); - XFREE(dns_name, nullptr, DYNAMIC_TYPE_OPENSSL); - } - break; - } - case WOLFSSL_GEN_IPADD: { - entry.type = SanType::IP; - unsigned char *ip_data = wolfSSL_ASN1_STRING_data(name->d.iPAddress); - int ip_len = wolfSSL_ASN1_STRING_length(name->d.iPAddress); - if (ip_data && ip_len == 4) { - char buf[16]; - snprintf(buf, sizeof(buf), "%d.%d.%d.%d", ip_data[0], ip_data[1], - ip_data[2], ip_data[3]); - entry.value = buf; - } else if (ip_data && ip_len == 16) { - char buf[64]; - snprintf(buf, sizeof(buf), - "%02x%02x:%02x%02x:%02x%02x:%02x%02x:" - "%02x%02x:%02x%02x:%02x%02x:%02x%02x", - ip_data[0], ip_data[1], ip_data[2], ip_data[3], ip_data[4], - ip_data[5], ip_data[6], ip_data[7], ip_data[8], ip_data[9], - ip_data[10], ip_data[11], ip_data[12], ip_data[13], - ip_data[14], ip_data[15]); - entry.value = buf; - } - break; - } - case WOLFSSL_GEN_EMAIL: - entry.type = SanType::EMAIL; - { - unsigned char *email = nullptr; - int email_len = wolfSSL_ASN1_STRING_to_UTF8(&email, name->d.rfc822Name); - if (email && email_len > 0) { - entry.value = std::string(reinterpret_cast(email), - static_cast(email_len)); - XFREE(email, nullptr, DYNAMIC_TYPE_OPENSSL); - } - } - break; - case WOLFSSL_GEN_URI: - entry.type = SanType::URI; - { - unsigned char *uri = nullptr; - int uri_len = wolfSSL_ASN1_STRING_to_UTF8( - &uri, name->d.uniformResourceIdentifier); - if (uri && uri_len > 0) { - entry.value = std::string(reinterpret_cast(uri), - static_cast(uri_len)); - XFREE(uri, nullptr, DYNAMIC_TYPE_OPENSSL); - } - } - break; - default: entry.type = SanType::OTHER; break; - } - - if (!entry.value.empty()) { sans.push_back(std::move(entry)); } - } - wolfSSL_sk_free(san_names); - return true; -} - -bool get_cert_validity(cert_t cert, time_t ¬_before, - time_t ¬_after) { - if (!cert) return false; - auto x509 = static_cast(cert); - - const WOLFSSL_ASN1_TIME *nb = wolfSSL_X509_get_notBefore(x509); - const WOLFSSL_ASN1_TIME *na = wolfSSL_X509_get_notAfter(x509); - - if (!nb || !na) return false; - - // wolfSSL_ASN1_TIME_to_tm is available - struct tm tm_nb = {}, tm_na = {}; - if (wolfSSL_ASN1_TIME_to_tm(nb, &tm_nb) != WOLFSSL_SUCCESS) return false; - if (wolfSSL_ASN1_TIME_to_tm(na, &tm_na) != WOLFSSL_SUCCESS) return false; - -#ifdef _WIN32 - not_before = _mkgmtime(&tm_nb); - not_after = _mkgmtime(&tm_na); -#else - not_before = timegm(&tm_nb); - not_after = timegm(&tm_na); -#endif - return true; -} - -std::string get_cert_serial(cert_t cert) { - if (!cert) return ""; - auto x509 = static_cast(cert); - - WOLFSSL_ASN1_INTEGER *serial_asn1 = wolfSSL_X509_get_serialNumber(x509); - if (!serial_asn1) return ""; - - // Get the serial number data - int len = serial_asn1->length; - unsigned char *data = serial_asn1->data; - if (!data || len <= 0) return ""; - - std::string result; - result.reserve(static_cast(len) * 2); - for (int i = 0; i < len; i++) { - char hex[3]; - snprintf(hex, sizeof(hex), "%02X", data[i]); - result += hex; - } - return result; -} - -bool get_cert_der(cert_t cert, std::vector &der) { - if (!cert) return false; - auto x509 = static_cast(cert); - - int der_len = 0; - const unsigned char *der_data = wolfSSL_X509_get_der(x509, &der_len); - if (!der_data || der_len <= 0) return false; - - der.assign(der_data, der_data + der_len); - return true; -} - -const char *get_sni(const_session_t session) { - if (!session) return nullptr; - auto wsession = static_cast(session); - - // For server: return SNI received from client during handshake - if (!wsession->sni_hostname.empty()) { - return wsession->sni_hostname.c_str(); - } - - // For client: return the hostname set via set_sni - if (!wsession->hostname.empty()) { return wsession->hostname.c_str(); } - - return nullptr; -} - -uint64_t peek_error() { - return static_cast(wolfSSL_ERR_peek_last_error()); -} - -uint64_t get_error() { - uint64_t err = impl::wolfssl_last_error(); - impl::wolfssl_last_error() = 0; - return err; -} - -std::string error_string(uint64_t code) { - char buf[256]; - wolfSSL_ERR_error_string(static_cast(code), buf); - return std::string(buf); -} - -ca_store_t create_ca_store(const char *pem, size_t len) { - if (!pem || len == 0) { return nullptr; } - // Validate by attempting to load into a temporary ctx - WOLFSSL_CTX *tmp_ctx = wolfSSL_CTX_new(wolfTLSv1_2_client_method()); - if (!tmp_ctx) { return nullptr; } - int ret = wolfSSL_CTX_load_verify_buffer( - tmp_ctx, reinterpret_cast(pem), - static_cast(len), SSL_FILETYPE_PEM); - wolfSSL_CTX_free(tmp_ctx); - if (ret != SSL_SUCCESS) { return nullptr; } - return static_cast( - new impl::WolfSSLCAStore{std::string(pem, len)}); -} - -void free_ca_store(ca_store_t store) { - delete static_cast(store); -} - -bool set_ca_store(ctx_t ctx, ca_store_t store) { - if (!ctx || !store) { return false; } - auto *wctx = static_cast(ctx); - auto *ca = static_cast(store); - int ret = wolfSSL_CTX_load_verify_buffer( - wctx->ctx, reinterpret_cast(ca->pem_data.data()), - static_cast(ca->pem_data.size()), SSL_FILETYPE_PEM); - if (ret == SSL_SUCCESS) { wctx->ca_pem_data_ += ca->pem_data; } - return ret == SSL_SUCCESS; -} - -size_t get_ca_certs(ctx_t ctx, std::vector &certs) { - certs.clear(); - if (!ctx) { return 0; } - auto *wctx = static_cast(ctx); - if (wctx->ca_pem_data_.empty()) { return 0; } - - const std::string &pem = wctx->ca_pem_data_; - const std::string begin_marker = "-----BEGIN CERTIFICATE-----"; - const std::string end_marker = "-----END CERTIFICATE-----"; - size_t pos = 0; - while ((pos = pem.find(begin_marker, pos)) != std::string::npos) { - size_t end_pos = pem.find(end_marker, pos); - if (end_pos == std::string::npos) { break; } - end_pos += end_marker.size(); - std::string cert_pem = pem.substr(pos, end_pos - pos); - WOLFSSL_X509 *x509 = wolfSSL_X509_load_certificate_buffer( - reinterpret_cast(cert_pem.data()), - static_cast(cert_pem.size()), WOLFSSL_FILETYPE_PEM); - if (x509) { certs.push_back(static_cast(x509)); } - pos = end_pos; - } - return certs.size(); -} - -std::vector get_ca_names(ctx_t ctx) { - std::vector names; - if (!ctx) { return names; } - auto *wctx = static_cast(ctx); - if (wctx->ca_pem_data_.empty()) { return names; } - - const std::string &pem = wctx->ca_pem_data_; - const std::string begin_marker = "-----BEGIN CERTIFICATE-----"; - const std::string end_marker = "-----END CERTIFICATE-----"; - size_t pos = 0; - while ((pos = pem.find(begin_marker, pos)) != std::string::npos) { - size_t end_pos = pem.find(end_marker, pos); - if (end_pos == std::string::npos) { break; } - end_pos += end_marker.size(); - std::string cert_pem = pem.substr(pos, end_pos - pos); - WOLFSSL_X509 *x509 = wolfSSL_X509_load_certificate_buffer( - reinterpret_cast(cert_pem.data()), - static_cast(cert_pem.size()), WOLFSSL_FILETYPE_PEM); - if (x509) { - WOLFSSL_X509_NAME *subject = wolfSSL_X509_get_subject_name(x509); - if (subject) { - char *name_str = wolfSSL_X509_NAME_oneline(subject, nullptr, 0); - if (name_str) { - names.push_back(name_str); - XFREE(name_str, nullptr, DYNAMIC_TYPE_OPENSSL); - } - } - wolfSSL_X509_free(x509); - } - pos = end_pos; - } - return names; -} - -bool update_server_cert(ctx_t ctx, const char *cert_pem, - const char *key_pem, const char *password) { - if (!ctx || !cert_pem || !key_pem) { return false; } - auto *wctx = static_cast(ctx); - - // Load new certificate - int ret = wolfSSL_CTX_use_certificate_buffer( - wctx->ctx, reinterpret_cast(cert_pem), - static_cast(strlen(cert_pem)), SSL_FILETYPE_PEM); - if (ret != SSL_SUCCESS) { - impl::wolfssl_last_error() = - static_cast(wolfSSL_ERR_peek_last_error()); - return false; - } - - // Set password if provided - if (password) { impl::set_wolfssl_password_cb(wctx->ctx, password); } - - // Load new private key - ret = wolfSSL_CTX_use_PrivateKey_buffer( - wctx->ctx, reinterpret_cast(key_pem), - static_cast(strlen(key_pem)), SSL_FILETYPE_PEM); - if (ret != SSL_SUCCESS) { - impl::wolfssl_last_error() = - static_cast(wolfSSL_ERR_peek_last_error()); - return false; - } - - return true; -} - -bool update_server_client_ca(ctx_t ctx, const char *ca_pem) { - if (!ctx || !ca_pem) { return false; } - auto *wctx = static_cast(ctx); - - int ret = wolfSSL_CTX_load_verify_buffer( - wctx->ctx, reinterpret_cast(ca_pem), - static_cast(strlen(ca_pem)), SSL_FILETYPE_PEM); - if (ret != SSL_SUCCESS) { - impl::wolfssl_last_error() = - static_cast(wolfSSL_ERR_peek_last_error()); - return false; - } - return true; -} - -bool set_verify_callback(ctx_t ctx, VerifyCallback callback) { - if (!ctx) { return false; } - auto *wctx = static_cast(ctx); - - impl::get_verify_callback() = std::move(callback); - wctx->has_verify_callback = static_cast(impl::get_verify_callback()); - - if (wctx->has_verify_callback) { - wolfSSL_CTX_set_verify(wctx->ctx, SSL_VERIFY_PEER, - impl::wolfssl_verify_callback); - } else { - wolfSSL_CTX_set_verify( - wctx->ctx, - wctx->verify_client - ? (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT) - : SSL_VERIFY_NONE, - nullptr); - } - return true; -} - -long get_verify_error(const_session_t session) { - if (!session) { return -1; } - auto *wsession = - static_cast(const_cast(session)); - return wolfSSL_get_verify_result(wsession->ssl); -} - -std::string verify_error_string(long error_code) { - if (error_code == 0) { return ""; } - const char *str = - wolfSSL_X509_verify_cert_error_string(static_cast(error_code)); - return str ? std::string(str) : std::string(); -} - -} // namespace tls - -#endif // CPPHTTPLIB_WOLFSSL_SUPPORT - -// WebSocket implementation -namespace ws { - -bool WebSocket::send_frame(Opcode op, const char *data, size_t len, - bool fin) { - std::lock_guard lock(write_mutex_); - if (closed_) { return false; } - return detail::write_websocket_frame(strm_, op, data, len, fin, !is_server_); -} - -ReadResult WebSocket::read(std::string &msg) { - while (!closed_) { - Opcode opcode; - std::string payload; - bool fin; - - if (!impl::read_websocket_frame(strm_, opcode, payload, fin, is_server_, - CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH)) { - closed_ = true; - return Fail; - } - - switch (opcode) { - case Opcode::Ping: { - std::lock_guard lock(write_mutex_); - detail::write_websocket_frame(strm_, Opcode::Pong, payload.data(), - payload.size(), true, !is_server_); - continue; - } - case Opcode::Pong: { - std::lock_guard lock(ping_mutex_); - unacked_pings_ = 0; - continue; - } - case Opcode::Close: { - if (!closed_.exchange(true)) { - // Echo close frame back - std::lock_guard lock(write_mutex_); - detail::write_websocket_frame(strm_, Opcode::Close, payload.data(), - payload.size(), true, !is_server_); - } - return Fail; - } - case Opcode::Text: - case Opcode::Binary: { - auto result = opcode == Opcode::Text ? Text : Binary; - msg = std::move(payload); - - // Handle fragmentation - if (!fin) { - while (true) { - Opcode cont_opcode; - std::string cont_payload; - bool cont_fin; - if (!impl::read_websocket_frame( - strm_, cont_opcode, cont_payload, cont_fin, is_server_, - CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH)) { - closed_ = true; - return Fail; - } - if (cont_opcode == Opcode::Ping) { - std::lock_guard lock(write_mutex_); - detail::write_websocket_frame( - strm_, Opcode::Pong, cont_payload.data(), cont_payload.size(), - true, !is_server_); - continue; - } - if (cont_opcode == Opcode::Pong) { - std::lock_guard lock(ping_mutex_); - unacked_pings_ = 0; - continue; - } - if (cont_opcode == Opcode::Close) { - if (!closed_.exchange(true)) { - std::lock_guard lock(write_mutex_); - detail::write_websocket_frame( - strm_, Opcode::Close, cont_payload.data(), - cont_payload.size(), true, !is_server_); - } - return Fail; - } - // RFC 6455: continuation frames must use opcode 0x0 - if (cont_opcode != Opcode::Continuation) { - closed_ = true; - return Fail; - } - msg += cont_payload; - if (msg.size() > CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH) { - closed_ = true; - return Fail; - } - if (cont_fin) { break; } - } - } - // RFC 6455 Section 5.6: text frames must contain valid UTF-8 - if (result == Text && !impl::is_valid_utf8(msg)) { - close(CloseStatus::InvalidPayload, "invalid UTF-8"); - return Fail; - } - return result; - } - default: closed_ = true; return Fail; - } - } - return Fail; -} - -bool WebSocket::send(const std::string &data) { - return send_frame(Opcode::Text, data.data(), data.size()); -} - -bool WebSocket::send(const char *data, size_t len) { - return send_frame(Opcode::Binary, data, len); -} - -void WebSocket::close(CloseStatus status, const std::string &reason) { - if (closed_.exchange(true)) { return; } - ping_cv_.notify_all(); - std::string payload; - auto code = static_cast(status); - payload.push_back(static_cast((code >> 8) & 0xFF)); - payload.push_back(static_cast(code & 0xFF)); - // RFC 6455 Section 5.5: control frame payload must not exceed 125 bytes - // Close frame has 2-byte status code, so reason is limited to 123 bytes - payload += reason.substr(0, 123); - { - std::lock_guard lock(write_mutex_); - detail::write_websocket_frame(strm_, Opcode::Close, payload.data(), - payload.size(), true, !is_server_); - } - - // RFC 6455 Section 7.1.1: after sending a Close frame, wait for the peer's - // Close response before closing the TCP connection. Use a short timeout to - // avoid hanging if the peer doesn't respond. - strm_.set_read_timeout(CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND, 0); - Opcode op; - std::string resp; - bool fin; - while (impl::read_websocket_frame(strm_, op, resp, fin, is_server_, 125)) { - if (op == Opcode::Close) { break; } - } -} - -WebSocket::~WebSocket() { - { - std::lock_guard lock(ping_mutex_); - closed_ = true; - } - ping_cv_.notify_all(); - if (ping_thread_.joinable()) { ping_thread_.join(); } -} - -void WebSocket::start_heartbeat() { - if (ping_interval_sec_ == 0) { return; } - ping_thread_ = std::thread([this]() { - std::unique_lock lock(ping_mutex_); - while (!closed_) { - ping_cv_.wait_for(lock, std::chrono::seconds(ping_interval_sec_)); - if (closed_) { break; } - // If the peer has failed to respond to the previous pings, give up. - // RFC 6455 does not define a pong-timeout mechanism; this is an - // opt-in liveness check controlled by max_missed_pongs_. - if (max_missed_pongs_ > 0 && unacked_pings_ >= max_missed_pongs_) { - lock.unlock(); - close(CloseStatus::GoingAway, "pong timeout"); - return; - } - lock.unlock(); - if (!send_frame(Opcode::Ping, nullptr, 0)) { - lock.lock(); - closed_ = true; - break; - } - lock.lock(); - unacked_pings_++; - } - }); -} - -const Request &WebSocket::request() const { return req_; } - -bool WebSocket::is_open() const { return !closed_; } - -// WebSocketClient implementation -WebSocketClient::WebSocketClient( - const std::string &scheme_host_port_path, const Headers &headers) - : headers_(headers) { - detail::UrlComponents uc; - if (detail::parse_url(scheme_host_port_path, uc) && !uc.scheme.empty() && - !uc.host.empty() && !uc.path.empty()) { - auto &scheme = uc.scheme; - -#ifdef CPPHTTPLIB_SSL_ENABLED - if (scheme != "ws" && scheme != "wss") { -#else - if (scheme != "ws") { -#endif -#ifndef CPPHTTPLIB_NO_EXCEPTIONS - std::string msg = "'" + scheme + "' scheme is not supported."; - throw std::invalid_argument(msg); -#endif - return; - } - - auto is_ssl = scheme == "wss"; - - host_ = std::move(uc.host); - - port_ = is_ssl ? 443 : 80; - if (!uc.port.empty() && !detail::parse_port(uc.port, port_)) { return; } - - path_ = std::move(uc.path); - -#ifdef CPPHTTPLIB_SSL_ENABLED - is_ssl_ = is_ssl; -#else - if (is_ssl) { return; } -#endif - - is_valid_ = true; - } -} - -WebSocketClient::~WebSocketClient() { shutdown_and_close(); } - -bool WebSocketClient::is_valid() const { return is_valid_; } - -void WebSocketClient::shutdown_and_close() { -#ifdef CPPHTTPLIB_SSL_ENABLED - if (is_ssl_) { - if (tls_session_) { - tls::shutdown(tls_session_, true); - tls::free_session(tls_session_); - tls_session_ = nullptr; - } - if (tls_ctx_) { - tls::free_context(tls_ctx_); - tls_ctx_ = nullptr; - } - } -#endif - if (ws_ && ws_->is_open()) { ws_->close(); } - ws_.reset(); - if (sock_ != INVALID_SOCKET) { - detail::shutdown_socket(sock_); - detail::close_socket(sock_); - sock_ = INVALID_SOCKET; - } -} - -bool WebSocketClient::create_stream(std::unique_ptr &strm) { -#ifdef CPPHTTPLIB_SSL_ENABLED - if (is_ssl_) { - if (!detail::setup_client_tls_session( - host_, tls_ctx_, tls_session_, sock_, - server_certificate_verification_, ca_cert_file_path_, - ca_cert_store_, read_timeout_sec_, read_timeout_usec_)) { - return false; - } - - strm = std::unique_ptr(new detail::SSLSocketStream( - sock_, tls_session_, read_timeout_sec_, read_timeout_usec_, - write_timeout_sec_, write_timeout_usec_)); - return true; - } -#endif - strm = std::unique_ptr( - new detail::SocketStream(sock_, read_timeout_sec_, read_timeout_usec_, - write_timeout_sec_, write_timeout_usec_)); - return true; -} - -bool WebSocketClient::connect() { - if (!is_valid_) { return false; } - shutdown_and_close(); - - Error error; - sock_ = detail::create_client_socket( - host_, std::string(), port_, address_family_, tcp_nodelay_, ipv6_v6only_, - socket_options_, connection_timeout_sec_, connection_timeout_usec_, - read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, - write_timeout_usec_, interface_, error); - - if (sock_ == INVALID_SOCKET) { return false; } - - std::unique_ptr strm; - if (!create_stream(strm)) { - shutdown_and_close(); - return false; - } - - std::string selected_subprotocol; - if (!detail::perform_websocket_handshake(*strm, host_, port_, path_, headers_, - selected_subprotocol)) { - shutdown_and_close(); - return false; - } - subprotocol_ = std::move(selected_subprotocol); - - Request req; - req.method = "GET"; - req.path = path_; - ws_ = std::unique_ptr(new WebSocket(std::move(strm), req, false, - websocket_ping_interval_sec_, - websocket_max_missed_pongs_)); - return true; -} - -ReadResult WebSocketClient::read(std::string &msg) { - if (!ws_) { return Fail; } - return ws_->read(msg); -} - -bool WebSocketClient::send(const std::string &data) { - if (!ws_) { return false; } - return ws_->send(data); -} - -bool WebSocketClient::send(const char *data, size_t len) { - if (!ws_) { return false; } - return ws_->send(data, len); -} - -void WebSocketClient::close(CloseStatus status, - const std::string &reason) { - if (ws_) { ws_->close(status, reason); } -} - -bool WebSocketClient::is_open() const { return ws_ && ws_->is_open(); } - -const std::string &WebSocketClient::subprotocol() const { - return subprotocol_; -} - -void WebSocketClient::set_read_timeout(time_t sec, time_t usec) { - read_timeout_sec_ = sec; - read_timeout_usec_ = usec; -} - -void WebSocketClient::set_write_timeout(time_t sec, time_t usec) { - write_timeout_sec_ = sec; - write_timeout_usec_ = usec; -} - -void WebSocketClient::set_websocket_ping_interval(time_t sec) { - websocket_ping_interval_sec_ = sec; -} - -void WebSocketClient::set_websocket_max_missed_pongs(int count) { - websocket_max_missed_pongs_ = count; -} - -void WebSocketClient::set_tcp_nodelay(bool on) { tcp_nodelay_ = on; } - -void WebSocketClient::set_address_family(int family) { - address_family_ = family; -} - -void WebSocketClient::set_ipv6_v6only(bool on) { ipv6_v6only_ = on; } - -void WebSocketClient::set_socket_options(SocketOptions socket_options) { - socket_options_ = std::move(socket_options); -} - -void WebSocketClient::set_connection_timeout(time_t sec, time_t usec) { - connection_timeout_sec_ = sec; - connection_timeout_usec_ = usec; -} - -void WebSocketClient::set_interface(const std::string &intf) { - interface_ = intf; -} - -#ifdef CPPHTTPLIB_SSL_ENABLED - -void WebSocketClient::set_ca_cert_path(const std::string &path) { - ca_cert_file_path_ = path; -} - -void WebSocketClient::set_ca_cert_store(tls::ca_store_t store) { - ca_cert_store_ = store; -} - -void -WebSocketClient::enable_server_certificate_verification(bool enabled) { - server_certificate_verification_ = enabled; -} - -#endif // CPPHTTPLIB_SSL_ENABLED - -} // namespace ws - -} // namespace httplib diff --git a/third_party/httplib/httplib.h b/third_party/httplib/httplib.h deleted file mode 100644 index cbb549e..0000000 --- a/third_party/httplib/httplib.h +++ /dev/null @@ -1,3903 +0,0 @@ -// -// httplib.h -// -// Copyright (c) 2026 Yuji Hirose. All rights reserved. -// MIT License -// - -#ifndef CPPHTTPLIB_HTTPLIB_H -#define CPPHTTPLIB_HTTPLIB_H - -#define CPPHTTPLIB_VERSION "0.46.1" -#define CPPHTTPLIB_VERSION_NUM "0x002e01" - -#ifdef _WIN32 -#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00 -#error \ - "cpp-httplib doesn't support Windows 8 or lower. Please use Windows 10 or later." -#endif -#endif - -/* - * Configuration - */ - -#ifndef CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND -#define CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND 5 -#endif - -#ifndef CPPHTTPLIB_KEEPALIVE_TIMEOUT_CHECK_INTERVAL_USECOND -#define CPPHTTPLIB_KEEPALIVE_TIMEOUT_CHECK_INTERVAL_USECOND 10000 -#endif - -#ifndef CPPHTTPLIB_KEEPALIVE_MAX_COUNT -#define CPPHTTPLIB_KEEPALIVE_MAX_COUNT 100 -#endif - -#ifndef CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND -#define CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND 300 -#endif - -#ifndef CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND -#define CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND 0 -#endif - -#ifndef CPPHTTPLIB_SERVER_READ_TIMEOUT_SECOND -#define CPPHTTPLIB_SERVER_READ_TIMEOUT_SECOND 5 -#endif - -#ifndef CPPHTTPLIB_SERVER_READ_TIMEOUT_USECOND -#define CPPHTTPLIB_SERVER_READ_TIMEOUT_USECOND 0 -#endif - -#ifndef CPPHTTPLIB_SERVER_WRITE_TIMEOUT_SECOND -#define CPPHTTPLIB_SERVER_WRITE_TIMEOUT_SECOND 5 -#endif - -#ifndef CPPHTTPLIB_SERVER_WRITE_TIMEOUT_USECOND -#define CPPHTTPLIB_SERVER_WRITE_TIMEOUT_USECOND 0 -#endif - -#ifndef CPPHTTPLIB_CLIENT_READ_TIMEOUT_SECOND -#define CPPHTTPLIB_CLIENT_READ_TIMEOUT_SECOND 300 -#endif - -#ifndef CPPHTTPLIB_CLIENT_READ_TIMEOUT_USECOND -#define CPPHTTPLIB_CLIENT_READ_TIMEOUT_USECOND 0 -#endif - -#ifndef CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND -#define CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND 5 -#endif - -#ifndef CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND -#define CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND 0 -#endif - -#ifndef CPPHTTPLIB_CLIENT_MAX_TIMEOUT_MSECOND -#define CPPHTTPLIB_CLIENT_MAX_TIMEOUT_MSECOND 0 -#endif - -#ifndef CPPHTTPLIB_EXPECT_100_THRESHOLD -#define CPPHTTPLIB_EXPECT_100_THRESHOLD 1024 -#endif - -#ifndef CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND -#define CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND 1000 -#endif - -#ifndef CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_THRESHOLD -#define CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_THRESHOLD (1024 * 1024) -#endif - -#ifndef CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND -#define CPPHTTPLIB_WAIT_EARLY_SERVER_RESPONSE_TIMEOUT_MSECOND 50 -#endif - -#ifndef CPPHTTPLIB_IDLE_INTERVAL_SECOND -#define CPPHTTPLIB_IDLE_INTERVAL_SECOND 0 -#endif - -#ifndef CPPHTTPLIB_IDLE_INTERVAL_USECOND -#ifdef _WIN32 -#define CPPHTTPLIB_IDLE_INTERVAL_USECOND 1000 -#else -#define CPPHTTPLIB_IDLE_INTERVAL_USECOND 0 -#endif -#endif - -#ifndef CPPHTTPLIB_REQUEST_URI_MAX_LENGTH -#define CPPHTTPLIB_REQUEST_URI_MAX_LENGTH 8192 -#endif - -#ifndef CPPHTTPLIB_HEADER_MAX_LENGTH -#define CPPHTTPLIB_HEADER_MAX_LENGTH 8192 -#endif - -#ifndef CPPHTTPLIB_HEADER_MAX_COUNT -#define CPPHTTPLIB_HEADER_MAX_COUNT 100 -#endif - -#ifndef CPPHTTPLIB_REDIRECT_MAX_COUNT -#define CPPHTTPLIB_REDIRECT_MAX_COUNT 20 -#endif - -#ifndef CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT -#define CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT 1024 -#endif - -#ifndef CPPHTTPLIB_PAYLOAD_MAX_LENGTH -#define CPPHTTPLIB_PAYLOAD_MAX_LENGTH (100 * 1024 * 1024) // 100MB -#endif - -#ifndef CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH -#define CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH 8192 -#endif - -#ifndef CPPHTTPLIB_RANGE_MAX_COUNT -#define CPPHTTPLIB_RANGE_MAX_COUNT 1024 -#endif - -#ifndef CPPHTTPLIB_TCP_NODELAY -#define CPPHTTPLIB_TCP_NODELAY false -#endif - -#ifndef CPPHTTPLIB_IPV6_V6ONLY -#define CPPHTTPLIB_IPV6_V6ONLY false -#endif - -#ifndef CPPHTTPLIB_RECV_BUFSIZ -#define CPPHTTPLIB_RECV_BUFSIZ size_t(16384u) -#endif - -#ifndef CPPHTTPLIB_SEND_BUFSIZ -#define CPPHTTPLIB_SEND_BUFSIZ size_t(16384u) -#endif - -#ifndef CPPHTTPLIB_COMPRESSION_BUFSIZ -#define CPPHTTPLIB_COMPRESSION_BUFSIZ size_t(16384u) -#endif - -#ifndef CPPHTTPLIB_THREAD_POOL_COUNT -#define CPPHTTPLIB_THREAD_POOL_COUNT \ - ((std::max)(8u, std::thread::hardware_concurrency() > 0 \ - ? std::thread::hardware_concurrency() - 1 \ - : 0)) -#endif - -#ifndef CPPHTTPLIB_THREAD_POOL_MAX_COUNT -#define CPPHTTPLIB_THREAD_POOL_MAX_COUNT (CPPHTTPLIB_THREAD_POOL_COUNT * 4) -#endif - -#ifndef CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT -#define CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT 3 // seconds -#endif - -#ifndef CPPHTTPLIB_RECV_FLAGS -#define CPPHTTPLIB_RECV_FLAGS 0 -#endif - -#ifndef CPPHTTPLIB_SEND_FLAGS -#define CPPHTTPLIB_SEND_FLAGS 0 -#endif - -#ifndef CPPHTTPLIB_LISTEN_BACKLOG -#define CPPHTTPLIB_LISTEN_BACKLOG 5 -#endif - -#ifndef CPPHTTPLIB_MAX_LINE_LENGTH -#define CPPHTTPLIB_MAX_LINE_LENGTH 32768 -#endif - -#ifndef CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH -#define CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH 16777216 -#endif - -#ifndef CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND -#define CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND 300 -#endif - -#ifndef CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND -#define CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND 5 -#endif - -#ifndef CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND -#define CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND 30 -#endif - -#ifndef CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS -#define CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS 0 -#endif - -/* - * Headers - */ - -#ifdef _WIN32 -#ifndef _CRT_SECURE_NO_WARNINGS -#define _CRT_SECURE_NO_WARNINGS -#endif //_CRT_SECURE_NO_WARNINGS - -#ifndef _CRT_NONSTDC_NO_DEPRECATE -#define _CRT_NONSTDC_NO_DEPRECATE -#endif //_CRT_NONSTDC_NO_DEPRECATE - -#if defined(_MSC_VER) -#if _MSC_VER < 1900 -#error Sorry, Visual Studio versions prior to 2015 are not supported -#endif - -#pragma comment(lib, "ws2_32.lib") - -#ifndef _SSIZE_T_DEFINED -using ssize_t = __int64; -#define _SSIZE_T_DEFINED -#endif -#endif // _MSC_VER - -#ifndef S_ISREG -#define S_ISREG(m) (((m) & S_IFREG) == S_IFREG) -#endif // S_ISREG - -#ifndef S_ISDIR -#define S_ISDIR(m) (((m) & S_IFDIR) == S_IFDIR) -#endif // S_ISDIR - -#ifndef NOMINMAX -#define NOMINMAX -#endif // NOMINMAX - -#include -#include -#include - -#if defined(__has_include) -#if __has_include() -// afunix.h uses types declared in winsock2.h, so has to be included after it. -#include -#define CPPHTTPLIB_HAVE_AFUNIX_H 1 -#endif -#endif - -#ifndef WSA_FLAG_NO_HANDLE_INHERIT -#define WSA_FLAG_NO_HANDLE_INHERIT 0x80 -#endif - -using nfds_t = unsigned long; -using socket_t = SOCKET; -using socklen_t = int; - -#else // not _WIN32 - -#include -#if !defined(_AIX) && !defined(__MVS__) -#include -#endif -#ifdef __MVS__ -#include -#ifndef NI_MAXHOST -#define NI_MAXHOST 1025 -#endif -#endif -#include -#include -#include -#ifdef __linux__ -#include -#undef _res // Undefine _res macro to avoid conflicts with user code (#2278) -#endif -#include -#include -#include -#include -#include -#include -#include -#include - -using socket_t = int; -#ifndef INVALID_SOCKET -#define INVALID_SOCKET (-1) -#endif -#endif //_WIN32 - -#if defined(__APPLE__) -#include -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// On macOS with a TLS backend, enable Keychain root certificates by default -// unless the user explicitly opts out. Not enabled on iOS/tvOS/watchOS since -// the SecTrustSettings APIs used to enumerate anchor certificates are macOS -// only; on those platforms the user must provide a CA bundle explicitly. -#if defined(__APPLE__) && defined(__clang__) && \ - !defined(CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES) && \ - (defined(CPPHTTPLIB_OPENSSL_SUPPORT) || \ - defined(CPPHTTPLIB_MBEDTLS_SUPPORT) || \ - defined(CPPHTTPLIB_WOLFSSL_SUPPORT)) -#if TARGET_OS_OSX -#ifndef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -#define CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -#endif -#endif -#endif - -#if defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && \ - defined(__APPLE__) && !TARGET_OS_OSX -#error \ - "CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN is only supported on macOS. On iOS/tvOS/watchOS, supply a CA bundle via set_ca_cert_path()." -#endif - -// On Windows, enable Schannel certificate verification by default -// unless the user explicitly opts out. -#if defined(_WIN32) && \ - !defined(CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE) -#define CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE -#endif - -#if defined(CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO) || \ - defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) -#if TARGET_OS_MAC && defined(__clang__) -#include -#include -#endif -#endif - -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT -#ifdef _WIN32 -#include - -// these are defined in wincrypt.h and it breaks compilation if BoringSSL is -// used -#undef X509_NAME -#undef X509_CERT_PAIR -#undef X509_EXTENSIONS -#undef PKCS7_SIGNER_INFO - -#ifdef _MSC_VER -#pragma comment(lib, "crypt32.lib") -#endif -#endif // _WIN32 - -#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -#if TARGET_OS_OSX -#include -#endif -#endif - -#include -#include -#include -#include - -#if defined(_WIN32) && defined(OPENSSL_USE_APPLINK) -#include -#endif - -#include -#include - -#if defined(OPENSSL_IS_BORINGSSL) || defined(LIBRESSL_VERSION_NUMBER) -#if OPENSSL_VERSION_NUMBER < 0x1010107f -#error Please use OpenSSL or a current version of BoringSSL -#endif -#define SSL_get1_peer_certificate SSL_get_peer_certificate -#elif OPENSSL_VERSION_NUMBER < 0x30000000L -#error Sorry, OpenSSL versions prior to 3.0.0 are not supported -#endif - -#endif // CPPHTTPLIB_OPENSSL_SUPPORT - -#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef _WIN32 -#include -#ifdef _MSC_VER -#pragma comment(lib, "crypt32.lib") -#endif -#endif // _WIN32 -#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -#if TARGET_OS_OSX -#include -#endif -#endif - -// Mbed TLS 3.x API compatibility -#if MBEDTLS_VERSION_MAJOR >= 3 -#define CPPHTTPLIB_MBEDTLS_V3 -#endif - -#endif // CPPHTTPLIB_MBEDTLS_SUPPORT - -#ifdef CPPHTTPLIB_WOLFSSL_SUPPORT -#include - -#include - -// Fallback definitions for older wolfSSL versions (e.g., 5.6.6) -#ifndef WOLFSSL_GEN_EMAIL -#define WOLFSSL_GEN_EMAIL 1 -#endif -#ifndef WOLFSSL_GEN_DNS -#define WOLFSSL_GEN_DNS 2 -#endif -#ifndef WOLFSSL_GEN_URI -#define WOLFSSL_GEN_URI 6 -#endif -#ifndef WOLFSSL_GEN_IPADD -#define WOLFSSL_GEN_IPADD 7 -#endif - -#include -#include -#include -#include -#include -#ifdef _WIN32 -#include -#ifdef _MSC_VER -#pragma comment(lib, "crypt32.lib") -#endif -#endif // _WIN32 -#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -#if TARGET_OS_OSX -#include -#endif -#endif -#endif // CPPHTTPLIB_WOLFSSL_SUPPORT - -// Define CPPHTTPLIB_SSL_ENABLED if any SSL backend is available -#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) || \ - defined(CPPHTTPLIB_MBEDTLS_SUPPORT) || defined(CPPHTTPLIB_WOLFSSL_SUPPORT) -#define CPPHTTPLIB_SSL_ENABLED -#endif - -#ifdef CPPHTTPLIB_ZLIB_SUPPORT -#include -#endif - -#ifdef CPPHTTPLIB_BROTLI_SUPPORT -#include -#include -#endif - -#ifdef CPPHTTPLIB_ZSTD_SUPPORT -#include -#endif - -/* - * Declaration - */ -namespace httplib { - -namespace ws { -class WebSocket; -} // namespace ws - -namespace detail { - -/* - * Backport std::make_unique from C++14. - * - * NOTE: This code came up with the following stackoverflow post: - * https://stackoverflow.com/questions/10149840/c-arrays-and-make-unique - * - */ - -template -typename std::enable_if::value, std::unique_ptr>::type -make_unique(Args &&...args) { - return std::unique_ptr(new T(std::forward(args)...)); -} - -template -typename std::enable_if::value, std::unique_ptr>::type -make_unique(std::size_t n) { - typedef typename std::remove_extent::type RT; - return std::unique_ptr(new RT[n]); -} - -namespace case_ignore { - -inline unsigned char to_lower(int c) { - const static unsigned char table[256] = { - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, - 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, - 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, - 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, - 60, 61, 62, 63, 64, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, - 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, - 122, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, - 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, - 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, - 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, - 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, - 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, - 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 224, 225, 226, - 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, - 242, 243, 244, 245, 246, 215, 248, 249, 250, 251, 252, 253, 254, 223, 224, - 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, - 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, - 255, - }; - return table[(unsigned char)(char)c]; -} - -inline std::string to_lower(const std::string &s) { - std::string result = s; - std::transform( - result.begin(), result.end(), result.begin(), - [](unsigned char c) { return static_cast(to_lower(c)); }); - return result; -} - -inline bool equal(const std::string &a, const std::string &b) { - return a.size() == b.size() && - std::equal(a.begin(), a.end(), b.begin(), [](char ca, char cb) { - return to_lower(ca) == to_lower(cb); - }); -} - -struct equal_to { - bool operator()(const std::string &a, const std::string &b) const { - return equal(a, b); - } -}; - -struct hash { - size_t operator()(const std::string &key) const { - return hash_core(key.data(), key.size(), 0); - } - - size_t hash_core(const char *s, size_t l, size_t h) const { - return (l == 0) ? h - : hash_core(s + 1, l - 1, - // Unsets the 6 high bits of h, therefore no - // overflow happens - (((std::numeric_limits::max)() >> 6) & - h * 33) ^ - static_cast(to_lower(*s))); - } -}; - -template -using unordered_set = std::unordered_set; - -} // namespace case_ignore - -// This is based on -// "http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4189". - -struct scope_exit { - explicit scope_exit(std::function &&f) - : exit_function(std::move(f)), execute_on_destruction{true} {} - - scope_exit(scope_exit &&rhs) noexcept - : exit_function(std::move(rhs.exit_function)), - execute_on_destruction{rhs.execute_on_destruction} { - rhs.release(); - } - - ~scope_exit() { - if (execute_on_destruction) { this->exit_function(); } - } - - void release() { this->execute_on_destruction = false; } - -private: - scope_exit(const scope_exit &) = delete; - void operator=(const scope_exit &) = delete; - scope_exit &operator=(scope_exit &&) = delete; - - std::function exit_function; - bool execute_on_destruction; -}; - -// Simple from_chars implementation for integer and double types (C++17 -// substitute) -template struct from_chars_result { - const char *ptr; - std::errc ec; -}; - -template -inline from_chars_result from_chars(const char *first, const char *last, - T &value, int base = 10) { - value = 0; - const char *p = first; - bool negative = false; - - if (p != last && *p == '-') { - negative = true; - ++p; - } - if (p == last) { return {first, std::errc::invalid_argument}; } - - T result = 0; - for (; p != last; ++p) { - char c = *p; - int digit = -1; - if ('0' <= c && c <= '9') { - digit = c - '0'; - } else if ('a' <= c && c <= 'z') { - digit = c - 'a' + 10; - } else if ('A' <= c && c <= 'Z') { - digit = c - 'A' + 10; - } else { - break; - } - - if (digit < 0 || digit >= base) { break; } - if (result > ((std::numeric_limits::max)() - digit) / base) { - return {p, std::errc::result_out_of_range}; - } - result = result * base + digit; - } - - if (p == first || (negative && p == first + 1)) { - return {first, std::errc::invalid_argument}; - } - - value = negative ? -result : result; - return {p, std::errc{}}; -} - -// from_chars for double (simple wrapper for strtod) -inline from_chars_result from_chars(const char *first, const char *last, - double &value) { - std::string s(first, last); - char *endptr = nullptr; - errno = 0; - value = std::strtod(s.c_str(), &endptr); - if (endptr == s.c_str()) { return {first, std::errc::invalid_argument}; } - if (errno == ERANGE) { - return {first + (endptr - s.c_str()), std::errc::result_out_of_range}; - } - return {first + (endptr - s.c_str()), std::errc{}}; -} - -inline bool parse_port(const char *s, size_t len, int &port) { - int val = 0; - auto r = from_chars(s, s + len, val); - if (r.ec != std::errc{} || val < 1 || val > 65535) { return false; } - port = val; - return true; -} - -inline bool parse_port(const std::string &s, int &port) { - return parse_port(s.data(), s.size(), port); -} - -struct UrlComponents { - std::string scheme; - std::string host; - std::string port; - std::string path; - std::string query; -}; - -inline bool parse_url(const std::string &url, UrlComponents &uc) { - uc = {}; - size_t pos = 0; - - auto sep = url.find("://"); - if (sep != std::string::npos) { - uc.scheme = url.substr(0, sep); - - // Scheme must be [a-z]+ only - if (uc.scheme.empty()) { return false; } - for (auto c : uc.scheme) { - if (c < 'a' || c > 'z') { return false; } - } - - pos = sep + 3; - } else if (url.compare(0, 2, "//") == 0) { - pos = 2; - } - - auto has_authority_prefix = pos > 0; - auto has_authority = has_authority_prefix || (!url.empty() && url[0] != '/' && - url[0] != '?' && url[0] != '#'); - if (has_authority) { - if (pos < url.size() && url[pos] == '[') { - auto close = url.find(']', pos); - if (close == std::string::npos) { return false; } - uc.host = url.substr(pos + 1, close - pos - 1); - - // IPv6 host must be [a-fA-F0-9:]+ only - if (uc.host.empty()) { return false; } - for (auto c : uc.host) { - if (!((c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') || - (c >= '0' && c <= '9') || c == ':')) { - return false; - } - } - - pos = close + 1; - } else { - auto end = url.find_first_of(":/?#", pos); - if (end == std::string::npos) { end = url.size(); } - uc.host = url.substr(pos, end - pos); - pos = end; - } - - if (pos < url.size() && url[pos] == ':') { - ++pos; - auto end = url.find_first_of("/?#", pos); - if (end == std::string::npos) { end = url.size(); } - uc.port = url.substr(pos, end - pos); - pos = end; - } - - // Without :// or //, the entire input must be consumed as host[:port]. - // If there is leftover (path, query, etc.), this is not a valid - // host[:port] string — clear and reparse as a plain path. - if (!has_authority_prefix && pos < url.size()) { - uc.host.clear(); - uc.port.clear(); - pos = 0; - } - } - - if (pos < url.size() && url[pos] != '?' && url[pos] != '#') { - auto end = url.find_first_of("?#", pos); - if (end == std::string::npos) { end = url.size(); } - uc.path = url.substr(pos, end - pos); - pos = end; - } - - if (pos < url.size() && url[pos] == '?') { - auto end = url.find('#', pos); - if (end == std::string::npos) { end = url.size(); } - uc.query = url.substr(pos, end - pos); - } - - return true; -} - -} // namespace detail - -enum class SSLVerifierResponse { - // no decision has been made, use the built-in certificate verifier - NoDecisionMade, - // connection certificate is verified and accepted - CertificateAccepted, - // connection certificate was processed but is rejected - CertificateRejected -}; - -enum StatusCode { - // Information responses - Continue_100 = 100, - SwitchingProtocol_101 = 101, - Processing_102 = 102, - EarlyHints_103 = 103, - - // Successful responses - OK_200 = 200, - Created_201 = 201, - Accepted_202 = 202, - NonAuthoritativeInformation_203 = 203, - NoContent_204 = 204, - ResetContent_205 = 205, - PartialContent_206 = 206, - MultiStatus_207 = 207, - AlreadyReported_208 = 208, - IMUsed_226 = 226, - - // Redirection messages - MultipleChoices_300 = 300, - MovedPermanently_301 = 301, - Found_302 = 302, - SeeOther_303 = 303, - NotModified_304 = 304, - UseProxy_305 = 305, - unused_306 = 306, - TemporaryRedirect_307 = 307, - PermanentRedirect_308 = 308, - - // Client error responses - BadRequest_400 = 400, - Unauthorized_401 = 401, - PaymentRequired_402 = 402, - Forbidden_403 = 403, - NotFound_404 = 404, - MethodNotAllowed_405 = 405, - NotAcceptable_406 = 406, - ProxyAuthenticationRequired_407 = 407, - RequestTimeout_408 = 408, - Conflict_409 = 409, - Gone_410 = 410, - LengthRequired_411 = 411, - PreconditionFailed_412 = 412, - PayloadTooLarge_413 = 413, - UriTooLong_414 = 414, - UnsupportedMediaType_415 = 415, - RangeNotSatisfiable_416 = 416, - ExpectationFailed_417 = 417, - ImATeapot_418 = 418, - MisdirectedRequest_421 = 421, - UnprocessableContent_422 = 422, - Locked_423 = 423, - FailedDependency_424 = 424, - TooEarly_425 = 425, - UpgradeRequired_426 = 426, - PreconditionRequired_428 = 428, - TooManyRequests_429 = 429, - RequestHeaderFieldsTooLarge_431 = 431, - UnavailableForLegalReasons_451 = 451, - - // Server error responses - InternalServerError_500 = 500, - NotImplemented_501 = 501, - BadGateway_502 = 502, - ServiceUnavailable_503 = 503, - GatewayTimeout_504 = 504, - HttpVersionNotSupported_505 = 505, - VariantAlsoNegotiates_506 = 506, - InsufficientStorage_507 = 507, - LoopDetected_508 = 508, - NotExtended_510 = 510, - NetworkAuthenticationRequired_511 = 511, -}; - -using Headers = - std::unordered_multimap; - -using Params = std::multimap; -using Match = std::smatch; - -using DownloadProgress = std::function; -using UploadProgress = std::function; - -/* - * detail: type-erased storage used by UserData. - * ABI-stable regardless of C++ standard — always uses this custom - * implementation instead of std::any. - */ -namespace detail { - -using any_type_id = const void *; - -template any_type_id any_typeid() noexcept { - static const char id = 0; - return &id; -} - -struct any_storage { - virtual ~any_storage() = default; - virtual std::unique_ptr clone() const = 0; - virtual any_type_id type_id() const noexcept = 0; -}; - -template struct any_value final : any_storage { - T value; - template explicit any_value(U &&v) : value(std::forward(v)) {} - std::unique_ptr clone() const override { - return std::unique_ptr(new any_value(value)); - } - any_type_id type_id() const noexcept override { return any_typeid(); } -}; - -} // namespace detail - -class UserData { -public: - UserData() = default; - UserData(UserData &&) noexcept = default; - UserData &operator=(UserData &&) noexcept = default; - - UserData(const UserData &o) { - for (const auto &e : o.entries_) { - if (e.second) { entries_[e.first] = e.second->clone(); } - } - } - - UserData &operator=(const UserData &o) { - if (this != &o) { - entries_.clear(); - for (const auto &e : o.entries_) { - if (e.second) { entries_[e.first] = e.second->clone(); } - } - } - return *this; - } - - template void set(const std::string &key, T &&value) { - using D = typename std::decay::type; - entries_[key].reset(new detail::any_value(std::forward(value))); - } - - template T *get(const std::string &key) noexcept { - auto it = entries_.find(key); - if (it == entries_.end() || !it->second) { return nullptr; } - if (it->second->type_id() != detail::any_typeid()) { return nullptr; } - return &static_cast *>(it->second.get())->value; - } - - template const T *get(const std::string &key) const noexcept { - auto it = entries_.find(key); - if (it == entries_.end() || !it->second) { return nullptr; } - if (it->second->type_id() != detail::any_typeid()) { return nullptr; } - return &static_cast *>(it->second.get())->value; - } - - bool has(const std::string &key) const noexcept { - return entries_.find(key) != entries_.end(); - } - - void erase(const std::string &key) { entries_.erase(key); } - - void clear() noexcept { entries_.clear(); } - -private: - std::unordered_map> - entries_; -}; - -struct Response; -using ResponseHandler = std::function; - -struct FormData { - std::string name; - std::string content; - std::string filename; - std::string content_type; - Headers headers; -}; - -struct FormField { - std::string name; - std::string content; - Headers headers; -}; -using FormFields = std::multimap; - -using FormFiles = std::multimap; - -struct MultipartFormData { - FormFields fields; // Text fields from multipart - FormFiles files; // Files from multipart - - // Text field access - std::string get_field(const std::string &key, size_t id = 0) const; - std::vector get_fields(const std::string &key) const; - bool has_field(const std::string &key) const; - size_t get_field_count(const std::string &key) const; - - // File access - FormData get_file(const std::string &key, size_t id = 0) const; - std::vector get_files(const std::string &key) const; - bool has_file(const std::string &key) const; - size_t get_file_count(const std::string &key) const; -}; - -struct UploadFormData { - std::string name; - std::string content; - std::string filename; - std::string content_type; -}; -using UploadFormDataItems = std::vector; - -class DataSink { -public: - DataSink() : os(&sb_), sb_(*this) {} - - DataSink(const DataSink &) = delete; - DataSink &operator=(const DataSink &) = delete; - DataSink(DataSink &&) = delete; - DataSink &operator=(DataSink &&) = delete; - - std::function write; - std::function is_writable; - std::function done; - std::function done_with_trailer; - std::ostream os; - -private: - class data_sink_streambuf final : public std::streambuf { - public: - explicit data_sink_streambuf(DataSink &sink) : sink_(sink) {} - - protected: - std::streamsize xsputn(const char *s, std::streamsize n) override { - if (sink_.write(s, static_cast(n))) { return n; } - return 0; - } - - private: - DataSink &sink_; - }; - - data_sink_streambuf sb_; -}; - -using ContentProvider = - std::function; - -using ContentProviderWithoutLength = - std::function; - -using ContentProviderResourceReleaser = std::function; - -struct FormDataProvider { - std::string name; - ContentProviderWithoutLength provider; - std::string filename; - std::string content_type; -}; -using FormDataProviderItems = std::vector; - -inline FormDataProvider -make_file_provider(const std::string &name, const std::string &filepath, - const std::string &filename = std::string(), - const std::string &content_type = std::string()) { - FormDataProvider fdp; - fdp.name = name; - fdp.filename = filename.empty() ? filepath : filename; - fdp.content_type = content_type; - fdp.provider = [filepath](size_t offset, DataSink &sink) -> bool { - std::ifstream f(filepath, std::ios::binary); - if (!f) { return false; } - if (offset > 0) { - f.seekg(static_cast(offset)); - if (!f.good()) { - sink.done(); - return true; - } - } - char buf[8192]; - f.read(buf, sizeof(buf)); - auto n = static_cast(f.gcount()); - if (n > 0) { return sink.write(buf, n); } - sink.done(); // EOF - return true; - }; - return fdp; -} - -inline std::pair -make_file_body(const std::string &filepath) { - size_t size = 0; - { - std::ifstream f(filepath, std::ios::binary | std::ios::ate); - if (!f) { return {0, ContentProvider{}}; } - size = static_cast(f.tellg()); - } - - ContentProvider provider = [filepath](size_t offset, size_t length, - DataSink &sink) -> bool { - std::ifstream f(filepath, std::ios::binary); - if (!f) { return false; } - f.seekg(static_cast(offset)); - if (!f.good()) { return false; } - char buf[8192]; - while (length > 0) { - auto to_read = (std::min)(sizeof(buf), length); - f.read(buf, static_cast(to_read)); - auto n = static_cast(f.gcount()); - if (n == 0) { break; } - if (!sink.write(buf, n)) { return false; } - length -= n; - } - return true; - }; - return {size, std::move(provider)}; -} - -using ContentReceiverWithProgress = std::function; - -using ContentReceiver = - std::function; - -using FormDataHeader = std::function; - -class ContentReader { -public: - using Reader = std::function; - using FormDataReader = - std::function; - - ContentReader(Reader reader, FormDataReader multipart_reader) - : reader_(std::move(reader)), - formdata_reader_(std::move(multipart_reader)) {} - - bool operator()(FormDataHeader header, ContentReceiver receiver) const { - return formdata_reader_(std::move(header), std::move(receiver)); - } - - bool operator()(ContentReceiver receiver) const { - return reader_(std::move(receiver)); - } - - Reader reader_; - FormDataReader formdata_reader_; -}; - -using Range = std::pair; -using Ranges = std::vector; - -#ifdef CPPHTTPLIB_SSL_ENABLED -// TLS abstraction layer - public type definitions and API -namespace tls { - -// Opaque handles (defined as void* for abstraction) -using ctx_t = void *; -using session_t = void *; -using const_session_t = const void *; // For read-only session access -using cert_t = void *; -using ca_store_t = void *; - -// TLS versions -enum class Version { - TLS1_2 = 0x0303, - TLS1_3 = 0x0304, -}; - -// Subject Alternative Names (SAN) entry types -enum class SanType { DNS, IP, EMAIL, URI, OTHER }; - -// SAN entry structure -struct SanEntry { - SanType type; - std::string value; -}; - -// Verification context for certificate verification callback -struct VerifyContext { - session_t session; // TLS session handle - cert_t cert; // Current certificate being verified - int depth; // Certificate chain depth (0 = leaf) - bool preverify_ok; // OpenSSL/Mbed TLS pre-verification result - long error_code; // Backend-specific error code (0 = no error) - const char *error_string; // Human-readable error description - - // Certificate introspection methods - std::string subject_cn() const; - std::string issuer_name() const; - bool check_hostname(const char *hostname) const; - std::vector sans() const; - bool validity(time_t ¬_before, time_t ¬_after) const; - std::string serial() const; -}; - -using VerifyCallback = std::function; - -// TlsError codes for TLS operations (backend-independent) -enum class ErrorCode : int { - Success = 0, - WantRead, // Non-blocking: need to wait for read - WantWrite, // Non-blocking: need to wait for write - PeerClosed, // Peer closed the connection - Fatal, // Unrecoverable error - SyscallError, // System call error (check sys_errno) - CertVerifyFailed, // Certificate verification failed - HostnameMismatch, // Hostname verification failed -}; - -// TLS error information -struct TlsError { - ErrorCode code = ErrorCode::Fatal; - uint64_t backend_code = 0; // OpenSSL: ERR_get_error(), mbedTLS: return value - int sys_errno = 0; // errno when SyscallError - - // Convert verification error code to human-readable string - static std::string verify_error_to_string(long error_code); -}; - -// RAII wrapper for peer certificate -class PeerCert { -public: - PeerCert(); - PeerCert(PeerCert &&other) noexcept; - PeerCert &operator=(PeerCert &&other) noexcept; - ~PeerCert(); - - PeerCert(const PeerCert &) = delete; - PeerCert &operator=(const PeerCert &) = delete; - - explicit operator bool() const; - std::string subject_cn() const; - std::string issuer_name() const; - bool check_hostname(const char *hostname) const; - std::vector sans() const; - bool validity(time_t ¬_before, time_t ¬_after) const; - std::string serial() const; - -private: - explicit PeerCert(cert_t cert); - cert_t cert_ = nullptr; - friend PeerCert get_peer_cert_from_session(const_session_t session); -}; - -// Callback for TLS context setup (used by SSLServer constructor) -using ContextSetupCallback = std::function; - -} // namespace tls -#endif - -struct Request { - std::string method; - std::string path; - std::string matched_route; - Params params; - Headers headers; - Headers trailers; - std::string body; - - std::string remote_addr; - int remote_port = -1; - std::string local_addr; - int local_port = -1; - - // for server - std::string version; - std::string target; - MultipartFormData form; - Ranges ranges; - Match matches; - std::unordered_map path_params; - std::function is_connection_closed = []() { return true; }; - - // for client - std::vector accept_content_types; - ResponseHandler response_handler; - ContentReceiverWithProgress content_receiver; - DownloadProgress download_progress; - UploadProgress upload_progress; - - bool has_header(const std::string &key) const; - std::string get_header_value(const std::string &key, const char *def = "", - size_t id = 0) const; - size_t get_header_value_u64(const std::string &key, size_t def = 0, - size_t id = 0) const; - size_t get_header_value_count(const std::string &key) const; - void set_header(const std::string &key, const std::string &val); - - bool has_trailer(const std::string &key) const; - std::string get_trailer_value(const std::string &key, size_t id = 0) const; - size_t get_trailer_value_count(const std::string &key) const; - - bool has_param(const std::string &key) const; - std::string get_param_value(const std::string &key, size_t id = 0) const; - std::vector get_param_values(const std::string &key) const; - size_t get_param_value_count(const std::string &key) const; - - bool is_multipart_form_data() const; - - // private members... - bool body_consumed_ = false; - size_t redirect_count_ = CPPHTTPLIB_REDIRECT_MAX_COUNT; - size_t content_length_ = 0; - ContentProvider content_provider_; - bool is_chunked_content_provider_ = false; - size_t authorization_count_ = 0; - std::chrono::time_point start_time_ = - (std::chrono::steady_clock::time_point::min)(); - -#ifdef CPPHTTPLIB_SSL_ENABLED - tls::const_session_t ssl = nullptr; - tls::PeerCert peer_cert() const; - std::string sni() const; -#endif -}; - -struct Response { - std::string version; - int status = -1; - std::string reason; - Headers headers; - Headers trailers; - std::string body; - std::string location; // Redirect location - - // User-defined context — set by pre-routing/pre-request handlers and read - // by route handlers to pass arbitrary data (e.g. decoded auth tokens). - UserData user_data; - - bool has_header(const std::string &key) const; - std::string get_header_value(const std::string &key, const char *def = "", - size_t id = 0) const; - size_t get_header_value_u64(const std::string &key, size_t def = 0, - size_t id = 0) const; - size_t get_header_value_count(const std::string &key) const; - void set_header(const std::string &key, const std::string &val); - - bool has_trailer(const std::string &key) const; - std::string get_trailer_value(const std::string &key, size_t id = 0) const; - size_t get_trailer_value_count(const std::string &key) const; - - void set_redirect(const std::string &url, int status = StatusCode::Found_302); - void set_content(const char *s, size_t n, const std::string &content_type); - void set_content(const std::string &s, const std::string &content_type); - void set_content(std::string &&s, const std::string &content_type); - - void set_content_provider( - size_t length, const std::string &content_type, ContentProvider provider, - ContentProviderResourceReleaser resource_releaser = nullptr); - - void set_content_provider( - const std::string &content_type, ContentProviderWithoutLength provider, - ContentProviderResourceReleaser resource_releaser = nullptr); - - void set_chunked_content_provider( - const std::string &content_type, ContentProviderWithoutLength provider, - ContentProviderResourceReleaser resource_releaser = nullptr); - - void set_file_content(const std::string &path, - const std::string &content_type); - void set_file_content(const std::string &path); - - Response() = default; - Response(const Response &) = default; - Response &operator=(const Response &) = default; - Response(Response &&) = default; - Response &operator=(Response &&) = default; - ~Response() { - if (content_provider_resource_releaser_) { - content_provider_resource_releaser_(content_provider_success_); - } - } - - // private members... - size_t content_length_ = 0; - ContentProvider content_provider_; - ContentProviderResourceReleaser content_provider_resource_releaser_; - bool is_chunked_content_provider_ = false; - bool content_provider_success_ = false; - std::string file_content_path_; - std::string file_content_content_type_; -}; - -enum class Error { - Success = 0, - Unknown, - Connection, - BindIPAddress, - Read, - Write, - ExceedRedirectCount, - Canceled, - SSLConnection, - SSLLoadingCerts, - SSLServerVerification, - SSLServerHostnameVerification, - UnsupportedMultipartBoundaryChars, - Compression, - ConnectionTimeout, - ProxyConnection, - ConnectionClosed, - Timeout, - ResourceExhaustion, - TooManyFormDataFiles, - ExceedMaxPayloadSize, - ExceedUriMaxLength, - ExceedMaxSocketDescriptorCount, - InvalidRequestLine, - InvalidHTTPMethod, - InvalidHTTPVersion, - InvalidHeaders, - MultipartParsing, - OpenFile, - Listen, - GetSockName, - UnsupportedAddressFamily, - HTTPParsing, - InvalidRangeHeader, - - // For internal use only - SSLPeerCouldBeClosed_, -}; - -std::string to_string(Error error); - -std::ostream &operator<<(std::ostream &os, const Error &obj); - -class Stream { -public: - virtual ~Stream() = default; - - virtual bool is_readable() const = 0; - virtual bool wait_readable() const = 0; - virtual bool wait_writable() const = 0; - virtual bool is_peer_alive() const { return wait_writable(); } - - virtual ssize_t read(char *ptr, size_t size) = 0; - virtual ssize_t write(const char *ptr, size_t size) = 0; - virtual void get_remote_ip_and_port(std::string &ip, int &port) const = 0; - virtual void get_local_ip_and_port(std::string &ip, int &port) const = 0; - virtual socket_t socket() const = 0; - - virtual time_t duration() const = 0; - - virtual void set_read_timeout(time_t sec, time_t usec = 0) { - (void)sec; - (void)usec; - } - - ssize_t write(const char *ptr); - ssize_t write(const std::string &s); - - Error get_error() const { return error_; } - -protected: - Error error_ = Error::Success; -}; - -class TaskQueue { -public: - TaskQueue() = default; - virtual ~TaskQueue() = default; - - virtual bool enqueue(std::function fn) = 0; - virtual void shutdown() = 0; - - virtual void on_idle() {} -}; - -class ThreadPool final : public TaskQueue { -public: - explicit ThreadPool(size_t n, size_t max_n = 0, size_t mqr = 0); - ThreadPool(const ThreadPool &) = delete; - ~ThreadPool() override = default; - - bool enqueue(std::function fn) override; - void shutdown() override; - -private: - void worker(bool is_dynamic); - void move_to_finished(std::thread::id id); - void cleanup_finished_threads(); - - size_t base_thread_count_; - size_t max_thread_count_; - size_t max_queued_requests_; - size_t idle_thread_count_; - - bool shutdown_; - - std::list> jobs_; - std::vector threads_; // base threads - std::list dynamic_threads_; // dynamic threads - std::vector - finished_threads_; // exited dynamic threads awaiting join - - std::condition_variable cond_; - std::mutex mutex_; -}; - -using Logger = std::function; - -// Forward declaration for Error type -enum class Error; -using ErrorLogger = std::function; - -using SocketOptions = std::function; - -void default_socket_options(socket_t sock); - -bool set_socket_opt(socket_t sock, int level, int optname, int optval); - -const char *status_message(int status); - -std::string to_string(Error error); - -std::ostream &operator<<(std::ostream &os, const Error &obj); - -std::string get_bearer_token_auth(const Request &req); - -namespace detail { - -class MatcherBase { -public: - MatcherBase(std::string pattern) : pattern_(std::move(pattern)) {} - virtual ~MatcherBase() = default; - - const std::string &pattern() const { return pattern_; } - - // Match request path and populate its matches and - virtual bool match(Request &request) const = 0; - -private: - std::string pattern_; -}; - -/** - * Captures parameters in request path and stores them in Request::path_params - * - * Capture name is a substring of a pattern from : to /. - * The rest of the pattern is matched against the request path directly - * Parameters are captured starting from the next character after - * the end of the last matched static pattern fragment until the next /. - * - * Example pattern: - * "/path/fragments/:capture/more/fragments/:second_capture" - * Static fragments: - * "/path/fragments/", "more/fragments/" - * - * Given the following request path: - * "/path/fragments/:1/more/fragments/:2" - * the resulting capture will be - * {{"capture", "1"}, {"second_capture", "2"}} - */ -class PathParamsMatcher final : public MatcherBase { -public: - PathParamsMatcher(const std::string &pattern); - - bool match(Request &request) const override; - -private: - // Treat segment separators as the end of path parameter capture - // Does not need to handle query parameters as they are parsed before path - // matching - static constexpr char separator = '/'; - - // Contains static path fragments to match against, excluding the '/' after - // path params - // Fragments are separated by path params - std::vector static_fragments_; - // Stores the names of the path parameters to be used as keys in the - // Request::path_params map - std::vector param_names_; -}; - -/** - * Performs std::regex_match on request path - * and stores the result in Request::matches - * - * Note that regex match is performed directly on the whole request. - * This means that wildcard patterns may match multiple path segments with /: - * "/begin/(.*)/end" will match both "/begin/middle/end" and "/begin/1/2/end". - */ -class RegexMatcher final : public MatcherBase { -public: - RegexMatcher(const std::string &pattern) - : MatcherBase(pattern), regex_(pattern) {} - - bool match(Request &request) const override; - -private: - std::regex regex_; -}; - -int close_socket(socket_t sock) noexcept; - -ssize_t write_headers(Stream &strm, const Headers &headers); - -bool set_socket_opt_time(socket_t sock, int level, int optname, time_t sec, - time_t usec); - -size_t get_multipart_content_length(const UploadFormDataItems &items, - const std::string &boundary); - -ContentProvider -make_multipart_content_provider(const UploadFormDataItems &items, - const std::string &boundary); - -} // namespace detail - -class Server { -public: - using Handler = std::function; - - using ExceptionHandler = - std::function; - - enum class HandlerResponse { - Handled, - Unhandled, - }; - using HandlerWithResponse = - std::function; - - using HandlerWithContentReader = std::function; - - using Expect100ContinueHandler = - std::function; - - using WebSocketHandler = - std::function; - using SubProtocolSelector = - std::function &protocols)>; - - Server(); - - virtual ~Server(); - - virtual bool is_valid() const; - - Server &Get(const std::string &pattern, Handler handler); - Server &Post(const std::string &pattern, Handler handler); - Server &Post(const std::string &pattern, HandlerWithContentReader handler); - Server &Put(const std::string &pattern, Handler handler); - Server &Put(const std::string &pattern, HandlerWithContentReader handler); - Server &Patch(const std::string &pattern, Handler handler); - Server &Patch(const std::string &pattern, HandlerWithContentReader handler); - Server &Delete(const std::string &pattern, Handler handler); - Server &Delete(const std::string &pattern, HandlerWithContentReader handler); - Server &Options(const std::string &pattern, Handler handler); - - Server &WebSocket(const std::string &pattern, WebSocketHandler handler); - Server &WebSocket(const std::string &pattern, WebSocketHandler handler, - SubProtocolSelector sub_protocol_selector); - - bool set_base_dir(const std::string &dir, - const std::string &mount_point = std::string()); - bool set_mount_point(const std::string &mount_point, const std::string &dir, - Headers headers = Headers()); - bool remove_mount_point(const std::string &mount_point); - Server &set_file_extension_and_mimetype_mapping(const std::string &ext, - const std::string &mime); - Server &set_default_file_mimetype(const std::string &mime); - Server &set_file_request_handler(Handler handler); - - template - Server &set_error_handler(ErrorHandlerFunc &&handler) { - return set_error_handler_core( - std::forward(handler), - std::is_convertible{}); - } - - Server &set_exception_handler(ExceptionHandler handler); - - Server &set_pre_routing_handler(HandlerWithResponse handler); - Server &set_post_routing_handler(Handler handler); - - Server &set_pre_request_handler(HandlerWithResponse handler); - - Server &set_expect_100_continue_handler(Expect100ContinueHandler handler); - Server &set_logger(Logger logger); - Server &set_pre_compression_logger(Logger logger); - Server &set_error_logger(ErrorLogger error_logger); - - Server &set_address_family(int family); - Server &set_tcp_nodelay(bool on); - Server &set_ipv6_v6only(bool on); - Server &set_socket_options(SocketOptions socket_options); - - Server &set_default_headers(Headers headers); - Server & - set_header_writer(std::function const &writer); - - Server &set_trusted_proxies(const std::vector &proxies); - - Server &set_keep_alive_max_count(size_t count); - Server &set_keep_alive_timeout(time_t sec); - template - Server & - set_keep_alive_timeout(const std::chrono::duration &duration); - - Server &set_read_timeout(time_t sec, time_t usec = 0); - template - Server &set_read_timeout(const std::chrono::duration &duration); - - Server &set_write_timeout(time_t sec, time_t usec = 0); - template - Server &set_write_timeout(const std::chrono::duration &duration); - - Server &set_idle_interval(time_t sec, time_t usec = 0); - template - Server &set_idle_interval(const std::chrono::duration &duration); - - Server &set_payload_max_length(size_t length); - - Server &set_websocket_ping_interval(time_t sec); - template - Server &set_websocket_ping_interval( - const std::chrono::duration &duration); - - Server &set_websocket_max_missed_pongs(int count); - - bool bind_to_port(const std::string &host, int port, int socket_flags = 0); - int bind_to_any_port(const std::string &host, int socket_flags = 0); - bool listen_after_bind(); - - bool listen(const std::string &host, int port, int socket_flags = 0); - - bool is_running() const; - void wait_until_ready() const; - void stop() noexcept; - void decommission(); - - std::function new_task_queue; - -protected: - bool process_request(Stream &strm, const std::string &remote_addr, - int remote_port, const std::string &local_addr, - int local_port, bool close_connection, - bool &connection_closed, - const std::function &setup_request, - bool *websocket_upgraded = nullptr); - - std::atomic svr_sock_{INVALID_SOCKET}; - - std::vector trusted_proxies_; - - size_t keep_alive_max_count_ = CPPHTTPLIB_KEEPALIVE_MAX_COUNT; - time_t keep_alive_timeout_sec_ = CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND; - time_t read_timeout_sec_ = CPPHTTPLIB_SERVER_READ_TIMEOUT_SECOND; - time_t read_timeout_usec_ = CPPHTTPLIB_SERVER_READ_TIMEOUT_USECOND; - time_t write_timeout_sec_ = CPPHTTPLIB_SERVER_WRITE_TIMEOUT_SECOND; - time_t write_timeout_usec_ = CPPHTTPLIB_SERVER_WRITE_TIMEOUT_USECOND; - time_t idle_interval_sec_ = CPPHTTPLIB_IDLE_INTERVAL_SECOND; - time_t idle_interval_usec_ = CPPHTTPLIB_IDLE_INTERVAL_USECOND; - size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH; - time_t websocket_ping_interval_sec_ = - CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND; - int websocket_max_missed_pongs_ = CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS; - -private: - using Handlers = - std::vector, Handler>>; - using HandlersForContentReader = - std::vector, - HandlerWithContentReader>>; - - static std::unique_ptr - make_matcher(const std::string &pattern); - - template - Server &add_handler( - std::vector, H>> &handlers, - const std::string &pattern, H handler) { - handlers.emplace_back(make_matcher(pattern), std::move(handler)); - return *this; - } - - Server &set_error_handler_core(HandlerWithResponse handler, std::true_type); - Server &set_error_handler_core(Handler handler, std::false_type); - - socket_t create_server_socket(const std::string &host, int port, - int socket_flags, - SocketOptions socket_options) const; - int bind_internal(const std::string &host, int port, int socket_flags); - bool listen_internal(); - - bool routing(Request &req, Response &res, Stream &strm); - bool handle_file_request(Request &req, Response &res); - bool check_if_not_modified(const Request &req, Response &res, - const std::string &etag, time_t mtime) const; - bool check_if_range(Request &req, const std::string &etag, - time_t mtime) const; - bool dispatch_request(Request &req, Response &res, - const Handlers &handlers) const; - bool dispatch_request_for_content_reader( - Request &req, Response &res, ContentReader content_reader, - const HandlersForContentReader &handlers) const; - - bool parse_request_line(const char *s, Request &req) const; - void apply_ranges(const Request &req, Response &res, - std::string &content_type, std::string &boundary) const; - bool write_response(Stream &strm, bool close_connection, Request &req, - Response &res); - bool write_response_with_content(Stream &strm, bool close_connection, - const Request &req, Response &res); - bool write_response_core(Stream &strm, bool close_connection, - const Request &req, Response &res, - bool need_apply_ranges); - bool write_content_with_provider(Stream &strm, const Request &req, - Response &res, const std::string &boundary, - const std::string &content_type); - bool read_content(Stream &strm, Request &req, Response &res); - bool read_content_with_content_receiver(Stream &strm, Request &req, - Response &res, - ContentReceiver receiver, - FormDataHeader multipart_header, - ContentReceiver multipart_receiver); - bool read_content_core(Stream &strm, Request &req, Response &res, - ContentReceiver receiver, - FormDataHeader multipart_header, - ContentReceiver multipart_receiver) const; - - virtual bool process_and_close_socket(socket_t sock); - - void output_log(const Request &req, const Response &res) const; - void output_pre_compression_log(const Request &req, - const Response &res) const; - void output_error_log(const Error &err, const Request *req) const; - - std::atomic is_running_{false}; - std::atomic is_decommissioned{false}; - - struct MountPointEntry { - std::string mount_point; - std::string base_dir; - std::string resolved_base_dir; - Headers headers; - }; - std::vector base_dirs_; - std::map file_extension_and_mimetype_map_; - std::string default_file_mimetype_ = "application/octet-stream"; - Handler file_request_handler_; - - Handlers get_handlers_; - Handlers post_handlers_; - HandlersForContentReader post_handlers_for_content_reader_; - Handlers put_handlers_; - HandlersForContentReader put_handlers_for_content_reader_; - Handlers patch_handlers_; - HandlersForContentReader patch_handlers_for_content_reader_; - Handlers delete_handlers_; - HandlersForContentReader delete_handlers_for_content_reader_; - Handlers options_handlers_; - - struct WebSocketHandlerEntry { - std::unique_ptr matcher; - WebSocketHandler handler; - SubProtocolSelector sub_protocol_selector; - }; - using WebSocketHandlers = std::vector; - WebSocketHandlers websocket_handlers_; - - HandlerWithResponse error_handler_; - ExceptionHandler exception_handler_; - HandlerWithResponse pre_routing_handler_; - Handler post_routing_handler_; - HandlerWithResponse pre_request_handler_; - Expect100ContinueHandler expect_100_continue_handler_; - - mutable std::mutex logger_mutex_; - Logger logger_; - Logger pre_compression_logger_; - ErrorLogger error_logger_; - - int address_family_ = AF_UNSPEC; - bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY; - bool ipv6_v6only_ = CPPHTTPLIB_IPV6_V6ONLY; - SocketOptions socket_options_ = default_socket_options; - - Headers default_headers_; - std::function header_writer_ = - detail::write_headers; -}; - -class Result { -public: - Result() = default; - Result(std::unique_ptr &&res, Error err, - Headers &&request_headers = Headers{}) - : res_(std::move(res)), err_(err), - request_headers_(std::move(request_headers)) {} - // Response - operator bool() const { return res_ != nullptr; } - bool operator==(std::nullptr_t) const { return res_ == nullptr; } - bool operator!=(std::nullptr_t) const { return res_ != nullptr; } - const Response &value() const { return *res_; } - Response &value() { return *res_; } - const Response &operator*() const { return *res_; } - Response &operator*() { return *res_; } - const Response *operator->() const { return res_.get(); } - Response *operator->() { return res_.get(); } - - // Error - Error error() const { return err_; } - - // Request Headers - bool has_request_header(const std::string &key) const; - std::string get_request_header_value(const std::string &key, - const char *def = "", - size_t id = 0) const; - size_t get_request_header_value_u64(const std::string &key, size_t def = 0, - size_t id = 0) const; - size_t get_request_header_value_count(const std::string &key) const; - -private: - std::unique_ptr res_; - Error err_ = Error::Unknown; - Headers request_headers_; - -#ifdef CPPHTTPLIB_SSL_ENABLED -public: - Result(std::unique_ptr &&res, Error err, Headers &&request_headers, - int ssl_error) - : res_(std::move(res)), err_(err), - request_headers_(std::move(request_headers)), ssl_error_(ssl_error) {} - Result(std::unique_ptr &&res, Error err, Headers &&request_headers, - int ssl_error, uint64_t ssl_backend_error) - : res_(std::move(res)), err_(err), - request_headers_(std::move(request_headers)), ssl_error_(ssl_error), - ssl_backend_error_(ssl_backend_error) {} - - int ssl_error() const { return ssl_error_; } - uint64_t ssl_backend_error() const { return ssl_backend_error_; } - -private: - int ssl_error_ = 0; - uint64_t ssl_backend_error_ = 0; -#endif -}; - -struct ClientConnection { - socket_t sock = INVALID_SOCKET; - - bool is_open() const { return sock != INVALID_SOCKET; } - - ClientConnection() = default; - - ~ClientConnection(); - - ClientConnection(const ClientConnection &) = delete; - ClientConnection &operator=(const ClientConnection &) = delete; - - ClientConnection(ClientConnection &&other) noexcept - : sock(other.sock) -#ifdef CPPHTTPLIB_SSL_ENABLED - , - session(other.session) -#endif - { - other.sock = INVALID_SOCKET; -#ifdef CPPHTTPLIB_SSL_ENABLED - other.session = nullptr; -#endif - } - - ClientConnection &operator=(ClientConnection &&other) noexcept { - if (this != &other) { - sock = other.sock; - other.sock = INVALID_SOCKET; -#ifdef CPPHTTPLIB_SSL_ENABLED - session = other.session; - other.session = nullptr; -#endif - } - return *this; - } - -#ifdef CPPHTTPLIB_SSL_ENABLED - tls::session_t session = nullptr; -#endif -}; - -namespace detail { - -struct ChunkedDecoder; - -struct BodyReader { - Stream *stream = nullptr; - bool has_content_length = false; - size_t content_length = 0; - size_t payload_max_length = CPPHTTPLIB_PAYLOAD_MAX_LENGTH; - size_t bytes_read = 0; - bool chunked = false; - bool eof = false; - std::unique_ptr chunked_decoder; - Error last_error = Error::Success; - - ssize_t read(char *buf, size_t len); - bool has_error() const { return last_error != Error::Success; } -}; - -inline ssize_t read_body_content(Stream *stream, BodyReader &br, char *buf, - size_t len) { - (void)stream; - return br.read(buf, len); -} - -class decompressor; - -enum class NoProxyKind { - Wildcard, // "*" - HostnameSuffix, // "example.com" or ".example.com" - IPv4Cidr, // "10.0.0.0/8" (or single IP, treated as /32) - IPv6Cidr, // "fe80::/10" (or single IP, treated as /128) -}; - -// Unified 16-byte buffer holding either a v4 (first 4 bytes) or v6 address. -// Lets one CIDR matcher cover both families. -using IPBytes = std::array; - -struct NoProxyEntry { - NoProxyKind kind = NoProxyKind::Wildcard; - std::string hostname_pattern; // lowercased, leading/trailing dot stripped - IPBytes net{}; - int prefix_bits = 0; -}; - -struct NormalizedTarget { - std::string hostname; // lowercase; brackets and trailing dot removed - bool is_ipv4 = false; - bool is_ipv6 = false; - IPBytes ip{}; -}; - -} // namespace detail - -class ClientImpl { -public: - explicit ClientImpl(const std::string &host); - - explicit ClientImpl(const std::string &host, int port); - - explicit ClientImpl(const std::string &host, int port, - const std::string &client_cert_path, - const std::string &client_key_path); - - virtual ~ClientImpl(); - - virtual bool is_valid() const; - - struct StreamHandle { - std::unique_ptr response; - Error error = Error::Success; - - StreamHandle() = default; - StreamHandle(const StreamHandle &) = delete; - StreamHandle &operator=(const StreamHandle &) = delete; - StreamHandle(StreamHandle &&) = default; - StreamHandle &operator=(StreamHandle &&) = default; - ~StreamHandle() = default; - - bool is_valid() const { - return response != nullptr && error == Error::Success; - } - - ssize_t read(char *buf, size_t len); - void parse_trailers_if_needed(); - Error get_read_error() const { return body_reader_.last_error; } - bool has_read_error() const { return body_reader_.has_error(); } - - bool trailers_parsed_ = false; - - private: - friend class ClientImpl; - - ssize_t read_with_decompression(char *buf, size_t len); - - std::unique_ptr connection_; - std::unique_ptr socket_stream_; - Stream *stream_ = nullptr; - detail::BodyReader body_reader_; - - std::unique_ptr decompressor_; - std::string decompress_buffer_; - size_t decompress_offset_ = 0; - size_t decompressed_bytes_read_ = 0; - }; - - // clang-format off - Result Get(const std::string &path, DownloadProgress progress = nullptr); - Result Get(const std::string &path, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - Result Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - Result Get(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr); - Result Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - Result Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - Result Get(const std::string &path, const Params ¶ms, const Headers &headers, DownloadProgress progress = nullptr); - Result Get(const std::string &path, const Params ¶ms, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - Result Get(const std::string &path, const Params ¶ms, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - - Result Head(const std::string &path); - Result Head(const std::string &path, const Headers &headers); - - Result Post(const std::string &path); - Result Post(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); - Result Post(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); - Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); - Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); - Result Post(const std::string &path, const Params ¶ms); - Result Post(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr); - Result Post(const std::string &path, const Headers &headers); - Result Post(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); - Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); - Result Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - Result Post(const std::string &path, const Headers &headers, const Params ¶ms); - Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr); - Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr); - Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr); - Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - - Result Put(const std::string &path); - Result Put(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); - Result Put(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); - Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); - Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Params ¶ms); - Result Put(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Headers &headers); - Result Put(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Headers &headers, const Params ¶ms); - Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - - Result Patch(const std::string &path); - Result Patch(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); - Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); - Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Params ¶ms); - Result Patch(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers, const Params ¶ms); - Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - - Result Delete(const std::string &path, DownloadProgress progress = nullptr); - Result Delete(const std::string &path, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress = nullptr); - Result Delete(const std::string &path, const std::string &body, const std::string &content_type, DownloadProgress progress = nullptr); - Result Delete(const std::string &path, const Params ¶ms, DownloadProgress progress = nullptr); - Result Delete(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr); - Result Delete(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress = nullptr); - Result Delete(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, DownloadProgress progress = nullptr); - Result Delete(const std::string &path, const Headers &headers, const Params ¶ms, DownloadProgress progress = nullptr); - - Result Options(const std::string &path); - Result Options(const std::string &path, const Headers &headers); - // clang-format on - - // Streaming API: Open a stream for reading response body incrementally - // Socket ownership is transferred to StreamHandle for true streaming - // Supports all HTTP methods (GET, POST, PUT, PATCH, DELETE, etc.) - StreamHandle open_stream(const std::string &method, const std::string &path, - const Params ¶ms = {}, - const Headers &headers = {}, - const std::string &body = {}, - const std::string &content_type = {}); - - bool send(Request &req, Response &res, Error &error); - Result send(const Request &req); - - void stop(); - - std::string host() const; - int port() const; - - size_t is_socket_open() const; - socket_t socket() const; - - void set_hostname_addr_map(std::map addr_map); - - void set_default_headers(Headers headers); - - void - set_header_writer(std::function const &writer); - - void set_address_family(int family); - void set_tcp_nodelay(bool on); - void set_ipv6_v6only(bool on); - void set_socket_options(SocketOptions socket_options); - - void set_connection_timeout(time_t sec, time_t usec = 0); - template - void - set_connection_timeout(const std::chrono::duration &duration); - - void set_read_timeout(time_t sec, time_t usec = 0); - template - void set_read_timeout(const std::chrono::duration &duration); - - void set_write_timeout(time_t sec, time_t usec = 0); - template - void set_write_timeout(const std::chrono::duration &duration); - - void set_max_timeout(time_t msec); - template - void set_max_timeout(const std::chrono::duration &duration); - - void set_basic_auth(const std::string &username, const std::string &password); - void set_bearer_token_auth(const std::string &token); - - void set_keep_alive(bool on); - void set_follow_location(bool on); - - void set_path_encode(bool on); - - void set_compress(bool on); - - void set_decompress(bool on); - - void set_payload_max_length(size_t length); - - void set_interface(const std::string &intf); - - void set_proxy(const std::string &host, int port); - void set_proxy_basic_auth(const std::string &username, - const std::string &password); - void set_proxy_bearer_token_auth(const std::string &token); - void set_no_proxy(const std::vector &patterns); - - void set_logger(Logger logger); - void set_error_logger(ErrorLogger error_logger); - -protected: - struct Socket { - socket_t sock = INVALID_SOCKET; - - // For Mbed TLS compatibility: start_time for request timeout tracking - std::chrono::time_point start_time_; - - bool is_open() const { return sock != INVALID_SOCKET; } - -#ifdef CPPHTTPLIB_SSL_ENABLED - tls::session_t ssl = nullptr; -#endif - }; - - virtual bool create_and_connect_socket(Socket &socket, Error &error); - virtual bool ensure_socket_connection(Socket &socket, Error &error); - virtual bool setup_proxy_connection( - Socket &socket, - std::chrono::time_point start_time, - Response &res, bool &success, Error &error); - - bool is_proxy_enabled_for_host(const std::string &host) const; - - // All of: - // shutdown_ssl - // shutdown_socket - // close_socket - // disconnect - // should ONLY be called when socket_mutex_ is locked, and only when - // no other thread is using the socket. - virtual void shutdown_ssl(Socket &socket, bool shutdown_gracefully); - void shutdown_socket(Socket &socket) const; - void close_socket(Socket &socket); - void disconnect(bool gracefully); - - bool process_request(Stream &strm, Request &req, Response &res, - bool close_connection, Error &error); - - bool write_content_with_provider(Stream &strm, const Request &req, - Error &error) const; - - void copy_settings(const ClientImpl &rhs); - - void output_log(const Request &req, const Response &res) const; - void output_error_log(const Error &err, const Request *req) const; - - // Socket endpoint information - const std::string host_; - const int port_; - - // Current open socket - Socket socket_; - mutable std::mutex socket_mutex_; - std::recursive_mutex request_mutex_; - - // These are all protected under socket_mutex - size_t socket_requests_in_flight_ = 0; - std::thread::id socket_requests_are_from_thread_ = std::thread::id(); - bool socket_should_be_closed_when_request_is_done_ = false; - - // Hostname-IP map - std::map addr_map_; - - // Default headers - Headers default_headers_; - - // Header writer - std::function header_writer_ = - detail::write_headers; - - // Settings - std::string client_cert_path_; - std::string client_key_path_; - - time_t connection_timeout_sec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND; - time_t connection_timeout_usec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND; - time_t read_timeout_sec_ = CPPHTTPLIB_CLIENT_READ_TIMEOUT_SECOND; - time_t read_timeout_usec_ = CPPHTTPLIB_CLIENT_READ_TIMEOUT_USECOND; - time_t write_timeout_sec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND; - time_t write_timeout_usec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND; - time_t max_timeout_msec_ = CPPHTTPLIB_CLIENT_MAX_TIMEOUT_MSECOND; - - std::string basic_auth_username_; - std::string basic_auth_password_; - std::string bearer_token_auth_token_; - - bool keep_alive_ = false; - bool follow_location_ = false; - - bool path_encode_ = true; - - int address_family_ = AF_UNSPEC; - bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY; - bool ipv6_v6only_ = CPPHTTPLIB_IPV6_V6ONLY; - SocketOptions socket_options_ = nullptr; - - bool compress_ = false; - bool decompress_ = true; - - size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH; - bool has_payload_max_length_ = false; - - std::string interface_; - - std::string proxy_host_; - int proxy_port_ = -1; - - std::string proxy_basic_auth_username_; - std::string proxy_basic_auth_password_; - std::string proxy_bearer_token_auth_token_; - - std::vector no_proxy_entries_; - - mutable detail::NormalizedTarget host_normalized_; - mutable bool host_normalized_valid_ = false; - - mutable std::mutex logger_mutex_; - Logger logger_; - ErrorLogger error_logger_; - -private: - bool send_(Request &req, Response &res, Error &error); - Result send_(Request &&req); - - socket_t create_client_socket(Error &error) const; - bool read_response_line(Stream &strm, const Request &req, Response &res, - bool skip_100_continue = true) const; - bool write_request(Stream &strm, Request &req, bool close_connection, - Error &error, bool skip_body = false); - bool write_request_body(Stream &strm, Request &req, Error &error); - void prepare_default_headers(Request &r, bool for_stream, - const std::string &ct); - bool redirect(Request &req, Response &res, Error &error); - bool create_redirect_client(const std::string &scheme, - const std::string &host, int port, Request &req, - Response &res, const std::string &path, - const std::string &location, Error &error); - template void setup_redirect_client(ClientType &client); - bool handle_request(Stream &strm, Request &req, Response &res, - bool close_connection, Error &error); - std::unique_ptr send_with_content_provider_and_receiver( - Request &req, const char *body, size_t content_length, - ContentProvider content_provider, - ContentProviderWithoutLength content_provider_without_length, - const std::string &content_type, ContentReceiver content_receiver, - Error &error); - Result send_with_content_provider_and_receiver( - const std::string &method, const std::string &path, - const Headers &headers, const char *body, size_t content_length, - ContentProvider content_provider, - ContentProviderWithoutLength content_provider_without_length, - const std::string &content_type, ContentReceiver content_receiver, - UploadProgress progress); - ContentProviderWithoutLength get_multipart_content_provider( - const std::string &boundary, const UploadFormDataItems &items, - const FormDataProviderItems &provider_items) const; - - virtual bool - process_socket(const Socket &socket, - std::chrono::time_point start_time, - std::function callback); - virtual bool is_ssl() const; - - void transfer_socket_ownership_to_handle(StreamHandle &handle); - -#ifdef CPPHTTPLIB_SSL_ENABLED -public: - void set_digest_auth(const std::string &username, - const std::string &password); - void set_proxy_digest_auth(const std::string &username, - const std::string &password); - void set_ca_cert_path(const std::string &ca_cert_file_path, - const std::string &ca_cert_dir_path = std::string()); - void enable_server_certificate_verification(bool enabled); - void enable_server_hostname_verification(bool enabled); - -protected: - std::string digest_auth_username_; - std::string digest_auth_password_; - std::string proxy_digest_auth_username_; - std::string proxy_digest_auth_password_; - std::string ca_cert_file_path_; - std::string ca_cert_dir_path_; - bool server_certificate_verification_ = true; - bool server_hostname_verification_ = true; - std::string ca_cert_pem_; // Store CA cert PEM for redirect transfer - int last_ssl_error_ = 0; - uint64_t last_backend_error_ = 0; -#endif -}; - -class Client { -public: - // Universal interface - explicit Client(const std::string &scheme_host_port); - - explicit Client(const std::string &scheme_host_port, - const std::string &client_cert_path, - const std::string &client_key_path); - - // HTTP only interface - explicit Client(const std::string &host, int port); - - explicit Client(const std::string &host, int port, - const std::string &client_cert_path, - const std::string &client_key_path); - - Client(Client &&) = default; - Client &operator=(Client &&) = default; - - ~Client(); - - bool is_valid() const; - - // clang-format off - Result Get(const std::string &path, DownloadProgress progress = nullptr); - Result Get(const std::string &path, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - Result Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - Result Get(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr); - Result Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - Result Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - Result Get(const std::string &path, const Params ¶ms, const Headers &headers, DownloadProgress progress = nullptr); - Result Get(const std::string &path, const Params ¶ms, const Headers &headers, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - Result Get(const std::string &path, const Params ¶ms, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - - Result Head(const std::string &path); - Result Head(const std::string &path, const Headers &headers); - - Result Post(const std::string &path); - Result Post(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); - Result Post(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); - Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); - Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); - Result Post(const std::string &path, const Params ¶ms); - Result Post(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr); - Result Post(const std::string &path, const Headers &headers); - Result Post(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); - Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); - Result Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - Result Post(const std::string &path, const Headers &headers, const Params ¶ms); - Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr); - Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr); - Result Post(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr); - Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - - Result Put(const std::string &path); - Result Put(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); - Result Put(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); - Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); - Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Params ¶ms); - Result Put(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Headers &headers); - Result Put(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Headers &headers, const Params ¶ms); - Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr); - Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - - Result Patch(const std::string &path); - Result Patch(const std::string &path, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); - Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); - Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Params ¶ms); - Result Patch(const std::string &path, const UploadFormDataItems &items, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers); - Result Patch(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type, ContentReceiver content_receiver, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers, const Params ¶ms); - Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const std::string &boundary, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers, const UploadFormDataItems &items, const FormDataProviderItems &provider_items, UploadProgress progress = nullptr); - Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, ContentReceiver content_receiver, DownloadProgress progress = nullptr); - - Result Delete(const std::string &path, DownloadProgress progress = nullptr); - Result Delete(const std::string &path, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress = nullptr); - Result Delete(const std::string &path, const std::string &body, const std::string &content_type, DownloadProgress progress = nullptr); - Result Delete(const std::string &path, const Params ¶ms, DownloadProgress progress = nullptr); - Result Delete(const std::string &path, const Headers &headers, DownloadProgress progress = nullptr); - Result Delete(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type, DownloadProgress progress = nullptr); - Result Delete(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type, DownloadProgress progress = nullptr); - Result Delete(const std::string &path, const Headers &headers, const Params ¶ms, DownloadProgress progress = nullptr); - - Result Options(const std::string &path); - Result Options(const std::string &path, const Headers &headers); - // clang-format on - - // Streaming API: Open a stream for reading response body incrementally - // Socket ownership is transferred to StreamHandle for true streaming - // Supports all HTTP methods (GET, POST, PUT, PATCH, DELETE, etc.) - ClientImpl::StreamHandle open_stream(const std::string &method, - const std::string &path, - const Params ¶ms = {}, - const Headers &headers = {}, - const std::string &body = {}, - const std::string &content_type = {}); - - bool send(Request &req, Response &res, Error &error); - Result send(const Request &req); - - void stop(); - - std::string host() const; - int port() const; - - size_t is_socket_open() const; - socket_t socket() const; - - void set_hostname_addr_map(std::map addr_map); - - void set_default_headers(Headers headers); - - void - set_header_writer(std::function const &writer); - - void set_address_family(int family); - void set_tcp_nodelay(bool on); - void set_socket_options(SocketOptions socket_options); - - void set_connection_timeout(time_t sec, time_t usec = 0); - template - void - set_connection_timeout(const std::chrono::duration &duration); - - void set_read_timeout(time_t sec, time_t usec = 0); - template - void set_read_timeout(const std::chrono::duration &duration); - - void set_write_timeout(time_t sec, time_t usec = 0); - template - void set_write_timeout(const std::chrono::duration &duration); - - void set_max_timeout(time_t msec); - template - void set_max_timeout(const std::chrono::duration &duration); - - void set_basic_auth(const std::string &username, const std::string &password); - void set_bearer_token_auth(const std::string &token); - - void set_keep_alive(bool on); - void set_follow_location(bool on); - - void set_path_encode(bool on); - - void set_compress(bool on); - - void set_decompress(bool on); - - void set_payload_max_length(size_t length); - - void set_interface(const std::string &intf); - - void set_proxy(const std::string &host, int port); - void set_proxy_basic_auth(const std::string &username, - const std::string &password); - void set_proxy_bearer_token_auth(const std::string &token); - void set_no_proxy(const std::vector &patterns); - void set_logger(Logger logger); - void set_error_logger(ErrorLogger error_logger); - -private: - std::unique_ptr cli_; - -#ifdef CPPHTTPLIB_SSL_ENABLED -public: - void set_digest_auth(const std::string &username, - const std::string &password); - void set_proxy_digest_auth(const std::string &username, - const std::string &password); - void enable_server_certificate_verification(bool enabled); - void enable_server_hostname_verification(bool enabled); - void set_ca_cert_path(const std::string &ca_cert_file_path, - const std::string &ca_cert_dir_path = std::string()); - - void set_ca_cert_store(tls::ca_store_t ca_cert_store); - void load_ca_cert_store(const char *ca_cert, std::size_t size); - - void set_server_certificate_verifier(tls::VerifyCallback verifier); - - void set_session_verifier( - std::function verifier); - - tls::ctx_t tls_context() const; - -#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE - void enable_windows_certificate_verification(bool enabled); -#endif - -private: - bool is_ssl_ = false; -#endif -}; - -#ifdef CPPHTTPLIB_SSL_ENABLED -class SSLServer : public Server { -public: - SSLServer(const char *cert_path, const char *private_key_path, - const char *client_ca_cert_file_path = nullptr, - const char *client_ca_cert_dir_path = nullptr, - const char *private_key_password = nullptr); - - struct PemMemory { - const char *cert_pem; - size_t cert_pem_len; - const char *key_pem; - size_t key_pem_len; - const char *client_ca_pem; - size_t client_ca_pem_len; - const char *private_key_password; - }; - explicit SSLServer(const PemMemory &pem); - - // The callback receives the ctx_t handle which can be cast to the - // appropriate backend type (SSL_CTX* for OpenSSL, - // tls::impl::MbedTlsContext* for Mbed TLS) - explicit SSLServer(const tls::ContextSetupCallback &setup_callback); - - ~SSLServer() override; - - bool is_valid() const override; - - bool update_certs_pem(const char *cert_pem, const char *key_pem, - const char *client_ca_pem = nullptr, - const char *password = nullptr); - - tls::ctx_t tls_context() const { return ctx_; } - - int ssl_last_error() const { return last_ssl_error_; } - -private: - bool process_and_close_socket(socket_t sock) override; - - tls::ctx_t ctx_ = nullptr; - std::mutex ctx_mutex_; - - int last_ssl_error_ = 0; -}; - -class SSLClient final : public ClientImpl { -public: - explicit SSLClient(const std::string &host); - - explicit SSLClient(const std::string &host, int port); - - explicit SSLClient(const std::string &host, int port, - const std::string &client_cert_path, - const std::string &client_key_path, - const std::string &private_key_password = std::string()); - - struct PemMemory { - const char *cert_pem; - size_t cert_pem_len; - const char *key_pem; - size_t key_pem_len; - const char *private_key_password; - }; - explicit SSLClient(const std::string &host, int port, const PemMemory &pem); - - ~SSLClient() override; - - bool is_valid() const override; - - void set_ca_cert_store(tls::ca_store_t ca_cert_store); - void load_ca_cert_store(const char *ca_cert, std::size_t size); - - void set_server_certificate_verifier(tls::VerifyCallback verifier); - - // Post-handshake session verifier (backend-independent) - void set_session_verifier( - std::function verifier); - - tls::ctx_t tls_context() const { return ctx_; } - -#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE - void enable_windows_certificate_verification(bool enabled); -#endif - -private: - bool create_and_connect_socket(Socket &socket, Error &error) override; - bool ensure_socket_connection(Socket &socket, Error &error) override; - void shutdown_ssl(Socket &socket, bool shutdown_gracefully) override; - void shutdown_ssl_impl(Socket &socket, bool shutdown_gracefully); - - bool - process_socket(const Socket &socket, - std::chrono::time_point start_time, - std::function callback) override; - bool is_ssl() const override; - - bool setup_proxy_connection( - Socket &socket, - std::chrono::time_point start_time, - Response &res, bool &success, Error &error) override; - bool connect_with_proxy( - Socket &sock, - std::chrono::time_point start_time, - Response &res, bool &success, Error &error); - bool initialize_ssl(Socket &socket, Error &error); - - void init_ctx(); - void reset_ctx_on_error(); - - bool load_certs(); - - tls::ctx_t ctx_ = nullptr; - std::mutex ctx_mutex_; - std::once_flag initialize_cert_; - - long verify_result_ = 0; - - std::function session_verifier_; - -#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE - bool enable_windows_cert_verification_ = true; -#endif - - friend class ClientImpl; - -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT -private: - bool verify_host(X509 *server_cert) const; - bool verify_host_with_subject_alt_name(X509 *server_cert) const; - bool verify_host_with_common_name(X509 *server_cert) const; -#endif -}; -#endif // CPPHTTPLIB_SSL_ENABLED - -namespace detail { - -template -inline void duration_to_sec_and_usec(const T &duration, U callback) { - auto sec = std::chrono::duration_cast(duration).count(); - auto usec = std::chrono::duration_cast( - duration - std::chrono::seconds(sec)) - .count(); - callback(static_cast(sec), static_cast(usec)); -} - -template inline constexpr size_t str_len(const char (&)[N]) { - return N - 1; -} - -inline bool is_numeric(const std::string &str) { - return !str.empty() && - std::all_of(str.cbegin(), str.cend(), - [](unsigned char c) { return std::isdigit(c); }); -} - -inline size_t get_header_value_u64(const Headers &headers, - const std::string &key, size_t def, - size_t id, bool &is_invalid_value) { - is_invalid_value = false; - auto rng = headers.equal_range(key); - auto it = rng.first; - std::advance(it, static_cast(id)); - if (it != rng.second) { - if (is_numeric(it->second)) { - return static_cast(std::strtoull(it->second.data(), nullptr, 10)); - } else { - is_invalid_value = true; - } - } - return def; -} - -inline size_t get_header_value_u64(const Headers &headers, - const std::string &key, size_t def, - size_t id) { - auto dummy = false; - return get_header_value_u64(headers, key, def, id, dummy); -} - -} // namespace detail - -template -inline Server & -Server::set_read_timeout(const std::chrono::duration &duration) { - detail::duration_to_sec_and_usec( - duration, [&](time_t sec, time_t usec) { set_read_timeout(sec, usec); }); - return *this; -} - -template -inline Server & -Server::set_write_timeout(const std::chrono::duration &duration) { - detail::duration_to_sec_and_usec( - duration, [&](time_t sec, time_t usec) { set_write_timeout(sec, usec); }); - return *this; -} - -template -inline Server & -Server::set_idle_interval(const std::chrono::duration &duration) { - detail::duration_to_sec_and_usec( - duration, [&](time_t sec, time_t usec) { set_idle_interval(sec, usec); }); - return *this; -} - -template -inline void ClientImpl::set_connection_timeout( - const std::chrono::duration &duration) { - detail::duration_to_sec_and_usec(duration, [&](time_t sec, time_t usec) { - set_connection_timeout(sec, usec); - }); -} - -template -inline void ClientImpl::set_read_timeout( - const std::chrono::duration &duration) { - detail::duration_to_sec_and_usec( - duration, [&](time_t sec, time_t usec) { set_read_timeout(sec, usec); }); -} - -template -inline void ClientImpl::set_write_timeout( - const std::chrono::duration &duration) { - detail::duration_to_sec_and_usec( - duration, [&](time_t sec, time_t usec) { set_write_timeout(sec, usec); }); -} - -template -inline void ClientImpl::set_max_timeout( - const std::chrono::duration &duration) { - auto msec = - std::chrono::duration_cast(duration).count(); - set_max_timeout(msec); -} - -template -inline void Client::set_connection_timeout( - const std::chrono::duration &duration) { - cli_->set_connection_timeout(duration); -} - -template -inline void -Client::set_read_timeout(const std::chrono::duration &duration) { - cli_->set_read_timeout(duration); -} - -template -inline void -Client::set_write_timeout(const std::chrono::duration &duration) { - cli_->set_write_timeout(duration); -} - -inline void Client::set_max_timeout(time_t msec) { - cli_->set_max_timeout(msec); -} - -template -inline void -Client::set_max_timeout(const std::chrono::duration &duration) { - cli_->set_max_timeout(duration); -} - -/* - * Forward declarations and types that will be part of the .h file if split into - * .h + .cc. - */ - -std::string hosted_at(const std::string &hostname); - -void hosted_at(const std::string &hostname, std::vector &addrs); - -// JavaScript-style URL encoding/decoding functions -std::string encode_uri_component(const std::string &value); -std::string encode_uri(const std::string &value); -std::string decode_uri_component(const std::string &value); -std::string decode_uri(const std::string &value); - -// RFC 3986 compliant URL component encoding/decoding functions -std::string encode_path_component(const std::string &component); -std::string decode_path_component(const std::string &component); -std::string encode_query_component(const std::string &component, - bool space_as_plus = true); -std::string decode_query_component(const std::string &component, - bool plus_as_space = true); - -std::string sanitize_filename(const std::string &filename); - -std::string append_query_params(const std::string &path, const Params ¶ms); - -std::pair make_range_header(const Ranges &ranges); - -std::pair -make_basic_authentication_header(const std::string &username, - const std::string &password, - bool is_proxy = false); - -namespace detail { - -#if defined(_WIN32) -inline std::wstring u8string_to_wstring(const char *s) { - if (!s) { return std::wstring(); } - - auto len = static_cast(strlen(s)); - if (!len) { return std::wstring(); } - - auto wlen = ::MultiByteToWideChar(CP_UTF8, 0, s, len, nullptr, 0); - if (!wlen) { return std::wstring(); } - - std::wstring ws; - ws.resize(wlen); - wlen = ::MultiByteToWideChar( - CP_UTF8, 0, s, len, - const_cast(reinterpret_cast(ws.data())), wlen); - if (wlen != static_cast(ws.size())) { ws.clear(); } - return ws; -} -#endif - -struct FileStat { - FileStat(const std::string &path); - bool is_file() const; - bool is_dir() const; - time_t mtime() const; - size_t size() const; - -private: -#if defined(_WIN32) - struct _stat st_; -#else - struct stat st_; -#endif - int ret_ = -1; -}; - -std::string make_host_and_port_string(const std::string &host, int port, - bool is_ssl); - -std::string trim_copy(const std::string &s); - -void divide( - const char *data, std::size_t size, char d, - std::function - fn); - -void divide( - const std::string &str, char d, - std::function - fn); - -void split(const char *b, const char *e, char d, - std::function fn); - -void split(const char *b, const char *e, char d, size_t m, - std::function fn); - -bool process_client_socket( - socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, - time_t write_timeout_sec, time_t write_timeout_usec, - time_t max_timeout_msec, - std::chrono::time_point start_time, - std::function callback); - -socket_t create_client_socket(const std::string &host, const std::string &ip, - int port, int address_family, bool tcp_nodelay, - bool ipv6_v6only, SocketOptions socket_options, - time_t connection_timeout_sec, - time_t connection_timeout_usec, - time_t read_timeout_sec, time_t read_timeout_usec, - time_t write_timeout_sec, - time_t write_timeout_usec, - const std::string &intf, Error &error); - -const char *get_header_value(const Headers &headers, const std::string &key, - const char *def, size_t id); - -std::string params_to_query_str(const Params ¶ms); - -void parse_query_text(const char *data, std::size_t size, Params ¶ms); - -void parse_query_text(const std::string &s, Params ¶ms); - -bool parse_multipart_boundary(const std::string &content_type, - std::string &boundary); - -bool parse_range_header(const std::string &s, Ranges &ranges); - -bool parse_accept_header(const std::string &s, - std::vector &content_types); - -ssize_t send_socket(socket_t sock, const void *ptr, size_t size, int flags); - -ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags); - -enum class EncodingType { None = 0, Gzip, Brotli, Zstd }; - -EncodingType encoding_type(const Request &req, const Response &res); - -class BufferStream final : public Stream { -public: - BufferStream() = default; - ~BufferStream() override = default; - - bool is_readable() const override; - bool wait_readable() const override; - bool wait_writable() const override; - ssize_t read(char *ptr, size_t size) override; - ssize_t write(const char *ptr, size_t size) override; - void get_remote_ip_and_port(std::string &ip, int &port) const override; - void get_local_ip_and_port(std::string &ip, int &port) const override; - socket_t socket() const override; - time_t duration() const override; - - const std::string &get_buffer() const; - -private: - std::string buffer; - size_t position = 0; -}; - -class compressor { -public: - virtual ~compressor() = default; - - typedef std::function Callback; - virtual bool compress(const char *data, size_t data_length, bool last, - Callback callback) = 0; -}; - -class decompressor { -public: - virtual ~decompressor() = default; - - virtual bool is_valid() const = 0; - - typedef std::function Callback; - virtual bool decompress(const char *data, size_t data_length, - Callback callback) = 0; -}; - -class nocompressor final : public compressor { -public: - ~nocompressor() override = default; - - bool compress(const char *data, size_t data_length, bool /*last*/, - Callback callback) override; -}; - -#ifdef CPPHTTPLIB_ZLIB_SUPPORT -class gzip_compressor final : public compressor { -public: - gzip_compressor(); - ~gzip_compressor() override; - - bool compress(const char *data, size_t data_length, bool last, - Callback callback) override; - -private: - bool is_valid_ = false; - z_stream strm_; -}; - -class gzip_decompressor final : public decompressor { -public: - gzip_decompressor(); - ~gzip_decompressor() override; - - bool is_valid() const override; - - bool decompress(const char *data, size_t data_length, - Callback callback) override; - -private: - bool is_valid_ = false; - z_stream strm_; -}; -#endif - -#ifdef CPPHTTPLIB_BROTLI_SUPPORT -class brotli_compressor final : public compressor { -public: - brotli_compressor(); - ~brotli_compressor(); - - bool compress(const char *data, size_t data_length, bool last, - Callback callback) override; - -private: - BrotliEncoderState *state_ = nullptr; -}; - -class brotli_decompressor final : public decompressor { -public: - brotli_decompressor(); - ~brotli_decompressor(); - - bool is_valid() const override; - - bool decompress(const char *data, size_t data_length, - Callback callback) override; - -private: - BrotliDecoderResult decoder_r; - BrotliDecoderState *decoder_s = nullptr; -}; -#endif - -#ifdef CPPHTTPLIB_ZSTD_SUPPORT -class zstd_compressor : public compressor { -public: - zstd_compressor(); - ~zstd_compressor(); - - bool compress(const char *data, size_t data_length, bool last, - Callback callback) override; - -private: - ZSTD_CCtx *ctx_ = nullptr; -}; - -class zstd_decompressor : public decompressor { -public: - zstd_decompressor(); - ~zstd_decompressor(); - - bool is_valid() const override; - - bool decompress(const char *data, size_t data_length, - Callback callback) override; - -private: - ZSTD_DCtx *ctx_ = nullptr; -}; -#endif - -// NOTE: until the read size reaches `fixed_buffer_size`, use `fixed_buffer` -// to store data. The call can set memory on stack for performance. -class stream_line_reader { -public: - stream_line_reader(Stream &strm, char *fixed_buffer, - size_t fixed_buffer_size); - const char *ptr() const; - size_t size() const; - bool end_with_crlf() const; - bool getline(); - -private: - void append(char c); - - Stream &strm_; - char *fixed_buffer_; - const size_t fixed_buffer_size_; - size_t fixed_buffer_used_size_ = 0; - std::string growable_buffer_; -}; - -bool parse_trailers(stream_line_reader &line_reader, Headers &dest, - const Headers &src_headers); - -struct ChunkedDecoder { - Stream &strm; - size_t chunk_remaining = 0; - bool finished = false; - char line_buf[64]; - size_t last_chunk_total = 0; - size_t last_chunk_offset = 0; - - explicit ChunkedDecoder(Stream &s); - - ssize_t read_payload(char *buf, size_t len, size_t &out_chunk_offset, - size_t &out_chunk_total); - - bool parse_trailers_into(Headers &dest, const Headers &src_headers); -}; - -class mmap { -public: - mmap(const char *path); - ~mmap(); - - bool open(const char *path); - void close(); - - bool is_open() const; - size_t size() const; - const char *data() const; - -private: -#if defined(_WIN32) - HANDLE hFile_ = NULL; - HANDLE hMapping_ = NULL; -#else - int fd_ = -1; -#endif - size_t size_ = 0; - void *addr_ = nullptr; - bool is_open_empty_file = false; -}; - -// NOTE: https://www.rfc-editor.org/rfc/rfc9110#section-5 -namespace fields { - -bool is_token_char(char c); -bool is_token(const std::string &s); -bool is_field_name(const std::string &s); -bool is_vchar(char c); -bool is_obs_text(char c); -bool is_field_vchar(char c); -bool is_field_content(const std::string &s); -bool is_field_value(const std::string &s); - -} // namespace fields -} // namespace detail - -/* - * TLS Abstraction Layer Declarations - */ - -#ifdef CPPHTTPLIB_SSL_ENABLED -// TLS abstraction layer - backend-specific type declarations -#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT -namespace tls { -namespace impl { - -// Mbed TLS context wrapper (holds config, entropy, DRBG, CA chain, own -// cert/key). This struct is accessible via tls::impl for use in SSL context -// setup callbacks (cast ctx_t to tls::impl::MbedTlsContext*). -struct MbedTlsContext { - mbedtls_ssl_config conf; - mbedtls_entropy_context entropy; - mbedtls_ctr_drbg_context ctr_drbg; - mbedtls_x509_crt ca_chain; - mbedtls_x509_crt own_cert; - mbedtls_pk_context own_key; - bool is_server = false; - bool verify_client = false; - bool has_verify_callback = false; - - MbedTlsContext(); - ~MbedTlsContext(); - - MbedTlsContext(const MbedTlsContext &) = delete; - MbedTlsContext &operator=(const MbedTlsContext &) = delete; -}; - -} // namespace impl -} // namespace tls -#endif - -#ifdef CPPHTTPLIB_WOLFSSL_SUPPORT -namespace tls { -namespace impl { - -// wolfSSL context wrapper (holds WOLFSSL_CTX and related state). -// This struct is accessible via tls::impl for use in SSL context -// setup callbacks (cast ctx_t to tls::impl::WolfSSLContext*). -struct WolfSSLContext { - WOLFSSL_CTX *ctx = nullptr; - bool is_server = false; - bool verify_client = false; - bool has_verify_callback = false; - std::string ca_pem_data_; // accumulated PEM for get_ca_names/get_ca_certs - - WolfSSLContext(); - ~WolfSSLContext(); - - WolfSSLContext(const WolfSSLContext &) = delete; - WolfSSLContext &operator=(const WolfSSLContext &) = delete; -}; - -// CA store for wolfSSL: holds raw PEM bytes to allow reloading into any ctx -struct WolfSSLCAStore { - std::string pem_data; -}; - -} // namespace impl -} // namespace tls -#endif - -#endif // CPPHTTPLIB_SSL_ENABLED - -namespace stream { - -class Result { -public: - Result(); - explicit Result(ClientImpl::StreamHandle &&handle, size_t chunk_size = 8192); - Result(Result &&other) noexcept; - Result &operator=(Result &&other) noexcept; - Result(const Result &) = delete; - Result &operator=(const Result &) = delete; - - // Response info - bool is_valid() const; - explicit operator bool() const; - int status() const; - const Headers &headers() const; - std::string get_header_value(const std::string &key, - const char *def = "") const; - bool has_header(const std::string &key) const; - Error error() const; - Error read_error() const; - bool has_read_error() const; - - // Stream reading - bool next(); - const char *data() const; - size_t size() const; - std::string read_all(); - -private: - ClientImpl::StreamHandle handle_; - std::string buffer_; - size_t current_size_ = 0; - size_t chunk_size_; - bool finished_ = false; -}; - -// GET -template -inline Result Get(ClientType &cli, const std::string &path, - size_t chunk_size = 8192) { - return Result{cli.open_stream("GET", path), chunk_size}; -} - -template -inline Result Get(ClientType &cli, const std::string &path, - const Headers &headers, size_t chunk_size = 8192) { - return Result{cli.open_stream("GET", path, {}, headers), chunk_size}; -} - -template -inline Result Get(ClientType &cli, const std::string &path, - const Params ¶ms, size_t chunk_size = 8192) { - return Result{cli.open_stream("GET", path, params), chunk_size}; -} - -template -inline Result Get(ClientType &cli, const std::string &path, - const Params ¶ms, const Headers &headers, - size_t chunk_size = 8192) { - return Result{cli.open_stream("GET", path, params, headers), chunk_size}; -} - -// POST -template -inline Result Post(ClientType &cli, const std::string &path, - const std::string &body, const std::string &content_type, - size_t chunk_size = 8192) { - return Result{cli.open_stream("POST", path, {}, {}, body, content_type), - chunk_size}; -} - -template -inline Result Post(ClientType &cli, const std::string &path, - const Headers &headers, const std::string &body, - const std::string &content_type, size_t chunk_size = 8192) { - return Result{cli.open_stream("POST", path, {}, headers, body, content_type), - chunk_size}; -} - -template -inline Result Post(ClientType &cli, const std::string &path, - const Params ¶ms, const std::string &body, - const std::string &content_type, size_t chunk_size = 8192) { - return Result{cli.open_stream("POST", path, params, {}, body, content_type), - chunk_size}; -} - -template -inline Result Post(ClientType &cli, const std::string &path, - const Params ¶ms, const Headers &headers, - const std::string &body, const std::string &content_type, - size_t chunk_size = 8192) { - return Result{ - cli.open_stream("POST", path, params, headers, body, content_type), - chunk_size}; -} - -// PUT -template -inline Result Put(ClientType &cli, const std::string &path, - const std::string &body, const std::string &content_type, - size_t chunk_size = 8192) { - return Result{cli.open_stream("PUT", path, {}, {}, body, content_type), - chunk_size}; -} - -template -inline Result Put(ClientType &cli, const std::string &path, - const Headers &headers, const std::string &body, - const std::string &content_type, size_t chunk_size = 8192) { - return Result{cli.open_stream("PUT", path, {}, headers, body, content_type), - chunk_size}; -} - -template -inline Result Put(ClientType &cli, const std::string &path, - const Params ¶ms, const std::string &body, - const std::string &content_type, size_t chunk_size = 8192) { - return Result{cli.open_stream("PUT", path, params, {}, body, content_type), - chunk_size}; -} - -template -inline Result Put(ClientType &cli, const std::string &path, - const Params ¶ms, const Headers &headers, - const std::string &body, const std::string &content_type, - size_t chunk_size = 8192) { - return Result{ - cli.open_stream("PUT", path, params, headers, body, content_type), - chunk_size}; -} - -// PATCH -template -inline Result Patch(ClientType &cli, const std::string &path, - const std::string &body, const std::string &content_type, - size_t chunk_size = 8192) { - return Result{cli.open_stream("PATCH", path, {}, {}, body, content_type), - chunk_size}; -} - -template -inline Result Patch(ClientType &cli, const std::string &path, - const Headers &headers, const std::string &body, - const std::string &content_type, size_t chunk_size = 8192) { - return Result{cli.open_stream("PATCH", path, {}, headers, body, content_type), - chunk_size}; -} - -template -inline Result Patch(ClientType &cli, const std::string &path, - const Params ¶ms, const std::string &body, - const std::string &content_type, size_t chunk_size = 8192) { - return Result{cli.open_stream("PATCH", path, params, {}, body, content_type), - chunk_size}; -} - -template -inline Result Patch(ClientType &cli, const std::string &path, - const Params ¶ms, const Headers &headers, - const std::string &body, const std::string &content_type, - size_t chunk_size = 8192) { - return Result{ - cli.open_stream("PATCH", path, params, headers, body, content_type), - chunk_size}; -} - -// DELETE -template -inline Result Delete(ClientType &cli, const std::string &path, - size_t chunk_size = 8192) { - return Result{cli.open_stream("DELETE", path), chunk_size}; -} - -template -inline Result Delete(ClientType &cli, const std::string &path, - const Headers &headers, size_t chunk_size = 8192) { - return Result{cli.open_stream("DELETE", path, {}, headers), chunk_size}; -} - -template -inline Result Delete(ClientType &cli, const std::string &path, - const std::string &body, const std::string &content_type, - size_t chunk_size = 8192) { - return Result{cli.open_stream("DELETE", path, {}, {}, body, content_type), - chunk_size}; -} - -template -inline Result Delete(ClientType &cli, const std::string &path, - const Headers &headers, const std::string &body, - const std::string &content_type, - size_t chunk_size = 8192) { - return Result{ - cli.open_stream("DELETE", path, {}, headers, body, content_type), - chunk_size}; -} - -template -inline Result Delete(ClientType &cli, const std::string &path, - const Params ¶ms, size_t chunk_size = 8192) { - return Result{cli.open_stream("DELETE", path, params), chunk_size}; -} - -template -inline Result Delete(ClientType &cli, const std::string &path, - const Params ¶ms, const Headers &headers, - size_t chunk_size = 8192) { - return Result{cli.open_stream("DELETE", path, params, headers), chunk_size}; -} - -template -inline Result Delete(ClientType &cli, const std::string &path, - const Params ¶ms, const std::string &body, - const std::string &content_type, - size_t chunk_size = 8192) { - return Result{cli.open_stream("DELETE", path, params, {}, body, content_type), - chunk_size}; -} - -template -inline Result Delete(ClientType &cli, const std::string &path, - const Params ¶ms, const Headers &headers, - const std::string &body, const std::string &content_type, - size_t chunk_size = 8192) { - return Result{ - cli.open_stream("DELETE", path, params, headers, body, content_type), - chunk_size}; -} - -// HEAD -template -inline Result Head(ClientType &cli, const std::string &path, - size_t chunk_size = 8192) { - return Result{cli.open_stream("HEAD", path), chunk_size}; -} - -template -inline Result Head(ClientType &cli, const std::string &path, - const Headers &headers, size_t chunk_size = 8192) { - return Result{cli.open_stream("HEAD", path, {}, headers), chunk_size}; -} - -template -inline Result Head(ClientType &cli, const std::string &path, - const Params ¶ms, size_t chunk_size = 8192) { - return Result{cli.open_stream("HEAD", path, params), chunk_size}; -} - -template -inline Result Head(ClientType &cli, const std::string &path, - const Params ¶ms, const Headers &headers, - size_t chunk_size = 8192) { - return Result{cli.open_stream("HEAD", path, params, headers), chunk_size}; -} - -// OPTIONS -template -inline Result Options(ClientType &cli, const std::string &path, - size_t chunk_size = 8192) { - return Result{cli.open_stream("OPTIONS", path), chunk_size}; -} - -template -inline Result Options(ClientType &cli, const std::string &path, - const Headers &headers, size_t chunk_size = 8192) { - return Result{cli.open_stream("OPTIONS", path, {}, headers), chunk_size}; -} - -template -inline Result Options(ClientType &cli, const std::string &path, - const Params ¶ms, size_t chunk_size = 8192) { - return Result{cli.open_stream("OPTIONS", path, params), chunk_size}; -} - -template -inline Result Options(ClientType &cli, const std::string &path, - const Params ¶ms, const Headers &headers, - size_t chunk_size = 8192) { - return Result{cli.open_stream("OPTIONS", path, params, headers), chunk_size}; -} - -} // namespace stream - -namespace sse { - -struct SSEMessage { - std::string event; // Event type (default: "message") - std::string data; // Event payload - std::string id; // Event ID for Last-Event-ID header - - SSEMessage(); - void clear(); -}; - -class SSEClient { -public: - using MessageHandler = std::function; - using ErrorHandler = std::function; - using OpenHandler = std::function; - - SSEClient(Client &client, const std::string &path); - SSEClient(Client &client, const std::string &path, const Headers &headers); - ~SSEClient(); - - SSEClient(const SSEClient &) = delete; - SSEClient &operator=(const SSEClient &) = delete; - - // Event handlers - SSEClient &on_message(MessageHandler handler); - SSEClient &on_event(const std::string &type, MessageHandler handler); - SSEClient &on_open(OpenHandler handler); - SSEClient &on_error(ErrorHandler handler); - SSEClient &set_reconnect_interval(int ms); - SSEClient &set_max_reconnect_attempts(int n); - - // Update headers (thread-safe) - SSEClient &set_headers(const Headers &headers); - - // State accessors - bool is_connected() const; - const std::string &last_event_id() const; - - // Blocking start - runs event loop with auto-reconnect - void start(); - - // Non-blocking start - runs in background thread - void start_async(); - - // Stop the client (thread-safe) - void stop(); - -private: - bool parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms); - void run_event_loop(); - void dispatch_event(const SSEMessage &msg); - bool should_reconnect(int count) const; - void wait_for_reconnect(); - - // Client and path - Client &client_; - std::string path_; - Headers headers_; - mutable std::mutex headers_mutex_; - - // Callbacks - MessageHandler on_message_; - std::map event_handlers_; - OpenHandler on_open_; - ErrorHandler on_error_; - - // Configuration - int reconnect_interval_ms_ = 3000; - int max_reconnect_attempts_ = 0; // 0 = unlimited - - // State - std::atomic running_{false}; - std::atomic connected_{false}; - std::string last_event_id_; - - // Async support - std::thread async_thread_; -}; - -} // namespace sse - -namespace ws { - -enum class Opcode : uint8_t { - Continuation = 0x0, - Text = 0x1, - Binary = 0x2, - Close = 0x8, - Ping = 0x9, - Pong = 0xA, -}; - -enum class CloseStatus : uint16_t { - Normal = 1000, - GoingAway = 1001, - ProtocolError = 1002, - UnsupportedData = 1003, - NoStatus = 1005, - Abnormal = 1006, - InvalidPayload = 1007, - PolicyViolation = 1008, - MessageTooBig = 1009, - MandatoryExtension = 1010, - InternalError = 1011, -}; - -enum ReadResult : int { Fail = 0, Text = 1, Binary = 2 }; - -class WebSocket { -public: - WebSocket(const WebSocket &) = delete; - WebSocket &operator=(const WebSocket &) = delete; - ~WebSocket(); - - ReadResult read(std::string &msg); - bool send(const std::string &data); - bool send(const char *data, size_t len); - void close(CloseStatus status = CloseStatus::Normal, - const std::string &reason = ""); - const Request &request() const; - bool is_open() const; - -private: - friend class httplib::Server; - friend class WebSocketClient; - - WebSocket( - Stream &strm, const Request &req, bool is_server, - time_t ping_interval_sec = CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND, - int max_missed_pongs = CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS) - : strm_(strm), req_(req), is_server_(is_server), - ping_interval_sec_(ping_interval_sec), - max_missed_pongs_(max_missed_pongs) { - start_heartbeat(); - } - - WebSocket( - std::unique_ptr &&owned_strm, const Request &req, bool is_server, - time_t ping_interval_sec = CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND, - int max_missed_pongs = CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS) - : strm_(*owned_strm), owned_strm_(std::move(owned_strm)), req_(req), - is_server_(is_server), ping_interval_sec_(ping_interval_sec), - max_missed_pongs_(max_missed_pongs) { - start_heartbeat(); - } - - void start_heartbeat(); - bool send_frame(Opcode op, const char *data, size_t len, bool fin = true); - - Stream &strm_; - std::unique_ptr owned_strm_; - Request req_; - bool is_server_; - time_t ping_interval_sec_; - int max_missed_pongs_; - int unacked_pings_ = 0; - std::atomic closed_{false}; - std::mutex write_mutex_; - std::thread ping_thread_; - std::mutex ping_mutex_; - std::condition_variable ping_cv_; -}; - -class WebSocketClient { -public: - explicit WebSocketClient(const std::string &scheme_host_port_path, - const Headers &headers = {}); - - ~WebSocketClient(); - WebSocketClient(const WebSocketClient &) = delete; - WebSocketClient &operator=(const WebSocketClient &) = delete; - - bool is_valid() const; - - bool connect(); - ReadResult read(std::string &msg); - bool send(const std::string &data); - bool send(const char *data, size_t len); - void close(CloseStatus status = CloseStatus::Normal, - const std::string &reason = ""); - bool is_open() const; - const std::string &subprotocol() const; - void set_read_timeout(time_t sec, time_t usec = 0); - void set_write_timeout(time_t sec, time_t usec = 0); - void set_websocket_ping_interval(time_t sec); - void set_websocket_max_missed_pongs(int count); - void set_tcp_nodelay(bool on); - void set_address_family(int family); - void set_ipv6_v6only(bool on); - void set_socket_options(SocketOptions socket_options); - void set_connection_timeout(time_t sec, time_t usec = 0); - void set_interface(const std::string &intf); - -#ifdef CPPHTTPLIB_SSL_ENABLED - void set_ca_cert_path(const std::string &path); - void set_ca_cert_store(tls::ca_store_t store); - void enable_server_certificate_verification(bool enabled); -#endif - -private: - void shutdown_and_close(); - bool create_stream(std::unique_ptr &strm); - - std::string host_; - int port_; - std::string path_; - Headers headers_; - std::string subprotocol_; - bool is_valid_ = false; - socket_t sock_ = INVALID_SOCKET; - std::unique_ptr ws_; - time_t read_timeout_sec_ = CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND; - time_t read_timeout_usec_ = 0; - time_t write_timeout_sec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND; - time_t write_timeout_usec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND; - time_t websocket_ping_interval_sec_ = - CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND; - int websocket_max_missed_pongs_ = CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS; - int address_family_ = AF_UNSPEC; - bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY; - bool ipv6_v6only_ = CPPHTTPLIB_IPV6_V6ONLY; - SocketOptions socket_options_ = nullptr; - time_t connection_timeout_sec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND; - time_t connection_timeout_usec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND; - std::string interface_; - -#ifdef CPPHTTPLIB_SSL_ENABLED - bool is_ssl_ = false; - tls::ctx_t tls_ctx_ = nullptr; - tls::session_t tls_session_ = nullptr; - std::string ca_cert_file_path_; - tls::ca_store_t ca_cert_store_ = nullptr; - bool server_certificate_verification_ = true; -#endif -}; - -namespace impl { - -bool is_valid_utf8(const std::string &s); - -bool read_websocket_frame(Stream &strm, Opcode &opcode, std::string &payload, - bool &fin, bool expect_masked, size_t max_len); - -} // namespace impl - -} // namespace ws - - -} // namespace httplib - -#endif // CPPHTTPLIB_HTTPLIB_H diff --git a/third_party/nlohmann/nlohmann/json.hpp b/third_party/nlohmann/nlohmann/json.hpp deleted file mode 100644 index 8b72ea6..0000000 --- a/third_party/nlohmann/nlohmann/json.hpp +++ /dev/null @@ -1,24765 +0,0 @@ -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.3 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann -// SPDX-License-Identifier: MIT - -/****************************************************************************\ - * Note on documentation: The source files contain links to the online * - * documentation of the public API at https://json.nlohmann.me. This URL * - * contains the most recent documentation and should also be applicable to * - * previous versions; documentation for deprecated functions is not * - * removed, but marked deprecated. See "Generate documentation" section in * - * file docs/README.md. * -\****************************************************************************/ - -#ifndef INCLUDE_NLOHMANN_JSON_HPP_ -#define INCLUDE_NLOHMANN_JSON_HPP_ - -#include // all_of, find, for_each -#include // nullptr_t, ptrdiff_t, size_t -#include // hash, less -#include // initializer_list -#ifndef JSON_NO_IO - #include // istream, ostream -#endif // JSON_NO_IO -#include // random_access_iterator_tag -#include // unique_ptr -#include // string, stoi, to_string -#include // declval, forward, move, pair, swap -#include // vector - -// #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.3 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -#include - -// #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.3 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -// This file contains all macro definitions affecting or depending on the ABI - -#ifndef JSON_SKIP_LIBRARY_VERSION_CHECK - #if defined(NLOHMANN_JSON_VERSION_MAJOR) && defined(NLOHMANN_JSON_VERSION_MINOR) && defined(NLOHMANN_JSON_VERSION_PATCH) - #if NLOHMANN_JSON_VERSION_MAJOR != 3 || NLOHMANN_JSON_VERSION_MINOR != 11 || NLOHMANN_JSON_VERSION_PATCH != 3 - #warning "Already included a different version of the library!" - #endif - #endif -#endif - -#define NLOHMANN_JSON_VERSION_MAJOR 3 // NOLINT(modernize-macro-to-enum) -#define NLOHMANN_JSON_VERSION_MINOR 11 // NOLINT(modernize-macro-to-enum) -#define NLOHMANN_JSON_VERSION_PATCH 3 // NOLINT(modernize-macro-to-enum) - -#ifndef JSON_DIAGNOSTICS - #define JSON_DIAGNOSTICS 0 -#endif - -#ifndef JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON - #define JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON 0 -#endif - -#if JSON_DIAGNOSTICS - #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag -#else - #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS -#endif - -#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON - #define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON _ldvcmp -#else - #define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON -#endif - -#ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION - #define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0 -#endif - -// Construct the namespace ABI tags component -#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b) json_abi ## a ## b -#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b) \ - NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b) - -#define NLOHMANN_JSON_ABI_TAGS \ - NLOHMANN_JSON_ABI_TAGS_CONCAT( \ - NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS, \ - NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON) - -// Construct the namespace version component -#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \ - _v ## major ## _ ## minor ## _ ## patch -#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(major, minor, patch) \ - NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) - -#if NLOHMANN_JSON_NAMESPACE_NO_VERSION -#define NLOHMANN_JSON_NAMESPACE_VERSION -#else -#define NLOHMANN_JSON_NAMESPACE_VERSION \ - NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(NLOHMANN_JSON_VERSION_MAJOR, \ - NLOHMANN_JSON_VERSION_MINOR, \ - NLOHMANN_JSON_VERSION_PATCH) -#endif - -// Combine namespace components -#define NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) a ## b -#define NLOHMANN_JSON_NAMESPACE_CONCAT(a, b) \ - NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) - -#ifndef NLOHMANN_JSON_NAMESPACE -#define NLOHMANN_JSON_NAMESPACE \ - nlohmann::NLOHMANN_JSON_NAMESPACE_CONCAT( \ - NLOHMANN_JSON_ABI_TAGS, \ - NLOHMANN_JSON_NAMESPACE_VERSION) -#endif - -#ifndef NLOHMANN_JSON_NAMESPACE_BEGIN -#define NLOHMANN_JSON_NAMESPACE_BEGIN \ - namespace nlohmann \ - { \ - inline namespace NLOHMANN_JSON_NAMESPACE_CONCAT( \ - NLOHMANN_JSON_ABI_TAGS, \ - NLOHMANN_JSON_NAMESPACE_VERSION) \ - { -#endif - -#ifndef NLOHMANN_JSON_NAMESPACE_END -#define NLOHMANN_JSON_NAMESPACE_END \ - } /* namespace (inline namespace) NOLINT(readability/namespace) */ \ - } // namespace nlohmann -#endif - -// #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.3 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -#include // transform -#include // array -#include // forward_list -#include // inserter, front_inserter, end -#include // map -#include // string -#include // tuple, make_tuple -#include // is_arithmetic, is_same, is_enum, underlying_type, is_convertible -#include // unordered_map -#include // pair, declval -#include // valarray - -// #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.3 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -#include // nullptr_t -#include // exception -#if JSON_DIAGNOSTICS - #include // accumulate -#endif -#include // runtime_error -#include // to_string -#include // vector - -// #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.3 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -#include // array -#include // size_t -#include // uint8_t -#include // string - -// #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.3 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -#include // declval, pair -// #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.3 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -#include - -// #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.3 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -// #include - - -NLOHMANN_JSON_NAMESPACE_BEGIN -namespace detail -{ - -template struct make_void -{ - using type = void; -}; -template using void_t = typename make_void::type; - -} // namespace detail -NLOHMANN_JSON_NAMESPACE_END - - -NLOHMANN_JSON_NAMESPACE_BEGIN -namespace detail -{ - -// https://en.cppreference.com/w/cpp/experimental/is_detected -struct nonesuch -{ - nonesuch() = delete; - ~nonesuch() = delete; - nonesuch(nonesuch const&) = delete; - nonesuch(nonesuch const&&) = delete; - void operator=(nonesuch const&) = delete; - void operator=(nonesuch&&) = delete; -}; - -template class Op, - class... Args> -struct detector -{ - using value_t = std::false_type; - using type = Default; -}; - -template class Op, class... Args> -struct detector>, Op, Args...> -{ - using value_t = std::true_type; - using type = Op; -}; - -template class Op, class... Args> -using is_detected = typename detector::value_t; - -template class Op, class... Args> -struct is_detected_lazy : is_detected { }; - -template class Op, class... Args> -using detected_t = typename detector::type; - -template class Op, class... Args> -using detected_or = detector; - -template class Op, class... Args> -using detected_or_t = typename detected_or::type; - -template class Op, class... Args> -using is_detected_exact = std::is_same>; - -template class Op, class... Args> -using is_detected_convertible = - std::is_convertible, To>; - -} // namespace detail -NLOHMANN_JSON_NAMESPACE_END - -// #include - - -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.3 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann -// SPDX-FileCopyrightText: 2016-2021 Evan Nemerson -// SPDX-License-Identifier: MIT - -/* Hedley - https://nemequ.github.io/hedley - * Created by Evan Nemerson - */ - -#if !defined(JSON_HEDLEY_VERSION) || (JSON_HEDLEY_VERSION < 15) -#if defined(JSON_HEDLEY_VERSION) - #undef JSON_HEDLEY_VERSION -#endif -#define JSON_HEDLEY_VERSION 15 - -#if defined(JSON_HEDLEY_STRINGIFY_EX) - #undef JSON_HEDLEY_STRINGIFY_EX -#endif -#define JSON_HEDLEY_STRINGIFY_EX(x) #x - -#if defined(JSON_HEDLEY_STRINGIFY) - #undef JSON_HEDLEY_STRINGIFY -#endif -#define JSON_HEDLEY_STRINGIFY(x) JSON_HEDLEY_STRINGIFY_EX(x) - -#if defined(JSON_HEDLEY_CONCAT_EX) - #undef JSON_HEDLEY_CONCAT_EX -#endif -#define JSON_HEDLEY_CONCAT_EX(a,b) a##b - -#if defined(JSON_HEDLEY_CONCAT) - #undef JSON_HEDLEY_CONCAT -#endif -#define JSON_HEDLEY_CONCAT(a,b) JSON_HEDLEY_CONCAT_EX(a,b) - -#if defined(JSON_HEDLEY_CONCAT3_EX) - #undef JSON_HEDLEY_CONCAT3_EX -#endif -#define JSON_HEDLEY_CONCAT3_EX(a,b,c) a##b##c - -#if defined(JSON_HEDLEY_CONCAT3) - #undef JSON_HEDLEY_CONCAT3 -#endif -#define JSON_HEDLEY_CONCAT3(a,b,c) JSON_HEDLEY_CONCAT3_EX(a,b,c) - -#if defined(JSON_HEDLEY_VERSION_ENCODE) - #undef JSON_HEDLEY_VERSION_ENCODE -#endif -#define JSON_HEDLEY_VERSION_ENCODE(major,minor,revision) (((major) * 1000000) + ((minor) * 1000) + (revision)) - -#if defined(JSON_HEDLEY_VERSION_DECODE_MAJOR) - #undef JSON_HEDLEY_VERSION_DECODE_MAJOR -#endif -#define JSON_HEDLEY_VERSION_DECODE_MAJOR(version) ((version) / 1000000) - -#if defined(JSON_HEDLEY_VERSION_DECODE_MINOR) - #undef JSON_HEDLEY_VERSION_DECODE_MINOR -#endif -#define JSON_HEDLEY_VERSION_DECODE_MINOR(version) (((version) % 1000000) / 1000) - -#if defined(JSON_HEDLEY_VERSION_DECODE_REVISION) - #undef JSON_HEDLEY_VERSION_DECODE_REVISION -#endif -#define JSON_HEDLEY_VERSION_DECODE_REVISION(version) ((version) % 1000) - -#if defined(JSON_HEDLEY_GNUC_VERSION) - #undef JSON_HEDLEY_GNUC_VERSION -#endif -#if defined(__GNUC__) && defined(__GNUC_PATCHLEVEL__) - #define JSON_HEDLEY_GNUC_VERSION JSON_HEDLEY_VERSION_ENCODE(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) -#elif defined(__GNUC__) - #define JSON_HEDLEY_GNUC_VERSION JSON_HEDLEY_VERSION_ENCODE(__GNUC__, __GNUC_MINOR__, 0) -#endif - -#if defined(JSON_HEDLEY_GNUC_VERSION_CHECK) - #undef JSON_HEDLEY_GNUC_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_GNUC_VERSION) - #define JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_GNUC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_MSVC_VERSION) - #undef JSON_HEDLEY_MSVC_VERSION -#endif -#if defined(_MSC_FULL_VER) && (_MSC_FULL_VER >= 140000000) && !defined(__ICL) - #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_FULL_VER / 10000000, (_MSC_FULL_VER % 10000000) / 100000, (_MSC_FULL_VER % 100000) / 100) -#elif defined(_MSC_FULL_VER) && !defined(__ICL) - #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_FULL_VER / 1000000, (_MSC_FULL_VER % 1000000) / 10000, (_MSC_FULL_VER % 10000) / 10) -#elif defined(_MSC_VER) && !defined(__ICL) - #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_VER / 100, _MSC_VER % 100, 0) -#endif - -#if defined(JSON_HEDLEY_MSVC_VERSION_CHECK) - #undef JSON_HEDLEY_MSVC_VERSION_CHECK -#endif -#if !defined(JSON_HEDLEY_MSVC_VERSION) - #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (0) -#elif defined(_MSC_VER) && (_MSC_VER >= 1400) - #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_FULL_VER >= ((major * 10000000) + (minor * 100000) + (patch))) -#elif defined(_MSC_VER) && (_MSC_VER >= 1200) - #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_FULL_VER >= ((major * 1000000) + (minor * 10000) + (patch))) -#else - #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_VER >= ((major * 100) + (minor))) -#endif - -#if defined(JSON_HEDLEY_INTEL_VERSION) - #undef JSON_HEDLEY_INTEL_VERSION -#endif -#if defined(__INTEL_COMPILER) && defined(__INTEL_COMPILER_UPDATE) && !defined(__ICL) - #define JSON_HEDLEY_INTEL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, __INTEL_COMPILER_UPDATE) -#elif defined(__INTEL_COMPILER) && !defined(__ICL) - #define JSON_HEDLEY_INTEL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, 0) -#endif - -#if defined(JSON_HEDLEY_INTEL_VERSION_CHECK) - #undef JSON_HEDLEY_INTEL_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_INTEL_VERSION) - #define JSON_HEDLEY_INTEL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_INTEL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_INTEL_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_INTEL_CL_VERSION) - #undef JSON_HEDLEY_INTEL_CL_VERSION -#endif -#if defined(__INTEL_COMPILER) && defined(__INTEL_COMPILER_UPDATE) && defined(__ICL) - #define JSON_HEDLEY_INTEL_CL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER, __INTEL_COMPILER_UPDATE, 0) -#endif - -#if defined(JSON_HEDLEY_INTEL_CL_VERSION_CHECK) - #undef JSON_HEDLEY_INTEL_CL_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_INTEL_CL_VERSION) - #define JSON_HEDLEY_INTEL_CL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_INTEL_CL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_INTEL_CL_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_PGI_VERSION) - #undef JSON_HEDLEY_PGI_VERSION -#endif -#if defined(__PGI) && defined(__PGIC__) && defined(__PGIC_MINOR__) && defined(__PGIC_PATCHLEVEL__) - #define JSON_HEDLEY_PGI_VERSION JSON_HEDLEY_VERSION_ENCODE(__PGIC__, __PGIC_MINOR__, __PGIC_PATCHLEVEL__) -#endif - -#if defined(JSON_HEDLEY_PGI_VERSION_CHECK) - #undef JSON_HEDLEY_PGI_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_PGI_VERSION) - #define JSON_HEDLEY_PGI_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_PGI_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_PGI_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_SUNPRO_VERSION) - #undef JSON_HEDLEY_SUNPRO_VERSION -#endif -#if defined(__SUNPRO_C) && (__SUNPRO_C > 0x1000) - #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((((__SUNPRO_C >> 16) & 0xf) * 10) + ((__SUNPRO_C >> 12) & 0xf), (((__SUNPRO_C >> 8) & 0xf) * 10) + ((__SUNPRO_C >> 4) & 0xf), (__SUNPRO_C & 0xf) * 10) -#elif defined(__SUNPRO_C) - #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((__SUNPRO_C >> 8) & 0xf, (__SUNPRO_C >> 4) & 0xf, (__SUNPRO_C) & 0xf) -#elif defined(__SUNPRO_CC) && (__SUNPRO_CC > 0x1000) - #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((((__SUNPRO_CC >> 16) & 0xf) * 10) + ((__SUNPRO_CC >> 12) & 0xf), (((__SUNPRO_CC >> 8) & 0xf) * 10) + ((__SUNPRO_CC >> 4) & 0xf), (__SUNPRO_CC & 0xf) * 10) -#elif defined(__SUNPRO_CC) - #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((__SUNPRO_CC >> 8) & 0xf, (__SUNPRO_CC >> 4) & 0xf, (__SUNPRO_CC) & 0xf) -#endif - -#if defined(JSON_HEDLEY_SUNPRO_VERSION_CHECK) - #undef JSON_HEDLEY_SUNPRO_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_SUNPRO_VERSION) - #define JSON_HEDLEY_SUNPRO_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_SUNPRO_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_SUNPRO_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION) - #undef JSON_HEDLEY_EMSCRIPTEN_VERSION -#endif -#if defined(__EMSCRIPTEN__) - #define JSON_HEDLEY_EMSCRIPTEN_VERSION JSON_HEDLEY_VERSION_ENCODE(__EMSCRIPTEN_major__, __EMSCRIPTEN_minor__, __EMSCRIPTEN_tiny__) -#endif - -#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK) - #undef JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION) - #define JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_EMSCRIPTEN_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_ARM_VERSION) - #undef JSON_HEDLEY_ARM_VERSION -#endif -#if defined(__CC_ARM) && defined(__ARMCOMPILER_VERSION) - #define JSON_HEDLEY_ARM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ARMCOMPILER_VERSION / 1000000, (__ARMCOMPILER_VERSION % 1000000) / 10000, (__ARMCOMPILER_VERSION % 10000) / 100) -#elif defined(__CC_ARM) && defined(__ARMCC_VERSION) - #define JSON_HEDLEY_ARM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ARMCC_VERSION / 1000000, (__ARMCC_VERSION % 1000000) / 10000, (__ARMCC_VERSION % 10000) / 100) -#endif - -#if defined(JSON_HEDLEY_ARM_VERSION_CHECK) - #undef JSON_HEDLEY_ARM_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_ARM_VERSION) - #define JSON_HEDLEY_ARM_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_ARM_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_ARM_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_IBM_VERSION) - #undef JSON_HEDLEY_IBM_VERSION -#endif -#if defined(__ibmxl__) - #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ibmxl_version__, __ibmxl_release__, __ibmxl_modification__) -#elif defined(__xlC__) && defined(__xlC_ver__) - #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__xlC__ >> 8, __xlC__ & 0xff, (__xlC_ver__ >> 8) & 0xff) -#elif defined(__xlC__) - #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__xlC__ >> 8, __xlC__ & 0xff, 0) -#endif - -#if defined(JSON_HEDLEY_IBM_VERSION_CHECK) - #undef JSON_HEDLEY_IBM_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_IBM_VERSION) - #define JSON_HEDLEY_IBM_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_IBM_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_IBM_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_TI_VERSION) - #undef JSON_HEDLEY_TI_VERSION -#endif -#if \ - defined(__TI_COMPILER_VERSION__) && \ - ( \ - defined(__TMS470__) || defined(__TI_ARM__) || \ - defined(__MSP430__) || \ - defined(__TMS320C2000__) \ - ) -#if (__TI_COMPILER_VERSION__ >= 16000000) - #define JSON_HEDLEY_TI_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) -#endif -#endif - -#if defined(JSON_HEDLEY_TI_VERSION_CHECK) - #undef JSON_HEDLEY_TI_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_TI_VERSION) - #define JSON_HEDLEY_TI_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_TI_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_TI_CL2000_VERSION) - #undef JSON_HEDLEY_TI_CL2000_VERSION -#endif -#if defined(__TI_COMPILER_VERSION__) && defined(__TMS320C2000__) - #define JSON_HEDLEY_TI_CL2000_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) -#endif - -#if defined(JSON_HEDLEY_TI_CL2000_VERSION_CHECK) - #undef JSON_HEDLEY_TI_CL2000_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_TI_CL2000_VERSION) - #define JSON_HEDLEY_TI_CL2000_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL2000_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_TI_CL2000_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_TI_CL430_VERSION) - #undef JSON_HEDLEY_TI_CL430_VERSION -#endif -#if defined(__TI_COMPILER_VERSION__) && defined(__MSP430__) - #define JSON_HEDLEY_TI_CL430_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) -#endif - -#if defined(JSON_HEDLEY_TI_CL430_VERSION_CHECK) - #undef JSON_HEDLEY_TI_CL430_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_TI_CL430_VERSION) - #define JSON_HEDLEY_TI_CL430_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL430_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_TI_CL430_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_TI_ARMCL_VERSION) - #undef JSON_HEDLEY_TI_ARMCL_VERSION -#endif -#if defined(__TI_COMPILER_VERSION__) && (defined(__TMS470__) || defined(__TI_ARM__)) - #define JSON_HEDLEY_TI_ARMCL_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) -#endif - -#if defined(JSON_HEDLEY_TI_ARMCL_VERSION_CHECK) - #undef JSON_HEDLEY_TI_ARMCL_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_TI_ARMCL_VERSION) - #define JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_ARMCL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_TI_CL6X_VERSION) - #undef JSON_HEDLEY_TI_CL6X_VERSION -#endif -#if defined(__TI_COMPILER_VERSION__) && defined(__TMS320C6X__) - #define JSON_HEDLEY_TI_CL6X_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) -#endif - -#if defined(JSON_HEDLEY_TI_CL6X_VERSION_CHECK) - #undef JSON_HEDLEY_TI_CL6X_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_TI_CL6X_VERSION) - #define JSON_HEDLEY_TI_CL6X_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL6X_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_TI_CL6X_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_TI_CL7X_VERSION) - #undef JSON_HEDLEY_TI_CL7X_VERSION -#endif -#if defined(__TI_COMPILER_VERSION__) && defined(__C7000__) - #define JSON_HEDLEY_TI_CL7X_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) -#endif - -#if defined(JSON_HEDLEY_TI_CL7X_VERSION_CHECK) - #undef JSON_HEDLEY_TI_CL7X_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_TI_CL7X_VERSION) - #define JSON_HEDLEY_TI_CL7X_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL7X_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_TI_CL7X_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_TI_CLPRU_VERSION) - #undef JSON_HEDLEY_TI_CLPRU_VERSION -#endif -#if defined(__TI_COMPILER_VERSION__) && defined(__PRU__) - #define JSON_HEDLEY_TI_CLPRU_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) -#endif - -#if defined(JSON_HEDLEY_TI_CLPRU_VERSION_CHECK) - #undef JSON_HEDLEY_TI_CLPRU_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_TI_CLPRU_VERSION) - #define JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CLPRU_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_CRAY_VERSION) - #undef JSON_HEDLEY_CRAY_VERSION -#endif -#if defined(_CRAYC) - #if defined(_RELEASE_PATCHLEVEL) - #define JSON_HEDLEY_CRAY_VERSION JSON_HEDLEY_VERSION_ENCODE(_RELEASE_MAJOR, _RELEASE_MINOR, _RELEASE_PATCHLEVEL) - #else - #define JSON_HEDLEY_CRAY_VERSION JSON_HEDLEY_VERSION_ENCODE(_RELEASE_MAJOR, _RELEASE_MINOR, 0) - #endif -#endif - -#if defined(JSON_HEDLEY_CRAY_VERSION_CHECK) - #undef JSON_HEDLEY_CRAY_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_CRAY_VERSION) - #define JSON_HEDLEY_CRAY_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_CRAY_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_CRAY_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_IAR_VERSION) - #undef JSON_HEDLEY_IAR_VERSION -#endif -#if defined(__IAR_SYSTEMS_ICC__) - #if __VER__ > 1000 - #define JSON_HEDLEY_IAR_VERSION JSON_HEDLEY_VERSION_ENCODE((__VER__ / 1000000), ((__VER__ / 1000) % 1000), (__VER__ % 1000)) - #else - #define JSON_HEDLEY_IAR_VERSION JSON_HEDLEY_VERSION_ENCODE(__VER__ / 100, __VER__ % 100, 0) - #endif -#endif - -#if defined(JSON_HEDLEY_IAR_VERSION_CHECK) - #undef JSON_HEDLEY_IAR_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_IAR_VERSION) - #define JSON_HEDLEY_IAR_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_IAR_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_IAR_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_TINYC_VERSION) - #undef JSON_HEDLEY_TINYC_VERSION -#endif -#if defined(__TINYC__) - #define JSON_HEDLEY_TINYC_VERSION JSON_HEDLEY_VERSION_ENCODE(__TINYC__ / 1000, (__TINYC__ / 100) % 10, __TINYC__ % 100) -#endif - -#if defined(JSON_HEDLEY_TINYC_VERSION_CHECK) - #undef JSON_HEDLEY_TINYC_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_TINYC_VERSION) - #define JSON_HEDLEY_TINYC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TINYC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_TINYC_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_DMC_VERSION) - #undef JSON_HEDLEY_DMC_VERSION -#endif -#if defined(__DMC__) - #define JSON_HEDLEY_DMC_VERSION JSON_HEDLEY_VERSION_ENCODE(__DMC__ >> 8, (__DMC__ >> 4) & 0xf, __DMC__ & 0xf) -#endif - -#if defined(JSON_HEDLEY_DMC_VERSION_CHECK) - #undef JSON_HEDLEY_DMC_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_DMC_VERSION) - #define JSON_HEDLEY_DMC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_DMC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_DMC_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_COMPCERT_VERSION) - #undef JSON_HEDLEY_COMPCERT_VERSION -#endif -#if defined(__COMPCERT_VERSION__) - #define JSON_HEDLEY_COMPCERT_VERSION JSON_HEDLEY_VERSION_ENCODE(__COMPCERT_VERSION__ / 10000, (__COMPCERT_VERSION__ / 100) % 100, __COMPCERT_VERSION__ % 100) -#endif - -#if defined(JSON_HEDLEY_COMPCERT_VERSION_CHECK) - #undef JSON_HEDLEY_COMPCERT_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_COMPCERT_VERSION) - #define JSON_HEDLEY_COMPCERT_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_COMPCERT_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_COMPCERT_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_PELLES_VERSION) - #undef JSON_HEDLEY_PELLES_VERSION -#endif -#if defined(__POCC__) - #define JSON_HEDLEY_PELLES_VERSION JSON_HEDLEY_VERSION_ENCODE(__POCC__ / 100, __POCC__ % 100, 0) -#endif - -#if defined(JSON_HEDLEY_PELLES_VERSION_CHECK) - #undef JSON_HEDLEY_PELLES_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_PELLES_VERSION) - #define JSON_HEDLEY_PELLES_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_PELLES_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_PELLES_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_MCST_LCC_VERSION) - #undef JSON_HEDLEY_MCST_LCC_VERSION -#endif -#if defined(__LCC__) && defined(__LCC_MINOR__) - #define JSON_HEDLEY_MCST_LCC_VERSION JSON_HEDLEY_VERSION_ENCODE(__LCC__ / 100, __LCC__ % 100, __LCC_MINOR__) -#endif - -#if defined(JSON_HEDLEY_MCST_LCC_VERSION_CHECK) - #undef JSON_HEDLEY_MCST_LCC_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_MCST_LCC_VERSION) - #define JSON_HEDLEY_MCST_LCC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_MCST_LCC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_MCST_LCC_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_GCC_VERSION) - #undef JSON_HEDLEY_GCC_VERSION -#endif -#if \ - defined(JSON_HEDLEY_GNUC_VERSION) && \ - !defined(__clang__) && \ - !defined(JSON_HEDLEY_INTEL_VERSION) && \ - !defined(JSON_HEDLEY_PGI_VERSION) && \ - !defined(JSON_HEDLEY_ARM_VERSION) && \ - !defined(JSON_HEDLEY_CRAY_VERSION) && \ - !defined(JSON_HEDLEY_TI_VERSION) && \ - !defined(JSON_HEDLEY_TI_ARMCL_VERSION) && \ - !defined(JSON_HEDLEY_TI_CL430_VERSION) && \ - !defined(JSON_HEDLEY_TI_CL2000_VERSION) && \ - !defined(JSON_HEDLEY_TI_CL6X_VERSION) && \ - !defined(JSON_HEDLEY_TI_CL7X_VERSION) && \ - !defined(JSON_HEDLEY_TI_CLPRU_VERSION) && \ - !defined(__COMPCERT__) && \ - !defined(JSON_HEDLEY_MCST_LCC_VERSION) - #define JSON_HEDLEY_GCC_VERSION JSON_HEDLEY_GNUC_VERSION -#endif - -#if defined(JSON_HEDLEY_GCC_VERSION_CHECK) - #undef JSON_HEDLEY_GCC_VERSION_CHECK -#endif -#if defined(JSON_HEDLEY_GCC_VERSION) - #define JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_GCC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) -#else - #define JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) (0) -#endif - -#if defined(JSON_HEDLEY_HAS_ATTRIBUTE) - #undef JSON_HEDLEY_HAS_ATTRIBUTE -#endif -#if \ - defined(__has_attribute) && \ - ( \ - (!defined(JSON_HEDLEY_IAR_VERSION) || JSON_HEDLEY_IAR_VERSION_CHECK(8,5,9)) \ - ) -# define JSON_HEDLEY_HAS_ATTRIBUTE(attribute) __has_attribute(attribute) -#else -# define JSON_HEDLEY_HAS_ATTRIBUTE(attribute) (0) -#endif - -#if defined(JSON_HEDLEY_GNUC_HAS_ATTRIBUTE) - #undef JSON_HEDLEY_GNUC_HAS_ATTRIBUTE -#endif -#if defined(__has_attribute) - #define JSON_HEDLEY_GNUC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) -#else - #define JSON_HEDLEY_GNUC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) -#endif - -#if defined(JSON_HEDLEY_GCC_HAS_ATTRIBUTE) - #undef JSON_HEDLEY_GCC_HAS_ATTRIBUTE -#endif -#if defined(__has_attribute) - #define JSON_HEDLEY_GCC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) -#else - #define JSON_HEDLEY_GCC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) -#endif - -#if defined(JSON_HEDLEY_HAS_CPP_ATTRIBUTE) - #undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE -#endif -#if \ - defined(__has_cpp_attribute) && \ - defined(__cplusplus) && \ - (!defined(JSON_HEDLEY_SUNPRO_VERSION) || JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0)) - #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) __has_cpp_attribute(attribute) -#else - #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) (0) -#endif - -#if defined(JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS) - #undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS -#endif -#if !defined(__cplusplus) || !defined(__has_cpp_attribute) - #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) (0) -#elif \ - !defined(JSON_HEDLEY_PGI_VERSION) && \ - !defined(JSON_HEDLEY_IAR_VERSION) && \ - (!defined(JSON_HEDLEY_SUNPRO_VERSION) || JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0)) && \ - (!defined(JSON_HEDLEY_MSVC_VERSION) || JSON_HEDLEY_MSVC_VERSION_CHECK(19,20,0)) - #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) JSON_HEDLEY_HAS_CPP_ATTRIBUTE(ns::attribute) -#else - #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) (0) -#endif - -#if defined(JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE) - #undef JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE -#endif -#if defined(__has_cpp_attribute) && defined(__cplusplus) - #define JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) __has_cpp_attribute(attribute) -#else - #define JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) -#endif - -#if defined(JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE) - #undef JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE -#endif -#if defined(__has_cpp_attribute) && defined(__cplusplus) - #define JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) __has_cpp_attribute(attribute) -#else - #define JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) -#endif - -#if defined(JSON_HEDLEY_HAS_BUILTIN) - #undef JSON_HEDLEY_HAS_BUILTIN -#endif -#if defined(__has_builtin) - #define JSON_HEDLEY_HAS_BUILTIN(builtin) __has_builtin(builtin) -#else - #define JSON_HEDLEY_HAS_BUILTIN(builtin) (0) -#endif - -#if defined(JSON_HEDLEY_GNUC_HAS_BUILTIN) - #undef JSON_HEDLEY_GNUC_HAS_BUILTIN -#endif -#if defined(__has_builtin) - #define JSON_HEDLEY_GNUC_HAS_BUILTIN(builtin,major,minor,patch) __has_builtin(builtin) -#else - #define JSON_HEDLEY_GNUC_HAS_BUILTIN(builtin,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) -#endif - -#if defined(JSON_HEDLEY_GCC_HAS_BUILTIN) - #undef JSON_HEDLEY_GCC_HAS_BUILTIN -#endif -#if defined(__has_builtin) - #define JSON_HEDLEY_GCC_HAS_BUILTIN(builtin,major,minor,patch) __has_builtin(builtin) -#else - #define JSON_HEDLEY_GCC_HAS_BUILTIN(builtin,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) -#endif - -#if defined(JSON_HEDLEY_HAS_FEATURE) - #undef JSON_HEDLEY_HAS_FEATURE -#endif -#if defined(__has_feature) - #define JSON_HEDLEY_HAS_FEATURE(feature) __has_feature(feature) -#else - #define JSON_HEDLEY_HAS_FEATURE(feature) (0) -#endif - -#if defined(JSON_HEDLEY_GNUC_HAS_FEATURE) - #undef JSON_HEDLEY_GNUC_HAS_FEATURE -#endif -#if defined(__has_feature) - #define JSON_HEDLEY_GNUC_HAS_FEATURE(feature,major,minor,patch) __has_feature(feature) -#else - #define JSON_HEDLEY_GNUC_HAS_FEATURE(feature,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) -#endif - -#if defined(JSON_HEDLEY_GCC_HAS_FEATURE) - #undef JSON_HEDLEY_GCC_HAS_FEATURE -#endif -#if defined(__has_feature) - #define JSON_HEDLEY_GCC_HAS_FEATURE(feature,major,minor,patch) __has_feature(feature) -#else - #define JSON_HEDLEY_GCC_HAS_FEATURE(feature,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) -#endif - -#if defined(JSON_HEDLEY_HAS_EXTENSION) - #undef JSON_HEDLEY_HAS_EXTENSION -#endif -#if defined(__has_extension) - #define JSON_HEDLEY_HAS_EXTENSION(extension) __has_extension(extension) -#else - #define JSON_HEDLEY_HAS_EXTENSION(extension) (0) -#endif - -#if defined(JSON_HEDLEY_GNUC_HAS_EXTENSION) - #undef JSON_HEDLEY_GNUC_HAS_EXTENSION -#endif -#if defined(__has_extension) - #define JSON_HEDLEY_GNUC_HAS_EXTENSION(extension,major,minor,patch) __has_extension(extension) -#else - #define JSON_HEDLEY_GNUC_HAS_EXTENSION(extension,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) -#endif - -#if defined(JSON_HEDLEY_GCC_HAS_EXTENSION) - #undef JSON_HEDLEY_GCC_HAS_EXTENSION -#endif -#if defined(__has_extension) - #define JSON_HEDLEY_GCC_HAS_EXTENSION(extension,major,minor,patch) __has_extension(extension) -#else - #define JSON_HEDLEY_GCC_HAS_EXTENSION(extension,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) -#endif - -#if defined(JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE) - #undef JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE -#endif -#if defined(__has_declspec_attribute) - #define JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) __has_declspec_attribute(attribute) -#else - #define JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) (0) -#endif - -#if defined(JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE) - #undef JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE -#endif -#if defined(__has_declspec_attribute) - #define JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) __has_declspec_attribute(attribute) -#else - #define JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) -#endif - -#if defined(JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE) - #undef JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE -#endif -#if defined(__has_declspec_attribute) - #define JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) __has_declspec_attribute(attribute) -#else - #define JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) -#endif - -#if defined(JSON_HEDLEY_HAS_WARNING) - #undef JSON_HEDLEY_HAS_WARNING -#endif -#if defined(__has_warning) - #define JSON_HEDLEY_HAS_WARNING(warning) __has_warning(warning) -#else - #define JSON_HEDLEY_HAS_WARNING(warning) (0) -#endif - -#if defined(JSON_HEDLEY_GNUC_HAS_WARNING) - #undef JSON_HEDLEY_GNUC_HAS_WARNING -#endif -#if defined(__has_warning) - #define JSON_HEDLEY_GNUC_HAS_WARNING(warning,major,minor,patch) __has_warning(warning) -#else - #define JSON_HEDLEY_GNUC_HAS_WARNING(warning,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) -#endif - -#if defined(JSON_HEDLEY_GCC_HAS_WARNING) - #undef JSON_HEDLEY_GCC_HAS_WARNING -#endif -#if defined(__has_warning) - #define JSON_HEDLEY_GCC_HAS_WARNING(warning,major,minor,patch) __has_warning(warning) -#else - #define JSON_HEDLEY_GCC_HAS_WARNING(warning,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) -#endif - -#if \ - (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) || \ - defined(__clang__) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) || \ - JSON_HEDLEY_PGI_VERSION_CHECK(18,4,0) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ - JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ - JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,7,0) || \ - JSON_HEDLEY_TI_CL430_VERSION_CHECK(2,0,1) || \ - JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,1,0) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,0,0) || \ - JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ - JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ - JSON_HEDLEY_CRAY_VERSION_CHECK(5,0,0) || \ - JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,17) || \ - JSON_HEDLEY_SUNPRO_VERSION_CHECK(8,0,0) || \ - (JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) && defined(__C99_PRAGMA_OPERATOR)) - #define JSON_HEDLEY_PRAGMA(value) _Pragma(#value) -#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) - #define JSON_HEDLEY_PRAGMA(value) __pragma(value) -#else - #define JSON_HEDLEY_PRAGMA(value) -#endif - -#if defined(JSON_HEDLEY_DIAGNOSTIC_PUSH) - #undef JSON_HEDLEY_DIAGNOSTIC_PUSH -#endif -#if defined(JSON_HEDLEY_DIAGNOSTIC_POP) - #undef JSON_HEDLEY_DIAGNOSTIC_POP -#endif -#if defined(__clang__) - #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("clang diagnostic push") - #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("clang diagnostic pop") -#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) - #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("warning(push)") - #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("warning(pop)") -#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) - #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("GCC diagnostic push") - #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("GCC diagnostic pop") -#elif \ - JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) || \ - JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) - #define JSON_HEDLEY_DIAGNOSTIC_PUSH __pragma(warning(push)) - #define JSON_HEDLEY_DIAGNOSTIC_POP __pragma(warning(pop)) -#elif JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) - #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("push") - #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("pop") -#elif \ - JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ - JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ - JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,4,0) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,1,0) || \ - JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ - JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) - #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("diag_push") - #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("diag_pop") -#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,90,0) - #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("warning(push)") - #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("warning(pop)") -#else - #define JSON_HEDLEY_DIAGNOSTIC_PUSH - #define JSON_HEDLEY_DIAGNOSTIC_POP -#endif - -/* JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_ is for - HEDLEY INTERNAL USE ONLY. API subject to change without notice. */ -#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_) - #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_ -#endif -#if defined(__cplusplus) -# if JSON_HEDLEY_HAS_WARNING("-Wc++98-compat") -# if JSON_HEDLEY_HAS_WARNING("-Wc++17-extensions") -# if JSON_HEDLEY_HAS_WARNING("-Wc++1z-extensions") -# define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \ - JSON_HEDLEY_DIAGNOSTIC_PUSH \ - _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \ - _Pragma("clang diagnostic ignored \"-Wc++17-extensions\"") \ - _Pragma("clang diagnostic ignored \"-Wc++1z-extensions\"") \ - xpr \ - JSON_HEDLEY_DIAGNOSTIC_POP -# else -# define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \ - JSON_HEDLEY_DIAGNOSTIC_PUSH \ - _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \ - _Pragma("clang diagnostic ignored \"-Wc++17-extensions\"") \ - xpr \ - JSON_HEDLEY_DIAGNOSTIC_POP -# endif -# else -# define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \ - JSON_HEDLEY_DIAGNOSTIC_PUSH \ - _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \ - xpr \ - JSON_HEDLEY_DIAGNOSTIC_POP -# endif -# endif -#endif -#if !defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(x) x -#endif - -#if defined(JSON_HEDLEY_CONST_CAST) - #undef JSON_HEDLEY_CONST_CAST -#endif -#if defined(__cplusplus) -# define JSON_HEDLEY_CONST_CAST(T, expr) (const_cast(expr)) -#elif \ - JSON_HEDLEY_HAS_WARNING("-Wcast-qual") || \ - JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) -# define JSON_HEDLEY_CONST_CAST(T, expr) (__extension__ ({ \ - JSON_HEDLEY_DIAGNOSTIC_PUSH \ - JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL \ - ((T) (expr)); \ - JSON_HEDLEY_DIAGNOSTIC_POP \ - })) -#else -# define JSON_HEDLEY_CONST_CAST(T, expr) ((T) (expr)) -#endif - -#if defined(JSON_HEDLEY_REINTERPRET_CAST) - #undef JSON_HEDLEY_REINTERPRET_CAST -#endif -#if defined(__cplusplus) - #define JSON_HEDLEY_REINTERPRET_CAST(T, expr) (reinterpret_cast(expr)) -#else - #define JSON_HEDLEY_REINTERPRET_CAST(T, expr) ((T) (expr)) -#endif - -#if defined(JSON_HEDLEY_STATIC_CAST) - #undef JSON_HEDLEY_STATIC_CAST -#endif -#if defined(__cplusplus) - #define JSON_HEDLEY_STATIC_CAST(T, expr) (static_cast(expr)) -#else - #define JSON_HEDLEY_STATIC_CAST(T, expr) ((T) (expr)) -#endif - -#if defined(JSON_HEDLEY_CPP_CAST) - #undef JSON_HEDLEY_CPP_CAST -#endif -#if defined(__cplusplus) -# if JSON_HEDLEY_HAS_WARNING("-Wold-style-cast") -# define JSON_HEDLEY_CPP_CAST(T, expr) \ - JSON_HEDLEY_DIAGNOSTIC_PUSH \ - _Pragma("clang diagnostic ignored \"-Wold-style-cast\"") \ - ((T) (expr)) \ - JSON_HEDLEY_DIAGNOSTIC_POP -# elif JSON_HEDLEY_IAR_VERSION_CHECK(8,3,0) -# define JSON_HEDLEY_CPP_CAST(T, expr) \ - JSON_HEDLEY_DIAGNOSTIC_PUSH \ - _Pragma("diag_suppress=Pe137") \ - JSON_HEDLEY_DIAGNOSTIC_POP -# else -# define JSON_HEDLEY_CPP_CAST(T, expr) ((T) (expr)) -# endif -#else -# define JSON_HEDLEY_CPP_CAST(T, expr) (expr) -#endif - -#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED) - #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED -#endif -#if JSON_HEDLEY_HAS_WARNING("-Wdeprecated-declarations") - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"") -#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("warning(disable:1478 1786)") -#elif JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED __pragma(warning(disable:1478 1786)) -#elif JSON_HEDLEY_PGI_VERSION_CHECK(20,7,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1215,1216,1444,1445") -#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1215,1444") -#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") -#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED __pragma(warning(disable:4996)) -#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1215,1444") -#elif \ - JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ - (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ - (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ - (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ - (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ - JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ - JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1291,1718") -#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) && !defined(__cplusplus) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("error_messages(off,E_DEPRECATED_ATT,E_DEPRECATED_ATT_MESS)") -#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) && defined(__cplusplus) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("error_messages(off,symdeprecated,symdeprecated2)") -#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress=Pe1444,Pe1215") -#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,90,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("warn(disable:2241)") -#else - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED -#endif - -#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS) - #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS -#endif -#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("clang diagnostic ignored \"-Wunknown-pragmas\"") -#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("warning(disable:161)") -#elif JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS __pragma(warning(disable:161)) -#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 1675") -#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("GCC diagnostic ignored \"-Wunknown-pragmas\"") -#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS __pragma(warning(disable:4068)) -#elif \ - JSON_HEDLEY_TI_VERSION_CHECK(16,9,0) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) || \ - JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ - JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,3,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 163") -#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 163") -#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress=Pe161") -#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 161") -#else - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS -#endif - -#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES) - #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES -#endif -#if JSON_HEDLEY_HAS_WARNING("-Wunknown-attributes") - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("clang diagnostic ignored \"-Wunknown-attributes\"") -#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") -#elif JSON_HEDLEY_INTEL_VERSION_CHECK(17,0,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("warning(disable:1292)") -#elif JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES __pragma(warning(disable:1292)) -#elif JSON_HEDLEY_MSVC_VERSION_CHECK(19,0,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES __pragma(warning(disable:5030)) -#elif JSON_HEDLEY_PGI_VERSION_CHECK(20,7,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1097,1098") -#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1097") -#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,14,0) && defined(__cplusplus) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("error_messages(off,attrskipunsup)") -#elif \ - JSON_HEDLEY_TI_VERSION_CHECK(18,1,0) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,3,0) || \ - JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1173") -#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress=Pe1097") -#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1097") -#else - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES -#endif - -#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL) - #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL -#endif -#if JSON_HEDLEY_HAS_WARNING("-Wcast-qual") - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("clang diagnostic ignored \"-Wcast-qual\"") -#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("warning(disable:2203 2331)") -#elif JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("GCC diagnostic ignored \"-Wcast-qual\"") -#else - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL -#endif - -#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION) - #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION -#endif -#if JSON_HEDLEY_HAS_WARNING("-Wunused-function") - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION _Pragma("clang diagnostic ignored \"-Wunused-function\"") -#elif JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION _Pragma("GCC diagnostic ignored \"-Wunused-function\"") -#elif JSON_HEDLEY_MSVC_VERSION_CHECK(1,0,0) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION __pragma(warning(disable:4505)) -#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION _Pragma("diag_suppress 3142") -#else - #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION -#endif - -#if defined(JSON_HEDLEY_DEPRECATED) - #undef JSON_HEDLEY_DEPRECATED -#endif -#if defined(JSON_HEDLEY_DEPRECATED_FOR) - #undef JSON_HEDLEY_DEPRECATED_FOR -#endif -#if \ - JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) || \ - JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) - #define JSON_HEDLEY_DEPRECATED(since) __declspec(deprecated("Since " # since)) - #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __declspec(deprecated("Since " #since "; use " #replacement)) -#elif \ - (JSON_HEDLEY_HAS_EXTENSION(attribute_deprecated_with_message) && !defined(JSON_HEDLEY_IAR_VERSION)) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(4,5,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) || \ - JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) || \ - JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ - JSON_HEDLEY_TI_VERSION_CHECK(18,1,0) || \ - JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(18,1,0) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,3,0) || \ - JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ - JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,3,0) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) - #define JSON_HEDLEY_DEPRECATED(since) __attribute__((__deprecated__("Since " #since))) - #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __attribute__((__deprecated__("Since " #since "; use " #replacement))) -#elif defined(__cplusplus) && (__cplusplus >= 201402L) - #define JSON_HEDLEY_DEPRECATED(since) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[deprecated("Since " #since)]]) - #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[deprecated("Since " #since "; use " #replacement)]]) -#elif \ - JSON_HEDLEY_HAS_ATTRIBUTE(deprecated) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ - JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ - (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ - (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ - (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ - (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ - JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ - JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) || \ - JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) - #define JSON_HEDLEY_DEPRECATED(since) __attribute__((__deprecated__)) - #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __attribute__((__deprecated__)) -#elif \ - JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ - JSON_HEDLEY_PELLES_VERSION_CHECK(6,50,0) || \ - JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) - #define JSON_HEDLEY_DEPRECATED(since) __declspec(deprecated) - #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __declspec(deprecated) -#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) - #define JSON_HEDLEY_DEPRECATED(since) _Pragma("deprecated") - #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) _Pragma("deprecated") -#else - #define JSON_HEDLEY_DEPRECATED(since) - #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) -#endif - -#if defined(JSON_HEDLEY_UNAVAILABLE) - #undef JSON_HEDLEY_UNAVAILABLE -#endif -#if \ - JSON_HEDLEY_HAS_ATTRIBUTE(warning) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) - #define JSON_HEDLEY_UNAVAILABLE(available_since) __attribute__((__warning__("Not available until " #available_since))) -#else - #define JSON_HEDLEY_UNAVAILABLE(available_since) -#endif - -#if defined(JSON_HEDLEY_WARN_UNUSED_RESULT) - #undef JSON_HEDLEY_WARN_UNUSED_RESULT -#endif -#if defined(JSON_HEDLEY_WARN_UNUSED_RESULT_MSG) - #undef JSON_HEDLEY_WARN_UNUSED_RESULT_MSG -#endif -#if \ - JSON_HEDLEY_HAS_ATTRIBUTE(warn_unused_result) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ - (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ - (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ - (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ - (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ - JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ - JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ - (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0) && defined(__cplusplus)) || \ - JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) - #define JSON_HEDLEY_WARN_UNUSED_RESULT __attribute__((__warn_unused_result__)) - #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) __attribute__((__warn_unused_result__)) -#elif (JSON_HEDLEY_HAS_CPP_ATTRIBUTE(nodiscard) >= 201907L) - #define JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) - #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard(msg)]]) -#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE(nodiscard) - #define JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) - #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) -#elif defined(_Check_return_) /* SAL */ - #define JSON_HEDLEY_WARN_UNUSED_RESULT _Check_return_ - #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) _Check_return_ -#else - #define JSON_HEDLEY_WARN_UNUSED_RESULT - #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) -#endif - -#if defined(JSON_HEDLEY_SENTINEL) - #undef JSON_HEDLEY_SENTINEL -#endif -#if \ - JSON_HEDLEY_HAS_ATTRIBUTE(sentinel) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(5,4,0) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) - #define JSON_HEDLEY_SENTINEL(position) __attribute__((__sentinel__(position))) -#else - #define JSON_HEDLEY_SENTINEL(position) -#endif - -#if defined(JSON_HEDLEY_NO_RETURN) - #undef JSON_HEDLEY_NO_RETURN -#endif -#if JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) - #define JSON_HEDLEY_NO_RETURN __noreturn -#elif \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) - #define JSON_HEDLEY_NO_RETURN __attribute__((__noreturn__)) -#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L - #define JSON_HEDLEY_NO_RETURN _Noreturn -#elif defined(__cplusplus) && (__cplusplus >= 201103L) - #define JSON_HEDLEY_NO_RETURN JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[noreturn]]) -#elif \ - JSON_HEDLEY_HAS_ATTRIBUTE(noreturn) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(3,2,0) || \ - JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ - JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ - JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ - (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ - (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ - (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ - (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ - JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ - JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ - JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) - #define JSON_HEDLEY_NO_RETURN __attribute__((__noreturn__)) -#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) - #define JSON_HEDLEY_NO_RETURN _Pragma("does_not_return") -#elif \ - JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ - JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) - #define JSON_HEDLEY_NO_RETURN __declspec(noreturn) -#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,0,0) && defined(__cplusplus) - #define JSON_HEDLEY_NO_RETURN _Pragma("FUNC_NEVER_RETURNS;") -#elif JSON_HEDLEY_COMPCERT_VERSION_CHECK(3,2,0) - #define JSON_HEDLEY_NO_RETURN __attribute((noreturn)) -#elif JSON_HEDLEY_PELLES_VERSION_CHECK(9,0,0) - #define JSON_HEDLEY_NO_RETURN __declspec(noreturn) -#else - #define JSON_HEDLEY_NO_RETURN -#endif - -#if defined(JSON_HEDLEY_NO_ESCAPE) - #undef JSON_HEDLEY_NO_ESCAPE -#endif -#if JSON_HEDLEY_HAS_ATTRIBUTE(noescape) - #define JSON_HEDLEY_NO_ESCAPE __attribute__((__noescape__)) -#else - #define JSON_HEDLEY_NO_ESCAPE -#endif - -#if defined(JSON_HEDLEY_UNREACHABLE) - #undef JSON_HEDLEY_UNREACHABLE -#endif -#if defined(JSON_HEDLEY_UNREACHABLE_RETURN) - #undef JSON_HEDLEY_UNREACHABLE_RETURN -#endif -#if defined(JSON_HEDLEY_ASSUME) - #undef JSON_HEDLEY_ASSUME -#endif -#if \ - JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) - #define JSON_HEDLEY_ASSUME(expr) __assume(expr) -#elif JSON_HEDLEY_HAS_BUILTIN(__builtin_assume) - #define JSON_HEDLEY_ASSUME(expr) __builtin_assume(expr) -#elif \ - JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) - #if defined(__cplusplus) - #define JSON_HEDLEY_ASSUME(expr) std::_nassert(expr) - #else - #define JSON_HEDLEY_ASSUME(expr) _nassert(expr) - #endif -#endif -#if \ - (JSON_HEDLEY_HAS_BUILTIN(__builtin_unreachable) && (!defined(JSON_HEDLEY_ARM_VERSION))) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(4,5,0) || \ - JSON_HEDLEY_PGI_VERSION_CHECK(18,10,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - JSON_HEDLEY_IBM_VERSION_CHECK(13,1,5) || \ - JSON_HEDLEY_CRAY_VERSION_CHECK(10,0,0) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) - #define JSON_HEDLEY_UNREACHABLE() __builtin_unreachable() -#elif defined(JSON_HEDLEY_ASSUME) - #define JSON_HEDLEY_UNREACHABLE() JSON_HEDLEY_ASSUME(0) -#endif -#if !defined(JSON_HEDLEY_ASSUME) - #if defined(JSON_HEDLEY_UNREACHABLE) - #define JSON_HEDLEY_ASSUME(expr) JSON_HEDLEY_STATIC_CAST(void, ((expr) ? 1 : (JSON_HEDLEY_UNREACHABLE(), 1))) - #else - #define JSON_HEDLEY_ASSUME(expr) JSON_HEDLEY_STATIC_CAST(void, expr) - #endif -#endif -#if defined(JSON_HEDLEY_UNREACHABLE) - #if \ - JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) - #define JSON_HEDLEY_UNREACHABLE_RETURN(value) return (JSON_HEDLEY_STATIC_CAST(void, JSON_HEDLEY_ASSUME(0)), (value)) - #else - #define JSON_HEDLEY_UNREACHABLE_RETURN(value) JSON_HEDLEY_UNREACHABLE() - #endif -#else - #define JSON_HEDLEY_UNREACHABLE_RETURN(value) return (value) -#endif -#if !defined(JSON_HEDLEY_UNREACHABLE) - #define JSON_HEDLEY_UNREACHABLE() JSON_HEDLEY_ASSUME(0) -#endif - -JSON_HEDLEY_DIAGNOSTIC_PUSH -#if JSON_HEDLEY_HAS_WARNING("-Wpedantic") - #pragma clang diagnostic ignored "-Wpedantic" -#endif -#if JSON_HEDLEY_HAS_WARNING("-Wc++98-compat-pedantic") && defined(__cplusplus) - #pragma clang diagnostic ignored "-Wc++98-compat-pedantic" -#endif -#if JSON_HEDLEY_GCC_HAS_WARNING("-Wvariadic-macros",4,0,0) - #if defined(__clang__) - #pragma clang diagnostic ignored "-Wvariadic-macros" - #elif defined(JSON_HEDLEY_GCC_VERSION) - #pragma GCC diagnostic ignored "-Wvariadic-macros" - #endif -#endif -#if defined(JSON_HEDLEY_NON_NULL) - #undef JSON_HEDLEY_NON_NULL -#endif -#if \ - JSON_HEDLEY_HAS_ATTRIBUTE(nonnull) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) - #define JSON_HEDLEY_NON_NULL(...) __attribute__((__nonnull__(__VA_ARGS__))) -#else - #define JSON_HEDLEY_NON_NULL(...) -#endif -JSON_HEDLEY_DIAGNOSTIC_POP - -#if defined(JSON_HEDLEY_PRINTF_FORMAT) - #undef JSON_HEDLEY_PRINTF_FORMAT -#endif -#if defined(__MINGW32__) && JSON_HEDLEY_GCC_HAS_ATTRIBUTE(format,4,4,0) && !defined(__USE_MINGW_ANSI_STDIO) - #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(ms_printf, string_idx, first_to_check))) -#elif defined(__MINGW32__) && JSON_HEDLEY_GCC_HAS_ATTRIBUTE(format,4,4,0) && defined(__USE_MINGW_ANSI_STDIO) - #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(gnu_printf, string_idx, first_to_check))) -#elif \ - JSON_HEDLEY_HAS_ATTRIBUTE(format) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) || \ - JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ - JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ - (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ - (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ - (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ - (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ - JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ - JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) - #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(__printf__, string_idx, first_to_check))) -#elif JSON_HEDLEY_PELLES_VERSION_CHECK(6,0,0) - #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __declspec(vaformat(printf,string_idx,first_to_check)) -#else - #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) -#endif - -#if defined(JSON_HEDLEY_CONSTEXPR) - #undef JSON_HEDLEY_CONSTEXPR -#endif -#if defined(__cplusplus) - #if __cplusplus >= 201103L - #define JSON_HEDLEY_CONSTEXPR JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(constexpr) - #endif -#endif -#if !defined(JSON_HEDLEY_CONSTEXPR) - #define JSON_HEDLEY_CONSTEXPR -#endif - -#if defined(JSON_HEDLEY_PREDICT) - #undef JSON_HEDLEY_PREDICT -#endif -#if defined(JSON_HEDLEY_LIKELY) - #undef JSON_HEDLEY_LIKELY -#endif -#if defined(JSON_HEDLEY_UNLIKELY) - #undef JSON_HEDLEY_UNLIKELY -#endif -#if defined(JSON_HEDLEY_UNPREDICTABLE) - #undef JSON_HEDLEY_UNPREDICTABLE -#endif -#if JSON_HEDLEY_HAS_BUILTIN(__builtin_unpredictable) - #define JSON_HEDLEY_UNPREDICTABLE(expr) __builtin_unpredictable((expr)) -#endif -#if \ - (JSON_HEDLEY_HAS_BUILTIN(__builtin_expect_with_probability) && !defined(JSON_HEDLEY_PGI_VERSION)) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(9,0,0) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) -# define JSON_HEDLEY_PREDICT(expr, value, probability) __builtin_expect_with_probability( (expr), (value), (probability)) -# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) __builtin_expect_with_probability(!!(expr), 1 , (probability)) -# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) __builtin_expect_with_probability(!!(expr), 0 , (probability)) -# define JSON_HEDLEY_LIKELY(expr) __builtin_expect (!!(expr), 1 ) -# define JSON_HEDLEY_UNLIKELY(expr) __builtin_expect (!!(expr), 0 ) -#elif \ - (JSON_HEDLEY_HAS_BUILTIN(__builtin_expect) && !defined(JSON_HEDLEY_INTEL_CL_VERSION)) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0) && defined(__cplusplus)) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ - JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ - JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ - JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,7,0) || \ - JSON_HEDLEY_TI_CL430_VERSION_CHECK(3,1,0) || \ - JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,1,0) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ - JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ - JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ - JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,27) || \ - JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) -# define JSON_HEDLEY_PREDICT(expr, expected, probability) \ - (((probability) >= 0.9) ? __builtin_expect((expr), (expected)) : (JSON_HEDLEY_STATIC_CAST(void, expected), (expr))) -# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) \ - (__extension__ ({ \ - double hedley_probability_ = (probability); \ - ((hedley_probability_ >= 0.9) ? __builtin_expect(!!(expr), 1) : ((hedley_probability_ <= 0.1) ? __builtin_expect(!!(expr), 0) : !!(expr))); \ - })) -# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) \ - (__extension__ ({ \ - double hedley_probability_ = (probability); \ - ((hedley_probability_ >= 0.9) ? __builtin_expect(!!(expr), 0) : ((hedley_probability_ <= 0.1) ? __builtin_expect(!!(expr), 1) : !!(expr))); \ - })) -# define JSON_HEDLEY_LIKELY(expr) __builtin_expect(!!(expr), 1) -# define JSON_HEDLEY_UNLIKELY(expr) __builtin_expect(!!(expr), 0) -#else -# define JSON_HEDLEY_PREDICT(expr, expected, probability) (JSON_HEDLEY_STATIC_CAST(void, expected), (expr)) -# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) (!!(expr)) -# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) (!!(expr)) -# define JSON_HEDLEY_LIKELY(expr) (!!(expr)) -# define JSON_HEDLEY_UNLIKELY(expr) (!!(expr)) -#endif -#if !defined(JSON_HEDLEY_UNPREDICTABLE) - #define JSON_HEDLEY_UNPREDICTABLE(expr) JSON_HEDLEY_PREDICT(expr, 1, 0.5) -#endif - -#if defined(JSON_HEDLEY_MALLOC) - #undef JSON_HEDLEY_MALLOC -#endif -#if \ - JSON_HEDLEY_HAS_ATTRIBUTE(malloc) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ - JSON_HEDLEY_IBM_VERSION_CHECK(12,1,0) || \ - JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ - (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ - (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ - (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ - (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ - JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ - JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) - #define JSON_HEDLEY_MALLOC __attribute__((__malloc__)) -#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) - #define JSON_HEDLEY_MALLOC _Pragma("returns_new_memory") -#elif \ - JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) || \ - JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) - #define JSON_HEDLEY_MALLOC __declspec(restrict) -#else - #define JSON_HEDLEY_MALLOC -#endif - -#if defined(JSON_HEDLEY_PURE) - #undef JSON_HEDLEY_PURE -#endif -#if \ - JSON_HEDLEY_HAS_ATTRIBUTE(pure) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(2,96,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ - JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ - JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ - (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ - (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ - (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ - (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ - JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ - JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ - JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) -# define JSON_HEDLEY_PURE __attribute__((__pure__)) -#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) -# define JSON_HEDLEY_PURE _Pragma("does_not_write_global_data") -#elif defined(__cplusplus) && \ - ( \ - JSON_HEDLEY_TI_CL430_VERSION_CHECK(2,0,1) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) || \ - JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) \ - ) -# define JSON_HEDLEY_PURE _Pragma("FUNC_IS_PURE;") -#else -# define JSON_HEDLEY_PURE -#endif - -#if defined(JSON_HEDLEY_CONST) - #undef JSON_HEDLEY_CONST -#endif -#if \ - JSON_HEDLEY_HAS_ATTRIBUTE(const) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(2,5,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ - JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ - JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ - (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ - (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ - (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ - (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ - JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ - JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ - JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) - #define JSON_HEDLEY_CONST __attribute__((__const__)) -#elif \ - JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) - #define JSON_HEDLEY_CONST _Pragma("no_side_effect") -#else - #define JSON_HEDLEY_CONST JSON_HEDLEY_PURE -#endif - -#if defined(JSON_HEDLEY_RESTRICT) - #undef JSON_HEDLEY_RESTRICT -#endif -#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && !defined(__cplusplus) - #define JSON_HEDLEY_RESTRICT restrict -#elif \ - JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ - JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ - JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ - JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ - JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ - JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,4) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,1,0) || \ - JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ - (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,14,0) && defined(__cplusplus)) || \ - JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) || \ - defined(__clang__) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) - #define JSON_HEDLEY_RESTRICT __restrict -#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,3,0) && !defined(__cplusplus) - #define JSON_HEDLEY_RESTRICT _Restrict -#else - #define JSON_HEDLEY_RESTRICT -#endif - -#if defined(JSON_HEDLEY_INLINE) - #undef JSON_HEDLEY_INLINE -#endif -#if \ - (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) || \ - (defined(__cplusplus) && (__cplusplus >= 199711L)) - #define JSON_HEDLEY_INLINE inline -#elif \ - defined(JSON_HEDLEY_GCC_VERSION) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(6,2,0) - #define JSON_HEDLEY_INLINE __inline__ -#elif \ - JSON_HEDLEY_MSVC_VERSION_CHECK(12,0,0) || \ - JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ - JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,1,0) || \ - JSON_HEDLEY_TI_CL430_VERSION_CHECK(3,1,0) || \ - JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) || \ - JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ - JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) - #define JSON_HEDLEY_INLINE __inline -#else - #define JSON_HEDLEY_INLINE -#endif - -#if defined(JSON_HEDLEY_ALWAYS_INLINE) - #undef JSON_HEDLEY_ALWAYS_INLINE -#endif -#if \ - JSON_HEDLEY_HAS_ATTRIBUTE(always_inline) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ - JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ - JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ - (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ - (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ - (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ - (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ - JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ - JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) || \ - JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) -# define JSON_HEDLEY_ALWAYS_INLINE __attribute__((__always_inline__)) JSON_HEDLEY_INLINE -#elif \ - JSON_HEDLEY_MSVC_VERSION_CHECK(12,0,0) || \ - JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) -# define JSON_HEDLEY_ALWAYS_INLINE __forceinline -#elif defined(__cplusplus) && \ - ( \ - JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ - JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ - JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ - JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ - JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) \ - ) -# define JSON_HEDLEY_ALWAYS_INLINE _Pragma("FUNC_ALWAYS_INLINE;") -#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) -# define JSON_HEDLEY_ALWAYS_INLINE _Pragma("inline=forced") -#else -# define JSON_HEDLEY_ALWAYS_INLINE JSON_HEDLEY_INLINE -#endif - -#if defined(JSON_HEDLEY_NEVER_INLINE) - #undef JSON_HEDLEY_NEVER_INLINE -#endif -#if \ - JSON_HEDLEY_HAS_ATTRIBUTE(noinline) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ - JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ - JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ - (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ - (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ - (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ - (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ - JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ - JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) || \ - JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) - #define JSON_HEDLEY_NEVER_INLINE __attribute__((__noinline__)) -#elif \ - JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ - JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) - #define JSON_HEDLEY_NEVER_INLINE __declspec(noinline) -#elif JSON_HEDLEY_PGI_VERSION_CHECK(10,2,0) - #define JSON_HEDLEY_NEVER_INLINE _Pragma("noinline") -#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,0,0) && defined(__cplusplus) - #define JSON_HEDLEY_NEVER_INLINE _Pragma("FUNC_CANNOT_INLINE;") -#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) - #define JSON_HEDLEY_NEVER_INLINE _Pragma("inline=never") -#elif JSON_HEDLEY_COMPCERT_VERSION_CHECK(3,2,0) - #define JSON_HEDLEY_NEVER_INLINE __attribute((noinline)) -#elif JSON_HEDLEY_PELLES_VERSION_CHECK(9,0,0) - #define JSON_HEDLEY_NEVER_INLINE __declspec(noinline) -#else - #define JSON_HEDLEY_NEVER_INLINE -#endif - -#if defined(JSON_HEDLEY_PRIVATE) - #undef JSON_HEDLEY_PRIVATE -#endif -#if defined(JSON_HEDLEY_PUBLIC) - #undef JSON_HEDLEY_PUBLIC -#endif -#if defined(JSON_HEDLEY_IMPORT) - #undef JSON_HEDLEY_IMPORT -#endif -#if defined(_WIN32) || defined(__CYGWIN__) -# define JSON_HEDLEY_PRIVATE -# define JSON_HEDLEY_PUBLIC __declspec(dllexport) -# define JSON_HEDLEY_IMPORT __declspec(dllimport) -#else -# if \ - JSON_HEDLEY_HAS_ATTRIBUTE(visibility) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ - JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ - JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ - ( \ - defined(__TI_EABI__) && \ - ( \ - (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) \ - ) \ - ) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) -# define JSON_HEDLEY_PRIVATE __attribute__((__visibility__("hidden"))) -# define JSON_HEDLEY_PUBLIC __attribute__((__visibility__("default"))) -# else -# define JSON_HEDLEY_PRIVATE -# define JSON_HEDLEY_PUBLIC -# endif -# define JSON_HEDLEY_IMPORT extern -#endif - -#if defined(JSON_HEDLEY_NO_THROW) - #undef JSON_HEDLEY_NO_THROW -#endif -#if \ - JSON_HEDLEY_HAS_ATTRIBUTE(nothrow) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) - #define JSON_HEDLEY_NO_THROW __attribute__((__nothrow__)) -#elif \ - JSON_HEDLEY_MSVC_VERSION_CHECK(13,1,0) || \ - JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) - #define JSON_HEDLEY_NO_THROW __declspec(nothrow) -#else - #define JSON_HEDLEY_NO_THROW -#endif - -#if defined(JSON_HEDLEY_FALL_THROUGH) - #undef JSON_HEDLEY_FALL_THROUGH -#endif -#if \ - JSON_HEDLEY_HAS_ATTRIBUTE(fallthrough) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(7,0,0) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) - #define JSON_HEDLEY_FALL_THROUGH __attribute__((__fallthrough__)) -#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(clang,fallthrough) - #define JSON_HEDLEY_FALL_THROUGH JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[clang::fallthrough]]) -#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE(fallthrough) - #define JSON_HEDLEY_FALL_THROUGH JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[fallthrough]]) -#elif defined(__fallthrough) /* SAL */ - #define JSON_HEDLEY_FALL_THROUGH __fallthrough -#else - #define JSON_HEDLEY_FALL_THROUGH -#endif - -#if defined(JSON_HEDLEY_RETURNS_NON_NULL) - #undef JSON_HEDLEY_RETURNS_NON_NULL -#endif -#if \ - JSON_HEDLEY_HAS_ATTRIBUTE(returns_nonnull) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(4,9,0) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) - #define JSON_HEDLEY_RETURNS_NON_NULL __attribute__((__returns_nonnull__)) -#elif defined(_Ret_notnull_) /* SAL */ - #define JSON_HEDLEY_RETURNS_NON_NULL _Ret_notnull_ -#else - #define JSON_HEDLEY_RETURNS_NON_NULL -#endif - -#if defined(JSON_HEDLEY_ARRAY_PARAM) - #undef JSON_HEDLEY_ARRAY_PARAM -#endif -#if \ - defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && \ - !defined(__STDC_NO_VLA__) && \ - !defined(__cplusplus) && \ - !defined(JSON_HEDLEY_PGI_VERSION) && \ - !defined(JSON_HEDLEY_TINYC_VERSION) - #define JSON_HEDLEY_ARRAY_PARAM(name) (name) -#else - #define JSON_HEDLEY_ARRAY_PARAM(name) -#endif - -#if defined(JSON_HEDLEY_IS_CONSTANT) - #undef JSON_HEDLEY_IS_CONSTANT -#endif -#if defined(JSON_HEDLEY_REQUIRE_CONSTEXPR) - #undef JSON_HEDLEY_REQUIRE_CONSTEXPR -#endif -/* JSON_HEDLEY_IS_CONSTEXPR_ is for - HEDLEY INTERNAL USE ONLY. API subject to change without notice. */ -#if defined(JSON_HEDLEY_IS_CONSTEXPR_) - #undef JSON_HEDLEY_IS_CONSTEXPR_ -#endif -#if \ - JSON_HEDLEY_HAS_BUILTIN(__builtin_constant_p) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,19) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ - JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ - JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ - (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) && !defined(__cplusplus)) || \ - JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) || \ - JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) - #define JSON_HEDLEY_IS_CONSTANT(expr) __builtin_constant_p(expr) -#endif -#if !defined(__cplusplus) -# if \ - JSON_HEDLEY_HAS_BUILTIN(__builtin_types_compatible_p) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ - JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(5,4,0) || \ - JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,24) -#if defined(__INTPTR_TYPE__) - #define JSON_HEDLEY_IS_CONSTEXPR_(expr) __builtin_types_compatible_p(__typeof__((1 ? (void*) ((__INTPTR_TYPE__) ((expr) * 0)) : (int*) 0)), int*) -#else - #include - #define JSON_HEDLEY_IS_CONSTEXPR_(expr) __builtin_types_compatible_p(__typeof__((1 ? (void*) ((intptr_t) ((expr) * 0)) : (int*) 0)), int*) -#endif -# elif \ - ( \ - defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) && \ - !defined(JSON_HEDLEY_SUNPRO_VERSION) && \ - !defined(JSON_HEDLEY_PGI_VERSION) && \ - !defined(JSON_HEDLEY_IAR_VERSION)) || \ - (JSON_HEDLEY_HAS_EXTENSION(c_generic_selections) && !defined(JSON_HEDLEY_IAR_VERSION)) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(4,9,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(17,0,0) || \ - JSON_HEDLEY_IBM_VERSION_CHECK(12,1,0) || \ - JSON_HEDLEY_ARM_VERSION_CHECK(5,3,0) -#if defined(__INTPTR_TYPE__) - #define JSON_HEDLEY_IS_CONSTEXPR_(expr) _Generic((1 ? (void*) ((__INTPTR_TYPE__) ((expr) * 0)) : (int*) 0), int*: 1, void*: 0) -#else - #include - #define JSON_HEDLEY_IS_CONSTEXPR_(expr) _Generic((1 ? (void*) ((intptr_t) * 0) : (int*) 0), int*: 1, void*: 0) -#endif -# elif \ - defined(JSON_HEDLEY_GCC_VERSION) || \ - defined(JSON_HEDLEY_INTEL_VERSION) || \ - defined(JSON_HEDLEY_TINYC_VERSION) || \ - defined(JSON_HEDLEY_TI_ARMCL_VERSION) || \ - JSON_HEDLEY_TI_CL430_VERSION_CHECK(18,12,0) || \ - defined(JSON_HEDLEY_TI_CL2000_VERSION) || \ - defined(JSON_HEDLEY_TI_CL6X_VERSION) || \ - defined(JSON_HEDLEY_TI_CL7X_VERSION) || \ - defined(JSON_HEDLEY_TI_CLPRU_VERSION) || \ - defined(__clang__) -# define JSON_HEDLEY_IS_CONSTEXPR_(expr) ( \ - sizeof(void) != \ - sizeof(*( \ - 1 ? \ - ((void*) ((expr) * 0L) ) : \ -((struct { char v[sizeof(void) * 2]; } *) 1) \ - ) \ - ) \ - ) -# endif -#endif -#if defined(JSON_HEDLEY_IS_CONSTEXPR_) - #if !defined(JSON_HEDLEY_IS_CONSTANT) - #define JSON_HEDLEY_IS_CONSTANT(expr) JSON_HEDLEY_IS_CONSTEXPR_(expr) - #endif - #define JSON_HEDLEY_REQUIRE_CONSTEXPR(expr) (JSON_HEDLEY_IS_CONSTEXPR_(expr) ? (expr) : (-1)) -#else - #if !defined(JSON_HEDLEY_IS_CONSTANT) - #define JSON_HEDLEY_IS_CONSTANT(expr) (0) - #endif - #define JSON_HEDLEY_REQUIRE_CONSTEXPR(expr) (expr) -#endif - -#if defined(JSON_HEDLEY_BEGIN_C_DECLS) - #undef JSON_HEDLEY_BEGIN_C_DECLS -#endif -#if defined(JSON_HEDLEY_END_C_DECLS) - #undef JSON_HEDLEY_END_C_DECLS -#endif -#if defined(JSON_HEDLEY_C_DECL) - #undef JSON_HEDLEY_C_DECL -#endif -#if defined(__cplusplus) - #define JSON_HEDLEY_BEGIN_C_DECLS extern "C" { - #define JSON_HEDLEY_END_C_DECLS } - #define JSON_HEDLEY_C_DECL extern "C" -#else - #define JSON_HEDLEY_BEGIN_C_DECLS - #define JSON_HEDLEY_END_C_DECLS - #define JSON_HEDLEY_C_DECL -#endif - -#if defined(JSON_HEDLEY_STATIC_ASSERT) - #undef JSON_HEDLEY_STATIC_ASSERT -#endif -#if \ - !defined(__cplusplus) && ( \ - (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L)) || \ - (JSON_HEDLEY_HAS_FEATURE(c_static_assert) && !defined(JSON_HEDLEY_INTEL_CL_VERSION)) || \ - JSON_HEDLEY_GCC_VERSION_CHECK(6,0,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ - defined(_Static_assert) \ - ) -# define JSON_HEDLEY_STATIC_ASSERT(expr, message) _Static_assert(expr, message) -#elif \ - (defined(__cplusplus) && (__cplusplus >= 201103L)) || \ - JSON_HEDLEY_MSVC_VERSION_CHECK(16,0,0) || \ - JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) -# define JSON_HEDLEY_STATIC_ASSERT(expr, message) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(static_assert(expr, message)) -#else -# define JSON_HEDLEY_STATIC_ASSERT(expr, message) -#endif - -#if defined(JSON_HEDLEY_NULL) - #undef JSON_HEDLEY_NULL -#endif -#if defined(__cplusplus) - #if __cplusplus >= 201103L - #define JSON_HEDLEY_NULL JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(nullptr) - #elif defined(NULL) - #define JSON_HEDLEY_NULL NULL - #else - #define JSON_HEDLEY_NULL JSON_HEDLEY_STATIC_CAST(void*, 0) - #endif -#elif defined(NULL) - #define JSON_HEDLEY_NULL NULL -#else - #define JSON_HEDLEY_NULL ((void*) 0) -#endif - -#if defined(JSON_HEDLEY_MESSAGE) - #undef JSON_HEDLEY_MESSAGE -#endif -#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") -# define JSON_HEDLEY_MESSAGE(msg) \ - JSON_HEDLEY_DIAGNOSTIC_PUSH \ - JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS \ - JSON_HEDLEY_PRAGMA(message msg) \ - JSON_HEDLEY_DIAGNOSTIC_POP -#elif \ - JSON_HEDLEY_GCC_VERSION_CHECK(4,4,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) -# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message msg) -#elif JSON_HEDLEY_CRAY_VERSION_CHECK(5,0,0) -# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(_CRI message msg) -#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) -# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message(msg)) -#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,0,0) -# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message(msg)) -#else -# define JSON_HEDLEY_MESSAGE(msg) -#endif - -#if defined(JSON_HEDLEY_WARNING) - #undef JSON_HEDLEY_WARNING -#endif -#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") -# define JSON_HEDLEY_WARNING(msg) \ - JSON_HEDLEY_DIAGNOSTIC_PUSH \ - JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS \ - JSON_HEDLEY_PRAGMA(clang warning msg) \ - JSON_HEDLEY_DIAGNOSTIC_POP -#elif \ - JSON_HEDLEY_GCC_VERSION_CHECK(4,8,0) || \ - JSON_HEDLEY_PGI_VERSION_CHECK(18,4,0) || \ - JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) -# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_PRAGMA(GCC warning msg) -#elif \ - JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) || \ - JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) -# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_PRAGMA(message(msg)) -#else -# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_MESSAGE(msg) -#endif - -#if defined(JSON_HEDLEY_REQUIRE) - #undef JSON_HEDLEY_REQUIRE -#endif -#if defined(JSON_HEDLEY_REQUIRE_MSG) - #undef JSON_HEDLEY_REQUIRE_MSG -#endif -#if JSON_HEDLEY_HAS_ATTRIBUTE(diagnose_if) -# if JSON_HEDLEY_HAS_WARNING("-Wgcc-compat") -# define JSON_HEDLEY_REQUIRE(expr) \ - JSON_HEDLEY_DIAGNOSTIC_PUSH \ - _Pragma("clang diagnostic ignored \"-Wgcc-compat\"") \ - __attribute__((diagnose_if(!(expr), #expr, "error"))) \ - JSON_HEDLEY_DIAGNOSTIC_POP -# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) \ - JSON_HEDLEY_DIAGNOSTIC_PUSH \ - _Pragma("clang diagnostic ignored \"-Wgcc-compat\"") \ - __attribute__((diagnose_if(!(expr), msg, "error"))) \ - JSON_HEDLEY_DIAGNOSTIC_POP -# else -# define JSON_HEDLEY_REQUIRE(expr) __attribute__((diagnose_if(!(expr), #expr, "error"))) -# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) __attribute__((diagnose_if(!(expr), msg, "error"))) -# endif -#else -# define JSON_HEDLEY_REQUIRE(expr) -# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) -#endif - -#if defined(JSON_HEDLEY_FLAGS) - #undef JSON_HEDLEY_FLAGS -#endif -#if JSON_HEDLEY_HAS_ATTRIBUTE(flag_enum) && (!defined(__cplusplus) || JSON_HEDLEY_HAS_WARNING("-Wbitfield-enum-conversion")) - #define JSON_HEDLEY_FLAGS __attribute__((__flag_enum__)) -#else - #define JSON_HEDLEY_FLAGS -#endif - -#if defined(JSON_HEDLEY_FLAGS_CAST) - #undef JSON_HEDLEY_FLAGS_CAST -#endif -#if JSON_HEDLEY_INTEL_VERSION_CHECK(19,0,0) -# define JSON_HEDLEY_FLAGS_CAST(T, expr) (__extension__ ({ \ - JSON_HEDLEY_DIAGNOSTIC_PUSH \ - _Pragma("warning(disable:188)") \ - ((T) (expr)); \ - JSON_HEDLEY_DIAGNOSTIC_POP \ - })) -#else -# define JSON_HEDLEY_FLAGS_CAST(T, expr) JSON_HEDLEY_STATIC_CAST(T, expr) -#endif - -#if defined(JSON_HEDLEY_EMPTY_BASES) - #undef JSON_HEDLEY_EMPTY_BASES -#endif -#if \ - (JSON_HEDLEY_MSVC_VERSION_CHECK(19,0,23918) && !JSON_HEDLEY_MSVC_VERSION_CHECK(20,0,0)) || \ - JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) - #define JSON_HEDLEY_EMPTY_BASES __declspec(empty_bases) -#else - #define JSON_HEDLEY_EMPTY_BASES -#endif - -/* Remaining macros are deprecated. */ - -#if defined(JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK) - #undef JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK -#endif -#if defined(__clang__) - #define JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK(major,minor,patch) (0) -#else - #define JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK(major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) -#endif - -#if defined(JSON_HEDLEY_CLANG_HAS_ATTRIBUTE) - #undef JSON_HEDLEY_CLANG_HAS_ATTRIBUTE -#endif -#define JSON_HEDLEY_CLANG_HAS_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) - -#if defined(JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE) - #undef JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE -#endif -#define JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) - -#if defined(JSON_HEDLEY_CLANG_HAS_BUILTIN) - #undef JSON_HEDLEY_CLANG_HAS_BUILTIN -#endif -#define JSON_HEDLEY_CLANG_HAS_BUILTIN(builtin) JSON_HEDLEY_HAS_BUILTIN(builtin) - -#if defined(JSON_HEDLEY_CLANG_HAS_FEATURE) - #undef JSON_HEDLEY_CLANG_HAS_FEATURE -#endif -#define JSON_HEDLEY_CLANG_HAS_FEATURE(feature) JSON_HEDLEY_HAS_FEATURE(feature) - -#if defined(JSON_HEDLEY_CLANG_HAS_EXTENSION) - #undef JSON_HEDLEY_CLANG_HAS_EXTENSION -#endif -#define JSON_HEDLEY_CLANG_HAS_EXTENSION(extension) JSON_HEDLEY_HAS_EXTENSION(extension) - -#if defined(JSON_HEDLEY_CLANG_HAS_DECLSPEC_DECLSPEC_ATTRIBUTE) - #undef JSON_HEDLEY_CLANG_HAS_DECLSPEC_DECLSPEC_ATTRIBUTE -#endif -#define JSON_HEDLEY_CLANG_HAS_DECLSPEC_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) - -#if defined(JSON_HEDLEY_CLANG_HAS_WARNING) - #undef JSON_HEDLEY_CLANG_HAS_WARNING -#endif -#define JSON_HEDLEY_CLANG_HAS_WARNING(warning) JSON_HEDLEY_HAS_WARNING(warning) - -#endif /* !defined(JSON_HEDLEY_VERSION) || (JSON_HEDLEY_VERSION < X) */ - - -// This file contains all internal macro definitions (except those affecting ABI) -// You MUST include macro_unscope.hpp at the end of json.hpp to undef all of them - -// #include - - -// exclude unsupported compilers -#if !defined(JSON_SKIP_UNSUPPORTED_COMPILER_CHECK) - #if defined(__clang__) - #if (__clang_major__ * 10000 + __clang_minor__ * 100 + __clang_patchlevel__) < 30400 - #error "unsupported Clang version - see https://github.com/nlohmann/json#supported-compilers" - #endif - #elif defined(__GNUC__) && !(defined(__ICC) || defined(__INTEL_COMPILER)) - #if (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) < 40800 - #error "unsupported GCC version - see https://github.com/nlohmann/json#supported-compilers" - #endif - #endif -#endif - -// C++ language standard detection -// if the user manually specified the used c++ version this is skipped -#if !defined(JSON_HAS_CPP_20) && !defined(JSON_HAS_CPP_17) && !defined(JSON_HAS_CPP_14) && !defined(JSON_HAS_CPP_11) - #if (defined(__cplusplus) && __cplusplus >= 202002L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L) - #define JSON_HAS_CPP_20 - #define JSON_HAS_CPP_17 - #define JSON_HAS_CPP_14 - #elif (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464 - #define JSON_HAS_CPP_17 - #define JSON_HAS_CPP_14 - #elif (defined(__cplusplus) && __cplusplus >= 201402L) || (defined(_HAS_CXX14) && _HAS_CXX14 == 1) - #define JSON_HAS_CPP_14 - #endif - // the cpp 11 flag is always specified because it is the minimal required version - #define JSON_HAS_CPP_11 -#endif - -#ifdef __has_include - #if __has_include() - #include - #endif -#endif - -#if !defined(JSON_HAS_FILESYSTEM) && !defined(JSON_HAS_EXPERIMENTAL_FILESYSTEM) - #ifdef JSON_HAS_CPP_17 - #if defined(__cpp_lib_filesystem) - #define JSON_HAS_FILESYSTEM 1 - #elif defined(__cpp_lib_experimental_filesystem) - #define JSON_HAS_EXPERIMENTAL_FILESYSTEM 1 - #elif !defined(__has_include) - #define JSON_HAS_EXPERIMENTAL_FILESYSTEM 1 - #elif __has_include() - #define JSON_HAS_FILESYSTEM 1 - #elif __has_include() - #define JSON_HAS_EXPERIMENTAL_FILESYSTEM 1 - #endif - - // std::filesystem does not work on MinGW GCC 8: https://sourceforge.net/p/mingw-w64/bugs/737/ - #if defined(__MINGW32__) && defined(__GNUC__) && __GNUC__ == 8 - #undef JSON_HAS_FILESYSTEM - #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM - #endif - - // no filesystem support before GCC 8: https://en.cppreference.com/w/cpp/compiler_support - #if defined(__GNUC__) && !defined(__clang__) && __GNUC__ < 8 - #undef JSON_HAS_FILESYSTEM - #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM - #endif - - // no filesystem support before Clang 7: https://en.cppreference.com/w/cpp/compiler_support - #if defined(__clang_major__) && __clang_major__ < 7 - #undef JSON_HAS_FILESYSTEM - #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM - #endif - - // no filesystem support before MSVC 19.14: https://en.cppreference.com/w/cpp/compiler_support - #if defined(_MSC_VER) && _MSC_VER < 1914 - #undef JSON_HAS_FILESYSTEM - #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM - #endif - - // no filesystem support before iOS 13 - #if defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED < 130000 - #undef JSON_HAS_FILESYSTEM - #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM - #endif - - // no filesystem support before macOS Catalina - #if defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED < 101500 - #undef JSON_HAS_FILESYSTEM - #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM - #endif - #endif -#endif - -#ifndef JSON_HAS_EXPERIMENTAL_FILESYSTEM - #define JSON_HAS_EXPERIMENTAL_FILESYSTEM 0 -#endif - -#ifndef JSON_HAS_FILESYSTEM - #define JSON_HAS_FILESYSTEM 0 -#endif - -#ifndef JSON_HAS_THREE_WAY_COMPARISON - #if defined(__cpp_impl_three_way_comparison) && __cpp_impl_three_way_comparison >= 201907L \ - && defined(__cpp_lib_three_way_comparison) && __cpp_lib_three_way_comparison >= 201907L - #define JSON_HAS_THREE_WAY_COMPARISON 1 - #else - #define JSON_HAS_THREE_WAY_COMPARISON 0 - #endif -#endif - -#ifndef JSON_HAS_RANGES - // ranges header shipping in GCC 11.1.0 (released 2021-04-27) has syntax error - #if defined(__GLIBCXX__) && __GLIBCXX__ == 20210427 - #define JSON_HAS_RANGES 0 - #elif defined(__cpp_lib_ranges) - #define JSON_HAS_RANGES 1 - #else - #define JSON_HAS_RANGES 0 - #endif -#endif - -#ifndef JSON_HAS_STATIC_RTTI - #if !defined(_HAS_STATIC_RTTI) || _HAS_STATIC_RTTI != 0 - #define JSON_HAS_STATIC_RTTI 1 - #else - #define JSON_HAS_STATIC_RTTI 0 - #endif -#endif - -#ifdef JSON_HAS_CPP_17 - #define JSON_INLINE_VARIABLE inline -#else - #define JSON_INLINE_VARIABLE -#endif - -#if JSON_HEDLEY_HAS_ATTRIBUTE(no_unique_address) - #define JSON_NO_UNIQUE_ADDRESS [[no_unique_address]] -#else - #define JSON_NO_UNIQUE_ADDRESS -#endif - -// disable documentation warnings on clang -#if defined(__clang__) - #pragma clang diagnostic push - #pragma clang diagnostic ignored "-Wdocumentation" - #pragma clang diagnostic ignored "-Wdocumentation-unknown-command" -#endif - -// allow disabling exceptions -#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) - #define JSON_THROW(exception) throw exception - #define JSON_TRY try - #define JSON_CATCH(exception) catch(exception) - #define JSON_INTERNAL_CATCH(exception) catch(exception) -#else - #include - #define JSON_THROW(exception) std::abort() - #define JSON_TRY if(true) - #define JSON_CATCH(exception) if(false) - #define JSON_INTERNAL_CATCH(exception) if(false) -#endif - -// override exception macros -#if defined(JSON_THROW_USER) - #undef JSON_THROW - #define JSON_THROW JSON_THROW_USER -#endif -#if defined(JSON_TRY_USER) - #undef JSON_TRY - #define JSON_TRY JSON_TRY_USER -#endif -#if defined(JSON_CATCH_USER) - #undef JSON_CATCH - #define JSON_CATCH JSON_CATCH_USER - #undef JSON_INTERNAL_CATCH - #define JSON_INTERNAL_CATCH JSON_CATCH_USER -#endif -#if defined(JSON_INTERNAL_CATCH_USER) - #undef JSON_INTERNAL_CATCH - #define JSON_INTERNAL_CATCH JSON_INTERNAL_CATCH_USER -#endif - -// allow overriding assert -#if !defined(JSON_ASSERT) - #include // assert - #define JSON_ASSERT(x) assert(x) -#endif - -// allow to access some private functions (needed by the test suite) -#if defined(JSON_TESTS_PRIVATE) - #define JSON_PRIVATE_UNLESS_TESTED public -#else - #define JSON_PRIVATE_UNLESS_TESTED private -#endif - -/*! -@brief macro to briefly define a mapping between an enum and JSON -@def NLOHMANN_JSON_SERIALIZE_ENUM -@since version 3.4.0 -*/ -#define NLOHMANN_JSON_SERIALIZE_ENUM(ENUM_TYPE, ...) \ - template \ - inline void to_json(BasicJsonType& j, const ENUM_TYPE& e) \ - { \ - static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ - static const std::pair m[] = __VA_ARGS__; \ - auto it = std::find_if(std::begin(m), std::end(m), \ - [e](const std::pair& ej_pair) -> bool \ - { \ - return ej_pair.first == e; \ - }); \ - j = ((it != std::end(m)) ? it : std::begin(m))->second; \ - } \ - template \ - inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \ - { \ - static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ - static const std::pair m[] = __VA_ARGS__; \ - auto it = std::find_if(std::begin(m), std::end(m), \ - [&j](const std::pair& ej_pair) -> bool \ - { \ - return ej_pair.second == j; \ - }); \ - e = ((it != std::end(m)) ? it : std::begin(m))->first; \ - } - -// Ugly macros to avoid uglier copy-paste when specializing basic_json. They -// may be removed in the future once the class is split. - -#define NLOHMANN_BASIC_JSON_TPL_DECLARATION \ - template class ObjectType, \ - template class ArrayType, \ - class StringType, class BooleanType, class NumberIntegerType, \ - class NumberUnsignedType, class NumberFloatType, \ - template class AllocatorType, \ - template class JSONSerializer, \ - class BinaryType, \ - class CustomBaseClass> - -#define NLOHMANN_BASIC_JSON_TPL \ - basic_json - -// Macros to simplify conversion from/to types - -#define NLOHMANN_JSON_EXPAND( x ) x -#define NLOHMANN_JSON_GET_MACRO(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, NAME,...) NAME -#define NLOHMANN_JSON_PASTE(...) NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_GET_MACRO(__VA_ARGS__, \ - NLOHMANN_JSON_PASTE64, \ - NLOHMANN_JSON_PASTE63, \ - NLOHMANN_JSON_PASTE62, \ - NLOHMANN_JSON_PASTE61, \ - NLOHMANN_JSON_PASTE60, \ - NLOHMANN_JSON_PASTE59, \ - NLOHMANN_JSON_PASTE58, \ - NLOHMANN_JSON_PASTE57, \ - NLOHMANN_JSON_PASTE56, \ - NLOHMANN_JSON_PASTE55, \ - NLOHMANN_JSON_PASTE54, \ - NLOHMANN_JSON_PASTE53, \ - NLOHMANN_JSON_PASTE52, \ - NLOHMANN_JSON_PASTE51, \ - NLOHMANN_JSON_PASTE50, \ - NLOHMANN_JSON_PASTE49, \ - NLOHMANN_JSON_PASTE48, \ - NLOHMANN_JSON_PASTE47, \ - NLOHMANN_JSON_PASTE46, \ - NLOHMANN_JSON_PASTE45, \ - NLOHMANN_JSON_PASTE44, \ - NLOHMANN_JSON_PASTE43, \ - NLOHMANN_JSON_PASTE42, \ - NLOHMANN_JSON_PASTE41, \ - NLOHMANN_JSON_PASTE40, \ - NLOHMANN_JSON_PASTE39, \ - NLOHMANN_JSON_PASTE38, \ - NLOHMANN_JSON_PASTE37, \ - NLOHMANN_JSON_PASTE36, \ - NLOHMANN_JSON_PASTE35, \ - NLOHMANN_JSON_PASTE34, \ - NLOHMANN_JSON_PASTE33, \ - NLOHMANN_JSON_PASTE32, \ - NLOHMANN_JSON_PASTE31, \ - NLOHMANN_JSON_PASTE30, \ - NLOHMANN_JSON_PASTE29, \ - NLOHMANN_JSON_PASTE28, \ - NLOHMANN_JSON_PASTE27, \ - NLOHMANN_JSON_PASTE26, \ - NLOHMANN_JSON_PASTE25, \ - NLOHMANN_JSON_PASTE24, \ - NLOHMANN_JSON_PASTE23, \ - NLOHMANN_JSON_PASTE22, \ - NLOHMANN_JSON_PASTE21, \ - NLOHMANN_JSON_PASTE20, \ - NLOHMANN_JSON_PASTE19, \ - NLOHMANN_JSON_PASTE18, \ - NLOHMANN_JSON_PASTE17, \ - NLOHMANN_JSON_PASTE16, \ - NLOHMANN_JSON_PASTE15, \ - NLOHMANN_JSON_PASTE14, \ - NLOHMANN_JSON_PASTE13, \ - NLOHMANN_JSON_PASTE12, \ - NLOHMANN_JSON_PASTE11, \ - NLOHMANN_JSON_PASTE10, \ - NLOHMANN_JSON_PASTE9, \ - NLOHMANN_JSON_PASTE8, \ - NLOHMANN_JSON_PASTE7, \ - NLOHMANN_JSON_PASTE6, \ - NLOHMANN_JSON_PASTE5, \ - NLOHMANN_JSON_PASTE4, \ - NLOHMANN_JSON_PASTE3, \ - NLOHMANN_JSON_PASTE2, \ - NLOHMANN_JSON_PASTE1)(__VA_ARGS__)) -#define NLOHMANN_JSON_PASTE2(func, v1) func(v1) -#define NLOHMANN_JSON_PASTE3(func, v1, v2) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE2(func, v2) -#define NLOHMANN_JSON_PASTE4(func, v1, v2, v3) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE3(func, v2, v3) -#define NLOHMANN_JSON_PASTE5(func, v1, v2, v3, v4) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE4(func, v2, v3, v4) -#define NLOHMANN_JSON_PASTE6(func, v1, v2, v3, v4, v5) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE5(func, v2, v3, v4, v5) -#define NLOHMANN_JSON_PASTE7(func, v1, v2, v3, v4, v5, v6) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE6(func, v2, v3, v4, v5, v6) -#define NLOHMANN_JSON_PASTE8(func, v1, v2, v3, v4, v5, v6, v7) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE7(func, v2, v3, v4, v5, v6, v7) -#define NLOHMANN_JSON_PASTE9(func, v1, v2, v3, v4, v5, v6, v7, v8) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE8(func, v2, v3, v4, v5, v6, v7, v8) -#define NLOHMANN_JSON_PASTE10(func, v1, v2, v3, v4, v5, v6, v7, v8, v9) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE9(func, v2, v3, v4, v5, v6, v7, v8, v9) -#define NLOHMANN_JSON_PASTE11(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE10(func, v2, v3, v4, v5, v6, v7, v8, v9, v10) -#define NLOHMANN_JSON_PASTE12(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE11(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) -#define NLOHMANN_JSON_PASTE13(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE12(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) -#define NLOHMANN_JSON_PASTE14(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE13(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) -#define NLOHMANN_JSON_PASTE15(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE14(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) -#define NLOHMANN_JSON_PASTE16(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE15(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) -#define NLOHMANN_JSON_PASTE17(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE16(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) -#define NLOHMANN_JSON_PASTE18(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE17(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17) -#define NLOHMANN_JSON_PASTE19(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE18(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18) -#define NLOHMANN_JSON_PASTE20(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE19(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19) -#define NLOHMANN_JSON_PASTE21(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE20(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20) -#define NLOHMANN_JSON_PASTE22(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE21(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21) -#define NLOHMANN_JSON_PASTE23(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE22(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22) -#define NLOHMANN_JSON_PASTE24(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE23(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23) -#define NLOHMANN_JSON_PASTE25(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE24(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24) -#define NLOHMANN_JSON_PASTE26(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE25(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25) -#define NLOHMANN_JSON_PASTE27(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE26(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26) -#define NLOHMANN_JSON_PASTE28(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE27(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27) -#define NLOHMANN_JSON_PASTE29(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE28(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28) -#define NLOHMANN_JSON_PASTE30(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE29(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29) -#define NLOHMANN_JSON_PASTE31(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE30(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30) -#define NLOHMANN_JSON_PASTE32(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE31(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31) -#define NLOHMANN_JSON_PASTE33(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE32(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32) -#define NLOHMANN_JSON_PASTE34(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE33(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33) -#define NLOHMANN_JSON_PASTE35(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE34(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34) -#define NLOHMANN_JSON_PASTE36(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE35(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35) -#define NLOHMANN_JSON_PASTE37(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE36(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36) -#define NLOHMANN_JSON_PASTE38(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE37(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37) -#define NLOHMANN_JSON_PASTE39(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE38(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38) -#define NLOHMANN_JSON_PASTE40(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE39(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39) -#define NLOHMANN_JSON_PASTE41(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE40(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40) -#define NLOHMANN_JSON_PASTE42(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE41(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41) -#define NLOHMANN_JSON_PASTE43(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE42(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42) -#define NLOHMANN_JSON_PASTE44(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE43(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43) -#define NLOHMANN_JSON_PASTE45(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE44(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44) -#define NLOHMANN_JSON_PASTE46(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE45(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45) -#define NLOHMANN_JSON_PASTE47(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE46(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46) -#define NLOHMANN_JSON_PASTE48(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE47(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47) -#define NLOHMANN_JSON_PASTE49(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE48(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48) -#define NLOHMANN_JSON_PASTE50(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE49(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49) -#define NLOHMANN_JSON_PASTE51(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE50(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50) -#define NLOHMANN_JSON_PASTE52(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE51(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51) -#define NLOHMANN_JSON_PASTE53(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE52(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52) -#define NLOHMANN_JSON_PASTE54(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE53(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53) -#define NLOHMANN_JSON_PASTE55(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE54(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54) -#define NLOHMANN_JSON_PASTE56(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE55(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55) -#define NLOHMANN_JSON_PASTE57(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE56(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56) -#define NLOHMANN_JSON_PASTE58(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE57(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57) -#define NLOHMANN_JSON_PASTE59(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE58(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58) -#define NLOHMANN_JSON_PASTE60(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE59(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59) -#define NLOHMANN_JSON_PASTE61(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE60(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60) -#define NLOHMANN_JSON_PASTE62(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE61(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61) -#define NLOHMANN_JSON_PASTE63(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE62(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62) -#define NLOHMANN_JSON_PASTE64(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62, v63) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE63(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62, v63) - -#define NLOHMANN_JSON_TO(v1) nlohmann_json_j[#v1] = nlohmann_json_t.v1; -#define NLOHMANN_JSON_FROM(v1) nlohmann_json_j.at(#v1).get_to(nlohmann_json_t.v1); -#define NLOHMANN_JSON_FROM_WITH_DEFAULT(v1) nlohmann_json_t.v1 = nlohmann_json_j.value(#v1, nlohmann_json_default_obj.v1); - -/*! -@brief macro -@def NLOHMANN_DEFINE_TYPE_INTRUSIVE -@since version 3.9.0 -*/ -#define NLOHMANN_DEFINE_TYPE_INTRUSIVE(Type, ...) \ - friend void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ - friend void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } - -#define NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Type, ...) \ - friend void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ - friend void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } - -#define NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(Type, ...) \ - friend void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } - -/*! -@brief macro -@def NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE -@since version 3.9.0 -*/ -#define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Type, ...) \ - inline void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ - inline void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } - -#define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE(Type, ...) \ - inline void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } - -#define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Type, ...) \ - inline void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ - inline void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } - -// inspired from https://stackoverflow.com/a/26745591 -// allows to call any std function as if (e.g. with begin): -// using std::begin; begin(x); -// -// it allows using the detected idiom to retrieve the return type -// of such an expression -#define NLOHMANN_CAN_CALL_STD_FUNC_IMPL(std_name) \ - namespace detail { \ - using std::std_name; \ - \ - template \ - using result_of_##std_name = decltype(std_name(std::declval()...)); \ - } \ - \ - namespace detail2 { \ - struct std_name##_tag \ - { \ - }; \ - \ - template \ - std_name##_tag std_name(T&&...); \ - \ - template \ - using result_of_##std_name = decltype(std_name(std::declval()...)); \ - \ - template \ - struct would_call_std_##std_name \ - { \ - static constexpr auto const value = ::nlohmann::detail:: \ - is_detected_exact::value; \ - }; \ - } /* namespace detail2 */ \ - \ - template \ - struct would_call_std_##std_name : detail2::would_call_std_##std_name \ - { \ - } - -#ifndef JSON_USE_IMPLICIT_CONVERSIONS - #define JSON_USE_IMPLICIT_CONVERSIONS 1 -#endif - -#if JSON_USE_IMPLICIT_CONVERSIONS - #define JSON_EXPLICIT -#else - #define JSON_EXPLICIT explicit -#endif - -#ifndef JSON_DISABLE_ENUM_SERIALIZATION - #define JSON_DISABLE_ENUM_SERIALIZATION 0 -#endif - -#ifndef JSON_USE_GLOBAL_UDLS - #define JSON_USE_GLOBAL_UDLS 1 -#endif - -#if JSON_HAS_THREE_WAY_COMPARISON - #include // partial_ordering -#endif - -NLOHMANN_JSON_NAMESPACE_BEGIN -namespace detail -{ - -/////////////////////////// -// JSON type enumeration // -/////////////////////////// - -/*! -@brief the JSON type enumeration - -This enumeration collects the different JSON types. It is internally used to -distinguish the stored values, and the functions @ref basic_json::is_null(), -@ref basic_json::is_object(), @ref basic_json::is_array(), -@ref basic_json::is_string(), @ref basic_json::is_boolean(), -@ref basic_json::is_number() (with @ref basic_json::is_number_integer(), -@ref basic_json::is_number_unsigned(), and @ref basic_json::is_number_float()), -@ref basic_json::is_discarded(), @ref basic_json::is_primitive(), and -@ref basic_json::is_structured() rely on it. - -@note There are three enumeration entries (number_integer, number_unsigned, and -number_float), because the library distinguishes these three types for numbers: -@ref basic_json::number_unsigned_t is used for unsigned integers, -@ref basic_json::number_integer_t is used for signed integers, and -@ref basic_json::number_float_t is used for floating-point numbers or to -approximate integers which do not fit in the limits of their respective type. - -@sa see @ref basic_json::basic_json(const value_t value_type) -- create a JSON -value with the default value for a given type - -@since version 1.0.0 -*/ -enum class value_t : std::uint8_t -{ - null, ///< null value - object, ///< object (unordered set of name/value pairs) - array, ///< array (ordered collection of values) - string, ///< string value - boolean, ///< boolean value - number_integer, ///< number value (signed integer) - number_unsigned, ///< number value (unsigned integer) - number_float, ///< number value (floating-point) - binary, ///< binary array (ordered collection of bytes) - discarded ///< discarded by the parser callback function -}; - -/*! -@brief comparison operator for JSON types - -Returns an ordering that is similar to Python: -- order: null < boolean < number < object < array < string < binary -- furthermore, each type is not smaller than itself -- discarded values are not comparable -- binary is represented as a b"" string in python and directly comparable to a - string; however, making a binary array directly comparable with a string would - be surprising behavior in a JSON file. - -@since version 1.0.0 -*/ -#if JSON_HAS_THREE_WAY_COMPARISON - inline std::partial_ordering operator<=>(const value_t lhs, const value_t rhs) noexcept // *NOPAD* -#else - inline bool operator<(const value_t lhs, const value_t rhs) noexcept -#endif -{ - static constexpr std::array order = {{ - 0 /* null */, 3 /* object */, 4 /* array */, 5 /* string */, - 1 /* boolean */, 2 /* integer */, 2 /* unsigned */, 2 /* float */, - 6 /* binary */ - } - }; - - const auto l_index = static_cast(lhs); - const auto r_index = static_cast(rhs); -#if JSON_HAS_THREE_WAY_COMPARISON - if (l_index < order.size() && r_index < order.size()) - { - return order[l_index] <=> order[r_index]; // *NOPAD* - } - return std::partial_ordering::unordered; -#else - return l_index < order.size() && r_index < order.size() && order[l_index] < order[r_index]; -#endif -} - -// GCC selects the built-in operator< over an operator rewritten from -// a user-defined spaceship operator -// Clang, MSVC, and ICC select the rewritten candidate -// (see GCC bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105200) -#if JSON_HAS_THREE_WAY_COMPARISON && defined(__GNUC__) -inline bool operator<(const value_t lhs, const value_t rhs) noexcept -{ - return std::is_lt(lhs <=> rhs); // *NOPAD* -} -#endif - -} // namespace detail -NLOHMANN_JSON_NAMESPACE_END - -// #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.3 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -// #include - - -NLOHMANN_JSON_NAMESPACE_BEGIN -namespace detail -{ - -/*! -@brief replace all occurrences of a substring by another string - -@param[in,out] s the string to manipulate; changed so that all - occurrences of @a f are replaced with @a t -@param[in] f the substring to replace with @a t -@param[in] t the string to replace @a f - -@pre The search string @a f must not be empty. **This precondition is -enforced with an assertion.** - -@since version 2.0.0 -*/ -template -inline void replace_substring(StringType& s, const StringType& f, - const StringType& t) -{ - JSON_ASSERT(!f.empty()); - for (auto pos = s.find(f); // find first occurrence of f - pos != StringType::npos; // make sure f was found - s.replace(pos, f.size(), t), // replace with t, and - pos = s.find(f, pos + t.size())) // find next occurrence of f - {} -} - -/*! - * @brief string escaping as described in RFC 6901 (Sect. 4) - * @param[in] s string to escape - * @return escaped string - * - * Note the order of escaping "~" to "~0" and "/" to "~1" is important. - */ -template -inline StringType escape(StringType s) -{ - replace_substring(s, StringType{"~"}, StringType{"~0"}); - replace_substring(s, StringType{"/"}, StringType{"~1"}); - return s; -} - -/*! - * @brief string unescaping as described in RFC 6901 (Sect. 4) - * @param[in] s string to unescape - * @return unescaped string - * - * Note the order of escaping "~1" to "/" and "~0" to "~" is important. - */ -template -static void unescape(StringType& s) -{ - replace_substring(s, StringType{"~1"}, StringType{"/"}); - replace_substring(s, StringType{"~0"}, StringType{"~"}); -} - -} // namespace detail -NLOHMANN_JSON_NAMESPACE_END - -// #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.3 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -#include // size_t - -// #include - - -NLOHMANN_JSON_NAMESPACE_BEGIN -namespace detail -{ - -/// struct to capture the start position of the current token -struct position_t -{ - /// the total number of characters read - std::size_t chars_read_total = 0; - /// the number of characters read in the current line - std::size_t chars_read_current_line = 0; - /// the number of lines read - std::size_t lines_read = 0; - - /// conversion to size_t to preserve SAX interface - constexpr operator size_t() const - { - return chars_read_total; - } -}; - -} // namespace detail -NLOHMANN_JSON_NAMESPACE_END - -// #include - -// #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.3 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann -// SPDX-FileCopyrightText: 2018 The Abseil Authors -// SPDX-License-Identifier: MIT - - - -#include // array -#include // size_t -#include // conditional, enable_if, false_type, integral_constant, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type -#include // index_sequence, make_index_sequence, index_sequence_for - -// #include - - -NLOHMANN_JSON_NAMESPACE_BEGIN -namespace detail -{ - -template -using uncvref_t = typename std::remove_cv::type>::type; - -#ifdef JSON_HAS_CPP_14 - -// the following utilities are natively available in C++14 -using std::enable_if_t; -using std::index_sequence; -using std::make_index_sequence; -using std::index_sequence_for; - -#else - -// alias templates to reduce boilerplate -template -using enable_if_t = typename std::enable_if::type; - -// The following code is taken from https://github.com/abseil/abseil-cpp/blob/10cb35e459f5ecca5b2ff107635da0bfa41011b4/absl/utility/utility.h -// which is part of Google Abseil (https://github.com/abseil/abseil-cpp), licensed under the Apache License 2.0. - -//// START OF CODE FROM GOOGLE ABSEIL - -// integer_sequence -// -// Class template representing a compile-time integer sequence. An instantiation -// of `integer_sequence` has a sequence of integers encoded in its -// type through its template arguments (which is a common need when -// working with C++11 variadic templates). `absl::integer_sequence` is designed -// to be a drop-in replacement for C++14's `std::integer_sequence`. -// -// Example: -// -// template< class T, T... Ints > -// void user_function(integer_sequence); -// -// int main() -// { -// // user_function's `T` will be deduced to `int` and `Ints...` -// // will be deduced to `0, 1, 2, 3, 4`. -// user_function(make_integer_sequence()); -// } -template -struct integer_sequence -{ - using value_type = T; - static constexpr std::size_t size() noexcept - { - return sizeof...(Ints); - } -}; - -// index_sequence -// -// A helper template for an `integer_sequence` of `size_t`, -// `absl::index_sequence` is designed to be a drop-in replacement for C++14's -// `std::index_sequence`. -template -using index_sequence = integer_sequence; - -namespace utility_internal -{ - -template -struct Extend; - -// Note that SeqSize == sizeof...(Ints). It's passed explicitly for efficiency. -template -struct Extend, SeqSize, 0> -{ - using type = integer_sequence < T, Ints..., (Ints + SeqSize)... >; -}; - -template -struct Extend, SeqSize, 1> -{ - using type = integer_sequence < T, Ints..., (Ints + SeqSize)..., 2 * SeqSize >; -}; - -// Recursion helper for 'make_integer_sequence'. -// 'Gen::type' is an alias for 'integer_sequence'. -template -struct Gen -{ - using type = - typename Extend < typename Gen < T, N / 2 >::type, N / 2, N % 2 >::type; -}; - -template -struct Gen -{ - using type = integer_sequence; -}; - -} // namespace utility_internal - -// Compile-time sequences of integers - -// make_integer_sequence -// -// This template alias is equivalent to -// `integer_sequence`, and is designed to be a drop-in -// replacement for C++14's `std::make_integer_sequence`. -template -using make_integer_sequence = typename utility_internal::Gen::type; - -// make_index_sequence -// -// This template alias is equivalent to `index_sequence<0, 1, ..., N-1>`, -// and is designed to be a drop-in replacement for C++14's -// `std::make_index_sequence`. -template -using make_index_sequence = make_integer_sequence; - -// index_sequence_for -// -// Converts a typename pack into an index sequence of the same length, and -// is designed to be a drop-in replacement for C++14's -// `std::index_sequence_for()` -template -using index_sequence_for = make_index_sequence; - -//// END OF CODE FROM GOOGLE ABSEIL - -#endif - -// dispatch utility (taken from ranges-v3) -template struct priority_tag : priority_tag < N - 1 > {}; -template<> struct priority_tag<0> {}; - -// taken from ranges-v3 -template -struct static_const -{ - static JSON_INLINE_VARIABLE constexpr T value{}; -}; - -#ifndef JSON_HAS_CPP_17 - template - constexpr T static_const::value; -#endif - -template -inline constexpr std::array make_array(Args&& ... args) -{ - return std::array {{static_cast(std::forward(args))...}}; -} - -} // namespace detail -NLOHMANN_JSON_NAMESPACE_END - -// #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.3 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -#include // numeric_limits -#include // false_type, is_constructible, is_integral, is_same, true_type -#include // declval -#include // tuple -#include // char_traits - -// #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.3 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -#include // random_access_iterator_tag - -// #include - -// #include - -// #include - - -NLOHMANN_JSON_NAMESPACE_BEGIN -namespace detail -{ - -template -struct iterator_types {}; - -template -struct iterator_types < - It, - void_t> -{ - using difference_type = typename It::difference_type; - using value_type = typename It::value_type; - using pointer = typename It::pointer; - using reference = typename It::reference; - using iterator_category = typename It::iterator_category; -}; - -// This is required as some compilers implement std::iterator_traits in a way that -// doesn't work with SFINAE. See https://github.com/nlohmann/json/issues/1341. -template -struct iterator_traits -{ -}; - -template -struct iterator_traits < T, enable_if_t < !std::is_pointer::value >> - : iterator_types -{ -}; - -template -struct iterator_traits::value>> -{ - using iterator_category = std::random_access_iterator_tag; - using value_type = T; - using difference_type = ptrdiff_t; - using pointer = T*; - using reference = T&; -}; - -} // namespace detail -NLOHMANN_JSON_NAMESPACE_END - -// #include - -// #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.3 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -// #include - - -NLOHMANN_JSON_NAMESPACE_BEGIN - -NLOHMANN_CAN_CALL_STD_FUNC_IMPL(begin); - -NLOHMANN_JSON_NAMESPACE_END - -// #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.3 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann -// SPDX-License-Identifier: MIT - - - -// #include - - -NLOHMANN_JSON_NAMESPACE_BEGIN - -NLOHMANN_CAN_CALL_STD_FUNC_IMPL(end); - -NLOHMANN_JSON_NAMESPACE_END - -// #include - -// #include - -// #include -// __ _____ _____ _____ -// __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.3 -// |_____|_____|_____|_|___| https://github.com/nlohmann/json -// -// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann -// SPDX-License-Identifier: MIT - -#ifndef INCLUDE_NLOHMANN_JSON_FWD_HPP_ - #define INCLUDE_NLOHMANN_JSON_FWD_HPP_ - - #include // int64_t, uint64_t - #include // map - #include // allocator - #include // string - #include // vector - - // #include - - - /*! - @brief namespace for Niels Lohmann - @see https://github.com/nlohmann - @since version 1.0.0 - */ - NLOHMANN_JSON_NAMESPACE_BEGIN - - /*! - @brief default JSONSerializer template argument - - This serializer ignores the template arguments and uses ADL - ([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl)) - for serialization. - */ - template - struct adl_serializer; - - /// a class to store JSON values - /// @sa https://json.nlohmann.me/api/basic_json/ - template class ObjectType = - std::map, - template class ArrayType = std::vector, - class StringType = std::string, class BooleanType = bool, - class NumberIntegerType = std::int64_t, - class NumberUnsignedType = std::uint64_t, - class NumberFloatType = double, - template class AllocatorType = std::allocator, - template class JSONSerializer = - adl_serializer, - class BinaryType = std::vector, // cppcheck-suppress syntaxError - class CustomBaseClass = void> - class basic_json; - - /// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document - /// @sa https://json.nlohmann.me/api/json_pointer/ - template - class json_pointer; - - /*! - @brief default specialization - @sa https://json.nlohmann.me/api/json/ - */ - using json = basic_json<>; - - /// @brief a minimal map-like container that preserves insertion order - /// @sa https://json.nlohmann.me/api/ordered_map/ - template - struct ordered_map; - - /// @brief specialization that maintains the insertion order of object keys - /// @sa https://json.nlohmann.me/api/ordered_json/ - using ordered_json = basic_json; - - NLOHMANN_JSON_NAMESPACE_END - -#endif // INCLUDE_NLOHMANN_JSON_FWD_HPP_ - - -NLOHMANN_JSON_NAMESPACE_BEGIN -/*! -@brief detail namespace with internal helper functions - -This namespace collects functions that should not be exposed, -implementations of some @ref basic_json methods, and meta-programming helpers. - -@since version 2.1.0 -*/ -namespace detail -{ - -///////////// -// helpers // -///////////// - -// Note to maintainers: -// -// Every trait in this file expects a non CV-qualified type. -// The only exceptions are in the 'aliases for detected' section -// (i.e. those of the form: decltype(T::member_function(std::declval()))) -// -// In this case, T has to be properly CV-qualified to constraint the function arguments -// (e.g. to_json(BasicJsonType&, const T&)) - -template struct is_basic_json : std::false_type {}; - -NLOHMANN_BASIC_JSON_TPL_DECLARATION -struct is_basic_json : std::true_type {}; - -// used by exceptions create() member functions -// true_type for pointer to possibly cv-qualified basic_json or std::nullptr_t -// false_type otherwise -template -struct is_basic_json_context : - std::integral_constant < bool, - is_basic_json::type>::type>::value - || std::is_same::value > -{}; - -////////////////////// -// json_ref helpers // -////////////////////// - -template -class json_ref; - -template -struct is_json_ref : std::false_type {}; - -template -struct is_json_ref> : std::true_type {}; - -////////////////////////// -// aliases for detected // -////////////////////////// - -template -using mapped_type_t = typename T::mapped_type; - -template -using key_type_t = typename T::key_type; - -template -using value_type_t = typename T::value_type; - -template -using difference_type_t = typename T::difference_type; - -template -using pointer_t = typename T::pointer; - -template -using reference_t = typename T::reference; - -template -using iterator_category_t = typename T::iterator_category; - -template -using to_json_function = decltype(T::to_json(std::declval()...)); - -template -using from_json_function = decltype(T::from_json(std::declval()...)); - -template -using get_template_function = decltype(std::declval().template get()); - -// trait checking if JSONSerializer::from_json(json const&, udt&) exists -template -struct has_from_json : std::false_type {}; - -// trait checking if j.get is valid -// use this trait instead of std::is_constructible or std::is_convertible, -// both rely on, or make use of implicit conversions, and thus fail when T -// has several constructors/operator= (see https://github.com/nlohmann/json/issues/958) -template -struct is_getable -{ - static constexpr bool value = is_detected::value; -}; - -template -struct has_from_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> -{ - using serializer = typename BasicJsonType::template json_serializer; - - static constexpr bool value = - is_detected_exact::value; -}; - -// This trait checks if JSONSerializer::from_json(json const&) exists -// this overload is used for non-default-constructible user-defined-types -template -struct has_non_default_from_json : std::false_type {}; - -template -struct has_non_default_from_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> -{ - using serializer = typename BasicJsonType::template json_serializer; - - static constexpr bool value = - is_detected_exact::value; -}; - -// This trait checks if BasicJsonType::json_serializer::to_json exists -// Do not evaluate the trait when T is a basic_json type, to avoid template instantiation infinite recursion. -template -struct has_to_json : std::false_type {}; - -template -struct has_to_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> -{ - using serializer = typename BasicJsonType::template json_serializer; - - static constexpr bool value = - is_detected_exact::value; -}; - -template -using detect_key_compare = typename T::key_compare; - -template -struct has_key_compare : std::integral_constant::value> {}; - -// obtains the actual object key comparator -template -struct actual_object_comparator -{ - using object_t = typename BasicJsonType::object_t; - using object_comparator_t = typename BasicJsonType::default_object_comparator_t; - using type = typename std::conditional < has_key_compare::value, - typename object_t::key_compare, object_comparator_t>::type; -}; - -template -using actual_object_comparator_t = typename actual_object_comparator::type; - -///////////////// -// char_traits // -///////////////// - -// Primary template of char_traits calls std char_traits -template -struct char_traits : std::char_traits -{}; - -// Explicitly define char traits for unsigned char since it is not standard -template<> -struct char_traits : std::char_traits -{ - using char_type = unsigned char; - using int_type = uint64_t; - - // Redefine to_int_type function - static int_type to_int_type(char_type c) noexcept - { - return static_cast(c); - } - - static char_type to_char_type(int_type i) noexcept - { - return static_cast(i); - } - - static constexpr int_type eof() noexcept - { - return static_cast(EOF); - } -}; - -// Explicitly define char traits for signed char since it is not standard -template<> -struct char_traits : std::char_traits -{ - using char_type = signed char; - using int_type = uint64_t; - - // Redefine to_int_type function - static int_type to_int_type(char_type c) noexcept - { - return static_cast(c); - } - - static char_type to_char_type(int_type i) noexcept - { - return static_cast(i); - } - - static constexpr int_type eof() noexcept - { - return static_cast(EOF); - } -}; - -/////////////////// -// is_ functions // -/////////////////// - -// https://en.cppreference.com/w/cpp/types/conjunction -template struct conjunction : std::true_type { }; -template struct conjunction : B { }; -template -struct conjunction -: std::conditional(B::value), conjunction, B>::type {}; - -// https://en.cppreference.com/w/cpp/types/negation -template struct negation : std::integral_constant < bool, !B::value > { }; - -// Reimplementation of is_constructible and is_default_constructible, due to them being broken for -// std::pair and std::tuple until LWG 2367 fix (see https://cplusplus.github.io/LWG/lwg-defects.html#2367). -// This causes compile errors in e.g. clang 3.5 or gcc 4.9. -template -struct is_default_constructible : std::is_default_constructible {}; - -template -struct is_default_constructible> - : conjunction, is_default_constructible> {}; - -template -struct is_default_constructible> - : conjunction, is_default_constructible> {}; - -template -struct is_default_constructible> - : conjunction...> {}; - -template -struct is_default_constructible> - : conjunction...> {}; - -template -struct is_constructible : std::is_constructible {}; - -template -struct is_constructible> : is_default_constructible> {}; - -template -struct is_constructible> : is_default_constructible> {}; - -template -struct is_constructible> : is_default_constructible> {}; - -template -struct is_constructible> : is_default_constructible> {}; - -template -struct is_iterator_traits : std::false_type {}; - -template -struct is_iterator_traits> -{ - private: - using traits = iterator_traits; - - public: - static constexpr auto value = - is_detected::value && - is_detected::value && - is_detected::value && - is_detected::value && - is_detected::value; -}; - -template -struct is_range -{ - private: - using t_ref = typename std::add_lvalue_reference::type; - - using iterator = detected_t; - using sentinel = detected_t; - - // to be 100% correct, it should use https://en.cppreference.com/w/cpp/iterator/input_or_output_iterator - // and https://en.cppreference.com/w/cpp/iterator/sentinel_for - // but reimplementing these would be too much work, as a lot of other concepts are used underneath - static constexpr auto is_iterator_begin = - is_iterator_traits>::value; - - public: - static constexpr bool value = !std::is_same::value && !std::is_same::value && is_iterator_begin; -}; - -template -using iterator_t = enable_if_t::value, result_of_begin())>>; - -template -using range_value_t = value_type_t>>; - -// The following implementation of is_complete_type is taken from -// https://blogs.msdn.microsoft.com/vcblog/2015/12/02/partial-support-for-expression-sfinae-in-vs-2015-update-1/ -// and is written by Xiang Fan who agreed to using it in this library. - -template -struct is_complete_type : std::false_type {}; - -template -struct is_complete_type : std::true_type {}; - -template -struct is_compatible_object_type_impl : std::false_type {}; - -template -struct is_compatible_object_type_impl < - BasicJsonType, CompatibleObjectType, - enable_if_t < is_detected::value&& - is_detected::value >> -{ - using object_t = typename BasicJsonType::object_t; - - // macOS's is_constructible does not play well with nonesuch... - static constexpr bool value = - is_constructible::value && - is_constructible::value; -}; - -template -struct is_compatible_object_type - : is_compatible_object_type_impl {}; - -template -struct is_constructible_object_type_impl : std::false_type {}; - -template -struct is_constructible_object_type_impl < - BasicJsonType, ConstructibleObjectType, - enable_if_t < is_detected::value&& - is_detected::value >> -{ - using object_t = typename BasicJsonType::object_t; - - static constexpr bool value = - (is_default_constructible::value && - (std::is_move_assignable::value || - std::is_copy_assignable::value) && - (is_constructible::value && - std::is_same < - typename object_t::mapped_type, - typename ConstructibleObjectType::mapped_type >::value)) || - (has_from_json::value || - has_non_default_from_json < - BasicJsonType, - typename ConstructibleObjectType::mapped_type >::value); -}; - -template -struct is_constructible_object_type - : is_constructible_object_type_impl {}; - -template -struct is_compatible_string_type -{ - static constexpr auto value = - is_constructible::value; -}; - -template -struct is_constructible_string_type -{ - // launder type through decltype() to fix compilation failure on ICPC -#ifdef __INTEL_COMPILER - using laundered_type = decltype(std::declval()); -#else - using laundered_type = ConstructibleStringType; -#endif - - static constexpr auto value = - conjunction < - is_constructible, - is_detected_exact>::value; -}; - -template -struct is_compatible_array_type_impl : std::false_type {}; - -template -struct is_compatible_array_type_impl < - BasicJsonType, CompatibleArrayType, - enable_if_t < - is_detected::value&& - is_iterator_traits>>::value&& -// special case for types like std::filesystem::path whose iterator's value_type are themselves -// c.f. https://github.com/nlohmann/json/pull/3073 - !std::is_same>::value >> -{ - static constexpr bool value = - is_constructible>::value; -}; - -template -struct is_compatible_array_type - : is_compatible_array_type_impl {}; - -template -struct is_constructible_array_type_impl : std::false_type {}; - -template -struct is_constructible_array_type_impl < - BasicJsonType, ConstructibleArrayType, - enable_if_t::value >> - : std::true_type {}; - -template -struct is_constructible_array_type_impl < - BasicJsonType, ConstructibleArrayType, - enable_if_t < !std::is_same::value&& - !is_compatible_string_type::value&& - is_default_constructible::value&& -(std::is_move_assignable::value || - std::is_copy_assignable::value)&& -is_detected::value&& -is_iterator_traits>>::value&& -is_detected::value&& -// special case for types like std::filesystem::path whose iterator's value_type are themselves -// c.f. https://github.com/nlohmann/json/pull/3073 -!std::is_same>::value&& - is_complete_type < - detected_t>::value >> -{ - using value_type = range_value_t; - - static constexpr bool value = - std::is_same::value || - has_from_json::value || - has_non_default_from_json < - BasicJsonType, - value_type >::value; -}; - -template -struct is_constructible_array_type - : is_constructible_array_type_impl {}; - -template -struct is_compatible_integer_type_impl : std::false_type {}; - -template -struct is_compatible_integer_type_impl < - RealIntegerType, CompatibleNumberIntegerType, - enable_if_t < std::is_integral::value&& - std::is_integral::value&& - !std::is_same::value >> -{ - // is there an assert somewhere on overflows? - using RealLimits = std::numeric_limits; - using CompatibleLimits = std::numeric_limits; - - static constexpr auto value = - is_constructible::value && - CompatibleLimits::is_integer && - RealLimits::is_signed == CompatibleLimits::is_signed; -}; - -template -struct is_compatible_integer_type - : is_compatible_integer_type_impl {}; - -template -struct is_compatible_type_impl: std::false_type {}; - -template -struct is_compatible_type_impl < - BasicJsonType, CompatibleType, - enable_if_t::value >> -{ - static constexpr bool value = - has_to_json::value; -}; - -template -struct is_compatible_type - : is_compatible_type_impl {}; - -template -struct is_constructible_tuple : std::false_type {}; - -template -struct is_constructible_tuple> : conjunction...> {}; - -template -struct is_json_iterator_of : std::false_type {}; - -template -struct is_json_iterator_of : std::true_type {}; - -template -struct is_json_iterator_of : std::true_type -{}; - -// checks if a given type T is a template specialization of Primary -template