From cfa25afd06ef6701faf315b21fcb69ea877cfca3 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Thu, 2 Apr 2026 19:52:13 +0200 Subject: [PATCH 01/18] feat: vendor cpp-httplib 0.14.3 as build farm fallback Add httplib.h v0.14.3 (MIT) to gateway vendored directory. Update medkit_find_cpp_httplib() macro to accept VENDORED_DIR parameter as fallback when system package is too old (Ubuntu 22.04 ships 0.10.x). Fixes #341 --- .../cmake/ROS2MedkitCompat.cmake | 7 + src/ros2_medkit_gateway/CMakeLists.txt | 4 +- .../src/vendored/cpp_httplib/LICENSE | 21 + .../src/vendored/cpp_httplib/httplib.h | 2272 +++++++++++++++++ 4 files changed, 2303 insertions(+), 1 deletion(-) create mode 100644 src/ros2_medkit_gateway/src/vendored/cpp_httplib/LICENSE create mode 100644 src/ros2_medkit_gateway/src/vendored/cpp_httplib/httplib.h diff --git a/src/ros2_medkit_cmake/cmake/ROS2MedkitCompat.cmake b/src/ros2_medkit_cmake/cmake/ROS2MedkitCompat.cmake index eb48ff881..550f74800 100644 --- a/src/ros2_medkit_cmake/cmake/ROS2MedkitCompat.cmake +++ b/src/ros2_medkit_cmake/cmake/ROS2MedkitCompat.cmake @@ -83,6 +83,7 @@ endmacro() # Creates a unified alias target `cpp_httplib_target` for consumers. # --------------------------------------------------------------------------- macro(medkit_find_cpp_httplib) + cmake_parse_arguments(_mfch "" "VENDORED_DIR" "" ${ARGN}) find_package(PkgConfig QUIET) if(PkgConfig_FOUND) pkg_check_modules(cpp_httplib IMPORTED_TARGET cpp-httplib>=0.14) @@ -95,6 +96,11 @@ macro(medkit_find_cpp_httplib) if(TARGET httplib::httplib) add_library(cpp_httplib_target ALIAS httplib::httplib) message(STATUS "[MedkitCompat] cpp-httplib: using cmake config (source build)") + elseif(_mfch_VENDORED_DIR AND EXISTS "${_mfch_VENDORED_DIR}/httplib.h") + add_library(cpp_httplib_vendored INTERFACE) + target_include_directories(cpp_httplib_vendored INTERFACE "${_mfch_VENDORED_DIR}") + add_library(cpp_httplib_target ALIAS cpp_httplib_vendored) + message(STATUS "[MedkitCompat] cpp-httplib: using vendored header (${_mfch_VENDORED_DIR}/httplib.h)") else() message(FATAL_ERROR "[MedkitCompat] Could not find cpp-httplib >= 0.14.\n" @@ -109,6 +115,7 @@ macro(medkit_find_cpp_httplib) " See: https://selfpatch.github.io/ros2_medkit/installation.html") endif() endif() + unset(_mfch_VENDORED_DIR) endmacro() # --------------------------------------------------------------------------- diff --git a/src/ros2_medkit_gateway/CMakeLists.txt b/src/ros2_medkit_gateway/CMakeLists.txt index c51c553a2..7ac4d608d 100644 --- a/src/ros2_medkit_gateway/CMakeLists.txt +++ b/src/ros2_medkit_gateway/CMakeLists.txt @@ -53,7 +53,7 @@ find_package(SQLite3 REQUIRED) find_package(rosidl_typesupport_cpp REQUIRED) find_package(rosidl_typesupport_introspection_cpp REQUIRED) -medkit_find_cpp_httplib() +medkit_find_cpp_httplib(VENDORED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/vendored/cpp_httplib") # Find OpenSSL (required by jwt-cpp for RS256 and optional TLS support) find_package(OpenSSL REQUIRED) @@ -319,6 +319,7 @@ if(BUILD_TESTING) "src/vendored/jwt_cpp/include/picojson/picojson.h" "src/vendored/tl_expected/include/tl/expected.hpp" "src/vendored/mdns/mdns.h" + "src/vendored/cpp_httplib/httplib.h" ) ament_copyright(EXCLUDE ${VENDORED_FILES}) @@ -772,6 +773,7 @@ endif() # Export include directories for downstream packages (plugins) install(DIRECTORY include/ DESTINATION include) install(DIRECTORY src/vendored/tl_expected/include/ DESTINATION include/ros2_medkit_gateway/vendored) +install(FILES src/vendored/cpp_httplib/httplib.h DESTINATION include/ros2_medkit_gateway/vendored) ament_export_include_directories(include include/ros2_medkit_gateway/vendored) ament_package() diff --git a/src/ros2_medkit_gateway/src/vendored/cpp_httplib/LICENSE b/src/ros2_medkit_gateway/src/vendored/cpp_httplib/LICENSE new file mode 100644 index 000000000..65892a3b2 --- /dev/null +++ b/src/ros2_medkit_gateway/src/vendored/cpp_httplib/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Yuji Hirose. All rights reserved. + +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/src/ros2_medkit_gateway/src/vendored/cpp_httplib/httplib.h b/src/ros2_medkit_gateway/src/vendored/cpp_httplib/httplib.h new file mode 100644 index 000000000..9510585a7 --- /dev/null +++ b/src/ros2_medkit_gateway/src/vendored/cpp_httplib/httplib.h @@ -0,0 +1,2272 @@ +// +// httplib.h +// +// Copyright (c) 2023 Yuji Hirose. All rights reserved. +// MIT License +// + +#ifndef CPPHTTPLIB_HTTPLIB_H +#define CPPHTTPLIB_HTTPLIB_H + +#define CPPHTTPLIB_VERSION "0.14.3" + +/* + * Configuration + */ + +#ifndef CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND +#define CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND 5 +#endif + +#ifndef CPPHTTPLIB_KEEPALIVE_MAX_COUNT +#define CPPHTTPLIB_KEEPALIVE_MAX_COUNT 5 +#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_READ_TIMEOUT_SECOND +#define CPPHTTPLIB_READ_TIMEOUT_SECOND 5 +#endif + +#ifndef CPPHTTPLIB_READ_TIMEOUT_USECOND +#define CPPHTTPLIB_READ_TIMEOUT_USECOND 0 +#endif + +#ifndef CPPHTTPLIB_WRITE_TIMEOUT_SECOND +#define CPPHTTPLIB_WRITE_TIMEOUT_SECOND 5 +#endif + +#ifndef CPPHTTPLIB_WRITE_TIMEOUT_USECOND +#define CPPHTTPLIB_WRITE_TIMEOUT_USECOND 0 +#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 10000 +#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_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 ((std::numeric_limits::max)()) +#endif + +#ifndef CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH +#define CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH 8192 +#endif + +#ifndef CPPHTTPLIB_TCP_NODELAY +#define CPPHTTPLIB_TCP_NODELAY false +#endif + +#ifndef CPPHTTPLIB_RECV_BUFSIZ +#define CPPHTTPLIB_RECV_BUFSIZ size_t(4096u) +#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_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 + +/* + * 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") + +#ifdef _WIN64 +using ssize_t = __int64; +#else +using ssize_t = long; +#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 + +#ifndef WSA_FLAG_NO_HANDLE_INHERIT +#define WSA_FLAG_NO_HANDLE_INHERIT 0x80 +#endif + +#ifndef strcasecmp +#define strcasecmp _stricmp +#endif // strcasecmp + +using socket_t = SOCKET; +#ifdef CPPHTTPLIB_USE_POLL +#define poll(fds, nfds, timeout) WSAPoll(fds, nfds, timeout) +#endif + +#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 +#endif +#include +#ifdef CPPHTTPLIB_USE_POLL +#include +#endif +#include +#include +#include +#include +#include +#include +#include + +using socket_t = int; +#ifndef INVALID_SOCKET +#define INVALID_SOCKET (-1) +#endif +#endif //_WIN32 + +#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 + +#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 +#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && defined(__APPLE__) +#include +#if TARGET_OS_OSX +#include +#include +#endif // TARGET_OS_OSX +#endif // _WIN32 + +#include +#include +#include +#include + +#if defined(_WIN32) && defined(OPENSSL_USE_APPLINK) +#include +#endif + +#include +#include + +#if OPENSSL_VERSION_NUMBER < 0x1010100fL +#error Sorry, OpenSSL versions prior to 1.1.1 are not supported +#elif OPENSSL_VERSION_NUMBER < 0x30000000L +#define SSL_get1_peer_certificate SSL_get_peer_certificate +#endif + +#endif + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT +#include +#endif + +#ifdef CPPHTTPLIB_BROTLI_SUPPORT +#include +#include +#endif + +/* + * Declaration + */ +namespace httplib { + +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]); +} + +struct ci { + bool operator()(const std::string &s1, const std::string &s2) const { + return std::lexicographical_compare(s1.begin(), s1.end(), s2.begin(), + s2.end(), + [](unsigned char c1, unsigned char c2) { + return ::tolower(c1) < ::tolower(c2); + }); + } +}; + +// 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; +}; + +} // namespace detail + +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::multimap; + +using Params = std::multimap; +using Match = std::smatch; + +using Progress = std::function; + +struct Response; +using ResponseHandler = std::function; + +struct MultipartFormData { + std::string name; + std::string content; + std::string filename; + std::string content_type; +}; +using MultipartFormDataItems = std::vector; +using MultipartFormDataMap = std::multimap; + +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 : public std::streambuf { + public: + explicit data_sink_streambuf(DataSink &sink) : sink_(sink) {} + + protected: + std::streamsize xsputn(const char *s, std::streamsize n) override { + sink_.write(s, static_cast(n)); + return n; + } + + private: + DataSink &sink_; + }; + + data_sink_streambuf sb_; +}; + +using ContentProvider = + std::function; + +using ContentProviderWithoutLength = + std::function; + +using ContentProviderResourceReleaser = std::function; + +struct MultipartFormDataProvider { + std::string name; + ContentProviderWithoutLength provider; + std::string filename; + std::string content_type; +}; +using MultipartFormDataProviderItems = std::vector; + +using ContentReceiverWithProgress = + std::function; + +using ContentReceiver = + std::function; + +using MultipartContentHeader = + std::function; + +class ContentReader { +public: + using Reader = std::function; + using MultipartReader = std::function; + + ContentReader(Reader reader, MultipartReader multipart_reader) + : reader_(std::move(reader)), + multipart_reader_(std::move(multipart_reader)) {} + + bool operator()(MultipartContentHeader header, + ContentReceiver receiver) const { + return multipart_reader_(std::move(header), std::move(receiver)); + } + + bool operator()(ContentReceiver receiver) const { + return reader_(std::move(receiver)); + } + + Reader reader_; + MultipartReader multipart_reader_; +}; + +using Range = std::pair; +using Ranges = std::vector; + +struct Request { + std::string method; + std::string path; + Headers headers; + 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; + Params params; + MultipartFormDataMap files; + Ranges ranges; + Match matches; + std::unordered_map path_params; + + // for client + ResponseHandler response_handler; + ContentReceiverWithProgress content_receiver; + Progress progress; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + const SSL *ssl = nullptr; +#endif + + bool has_header(const std::string &key) const; + std::string get_header_value(const std::string &key, size_t id = 0) const; + uint64_t get_header_value_u64(const std::string &key, 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_param(const std::string &key) const; + std::string get_param_value(const std::string &key, size_t id = 0) const; + size_t get_param_value_count(const std::string &key) const; + + bool is_multipart_form_data() const; + + bool has_file(const std::string &key) const; + MultipartFormData get_file_value(const std::string &key) const; + std::vector get_file_values(const std::string &key) const; + + // private members... + 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; +}; + +struct Response { + std::string version; + int status = -1; + std::string reason; + Headers headers; + std::string body; + std::string location; // Redirect location + + bool has_header(const std::string &key) const; + std::string get_header_value(const std::string &key, size_t id = 0) const; + uint64_t get_header_value_u64(const std::string &key, 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); + + 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_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); + + 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; +}; + +class Stream { +public: + virtual ~Stream() = default; + + virtual bool is_readable() const = 0; + virtual bool is_writable() const = 0; + + 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; + + template + ssize_t write_format(const char *fmt, const Args &...args); + ssize_t write(const char *ptr); + ssize_t write(const std::string &s); +}; + +class TaskQueue { +public: + TaskQueue() = default; + virtual ~TaskQueue() = default; + + virtual void enqueue(std::function fn) = 0; + virtual void shutdown() = 0; + + virtual void on_idle() {} +}; + +class ThreadPool : public TaskQueue { +public: + explicit ThreadPool(size_t n) : shutdown_(false) { + while (n) { + threads_.emplace_back(worker(*this)); + n--; + } + } + + ThreadPool(const ThreadPool &) = delete; + ~ThreadPool() override = default; + + void enqueue(std::function fn) override { + { + std::unique_lock lock(mutex_); + jobs_.push_back(std::move(fn)); + } + + cond_.notify_one(); + } + + void shutdown() override { + // Stop all worker threads... + { + std::unique_lock lock(mutex_); + shutdown_ = true; + } + + cond_.notify_all(); + + // Join... + for (auto &t : threads_) { + t.join(); + } + } + +private: + struct worker { + explicit worker(ThreadPool &pool) : pool_(pool) {} + + void operator()() { + for (;;) { + std::function fn; + { + std::unique_lock lock(pool_.mutex_); + + pool_.cond_.wait( + lock, [&] { return !pool_.jobs_.empty() || pool_.shutdown_; }); + + if (pool_.shutdown_ && pool_.jobs_.empty()) { break; } + + fn = std::move(pool_.jobs_.front()); + pool_.jobs_.pop_front(); + } + + assert(true == static_cast(fn)); + fn(); + } + } + + ThreadPool &pool_; + }; + friend struct worker; + + std::vector threads_; + std::list> jobs_; + + bool shutdown_; + + std::condition_variable cond_; + std::mutex mutex_; +}; + +using Logger = std::function; + +using SocketOptions = std::function; + +void default_socket_options(socket_t sock); + +const char *status_message(int status); + +namespace detail { + +class MatcherBase { +public: + virtual ~MatcherBase() = default; + + // Match request path and populate its matches and + virtual bool match(Request &request) const = 0; +}; + +/** + * 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 agains 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 : public MatcherBase { +public: + PathParamsMatcher(const std::string &pattern); + + bool match(Request &request) const override; + +private: + static constexpr char marker = ':'; + // 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 : public MatcherBase { +public: + RegexMatcher(const std::string &pattern) : regex_(pattern) {} + + bool match(Request &request) const override; + +private: + std::regex regex_; +}; + +ssize_t write_headers(Stream &strm, const Headers &headers); + +} // 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; + + 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); + + 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); + + Server &set_error_handler(HandlerWithResponse handler); + Server &set_error_handler(Handler handler); + Server &set_exception_handler(ExceptionHandler handler); + Server &set_pre_routing_handler(HandlerWithResponse handler); + Server &set_post_routing_handler(Handler handler); + + Server &set_expect_100_continue_handler(Expect100ContinueHandler handler); + Server &set_logger(Logger logger); + + Server &set_address_family(int family); + Server &set_tcp_nodelay(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_keep_alive_max_count(size_t count); + Server &set_keep_alive_timeout(time_t sec); + + 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); + + 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(); + + std::function new_task_queue; + +protected: + bool process_request(Stream &strm, bool close_connection, + bool &connection_closed, + const std::function &setup_request); + + std::atomic svr_sock_{INVALID_SOCKET}; + 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_READ_TIMEOUT_SECOND; + time_t read_timeout_usec_ = CPPHTTPLIB_READ_TIMEOUT_USECOND; + time_t write_timeout_sec_ = CPPHTTPLIB_WRITE_TIMEOUT_SECOND; + time_t write_timeout_usec_ = CPPHTTPLIB_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; + +private: + using Handlers = + std::vector, Handler>>; + using HandlersForContentReader = + std::vector, + HandlerWithContentReader>>; + + static std::unique_ptr + make_matcher(const std::string &pattern); + + 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(const Request &req, Response &res, + bool head = false); + 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, const 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, + MultipartContentHeader multipart_header, + ContentReceiver multipart_receiver); + bool read_content_core(Stream &strm, Request &req, Response &res, + ContentReceiver receiver, + MultipartContentHeader multipart_header, + ContentReceiver multipart_receiver) const; + + virtual bool process_and_close_socket(socket_t sock); + + std::atomic is_running_{false}; + std::atomic done_{false}; + + struct MountPointEntry { + std::string mount_point; + std::string 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_; + + HandlerWithResponse error_handler_; + ExceptionHandler exception_handler_; + HandlerWithResponse pre_routing_handler_; + Handler post_routing_handler_; + Expect100ContinueHandler expect_100_continue_handler_; + + Logger logger_; + + int address_family_ = AF_UNSPEC; + bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY; + SocketOptions socket_options_ = default_socket_options; + + Headers default_headers_; + std::function header_writer_ = + detail::write_headers; +}; + +enum class Error { + Success = 0, + Unknown, + Connection, + BindIPAddress, + Read, + Write, + ExceedRedirectCount, + Canceled, + SSLConnection, + SSLLoadingCerts, + SSLServerVerification, + UnsupportedMultipartBoundaryChars, + Compression, + ConnectionTimeout, + ProxyConnection, + + // For internal use only + SSLPeerCouldBeClosed_, +}; + +std::string to_string(Error error); + +std::ostream &operator<<(std::ostream &os, const Error &obj); + +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, + size_t id = 0) const; + uint64_t get_request_header_value_u64(const std::string &key, + 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_; +}; + +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; + + Result Get(const std::string &path); + Result Get(const std::string &path, const Headers &headers); + Result Get(const std::string &path, Progress progress); + Result Get(const std::string &path, const Headers &headers, + Progress progress); + Result Get(const std::string &path, ContentReceiver content_receiver); + Result Get(const std::string &path, const Headers &headers, + ContentReceiver content_receiver); + Result Get(const std::string &path, ContentReceiver content_receiver, + Progress progress); + Result Get(const std::string &path, const Headers &headers, + ContentReceiver content_receiver, Progress progress); + Result Get(const std::string &path, ResponseHandler response_handler, + ContentReceiver content_receiver); + Result Get(const std::string &path, const Headers &headers, + ResponseHandler response_handler, + ContentReceiver content_receiver); + Result Get(const std::string &path, ResponseHandler response_handler, + ContentReceiver content_receiver, Progress progress); + Result Get(const std::string &path, const Headers &headers, + ResponseHandler response_handler, ContentReceiver content_receiver, + Progress progress); + + Result Get(const std::string &path, const Params ¶ms, + const Headers &headers, Progress progress = nullptr); + Result Get(const std::string &path, const Params ¶ms, + const Headers &headers, ContentReceiver content_receiver, + Progress progress = nullptr); + Result Get(const std::string &path, const Params ¶ms, + const Headers &headers, ResponseHandler response_handler, + ContentReceiver content_receiver, Progress 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 Headers &headers); + Result Post(const std::string &path, const char *body, size_t content_length, + const std::string &content_type); + Result Post(const std::string &path, const Headers &headers, const char *body, + size_t content_length, const std::string &content_type); + Result Post(const std::string &path, const std::string &body, + const std::string &content_type); + Result Post(const std::string &path, const Headers &headers, + const std::string &body, const std::string &content_type); + Result Post(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type); + Result Post(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type); + Result Post(const std::string &path, const Headers &headers, + size_t content_length, ContentProvider content_provider, + const std::string &content_type); + Result Post(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type); + Result Post(const std::string &path, const Params ¶ms); + Result Post(const std::string &path, const Headers &headers, + const Params ¶ms); + Result Post(const std::string &path, const MultipartFormDataItems &items); + Result Post(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items); + Result Post(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items, const std::string &boundary); + Result Post(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items, + const MultipartFormDataProviderItems &provider_items); + + Result Put(const std::string &path); + Result Put(const std::string &path, const char *body, size_t content_length, + const std::string &content_type); + Result Put(const std::string &path, const Headers &headers, const char *body, + size_t content_length, const std::string &content_type); + Result Put(const std::string &path, const std::string &body, + const std::string &content_type); + Result Put(const std::string &path, const Headers &headers, + const std::string &body, const std::string &content_type); + Result Put(const std::string &path, size_t content_length, + ContentProvider content_provider, const std::string &content_type); + Result Put(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type); + Result Put(const std::string &path, const Headers &headers, + size_t content_length, ContentProvider content_provider, + const std::string &content_type); + Result Put(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type); + Result Put(const std::string &path, const Params ¶ms); + Result Put(const std::string &path, const Headers &headers, + const Params ¶ms); + Result Put(const std::string &path, const MultipartFormDataItems &items); + Result Put(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items); + Result Put(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items, const std::string &boundary); + Result Put(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items, + const MultipartFormDataProviderItems &provider_items); + + Result Patch(const std::string &path); + Result Patch(const std::string &path, const char *body, size_t content_length, + const std::string &content_type); + Result Patch(const std::string &path, const Headers &headers, + const char *body, size_t content_length, + const std::string &content_type); + Result Patch(const std::string &path, const std::string &body, + const std::string &content_type); + Result Patch(const std::string &path, const Headers &headers, + const std::string &body, const std::string &content_type); + Result Patch(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type); + Result Patch(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type); + Result Patch(const std::string &path, const Headers &headers, + size_t content_length, ContentProvider content_provider, + const std::string &content_type); + Result Patch(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type); + + Result Delete(const std::string &path); + Result Delete(const std::string &path, const Headers &headers); + Result Delete(const std::string &path, const char *body, + size_t content_length, const std::string &content_type); + Result Delete(const std::string &path, const Headers &headers, + const char *body, size_t content_length, + const std::string &content_type); + Result Delete(const std::string &path, const std::string &body, + const std::string &content_type); + Result Delete(const std::string &path, const Headers &headers, + const std::string &body, const std::string &content_type); + + Result Options(const std::string &path); + Result Options(const std::string &path, const Headers &headers); + + 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_basic_auth(const std::string &username, const std::string &password); + void set_bearer_token_auth(const std::string &token); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void set_digest_auth(const std::string &username, + const std::string &password); +#endif + + void set_keep_alive(bool on); + void set_follow_location(bool on); + + void set_url_encode(bool on); + + void set_compress(bool on); + + void set_decompress(bool on); + + 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); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void set_proxy_digest_auth(const std::string &username, + const std::string &password); +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + 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(X509_STORE *ca_cert_store); + X509_STORE *create_ca_cert_store(const char *ca_cert, std::size_t size) const; +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void enable_server_certificate_verification(bool enabled); +#endif + + void set_logger(Logger logger); + +protected: + struct Socket { + socket_t sock = INVALID_SOCKET; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + SSL *ssl = nullptr; +#endif + + bool is_open() const { return sock != INVALID_SOCKET; } + }; + + virtual bool create_and_connect_socket(Socket &socket, Error &error); + + // All of: + // shutdown_ssl + // shutdown_socket + // close_socket + // should ONLY be called when socket_mutex_ is locked. + // Also, shutdown_ssl and close_socket should also NOT be called concurrently + // with a DIFFERENT thread sending requests using that socket. + virtual void shutdown_ssl(Socket &socket, bool shutdown_gracefully); + void shutdown_socket(Socket &socket) const; + void close_socket(Socket &socket); + + 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); + + // Socket endpoint information + const std::string host_; + const int port_; + const std::string host_and_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_READ_TIMEOUT_SECOND; + time_t read_timeout_usec_ = CPPHTTPLIB_READ_TIMEOUT_USECOND; + time_t write_timeout_sec_ = CPPHTTPLIB_WRITE_TIMEOUT_SECOND; + time_t write_timeout_usec_ = CPPHTTPLIB_WRITE_TIMEOUT_USECOND; + + std::string basic_auth_username_; + std::string basic_auth_password_; + std::string bearer_token_auth_token_; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + std::string digest_auth_username_; + std::string digest_auth_password_; +#endif + + bool keep_alive_ = false; + bool follow_location_ = false; + + bool url_encode_ = true; + + int address_family_ = AF_UNSPEC; + bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY; + SocketOptions socket_options_ = nullptr; + + bool compress_ = false; + bool decompress_ = true; + + 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_; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + std::string proxy_digest_auth_username_; + std::string proxy_digest_auth_password_; +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + std::string ca_cert_file_path_; + std::string ca_cert_dir_path_; + + X509_STORE *ca_cert_store_ = nullptr; +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + bool server_certificate_verification_ = true; +#endif + + Logger 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) const; + bool write_request(Stream &strm, Request &req, bool close_connection, + Error &error); + bool redirect(Request &req, Response &res, Error &error); + bool handle_request(Stream &strm, Request &req, Response &res, + bool close_connection, Error &error); + std::unique_ptr send_with_content_provider( + Request &req, const char *body, size_t content_length, + ContentProvider content_provider, + ContentProviderWithoutLength content_provider_without_length, + const std::string &content_type, Error &error); + Result send_with_content_provider( + 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); + ContentProviderWithoutLength get_multipart_content_provider( + const std::string &boundary, const MultipartFormDataItems &items, + const MultipartFormDataProviderItems &provider_items) const; + + std::string adjust_host_string(const std::string &host) const; + + virtual bool process_socket(const Socket &socket, + std::function callback); + virtual bool is_ssl() const; +}; + +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(); + + bool is_valid() const; + + Result Get(const std::string &path); + Result Get(const std::string &path, const Headers &headers); + Result Get(const std::string &path, Progress progress); + Result Get(const std::string &path, const Headers &headers, + Progress progress); + Result Get(const std::string &path, ContentReceiver content_receiver); + Result Get(const std::string &path, const Headers &headers, + ContentReceiver content_receiver); + Result Get(const std::string &path, ContentReceiver content_receiver, + Progress progress); + Result Get(const std::string &path, const Headers &headers, + ContentReceiver content_receiver, Progress progress); + Result Get(const std::string &path, ResponseHandler response_handler, + ContentReceiver content_receiver); + Result Get(const std::string &path, const Headers &headers, + ResponseHandler response_handler, + ContentReceiver content_receiver); + Result Get(const std::string &path, const Headers &headers, + ResponseHandler response_handler, ContentReceiver content_receiver, + Progress progress); + Result Get(const std::string &path, ResponseHandler response_handler, + ContentReceiver content_receiver, Progress progress); + + Result Get(const std::string &path, const Params ¶ms, + const Headers &headers, Progress progress = nullptr); + Result Get(const std::string &path, const Params ¶ms, + const Headers &headers, ContentReceiver content_receiver, + Progress progress = nullptr); + Result Get(const std::string &path, const Params ¶ms, + const Headers &headers, ResponseHandler response_handler, + ContentReceiver content_receiver, Progress 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 Headers &headers); + Result Post(const std::string &path, const char *body, size_t content_length, + const std::string &content_type); + Result Post(const std::string &path, const Headers &headers, const char *body, + size_t content_length, const std::string &content_type); + Result Post(const std::string &path, const std::string &body, + const std::string &content_type); + Result Post(const std::string &path, const Headers &headers, + const std::string &body, const std::string &content_type); + Result Post(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type); + Result Post(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type); + Result Post(const std::string &path, const Headers &headers, + size_t content_length, ContentProvider content_provider, + const std::string &content_type); + Result Post(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type); + Result Post(const std::string &path, const Params ¶ms); + Result Post(const std::string &path, const Headers &headers, + const Params ¶ms); + Result Post(const std::string &path, const MultipartFormDataItems &items); + Result Post(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items); + Result Post(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items, const std::string &boundary); + Result Post(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items, + const MultipartFormDataProviderItems &provider_items); + + Result Put(const std::string &path); + Result Put(const std::string &path, const char *body, size_t content_length, + const std::string &content_type); + Result Put(const std::string &path, const Headers &headers, const char *body, + size_t content_length, const std::string &content_type); + Result Put(const std::string &path, const std::string &body, + const std::string &content_type); + Result Put(const std::string &path, const Headers &headers, + const std::string &body, const std::string &content_type); + Result Put(const std::string &path, size_t content_length, + ContentProvider content_provider, const std::string &content_type); + Result Put(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type); + Result Put(const std::string &path, const Headers &headers, + size_t content_length, ContentProvider content_provider, + const std::string &content_type); + Result Put(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type); + Result Put(const std::string &path, const Params ¶ms); + Result Put(const std::string &path, const Headers &headers, + const Params ¶ms); + Result Put(const std::string &path, const MultipartFormDataItems &items); + Result Put(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items); + Result Put(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items, const std::string &boundary); + Result Put(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items, + const MultipartFormDataProviderItems &provider_items); + + Result Patch(const std::string &path); + Result Patch(const std::string &path, const char *body, size_t content_length, + const std::string &content_type); + Result Patch(const std::string &path, const Headers &headers, + const char *body, size_t content_length, + const std::string &content_type); + Result Patch(const std::string &path, const std::string &body, + const std::string &content_type); + Result Patch(const std::string &path, const Headers &headers, + const std::string &body, const std::string &content_type); + Result Patch(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type); + Result Patch(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type); + Result Patch(const std::string &path, const Headers &headers, + size_t content_length, ContentProvider content_provider, + const std::string &content_type); + Result Patch(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type); + + Result Delete(const std::string &path); + Result Delete(const std::string &path, const Headers &headers); + Result Delete(const std::string &path, const char *body, + size_t content_length, const std::string &content_type); + Result Delete(const std::string &path, const Headers &headers, + const char *body, size_t content_length, + const std::string &content_type); + Result Delete(const std::string &path, const std::string &body, + const std::string &content_type); + Result Delete(const std::string &path, const Headers &headers, + const std::string &body, const std::string &content_type); + + Result Options(const std::string &path); + Result Options(const std::string &path, const Headers &headers); + + 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_basic_auth(const std::string &username, const std::string &password); + void set_bearer_token_auth(const std::string &token); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void set_digest_auth(const std::string &username, + const std::string &password); +#endif + + void set_keep_alive(bool on); + void set_follow_location(bool on); + + void set_url_encode(bool on); + + void set_compress(bool on); + + void set_decompress(bool on); + + 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); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void set_proxy_digest_auth(const std::string &username, + const std::string &password); +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + void enable_server_certificate_verification(bool enabled); +#endif + + void set_logger(Logger logger); + + // SSL +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + 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(X509_STORE *ca_cert_store); + void load_ca_cert_store(const char *ca_cert, std::size_t size); + + long get_openssl_verify_result() const; + + SSL_CTX *ssl_context() const; +#endif + +private: + std::unique_ptr cli_; + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + bool is_ssl_ = false; +#endif +}; + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +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); + + SSLServer(X509 *cert, EVP_PKEY *private_key, + X509_STORE *client_ca_cert_store = nullptr); + + SSLServer( + const std::function &setup_ssl_ctx_callback); + + ~SSLServer() override; + + bool is_valid() const override; + + SSL_CTX *ssl_context() const; + +private: + bool process_and_close_socket(socket_t sock) override; + + SSL_CTX *ctx_; + std::mutex ctx_mutex_; +}; + +class SSLClient : 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); + + explicit SSLClient(const std::string &host, int port, X509 *client_cert, + EVP_PKEY *client_key); + + ~SSLClient() override; + + bool is_valid() const override; + + void set_ca_cert_store(X509_STORE *ca_cert_store); + void load_ca_cert_store(const char *ca_cert, std::size_t size); + + long get_openssl_verify_result() const; + + SSL_CTX *ssl_context() const; + +private: + bool create_and_connect_socket(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::function callback) override; + bool is_ssl() const override; + + bool connect_with_proxy(Socket &sock, Response &res, bool &success, + Error &error); + bool initialize_ssl(Socket &socket, Error &error); + + bool load_certs(); + + 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; + bool check_host_name(const char *pattern, size_t pattern_len) const; + + SSL_CTX *ctx_; + std::mutex ctx_mutex_; + std::once_flag initialize_cert_; + + std::vector host_components_; + + long verify_result_ = 0; + + friend class ClientImpl; +}; +#endif + +/* + * Implementation of template methods. + */ + +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)); +} + +inline uint64_t get_header_value_u64(const Headers &headers, + const std::string &key, size_t id, + uint64_t def) { + auto rng = headers.equal_range(key); + auto it = rng.first; + std::advance(it, static_cast(id)); + if (it != rng.second) { + return std::strtoull(it->second.data(), nullptr, 10); + } + return def; +} + +} // namespace detail + +inline uint64_t Request::get_header_value_u64(const std::string &key, + size_t id) const { + return detail::get_header_value_u64(headers, key, id, 0); +} + +inline uint64_t Response::get_header_value_u64(const std::string &key, + size_t id) const { + return detail::get_header_value_u64(headers, key, id, 0); +} + +template +inline ssize_t Stream::write_format(const char *fmt, const Args &...args) { + const auto bufsiz = 2048; + std::array buf{}; + + auto sn = snprintf(buf.data(), buf.size() - 1, fmt, args...); + if (sn <= 0) { return sn; } + + auto n = static_cast(sn); + + if (n >= buf.size() - 1) { + std::vector glowable_buf(buf.size()); + + while (n >= glowable_buf.size() - 1) { + glowable_buf.resize(glowable_buf.size() * 2); + n = static_cast( + snprintf(&glowable_buf[0], glowable_buf.size() - 1, fmt, args...)); + } + return write(&glowable_buf[0], n); + } else { + return write(buf.data(), n); + } +} + +inline void default_socket_options(socket_t sock) { + int yes = 1; +#ifdef _WIN32 + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, + reinterpret_cast(&yes), sizeof(yes)); + setsockopt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, + reinterpret_cast(&yes), sizeof(yes)); +#else +#ifdef SO_REUSEPORT + setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, + reinterpret_cast(&yes), sizeof(yes)); +#else + setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, + reinterpret_cast(&yes), sizeof(yes)); +#endif +#endif +} + +inline 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"; + } +} + +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; +} + +inline std::string to_string(const Error error) { + switch (error) { + case Error::Success: return "Success (no error)"; + 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::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::Unknown: return "Unknown"; + default: break; + } + + return "Invalid"; +} + +inline std::ostream &operator<<(std::ostream &os, const Error &obj) { + os << to_string(obj); + os << " (" << static_cast::type>(obj) << ')'; + return os; +} + +inline uint64_t Result::get_request_header_value_u64(const std::string &key, + size_t id) const { + return detail::get_header_value_u64(request_headers_, key, id, 0); +} + +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 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); +} + +/* + * 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); + +std::string append_query_params(const std::string &path, const Params ¶ms); + +std::pair make_range_header(Ranges ranges); + +std::pair +make_basic_authentication_header(const std::string &username, + const std::string &password, + bool is_proxy = false); + +namespace detail { + +std::string encode_query_param(const std::string &value); + +std::string decode_url(const std::string &s, bool convert_plus_to_space); + +void read_file(const std::string &path, std::string &out); + +std::string trim_copy(const std::string &s); + +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, + std::function callback); + +socket_t create_client_socket( + const std::string &host, const std::string &ip, int port, + int address_family, bool tcp_nodelay, 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, + size_t id = 0, const char *def = nullptr); + +std::string params_to_query_str(const 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); + +int close_socket(socket_t sock); + +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 }; + +EncodingType encoding_type(const Request &req, const Response &res); + +class BufferStream : public Stream { +public: + BufferStream() = default; + ~BufferStream() override = default; + + bool is_readable() const override; + bool is_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; + + 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 : 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 : 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 : 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 : 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 : 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 + +// 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 glowable_buffer_; +}; + +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_; + HANDLE hMapping_; +#else + int fd_; +#endif + size_t size_; + void *addr_; +}; + +} // namespace detail + + +} // namespace httplib + +#if defined(_WIN32) && defined(CPPHTTPLIB_USE_POLL) +#undef poll +#endif + +#endif // CPPHTTPLIB_HTTPLIB_H From d350a3766156dc3a9e81f4e337469affb282e223 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Thu, 2 Apr 2026 20:03:43 +0200 Subject: [PATCH 02/18] feat: add PluginRequest/PluginResponse wrapper types Thin wrappers over httplib::Request/Response that hide the HTTP library from plugin code. Implementation compiled in gateway_lib; plugins resolve symbols at runtime from the gateway process. --- src/ros2_medkit_gateway/CMakeLists.txt | 6 ++ .../plugins/plugin_http_types.hpp | 66 ++++++++++++++ .../src/plugins/plugin_http_types.cpp | 64 ++++++++++++++ .../test/test_plugin_http_types.cpp | 86 +++++++++++++++++++ 4 files changed, 222 insertions(+) create mode 100644 src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_http_types.hpp create mode 100644 src/ros2_medkit_gateway/src/plugins/plugin_http_types.cpp create mode 100644 src/ros2_medkit_gateway/test/test_plugin_http_types.cpp diff --git a/src/ros2_medkit_gateway/CMakeLists.txt b/src/ros2_medkit_gateway/CMakeLists.txt index 7ac4d608d..2fed3da85 100644 --- a/src/ros2_medkit_gateway/CMakeLists.txt +++ b/src/ros2_medkit_gateway/CMakeLists.txt @@ -174,6 +174,7 @@ add_library(gateway_lib STATIC src/plugins/plugin_context.cpp src/plugins/plugin_loader.cpp src/plugins/plugin_manager.cpp + src/plugins/plugin_http_types.cpp # OpenAPI documentation module src/openapi/schema_builder.cpp src/openapi/path_builder.cpp @@ -592,6 +593,10 @@ if(BUILD_TESTING) ament_add_gtest(test_plugin_manager test/test_plugin_manager.cpp) target_link_libraries(test_plugin_manager gateway_lib) + # Plugin HTTP types tests + ament_add_gtest(test_plugin_http_types test/test_plugin_http_types.cpp) + target_link_libraries(test_plugin_http_types gateway_lib) + # Log manager tests # Dedicated ROS_DOMAIN_ID to prevent cross-talk with concurrent integration tests ament_add_gtest(test_log_manager test/test_log_manager.cpp) @@ -736,6 +741,7 @@ if(BUILD_TESTING) test_operation_handlers test_plugin_loader test_plugin_manager + test_plugin_http_types test_log_manager test_log_handlers test_merge_pipeline diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_http_types.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_http_types.hpp new file mode 100644 index 000000000..ab23cc5cb --- /dev/null +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_http_types.hpp @@ -0,0 +1,66 @@ +// Copyright 2026 bburda +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +namespace ros2_medkit_gateway { + +/// Thin read-only wrapper over an HTTP request. +/// Hides the underlying HTTP library (currently cpp-httplib) from plugin code. +/// Constructed by the gateway per-request; plugins receive it by const reference. +class PluginRequest { + public: + /// Construct from opaque pointer to httplib::Request. + explicit PluginRequest(const void * impl); + + /// Extract a path parameter by regex capture group index. + /// Index 0 is the full match; index 1 is the first capture group. + std::string path_param(size_t index) const; + + /// Get a request header value by name. Returns empty string if not present. + std::string header(const std::string & name) const; + + /// Full request path (e.g. "/api/v1/apps/my_app/data"). + std::string path() const; + + /// Request body as string. + std::string body() const; + + private: + const void * impl_; +}; + +/// Thin wrapper over an HTTP response. +/// Hides the underlying HTTP library from plugin code. +/// Constructed by the gateway per-request; plugins receive it by reference. +class PluginResponse { + public: + /// Construct from opaque pointer to httplib::Response. + explicit PluginResponse(void * impl); + + /// Send a JSON success response (HTTP 200). + void send_json(const nlohmann::json & data); + + /// Send a SOVD-compliant error response. + void send_error(int status, const std::string & error_code, const std::string & message, + const nlohmann::json & parameters = {}); + + private: + void * impl_; +}; + +} // namespace ros2_medkit_gateway diff --git a/src/ros2_medkit_gateway/src/plugins/plugin_http_types.cpp b/src/ros2_medkit_gateway/src/plugins/plugin_http_types.cpp new file mode 100644 index 000000000..a9e95742c --- /dev/null +++ b/src/ros2_medkit_gateway/src/plugins/plugin_http_types.cpp @@ -0,0 +1,64 @@ +// Copyright 2026 bburda +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ros2_medkit_gateway/plugins/plugin_http_types.hpp" + +#include + +#include "ros2_medkit_gateway/http/handlers/handler_context.hpp" + +namespace ros2_medkit_gateway { + +// --- PluginRequest --- + +PluginRequest::PluginRequest(const void * impl) : impl_(impl) { +} + +std::string PluginRequest::path_param(size_t index) const { + const auto & req = *static_cast(impl_); + if (index < req.matches.size()) { + return req.matches[static_cast(index)].str(); + } + return {}; +} + +std::string PluginRequest::header(const std::string & name) const { + const auto & req = *static_cast(impl_); + return req.get_header_value(name); +} + +std::string PluginRequest::path() const { + return static_cast(impl_)->path; +} + +std::string PluginRequest::body() const { + return static_cast(impl_)->body; +} + +// --- PluginResponse --- + +PluginResponse::PluginResponse(void * impl) : impl_(impl) { +} + +void PluginResponse::send_json(const nlohmann::json & data) { + handlers::HandlerContext::send_json(*static_cast(impl_), data); +} + +void PluginResponse::send_error(int status, const std::string & error_code, const std::string & message, + const nlohmann::json & parameters) { + handlers::HandlerContext::send_error(*static_cast(impl_), status, error_code, message, + parameters); +} + +} // namespace ros2_medkit_gateway diff --git a/src/ros2_medkit_gateway/test/test_plugin_http_types.cpp b/src/ros2_medkit_gateway/test/test_plugin_http_types.cpp new file mode 100644 index 000000000..afcb587ed --- /dev/null +++ b/src/ros2_medkit_gateway/test/test_plugin_http_types.cpp @@ -0,0 +1,86 @@ +// Copyright 2026 bburda +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include + +#include "ros2_medkit_gateway/plugins/plugin_http_types.hpp" + +using namespace ros2_medkit_gateway; + +TEST(PluginRequestTest, PathParam) { + httplib::Request req; + req.path = "/api/v1/apps/my_app/x-medkit-test"; + std::string path = "/api/v1/apps/my_app/x-medkit-test"; + std::regex pattern(R"(/api/v1/apps/([^/]+)/x-medkit-test)"); + std::regex_search(path, req.matches, pattern); + + PluginRequest preq(&req); + EXPECT_EQ(preq.path_param(1), "my_app"); +} + +TEST(PluginRequestTest, PathParamOutOfRange) { + httplib::Request req; + PluginRequest preq(&req); + EXPECT_EQ(preq.path_param(5), ""); +} + +TEST(PluginRequestTest, Header) { + httplib::Request req; + req.set_header("X-Client-Id", "test-client"); + + PluginRequest preq(&req); + EXPECT_EQ(preq.header("X-Client-Id"), "test-client"); + EXPECT_EQ(preq.header("Missing-Header"), ""); +} + +TEST(PluginRequestTest, Path) { + httplib::Request req; + req.path = "/api/v1/apps/my_app/data"; + + PluginRequest preq(&req); + EXPECT_EQ(preq.path(), "/api/v1/apps/my_app/data"); +} + +TEST(PluginRequestTest, Body) { + httplib::Request req; + req.body = R"({"key": "value"})"; + + PluginRequest preq(&req); + EXPECT_EQ(preq.body(), R"({"key": "value"})"); +} + +TEST(PluginResponseTest, SendJson) { + httplib::Response res; + + PluginResponse pres(&res); + pres.send_json({{"status", "ok"}}); + + auto body = nlohmann::json::parse(res.body); + EXPECT_EQ(body["status"], "ok"); +} + +TEST(PluginResponseTest, SendError) { + httplib::Response res; + + PluginResponse pres(&res); + pres.send_error(404, "not-found", "Entity not found"); + + EXPECT_EQ(res.status, 404); + auto body = nlohmann::json::parse(res.body); + EXPECT_EQ(body["error_code"], "not-found"); + EXPECT_EQ(body["message"], "Entity not found"); +} From 50089aec58f43571f46dcc63cdcf71673cee24da Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Fri, 3 Apr 2026 10:07:17 +0200 Subject: [PATCH 03/18] feat: replace register_routes with declarative get_routes Plugins now declare routes via get_routes() returning PluginRoute structs instead of imperatively registering on httplib::Server. The gateway wraps httplib types in PluginRequest/PluginResponse before calling handlers. Removes httplib from the public plugin API. PLUGIN_API_VERSION bumped 4->5. --- .../plugins/gateway_plugin.hpp | 35 +++++--------- .../plugins/plugin_context.hpp | 22 +++------ .../plugins/plugin_manager.hpp | 11 ++--- .../plugins/plugin_types.hpp | 2 +- .../src/http/rest_server.cpp | 2 +- .../src/plugins/plugin_context.cpp | 25 +++------- .../src/plugins/plugin_http_types.cpp | 2 +- .../src/plugins/plugin_manager.cpp | 46 +++++++++++++++---- .../test/demo_nodes/test_gateway_plugin.cpp | 43 +++++++++-------- .../test/test_plugin_manager.cpp | 16 +++---- 10 files changed, 100 insertions(+), 104 deletions(-) diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/gateway_plugin.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/gateway_plugin.hpp index e7325b9a3..229d57c72 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/gateway_plugin.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/gateway_plugin.hpp @@ -14,10 +14,10 @@ #pragma once +#include "ros2_medkit_gateway/plugins/plugin_http_types.hpp" #include "ros2_medkit_gateway/plugins/plugin_types.hpp" #include -#include #include #include #include @@ -42,10 +42,11 @@ class PluginContext; */ class GatewayPlugin { public: - /// Describes a single REST route registered by a plugin (for handle_root auto-registration) - struct RouteDescription { - std::string method; ///< HTTP method (e.g. "GET", "POST") - std::string pattern; ///< Path pattern relative to api_prefix (e.g. "apps/{app_id}/x-medkit-topic-beacon") + /// Describes a single REST route registered by a plugin + struct PluginRoute { + std::string method; ///< HTTP method ("GET", "POST", "PUT", "DELETE") + std::string pattern; ///< Regex pattern relative to api_prefix + std::function handler; }; virtual ~GatewayPlugin() = default; @@ -78,27 +79,15 @@ class GatewayPlugin { } /** - * @brief Optionally register custom REST routes + * @brief Return custom REST routes for this plugin * - * Called once during REST server setup. Plugins can register - * vendor-specific endpoints (e.g., /x-medkit/my-feature). + * Called once during REST server setup. The gateway registers the returned + * routes on the HTTP server, wrapping PluginRequest/PluginResponse around + * the underlying library types. * - * @param server httplib server instance - * @param api_prefix API path prefix (e.g., "/api/v1") + * @return Routes to register (method, pattern, handler) */ - virtual void register_routes(httplib::Server & /*server*/, const std::string & /*api_prefix*/) { - } - - /** - * @brief Describe routes registered by this plugin - * - * Used by handle_root to dynamically include plugin endpoints in the - * API root listing. Patterns should be relative to api_prefix - * (e.g. "apps/{app_id}/x-medkit-topic-beacon"). - * - * @return Route descriptions for all endpoints this plugin registers - */ - virtual std::vector get_route_descriptions() const { + virtual std::vector get_routes() { return {}; } diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_context.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_context.hpp index 32debaec4..00cc2aefa 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_context.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_context.hpp @@ -17,10 +17,10 @@ #include "ros2_medkit_gateway/http/error_codes.hpp" #include "ros2_medkit_gateway/lock_manager.hpp" #include "ros2_medkit_gateway/models/entity_types.hpp" +#include "ros2_medkit_gateway/plugins/plugin_http_types.hpp" #include "ros2_medkit_gateway/providers/introspection_provider.hpp" #include -#include #include #include #include @@ -92,25 +92,17 @@ class PluginContext { /** * @brief Validate entity exists and matches route type, sending SOVD error if not * - * Use this in register_routes() handlers to validate entity IDs from path params. + * Use this in get_routes() handlers to validate entity IDs from path params. * On failure, an appropriate SOVD GenericError response is sent automatically. * - * @param req HTTP request (extracts expected entity type from path) - * @param res HTTP response (error sent here on failure) - * @param entity_id Entity ID from path parameter (e.g., req.matches[1]) + * @param req Plugin request (extracts expected entity type from path) + * @param res Plugin response (error sent here on failure) + * @param entity_id Entity ID from path parameter (e.g., req.path_param(1)) * @return Entity info if valid, nullopt if error was sent */ - virtual std::optional validate_entity_for_route(const httplib::Request & req, - httplib::Response & res, + virtual std::optional validate_entity_for_route(const PluginRequest & req, PluginResponse & res, const std::string & entity_id) const = 0; - /// Send SOVD-compliant JSON error response - static void send_error(httplib::Response & res, int status, const std::string & error_code, - const std::string & message, const nlohmann::json & parameters = {}); - - /// Send JSON success response - static void send_json(httplib::Response & res, const nlohmann::json & data); - // ---- Capability registration ---- /** @@ -120,7 +112,7 @@ class PluginContext { * auto-generated href. For example, registering "x-medkit-traces" for * SovdEntityType::APP produces: {"name": "x-medkit-traces", "href": "/api/v1/apps/{id}/x-medkit-traces"} * - * The plugin must also register a matching route in register_routes(). + * The plugin must also return a matching route from get_routes(). * * @param entity_type Entity type to add the capability to * @param capability_name Capability name (use x- prefix for vendor extensions) diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_manager.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_manager.hpp index 12ea5c0f9..5b96d4e4f 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_manager.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_manager.hpp @@ -26,7 +26,6 @@ #include "ros2_medkit_gateway/resource_sampler.hpp" #include "ros2_medkit_gateway/subscription_transport.hpp" -#include #include #include #include @@ -97,10 +96,10 @@ class PluginManager { /** * @brief Register custom REST routes from all plugins - * @param server httplib server instance + * @param server Opaque pointer to httplib::Server (avoids httplib in header) * @param api_prefix API path prefix (e.g., "/api/v1") */ - void register_routes(httplib::Server & server, const std::string & api_prefix); + void register_routes(void * server, const std::string & api_prefix); /// Register a resource sampler for a vendor collection (must start with "x-") void register_resource_sampler(const std::string & collection, ResourceSamplerFn fn); @@ -165,10 +164,10 @@ class PluginManager { // ---- Route descriptions (for handle_root auto-registration) ---- /** - * @brief Collect route descriptions from all active plugins - * @return Combined route descriptions from all plugins + * @brief Collect route info (method, pattern) from all active plugins + * @return Combined (method, pattern) pairs from all plugins */ - std::vector get_all_route_descriptions() const; + std::vector> get_all_route_info() const; // ---- Info ---- bool has_plugins() const; diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_types.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_types.hpp index 32bdc228e..e57773333 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_types.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_types.hpp @@ -28,7 +28,7 @@ namespace ros2_medkit_gateway { /// Current plugin API version. Plugins must export this value from plugin_api_version(). -constexpr int PLUGIN_API_VERSION = 4; +constexpr int PLUGIN_API_VERSION = 5; /// Log severity levels for plugin logging callback enum class PluginLogLevel { kInfo, kWarn, kError }; diff --git a/src/ros2_medkit_gateway/src/http/rest_server.cpp b/src/ros2_medkit_gateway/src/http/rest_server.cpp index a84df2be9..7f64be19f 100644 --- a/src/ros2_medkit_gateway/src/http/rest_server.cpp +++ b/src/ros2_medkit_gateway/src/http/rest_server.cpp @@ -165,7 +165,7 @@ RESTServer::RESTServer(GatewayNode * node, const std::string & host, int port, c if (node_->get_plugin_manager()) { auto * plugin_srv = http_server_->get_server(); if (plugin_srv) { - node_->get_plugin_manager()->register_routes(*plugin_srv, API_BASE_PATH); + node_->get_plugin_manager()->register_routes(plugin_srv, API_BASE_PATH); } } } diff --git a/src/ros2_medkit_gateway/src/plugins/plugin_context.cpp b/src/ros2_medkit_gateway/src/plugins/plugin_context.cpp index 486c329e9..90e236dd2 100644 --- a/src/ros2_medkit_gateway/src/plugins/plugin_context.cpp +++ b/src/ros2_medkit_gateway/src/plugins/plugin_context.cpp @@ -22,7 +22,6 @@ #include "ros2_medkit_gateway/fault_manager.hpp" #include "ros2_medkit_gateway/gateway_node.hpp" #include "ros2_medkit_gateway/http/error_codes.hpp" -#include "ros2_medkit_gateway/http/handlers/handler_context.hpp" #include "ros2_medkit_gateway/http/http_utils.hpp" #include "ros2_medkit_gateway/lock_manager.hpp" #include "ros2_medkit_gateway/resource_change_notifier.hpp" @@ -30,17 +29,6 @@ namespace ros2_medkit_gateway { -// ---- Static utility methods (delegate to HandlerContext) ---- - -void PluginContext::send_error(httplib::Response & res, int status, const std::string & error_code, - const std::string & message, const nlohmann::json & parameters) { - handlers::HandlerContext::send_error(res, status, error_code, message, parameters); -} - -void PluginContext::send_json(httplib::Response & res, const nlohmann::json & data) { - handlers::HandlerContext::send_json(res, data); -} - // ---- Concrete implementation ---- class GatewayPluginContext : public PluginContext { @@ -110,27 +98,28 @@ class GatewayPluginContext : public PluginContext { return nlohmann::json::array(); } - std::optional validate_entity_for_route(const httplib::Request & req, httplib::Response & res, + std::optional validate_entity_for_route(const PluginRequest & req, PluginResponse & res, const std::string & entity_id) const override { // Validate entity ID format if (entity_id.empty() || entity_id.size() > 256) { - send_error(res, 400, ERR_INVALID_PARAMETER, "Invalid entity ID"); + res.send_error(400, ERR_INVALID_PARAMETER, "Invalid entity ID"); return std::nullopt; } // Determine expected type from route path (segment-boundary-aware matching) - auto expected_type = extract_entity_type_from_path(req.path); + auto expected_type = extract_entity_type_from_path(req.path()); auto entity = get_entity(entity_id); if (!entity) { - send_error(res, 404, ERR_ENTITY_NOT_FOUND, to_string(expected_type) + " not found: " + entity_id); + res.send_error(404, ERR_ENTITY_NOT_FOUND, to_string(expected_type) + " not found: " + entity_id); return std::nullopt; } // Check type matches route if (expected_type != SovdEntityType::UNKNOWN && entity->type != expected_type) { - send_error(res, 400, ERR_INVALID_PARAMETER, - "Entity '" + entity_id + "' is a " + to_string(entity->type) + ", not a " + to_string(expected_type)); + res.send_error(400, ERR_INVALID_PARAMETER, + "Entity '" + entity_id + "' is a " + to_string(entity->type) + ", not a " + + to_string(expected_type)); return std::nullopt; } diff --git a/src/ros2_medkit_gateway/src/plugins/plugin_http_types.cpp b/src/ros2_medkit_gateway/src/plugins/plugin_http_types.cpp index a9e95742c..3d6f824da 100644 --- a/src/ros2_medkit_gateway/src/plugins/plugin_http_types.cpp +++ b/src/ros2_medkit_gateway/src/plugins/plugin_http_types.cpp @@ -28,7 +28,7 @@ PluginRequest::PluginRequest(const void * impl) : impl_(impl) { std::string PluginRequest::path_param(size_t index) const { const auto & req = *static_cast(impl_); if (index < req.matches.size()) { - return req.matches[static_cast(index)].str(); + return req.matches[index].str(); } return {}; } diff --git a/src/ros2_medkit_gateway/src/plugins/plugin_manager.cpp b/src/ros2_medkit_gateway/src/plugins/plugin_manager.cpp index 2747fac9e..5df409f92 100644 --- a/src/ros2_medkit_gateway/src/plugins/plugin_manager.cpp +++ b/src/ros2_medkit_gateway/src/plugins/plugin_manager.cpp @@ -15,9 +15,12 @@ #include "ros2_medkit_gateway/plugins/plugin_manager.hpp" #include +#include #include +#include "ros2_medkit_gateway/plugins/plugin_http_types.hpp" + namespace ros2_medkit_gateway { namespace { @@ -260,20 +263,43 @@ void PluginManager::register_transport(std::unique_ptrregister_transport(std::move(provider)); } -void PluginManager::register_routes(httplib::Server & server, const std::string & api_prefix) { +void PluginManager::register_routes(void * server_ptr, const std::string & api_prefix) { + auto * server = static_cast(server_ptr); std::unique_lock lock(plugins_mutex_); for (auto & lp : plugins_) { if (!lp.load_result.plugin) { continue; } try { - lp.load_result.plugin->register_routes(server, api_prefix); + auto routes = lp.load_result.plugin->get_routes(); + for (auto & route : routes) { + std::string full_pattern = api_prefix + route.pattern; + auto handler_fn = route.handler; // capture by value for lambda + auto httplib_handler = [handler_fn](const httplib::Request & req, httplib::Response & res) { + PluginRequest plugin_req(&req); + PluginResponse plugin_res(&res); + handler_fn(plugin_req, plugin_res); + }; + + if (route.method == "GET") { + server->Get(full_pattern.c_str(), httplib_handler); + } else if (route.method == "POST") { + server->Post(full_pattern.c_str(), httplib_handler); + } else if (route.method == "PUT") { + server->Put(full_pattern.c_str(), httplib_handler); + } else if (route.method == "DELETE") { + server->Delete(full_pattern.c_str(), httplib_handler); + } else { + RCLCPP_WARN(logger(), "Plugin '%s' registered route with unknown method '%s' - skipping", + lp.load_result.plugin->name().c_str(), route.method.c_str()); + } + } } catch (const std::exception & e) { - RCLCPP_ERROR(logger(), "Plugin '%s' threw during register_routes(): %s - disabling", + RCLCPP_ERROR(logger(), "Plugin '%s' threw during get_routes(): %s - disabling", lp.load_result.plugin->name().c_str(), e.what()); disable_plugin(lp); } catch (...) { - RCLCPP_ERROR(logger(), "Plugin '%s' threw unknown exception during register_routes() - disabling", + RCLCPP_ERROR(logger(), "Plugin '%s' threw unknown exception during get_routes() - disabling", lp.load_result.plugin->name().c_str()); disable_plugin(lp); } @@ -355,18 +381,20 @@ std::vector> PluginManager::get_ return result; } -std::vector PluginManager::get_all_route_descriptions() const { +std::vector> PluginManager::get_all_route_info() const { std::shared_lock lock(plugins_mutex_); - std::vector all; + std::vector> all; for (const auto & lp : plugins_) { if (!lp.load_result.plugin) { continue; } try { - auto routes = lp.load_result.plugin->get_route_descriptions(); - all.insert(all.end(), routes.begin(), routes.end()); + auto routes = lp.load_result.plugin->get_routes(); + for (const auto & route : routes) { + all.emplace_back(route.method, route.pattern); + } } catch (const std::exception & e) { - RCLCPP_ERROR(rclcpp::get_logger("plugin_manager"), "Plugin '%s' threw in get_route_descriptions(): %s", + RCLCPP_ERROR(rclcpp::get_logger("plugin_manager"), "Plugin '%s' threw in get_routes(): %s", lp.load_result.plugin->name().c_str(), e.what()); } } diff --git a/src/ros2_medkit_gateway/test/demo_nodes/test_gateway_plugin.cpp b/src/ros2_medkit_gateway/test/demo_nodes/test_gateway_plugin.cpp index 7dbd13a50..93e1cc2fd 100644 --- a/src/ros2_medkit_gateway/test/demo_nodes/test_gateway_plugin.cpp +++ b/src/ros2_medkit_gateway/test/demo_nodes/test_gateway_plugin.cpp @@ -18,7 +18,6 @@ #include "ros2_medkit_gateway/providers/introspection_provider.hpp" #include "ros2_medkit_gateway/providers/update_provider.hpp" -#include #include #include #include @@ -51,27 +50,27 @@ class TestGatewayPlugin : public GatewayPlugin, public UpdateProvider, public In ctx_->register_capability(SovdEntityType::COMPONENT, "x-medkit-diagnostics"); } - void register_routes(httplib::Server & server, const std::string & api_prefix) override { - // Global vendor extension endpoint - server.Get((api_prefix + "/x-test/ping").c_str(), [](const httplib::Request &, httplib::Response & res) { - res.set_content("pong", "text/plain"); - }); - - // Entity-scoped vendor extension: GET /components/{id}/x-medkit-diagnostics - server.Get((api_prefix + R"(/components/([^/]+)/x-medkit-diagnostics)").c_str(), - [this](const httplib::Request & req, httplib::Response & res) { - auto entity_id = req.matches[1].str(); - auto entity = ctx_->validate_entity_for_route(req, res, entity_id); - if (!entity) { - return; - } - nlohmann::json data = {{"entity_id", entity->id}, - {"plugin", "test_plugin"}, - {"cpu_usage", 42.5}, - {"memory_mb", 128}, - {"uptime_seconds", 3600}}; - PluginContext::send_json(res, data); - }); + std::vector get_routes() override { + return { + {"GET", "x-test/ping", + [](const PluginRequest &, PluginResponse & res) { + res.send_json({{"response", "pong"}}); + }}, + {"GET", R"(components/([^/]+)/x-medkit-diagnostics)", + [this](const PluginRequest & req, PluginResponse & res) { + auto entity_id = req.path_param(1); + auto entity = ctx_->validate_entity_for_route(req, res, entity_id); + if (!entity) { + return; + } + nlohmann::json data = {{"entity_id", entity->id}, + {"plugin", "test_plugin"}, + {"cpu_usage", 42.5}, + {"memory_mb", 128}, + {"uptime_seconds", 3600}}; + res.send_json(data); + }}, + }; } private: diff --git a/src/ros2_medkit_gateway/test/test_plugin_manager.cpp b/src/ros2_medkit_gateway/test/test_plugin_manager.cpp index 625c089f1..d82c528bb 100644 --- a/src/ros2_medkit_gateway/test/test_plugin_manager.cpp +++ b/src/ros2_medkit_gateway/test/test_plugin_manager.cpp @@ -132,16 +132,16 @@ class MockThrowOnSetContext : public GatewayPlugin, public UpdateProvider { } }; -/// Plugin that throws during register_routes -class MockThrowOnRegisterRoutes : public GatewayPlugin, public IntrospectionProvider { +/// Plugin that throws during get_routes +class MockThrowOnGetRoutes : public GatewayPlugin, public IntrospectionProvider { public: std::string name() const override { - return "throw_register_routes"; + return "throw_get_routes"; } void configure(const json &) override { } - void register_routes(httplib::Server &, const std::string &) override { - throw std::runtime_error("register_routes failed"); + std::vector get_routes() override { + throw std::runtime_error("get_routes failed"); } IntrospectionResult introspect(const IntrospectionInput &) override { @@ -296,15 +296,15 @@ TEST(PluginManagerTest, ThrowOnSetContextDisablesPlugin) { EXPECT_EQ(mgr.plugin_names()[0], "mock"); } -TEST(PluginManagerTest, ThrowOnRegisterRoutesDisablesPlugin) { +TEST(PluginManagerTest, ThrowOnGetRoutesDisablesPlugin) { PluginManager mgr; - mgr.add_plugin(std::make_unique()); + mgr.add_plugin(std::make_unique()); auto good = std::make_unique(); mgr.add_plugin(std::move(good)); mgr.configure_plugins(); httplib::Server srv; - mgr.register_routes(srv, "/api/v1"); + mgr.register_routes(&srv, "/api/v1"); // Throwing plugin disabled, good plugin's IntrospectionProvider still works EXPECT_EQ(mgr.get_introspection_providers().size(), 1u); From 2cc20714adb00661a3d87cadf5953f8d613bd536 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Fri, 3 Apr 2026 10:40:14 +0200 Subject: [PATCH 04/18] refactor: migrate graph_provider to get_routes plugin API Replace register_routes(httplib::Server&, prefix) with get_routes() returning vector. Handler now uses PluginRequest/PluginResponse instead of httplib types directly. Remove httplib and OpenSSL build/package dependencies from plugin target; keep only for test target which does end-to-end HTTP testing. Update FakePluginContext::validate_entity_for_route and add PluginRequest/ PluginResponse stubs in the test. --- .../ros2_medkit_graph_provider/CMakeLists.txt | 16 ++-- .../graph_provider_plugin.hpp | 2 +- .../ros2_medkit_graph_provider/package.xml | 2 - .../src/graph_provider_plugin.cpp | 79 ++++++++++--------- .../test/test_graph_provider_plugin.cpp | 60 +++++++++++--- 5 files changed, 98 insertions(+), 61 deletions(-) diff --git a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/CMakeLists.txt b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/CMakeLists.txt index 6c57d573a..5bf93cec9 100644 --- a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/CMakeLists.txt +++ b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/CMakeLists.txt @@ -34,14 +34,6 @@ find_package(ros2_medkit_msgs REQUIRED) find_package(rclcpp REQUIRED) find_package(diagnostic_msgs REQUIRED) find_package(nlohmann_json REQUIRED) -find_package(OpenSSL REQUIRED) - -# cpp-httplib via multi-distro compatibility macro -medkit_find_cpp_httplib() - -# Enable OpenSSL support for cpp-httplib -add_compile_definitions(CPPHTTPLIB_OPENSSL_SUPPORT) - # MODULE target: loaded via dlopen at runtime by PluginManager. # Symbols from gateway_lib are resolved from the host process at runtime. # We only need headers at compile time (via medkit_target_dependencies). @@ -68,8 +60,6 @@ target_link_options(ros2_medkit_graph_provider_plugin PRIVATE target_link_libraries(ros2_medkit_graph_provider_plugin nlohmann_json::nlohmann_json - cpp_httplib_target - OpenSSL::SSL OpenSSL::Crypto ) install(TARGETS ros2_medkit_graph_provider_plugin @@ -95,6 +85,11 @@ if(BUILD_TESTING) find_package(ament_cmake_gtest REQUIRED) + # The route test uses httplib::Server/Client directly for end-to-end HTTP testing. + # Find cpp-httplib so the test binary can link against the compiled implementation. + find_package(OpenSSL REQUIRED) + medkit_find_cpp_httplib() + include(ROS2MedkitTestDomain) medkit_init_test_domains(START 120 END 129) @@ -111,6 +106,7 @@ if(BUILD_TESTING) diagnostic_msgs ros2_medkit_msgs ) + target_compile_definitions(test_graph_provider_plugin PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT) target_link_libraries(test_graph_provider_plugin nlohmann_json::nlohmann_json cpp_httplib_target diff --git a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/include/ros2_medkit_graph_provider/graph_provider_plugin.hpp b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/include/ros2_medkit_graph_provider/graph_provider_plugin.hpp index 4f69c328f..04d414a2b 100644 --- a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/include/ros2_medkit_graph_provider/graph_provider_plugin.hpp +++ b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/include/ros2_medkit_graph_provider/graph_provider_plugin.hpp @@ -58,7 +58,7 @@ class GraphProviderPlugin : public GatewayPlugin, public IntrospectionProvider { std::string name() const override; void configure(const nlohmann::json & config) override; void set_context(PluginContext & context) override; - void register_routes(httplib::Server & server, const std::string & api_prefix) override; + std::vector get_routes() override; IntrospectionResult introspect(const IntrospectionInput & input) override; static nlohmann::json build_graph_document(const std::string & function_id, const IntrospectionInput & input, diff --git a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/package.xml b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/package.xml index 10a469354..b3467df8d 100644 --- a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/package.xml +++ b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/package.xml @@ -15,8 +15,6 @@ ros2_medkit_msgs ros2_medkit_gateway nlohmann-json-dev - libcpp-httplib-dev - libssl-dev ament_lint_auto ament_lint_common diff --git a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/src/graph_provider_plugin.cpp b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/src/graph_provider_plugin.cpp index 4bc215c57..a759018a4 100644 --- a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/src/graph_provider_plugin.cpp +++ b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/src/graph_provider_plugin.cpp @@ -329,45 +329,46 @@ void GraphProviderPlugin::set_context(PluginContext & context) { log_info("Registered x-medkit-graph cyclic subscription sampler"); } -void GraphProviderPlugin::register_routes(httplib::Server & server, const std::string & api_prefix) { - server.Get(api_prefix + R"(/functions/([^/]+)/x-medkit-graph)", - [this](const httplib::Request & req, httplib::Response & res) { - if (!ctx_) { - PluginContext::send_error(res, 503, ERR_SERVICE_UNAVAILABLE, "Graph provider context not initialized"); - return; - } - - const auto function_id = req.matches[1].str(); - auto entity = ctx_->validate_entity_for_route(req, res, function_id); - if (!entity) { - return; - } - - // Check lock access for vendor extension collection - auto client_id = req.get_header_value("X-Client-Id"); - auto lock_result = ctx_->check_lock(function_id, client_id, "x-medkit-graph"); - if (!lock_result.allowed) { - nlohmann::json params = {{"entity_id", function_id}, {"collection", "x-medkit-graph"}}; - if (!lock_result.denied_by_lock_id.empty()) { - params["lock_id"] = lock_result.denied_by_lock_id; - } - if (lock_result.denied_code == "lock-required") { - PluginContext::send_error(res, 409, ERR_INVALID_REQUEST, lock_result.denied_reason, params); - } else { - PluginContext::send_error(res, 409, ERR_LOCK_BROKEN, lock_result.denied_reason, params); - } - return; - } - - auto payload = get_cached_or_built_graph(function_id); - if (!payload.has_value()) { - PluginContext::send_error(res, 503, ERR_SERVICE_UNAVAILABLE, "Graph snapshot not available", - {{"function_id", function_id}}); - return; - } - - PluginContext::send_json(res, *payload); - }); +std::vector GraphProviderPlugin::get_routes() { + std::vector routes; + routes.push_back( + {"GET", R"(functions/([^/]+)/x-medkit-graph)", [this](const PluginRequest & req, PluginResponse & res) { + if (!ctx_) { + res.send_error(503, ERR_SERVICE_UNAVAILABLE, "Graph provider context not initialized"); + return; + } + + const auto function_id = req.path_param(1); + auto entity = ctx_->validate_entity_for_route(req, res, function_id); + if (!entity) { + return; + } + + // Check lock access for vendor extension collection + auto client_id = req.header("X-Client-Id"); + auto lock_result = ctx_->check_lock(function_id, client_id, "x-medkit-graph"); + if (!lock_result.allowed) { + nlohmann::json params = {{"entity_id", function_id}, {"collection", "x-medkit-graph"}}; + if (!lock_result.denied_by_lock_id.empty()) { + params["lock_id"] = lock_result.denied_by_lock_id; + } + if (lock_result.denied_code == "lock-required") { + res.send_error(409, ERR_INVALID_REQUEST, lock_result.denied_reason, params); + } else { + res.send_error(409, ERR_LOCK_BROKEN, lock_result.denied_reason, params); + } + return; + } + + auto payload = get_cached_or_built_graph(function_id); + if (!payload.has_value()) { + res.send_error(503, ERR_SERVICE_UNAVAILABLE, "Graph snapshot not available", {{"function_id", function_id}}); + return; + } + + res.send_json(*payload); + }}); + return routes; } IntrospectionResult GraphProviderPlugin::introspect(const IntrospectionInput & input) { diff --git a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/test/test_graph_provider_plugin.cpp b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/test/test_graph_provider_plugin.cpp index 8841e2ae1..8ba767543 100644 --- a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/test/test_graph_provider_plugin.cpp +++ b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/test/test_graph_provider_plugin.cpp @@ -33,24 +33,66 @@ #include "ros2_medkit_gateway/discovery/models/app.hpp" #include "ros2_medkit_gateway/discovery/models/function.hpp" #include "ros2_medkit_gateway/plugins/plugin_context.hpp" +#include "ros2_medkit_gateway/plugins/plugin_http_types.hpp" #include "ros2_medkit_graph_provider/graph_provider_plugin.hpp" using namespace std::chrono_literals; using namespace ros2_medkit_gateway; -// Stubs for PluginContext static methods (defined in gateway_lib, not linked into tests) +// Stubs for PluginRequest/PluginResponse (implemented in gateway_lib, not linked into tests). +// These wrap httplib::Request/Response directly so the route tests can use a real HTTP server. namespace ros2_medkit_gateway { -void PluginContext::send_json(httplib::Response & res, const nlohmann::json & data) { - res.set_content(data.dump(), "application/json"); + +PluginRequest::PluginRequest(const void * impl) : impl_(impl) { +} +std::string PluginRequest::path_param(size_t index) const { + const auto & req = *static_cast(impl_); + if (index < req.matches.size()) { + return req.matches[index].str(); + } + return {}; +} +std::string PluginRequest::header(const std::string & name) const { + return static_cast(impl_)->get_header_value(name); +} +std::string PluginRequest::path() const { + return static_cast(impl_)->path; +} +std::string PluginRequest::body() const { + return static_cast(impl_)->body; +} + +PluginResponse::PluginResponse(void * impl) : impl_(impl) { +} +void PluginResponse::send_json(const nlohmann::json & data) { + static_cast(impl_)->set_content(data.dump(), "application/json"); } -void PluginContext::send_error(httplib::Response & res, int status, const std::string & /*error_code*/, - const std::string & message, const nlohmann::json & /*parameters*/) { +void PluginResponse::send_error(int status, const std::string & /*error_code*/, const std::string & message, + const nlohmann::json & /*parameters*/) { + auto & res = *static_cast(impl_); res.status = status; nlohmann::json err = {{"error", message}}; res.set_content(err.dump(), "application/json"); } + } // namespace ros2_medkit_gateway +/// Helper: register all routes from get_routes() on an httplib::Server with a given api_prefix. +static void register_plugin_routes(httplib::Server & server, const std::string & api_prefix, + ros2_medkit_gateway::GatewayPlugin & plugin) { + for (auto & route : plugin.get_routes()) { + auto pattern = api_prefix + "/" + route.pattern; + auto handler = route.handler; + if (route.method == "GET") { + server.Get(pattern.c_str(), [handler](const httplib::Request & req, httplib::Response & res) { + PluginRequest preq(&req); + PluginResponse pres(&res); + handler(preq, pres); + }); + } + } +} + namespace { App make_app(const std::string & id, std::vector publishes = {}, std::vector subscribes = {}, @@ -156,11 +198,11 @@ class FakePluginContext : public PluginContext { return nlohmann::json::array(); } - std::optional validate_entity_for_route(const httplib::Request & /*req*/, httplib::Response & res, + std::optional validate_entity_for_route(const PluginRequest & /*req*/, PluginResponse & res, const std::string & entity_id) const override { auto entity = get_entity(entity_id); if (!entity) { - send_error(res, 404, "entity-not-found", "Entity not found"); + res.send_error(404, "entity-not-found", "Entity not found"); return std::nullopt; } return entity; @@ -587,7 +629,7 @@ TEST(GraphProviderPluginRouteTest, ServesFunctionGraphFromCachedSnapshot) { plugin.introspect(input); httplib::Server server; - plugin.register_routes(server, "/api/v1"); + register_plugin_routes(server, "/api/v1", plugin); LocalHttpServer local_server; local_server.start(server); @@ -643,7 +685,7 @@ TEST(GraphProviderPluginRouteTest, UsesPreviousOnlineTimestampForOfflineLastSeen ctx.entity_snapshot_ = offline_input; httplib::Server server; - plugin.register_routes(server, "/api/v1"); + register_plugin_routes(server, "/api/v1", plugin); LocalHttpServer local_server; local_server.start(server); From bac756679d99b2b285ac9ebb901dc90cff51c391 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Fri, 3 Apr 2026 10:40:24 +0200 Subject: [PATCH 05/18] refactor: migrate param_beacon to get_routes plugin API Replace register_routes + get_route_descriptions with get_routes() returning vector. Handler now uses PluginRequest/PluginResponse instead of httplib types directly. Remove httplib and OpenSSL build/package dependencies. Update MockPluginContext::validate_entity_for_route and stubs in test. --- .../ros2_medkit_param_beacon/CMakeLists.txt | 13 +---- .../param_beacon_plugin.hpp | 5 +- .../ros2_medkit_param_beacon/package.xml | 2 - .../src/param_beacon_plugin.cpp | 49 +++++++++---------- .../test/test_param_beacon_plugin.cpp | 33 +++++++++---- 5 files changed, 49 insertions(+), 53 deletions(-) diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/CMakeLists.txt b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/CMakeLists.txt index 93b7f8b6f..d78fd634d 100644 --- a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/CMakeLists.txt +++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/CMakeLists.txt @@ -33,13 +33,6 @@ find_package(ros2_medkit_beacon_common REQUIRED) find_package(ros2_medkit_gateway REQUIRED) find_package(rclcpp REQUIRED) find_package(nlohmann_json REQUIRED) -find_package(OpenSSL REQUIRED) - -# cpp-httplib via multi-distro compatibility macro -medkit_find_cpp_httplib() - -# Enable OpenSSL support for cpp-httplib -add_compile_definitions(CPPHTTPLIB_OPENSSL_SUPPORT) # MODULE target: loaded via dlopen at runtime by PluginManager. add_library(param_beacon_plugin MODULE @@ -59,8 +52,6 @@ medkit_target_dependencies(param_beacon_plugin target_link_libraries(param_beacon_plugin nlohmann_json::nlohmann_json ros2_medkit_beacon_common::beacon_common_lib - cpp_httplib_target - OpenSSL::SSL OpenSSL::Crypto ) install(TARGETS param_beacon_plugin @@ -100,9 +91,7 @@ if(BUILD_TESTING) ros2_medkit_gateway ros2_medkit_beacon_common rclcpp) target_link_libraries(test_param_beacon_plugin nlohmann_json::nlohmann_json - ros2_medkit_beacon_common::beacon_common_lib - cpp_httplib_target - OpenSSL::SSL OpenSSL::Crypto) + ros2_medkit_beacon_common::beacon_common_lib) medkit_set_test_domain(test_param_beacon_plugin) endif() diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/include/ros2_medkit_param_beacon/param_beacon_plugin.hpp b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/include/ros2_medkit_param_beacon/param_beacon_plugin.hpp index d42417b95..45f87e62e 100644 --- a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/include/ros2_medkit_param_beacon/param_beacon_plugin.hpp +++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/include/ros2_medkit_param_beacon/param_beacon_plugin.hpp @@ -14,8 +14,6 @@ #pragma once -#include - #include #include #include @@ -58,8 +56,7 @@ class ParameterBeaconPlugin : public ros2_medkit_gateway::GatewayPlugin, void configure(const nlohmann::json & config) override; void set_context(ros2_medkit_gateway::PluginContext & context) override; void shutdown() override; - void register_routes(httplib::Server & server, const std::string & api_prefix) override; - std::vector get_route_descriptions() const override; + std::vector get_routes() override; // IntrospectionProvider ros2_medkit_gateway::IntrospectionResult introspect(const ros2_medkit_gateway::IntrospectionInput & input) override; diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/package.xml b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/package.xml index b4df2cd9b..9ca3cbb56 100644 --- a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/package.xml +++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/package.xml @@ -15,8 +15,6 @@ ros2_medkit_gateway rclcpp nlohmann-json-dev - libcpp-httplib-dev - libssl-dev ament_lint_auto ament_lint_common diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/src/param_beacon_plugin.cpp b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/src/param_beacon_plugin.cpp index 6d53eeccb..0dde8cd64 100644 --- a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/src/param_beacon_plugin.cpp +++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/src/param_beacon_plugin.cpp @@ -149,33 +149,30 @@ void ParameterBeaconPlugin::shutdown() { skip_remaining_.clear(); } -std::vector ParameterBeaconPlugin::get_route_descriptions() const { - return { - {"GET", "apps/{app_id}/x-medkit-param-beacon"}, - {"GET", "components/{component_id}/x-medkit-param-beacon"}, - }; -} - -void ParameterBeaconPlugin::register_routes(httplib::Server & server, const std::string & api_prefix) { - for (const auto & entity_type : {"apps", "components"}) { - auto pattern = api_prefix + "/" + entity_type + R"(/([^/]+)/x-medkit-param-beacon)"; - server.Get(pattern.c_str(), [this](const httplib::Request & req, httplib::Response & res) { - auto entity_id = req.matches[1].str(); - - auto entity = ctx_->validate_entity_for_route(req, res, entity_id); - if (!entity) { - return; - } - - auto stored = store_->get(entity_id); - if (!stored) { - PluginContext::send_error(res, 404, "x-medkit-beacon-not-found", "No beacon data for entity"); - return; - } - - PluginContext::send_json(res, ros2_medkit_beacon::build_beacon_response(entity_id, *stored)); - }); +std::vector ParameterBeaconPlugin::get_routes() { + std::vector routes; + for (const auto * entity_type : {"apps", "components"}) { + std::string pattern = std::string(entity_type) + R"(/([^/]+)/x-medkit-param-beacon)"; + routes.push_back( + {"GET", pattern, + [this](const ros2_medkit_gateway::PluginRequest & req, ros2_medkit_gateway::PluginResponse & res) { + auto entity_id = req.path_param(1); + + auto entity = ctx_->validate_entity_for_route(req, res, entity_id); + if (!entity) { + return; + } + + auto stored = store_->get(entity_id); + if (!stored) { + res.send_error(404, "x-medkit-beacon-not-found", "No beacon data for entity"); + return; + } + + res.send_json(ros2_medkit_beacon::build_beacon_response(entity_id, *stored)); + }}); } + return routes; } IntrospectionResult ParameterBeaconPlugin::introspect(const IntrospectionInput & input) { diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/test/test_param_beacon_plugin.cpp b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/test/test_param_beacon_plugin.cpp index a3d731050..5d84e065c 100644 --- a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/test/test_param_beacon_plugin.cpp +++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/test/test_param_beacon_plugin.cpp @@ -24,6 +24,7 @@ #include #include +#include "ros2_medkit_gateway/plugins/plugin_http_types.hpp" #include "ros2_medkit_param_beacon/param_beacon_plugin.hpp" using ros2_medkit_beacon::BeaconHint; @@ -34,6 +35,8 @@ using ros2_medkit_gateway::IntrospectionProvider; using ros2_medkit_gateway::PLUGIN_API_VERSION; using ros2_medkit_gateway::PluginContext; using ros2_medkit_gateway::PluginEntityInfo; +using ros2_medkit_gateway::PluginRequest; +using ros2_medkit_gateway::PluginResponse; using ros2_medkit_gateway::SovdEntityType; using ros2_medkit_param_beacon::ParameterClientInterface; using ::testing::_; @@ -44,16 +47,28 @@ extern "C" int plugin_api_version(); extern "C" GatewayPlugin * create_plugin(); extern "C" IntrospectionProvider * get_introspection_provider(GatewayPlugin * plugin); -// Stubs for PluginContext static methods +// Stubs for PluginRequest/PluginResponse (implemented in gateway_lib, not linked into tests) namespace ros2_medkit_gateway { -void PluginContext::send_json(httplib::Response & res, const nlohmann::json & data) { - res.set_content(data.dump(), "application/json"); +PluginRequest::PluginRequest(const void * impl) : impl_(impl) { } -void PluginContext::send_error(httplib::Response & res, int status, const std::string &, const std::string & message, - const nlohmann::json &) { - res.status = status; - nlohmann::json err = {{"error", message}}; - res.set_content(err.dump(), "application/json"); +std::string PluginRequest::path_param(size_t /*index*/) const { + return {}; +} +std::string PluginRequest::header(const std::string & /*name*/) const { + return {}; +} +std::string PluginRequest::path() const { + return {}; +} +std::string PluginRequest::body() const { + return {}; +} +PluginResponse::PluginResponse(void * impl) : impl_(impl) { +} +void PluginResponse::send_json(const nlohmann::json & /*data*/) { +} +void PluginResponse::send_error(int /*status*/, const std::string & /*error_code*/, const std::string & /*message*/, + const nlohmann::json & /*parameters*/) { } } // namespace ros2_medkit_gateway @@ -84,7 +99,7 @@ class MockPluginContext : public PluginContext { nlohmann::json list_entity_faults(const std::string &) const override { return nlohmann::json::array(); } - std::optional validate_entity_for_route(const httplib::Request &, httplib::Response &, + std::optional validate_entity_for_route(const PluginRequest &, PluginResponse &, const std::string &) const override { return std::nullopt; } From 9543239f9ccc624dc6ff75f8be7185d8632d80a2 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Fri, 3 Apr 2026 10:40:32 +0200 Subject: [PATCH 06/18] refactor: migrate topic_beacon to get_routes plugin API Replace register_routes + get_route_descriptions with get_routes() returning vector. Handler now uses PluginRequest/PluginResponse instead of httplib types directly. Remove httplib and OpenSSL build/package dependencies. Update MockPluginContext::validate_entity_for_route and stubs in test. --- .../ros2_medkit_topic_beacon/CMakeLists.txt | 13 +---- .../topic_beacon_plugin.hpp | 5 +- .../ros2_medkit_topic_beacon/package.xml | 2 - .../src/topic_beacon_plugin.cpp | 49 +++++++++---------- .../test/test_topic_beacon_plugin.cpp | 35 ++++++++----- 5 files changed, 49 insertions(+), 55 deletions(-) diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/CMakeLists.txt b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/CMakeLists.txt index 7703d0559..35e21bf5b 100644 --- a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/CMakeLists.txt +++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/CMakeLists.txt @@ -35,13 +35,6 @@ find_package(ros2_medkit_gateway REQUIRED) find_package(rclcpp REQUIRED) find_package(diagnostic_msgs REQUIRED) find_package(nlohmann_json REQUIRED) -find_package(OpenSSL REQUIRED) - -# cpp-httplib via multi-distro compatibility macro -medkit_find_cpp_httplib() - -# Enable OpenSSL support for cpp-httplib -add_compile_definitions(CPPHTTPLIB_OPENSSL_SUPPORT) # MODULE target: loaded via dlopen at runtime by PluginManager. # Symbols from gateway_lib are resolved from the host process at runtime. @@ -64,8 +57,6 @@ medkit_target_dependencies(topic_beacon_plugin target_link_libraries(topic_beacon_plugin nlohmann_json::nlohmann_json ros2_medkit_beacon_common::beacon_common_lib - cpp_httplib_target - OpenSSL::SSL OpenSSL::Crypto ) install(TARGETS topic_beacon_plugin @@ -105,9 +96,7 @@ if(BUILD_TESTING) ros2_medkit_msgs ros2_medkit_gateway ros2_medkit_beacon_common rclcpp diagnostic_msgs) target_link_libraries(test_topic_beacon_plugin nlohmann_json::nlohmann_json - ros2_medkit_beacon_common::beacon_common_lib - cpp_httplib_target - OpenSSL::SSL OpenSSL::Crypto) + ros2_medkit_beacon_common::beacon_common_lib) medkit_set_test_domain(test_topic_beacon_plugin) endif() diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/include/ros2_medkit_topic_beacon/topic_beacon_plugin.hpp b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/include/ros2_medkit_topic_beacon/topic_beacon_plugin.hpp index ee78f48dd..c39cf33c9 100644 --- a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/include/ros2_medkit_topic_beacon/topic_beacon_plugin.hpp +++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/include/ros2_medkit_topic_beacon/topic_beacon_plugin.hpp @@ -14,8 +14,6 @@ #pragma once -#include - #include #include #include @@ -83,8 +81,7 @@ class TopicBeaconPlugin : public ros2_medkit_gateway::GatewayPlugin, public ros2 void configure(const nlohmann::json & config) override; void set_context(ros2_medkit_gateway::PluginContext & context) override; void shutdown() override; - void register_routes(httplib::Server & server, const std::string & api_prefix) override; - std::vector get_route_descriptions() const override; + std::vector get_routes() override; ros2_medkit_gateway::IntrospectionResult introspect(const ros2_medkit_gateway::IntrospectionInput & input) override; // Expose for testing diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/package.xml b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/package.xml index 0becf8648..33b9ff4ff 100644 --- a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/package.xml +++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/package.xml @@ -17,8 +17,6 @@ rclcpp diagnostic_msgs nlohmann-json-dev - libcpp-httplib-dev - libssl-dev ament_lint_auto ament_lint_common diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/src/topic_beacon_plugin.cpp b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/src/topic_beacon_plugin.cpp index e11f62924..7d979ed7f 100644 --- a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/src/topic_beacon_plugin.cpp +++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/src/topic_beacon_plugin.cpp @@ -103,34 +103,31 @@ void TopicBeaconPlugin::shutdown() { subscription_.reset(); } -std::vector TopicBeaconPlugin::get_route_descriptions() const { - return { - {"GET", "apps/{app_id}/x-medkit-topic-beacon"}, - {"GET", "components/{component_id}/x-medkit-topic-beacon"}, - }; -} - -void TopicBeaconPlugin::register_routes(httplib::Server & server, const std::string & api_prefix) { +std::vector TopicBeaconPlugin::get_routes() { + std::vector routes; // Register beacon metadata endpoint for apps and components - for (const auto & entity_type : {"apps", "components"}) { - auto pattern = api_prefix + "/" + entity_type + R"(/([^/]+)/x-medkit-topic-beacon)"; - server.Get(pattern.c_str(), [this](const httplib::Request & req, httplib::Response & res) { - auto entity_id = req.matches[1].str(); - - auto entity = ctx_->validate_entity_for_route(req, res, entity_id); - if (!entity) { - return; - } - - auto stored = store_->get(entity_id); - if (!stored) { - PluginContext::send_error(res, 404, "x-medkit-beacon-not-found", "No beacon data for entity"); - return; - } - - PluginContext::send_json(res, ros2_medkit_beacon::build_beacon_response(entity_id, *stored)); - }); + for (const auto * entity_type : {"apps", "components"}) { + std::string pattern = std::string(entity_type) + R"(/([^/]+)/x-medkit-topic-beacon)"; + routes.push_back( + {"GET", pattern, + [this](const ros2_medkit_gateway::PluginRequest & req, ros2_medkit_gateway::PluginResponse & res) { + auto entity_id = req.path_param(1); + + auto entity = ctx_->validate_entity_for_route(req, res, entity_id); + if (!entity) { + return; + } + + auto stored = store_->get(entity_id); + if (!stored) { + res.send_error(404, "x-medkit-beacon-not-found", "No beacon data for entity"); + return; + } + + res.send_json(ros2_medkit_beacon::build_beacon_response(entity_id, *stored)); + }}); } + return routes; } IntrospectionResult TopicBeaconPlugin::introspect(const IntrospectionInput & input) { diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/test/test_topic_beacon_plugin.cpp b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/test/test_topic_beacon_plugin.cpp index 3f4b2531d..a28845624 100644 --- a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/test/test_topic_beacon_plugin.cpp +++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/test/test_topic_beacon_plugin.cpp @@ -24,6 +24,7 @@ #include #include +#include "ros2_medkit_gateway/plugins/plugin_http_types.hpp" #include "ros2_medkit_topic_beacon/topic_beacon_plugin.hpp" using ros2_medkit_beacon::BeaconHint; @@ -34,6 +35,8 @@ using ros2_medkit_gateway::IntrospectionProvider; using ros2_medkit_gateway::PLUGIN_API_VERSION; using ros2_medkit_gateway::PluginContext; using ros2_medkit_gateway::PluginEntityInfo; +using ros2_medkit_gateway::PluginRequest; +using ros2_medkit_gateway::PluginResponse; using ros2_medkit_gateway::SovdEntityType; // Extern "C" plugin exports (defined in topic_beacon_plugin.cpp, linked into test binary) @@ -41,17 +44,28 @@ extern "C" int plugin_api_version(); extern "C" GatewayPlugin * create_plugin(); extern "C" IntrospectionProvider * get_introspection_provider(GatewayPlugin * plugin); -// Stubs for PluginContext static methods (normally provided by gateway_lib). -// The test binary doesn't link gateway_lib, so we provide minimal implementations. +// Stubs for PluginRequest/PluginResponse (implemented in gateway_lib, not linked into tests) namespace ros2_medkit_gateway { -void PluginContext::send_json(httplib::Response & res, const nlohmann::json & data) { - res.set_content(data.dump(), "application/json"); +PluginRequest::PluginRequest(const void * impl) : impl_(impl) { } -void PluginContext::send_error(httplib::Response & res, int status, const std::string & /*error_code*/, - const std::string & message, const nlohmann::json & /*parameters*/) { - res.status = status; - nlohmann::json err = {{"error", message}}; - res.set_content(err.dump(), "application/json"); +std::string PluginRequest::path_param(size_t /*index*/) const { + return {}; +} +std::string PluginRequest::header(const std::string & /*name*/) const { + return {}; +} +std::string PluginRequest::path() const { + return {}; +} +std::string PluginRequest::body() const { + return {}; +} +PluginResponse::PluginResponse(void * impl) : impl_(impl) { +} +void PluginResponse::send_json(const nlohmann::json & /*data*/) { +} +void PluginResponse::send_error(int /*status*/, const std::string & /*error_code*/, const std::string & /*message*/, + const nlohmann::json & /*parameters*/) { } } // namespace ros2_medkit_gateway @@ -78,8 +92,7 @@ class MockPluginContext : public PluginContext { return nlohmann::json::array(); } - std::optional validate_entity_for_route(const httplib::Request & /*req*/, - httplib::Response & /*res*/, + std::optional validate_entity_for_route(const PluginRequest & /*req*/, PluginResponse & /*res*/, const std::string & /*entity_id*/) const override { return std::nullopt; } From 253983a6f65ad209c2d613711492ce768824af95 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Fri, 3 Apr 2026 10:40:43 +0200 Subject: [PATCH 07/18] refactor: migrate linux_introspection plugins to get_routes API Replace register_routes(httplib::Server&, prefix) with get_routes() returning vector in procfs, systemd, and container plugins. Handlers now use PluginRequest/PluginResponse instead of httplib types directly. Remove httplib and OpenSSL build/package dependencies from all three plugin targets and tests. --- .../CMakeLists.txt | 13 +++--- .../package.xml | 2 - .../src/container_plugin.cpp | 42 +++++++++--------- .../src/procfs_plugin.cpp | 43 +++++++++---------- .../src/systemd_plugin.cpp | 43 ++++++++++--------- 5 files changed, 68 insertions(+), 75 deletions(-) diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/CMakeLists.txt b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/CMakeLists.txt index 497235a12..db36d0474 100644 --- a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/CMakeLists.txt +++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/CMakeLists.txt @@ -19,9 +19,6 @@ endif() find_package(ament_cmake REQUIRED) find_package(ros2_medkit_gateway REQUIRED) find_package(nlohmann_json REQUIRED) -medkit_find_cpp_httplib() # From ROS2MedkitCompat - handles Humble/Jazzy/Rolling differences -find_package(OpenSSL REQUIRED) -add_compile_definitions(CPPHTTPLIB_OPENSSL_SUPPORT) # Must match gateway's definition for ODR safety # --- Static utility library --- add_library(medkit_linux_utils STATIC @@ -38,7 +35,7 @@ target_include_directories(medkit_linux_utils PUBLIC ${ros2_medkit_gateway_INCLU add_library(procfs_introspection MODULE src/procfs_plugin.cpp ) -target_link_libraries(procfs_introspection medkit_linux_utils cpp_httplib_target OpenSSL::SSL OpenSSL::Crypto) +target_link_libraries(procfs_introspection medkit_linux_utils) target_include_directories(procfs_introspection PRIVATE ${ros2_medkit_gateway_INCLUDE_DIRS}) medkit_target_dependencies(procfs_introspection nlohmann_json) install(TARGETS procfs_introspection LIBRARY DESTINATION lib/${PROJECT_NAME}) @@ -51,7 +48,7 @@ if(SYSTEMD_FOUND) add_library(systemd_introspection MODULE src/systemd_plugin.cpp ) - target_link_libraries(systemd_introspection medkit_linux_utils ${SYSTEMD_LIBRARIES} cpp_httplib_target OpenSSL::SSL OpenSSL::Crypto) + target_link_libraries(systemd_introspection medkit_linux_utils ${SYSTEMD_LIBRARIES}) target_include_directories(systemd_introspection PRIVATE ${SYSTEMD_INCLUDE_DIRS} ${ros2_medkit_gateway_INCLUDE_DIRS}) medkit_target_dependencies(systemd_introspection nlohmann_json) install(TARGETS systemd_introspection LIBRARY DESTINATION lib/${PROJECT_NAME}) @@ -63,7 +60,7 @@ endif() add_library(container_introspection MODULE src/container_plugin.cpp ) -target_link_libraries(container_introspection medkit_linux_utils cpp_httplib_target OpenSSL::SSL OpenSSL::Crypto) +target_link_libraries(container_introspection medkit_linux_utils) target_include_directories(container_introspection PRIVATE ${ros2_medkit_gateway_INCLUDE_DIRS}) medkit_target_dependencies(container_introspection nlohmann_json) install(TARGETS container_introspection LIBRARY DESTINATION lib/${PROJECT_NAME}) @@ -85,12 +82,12 @@ if(BUILD_TESTING) target_link_libraries(test_pid_cache medkit_linux_utils) ament_add_gtest(test_procfs_plugin test/test_procfs_plugin.cpp) - target_link_libraries(test_procfs_plugin medkit_linux_utils cpp_httplib_target) + target_link_libraries(test_procfs_plugin medkit_linux_utils) target_include_directories(test_procfs_plugin PRIVATE ${ros2_medkit_gateway_INCLUDE_DIRS}) medkit_target_dependencies(test_procfs_plugin nlohmann_json) ament_add_gtest(test_container_plugin test/test_container_plugin.cpp) - target_link_libraries(test_container_plugin medkit_linux_utils cpp_httplib_target) + target_link_libraries(test_container_plugin medkit_linux_utils) target_include_directories(test_container_plugin PRIVATE ${ros2_medkit_gateway_INCLUDE_DIRS}) medkit_target_dependencies(test_container_plugin nlohmann_json) diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/package.xml b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/package.xml index b13fb155a..51187f976 100644 --- a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/package.xml +++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/package.xml @@ -12,8 +12,6 @@ ros2_medkit_gateway nlohmann-json-dev - libcpp-httplib-dev - libssl-dev libsystemd-dev ament_lint_auto diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/src/container_plugin.cpp b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/src/container_plugin.cpp index 3f16110ce..e1d1fade3 100644 --- a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/src/container_plugin.cpp +++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/src/container_plugin.cpp @@ -14,6 +14,7 @@ #include "ros2_medkit_gateway/plugins/gateway_plugin.hpp" #include "ros2_medkit_gateway/plugins/plugin_context.hpp" +#include "ros2_medkit_gateway/plugins/plugin_http_types.hpp" #include "ros2_medkit_gateway/plugins/plugin_types.hpp" #include "ros2_medkit_gateway/providers/introspection_provider.hpp" #include "ros2_medkit_linux_introspection/cgroup_reader.hpp" @@ -21,7 +22,6 @@ #include "ros2_medkit_linux_introspection/plugin_config.hpp" #include "ros2_medkit_linux_introspection/proc_reader.hpp" -#include #include #include @@ -48,15 +48,17 @@ class ContainerPlugin : public GatewayPlugin, public IntrospectionProvider { ctx.register_capability(SovdEntityType::COMPONENT, "x-medkit-container"); } - void register_routes(httplib::Server & server, const std::string & api_prefix) override { - server.Get((api_prefix + R"(/apps/([^/]+)/x-medkit-container)").c_str(), - [this](const httplib::Request & req, httplib::Response & res) { - handle_app_request(req, res); - }); - server.Get((api_prefix + R"(/components/([^/]+)/x-medkit-container)").c_str(), - [this](const httplib::Request & req, httplib::Response & res) { - handle_component_request(req, res); - }); + std::vector get_routes() override { + return { + {"GET", R"(apps/([^/]+)/x-medkit-container)", + [this](const PluginRequest & req, PluginResponse & res) { + handle_app_request(req, res); + }}, + {"GET", R"(components/([^/]+)/x-medkit-container)", + [this](const PluginRequest & req, PluginResponse & res) { + handle_component_request(req, res); + }}, + }; } IntrospectionResult introspect(const IntrospectionInput & input) override { @@ -90,8 +92,8 @@ class ContainerPlugin : public GatewayPlugin, public IntrospectionProvider { std::make_unique(); std::string proc_root_{"/"}; - void handle_app_request(const httplib::Request & req, httplib::Response & res) { - auto entity_id = req.matches[1].str(); + void handle_app_request(const PluginRequest & req, PluginResponse & res) { + auto entity_id = req.path_param(1); auto entity = ctx_->validate_entity_for_route(req, res, entity_id); if (!entity) { return; @@ -99,28 +101,26 @@ class ContainerPlugin : public GatewayPlugin, public IntrospectionProvider { auto pid_opt = pid_cache_->lookup(entity->fqn, proc_root_); if (!pid_opt) { - PluginContext::send_error(res, 404, "x-medkit-pid-lookup-failed", "Process not found for entity " + entity_id); + res.send_error(404, "x-medkit-pid-lookup-failed", "Process not found for entity " + entity_id); return; } auto cgroup_info = ros2_medkit_linux_introspection::read_cgroup_info(*pid_opt, proc_root_); if (!cgroup_info) { - PluginContext::send_error(res, 503, "x-medkit-cgroup-read-failed", - "Failed to read cgroup information for entity " + entity_id); + res.send_error(503, "x-medkit-cgroup-read-failed", "Failed to read cgroup information for entity " + entity_id); return; } if (cgroup_info->container_id.empty()) { - PluginContext::send_error(res, 404, "x-medkit-not-containerized", - "Entity " + entity_id + " is not running in a container"); + res.send_error(404, "x-medkit-not-containerized", "Entity " + entity_id + " is not running in a container"); return; } - PluginContext::send_json(res, ros2_medkit_linux_introspection::cgroup_info_to_json(*cgroup_info)); + res.send_json(ros2_medkit_linux_introspection::cgroup_info_to_json(*cgroup_info)); } - void handle_component_request(const httplib::Request & req, httplib::Response & res) { - auto entity_id = req.matches[1].str(); + void handle_component_request(const PluginRequest & req, PluginResponse & res) { + auto entity_id = req.path_param(1); auto entity = ctx_->validate_entity_for_route(req, res, entity_id); if (!entity) { return; @@ -154,7 +154,7 @@ class ContainerPlugin : public GatewayPlugin, public IntrospectionProvider { for (auto & [_, container_json] : containers) { result["containers"].push_back(std::move(container_json)); } - PluginContext::send_json(res, result); + res.send_json(result); } }; diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/src/procfs_plugin.cpp b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/src/procfs_plugin.cpp index 97b7d44b9..dd27aecb8 100644 --- a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/src/procfs_plugin.cpp +++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/src/procfs_plugin.cpp @@ -14,13 +14,13 @@ #include "ros2_medkit_gateway/plugins/gateway_plugin.hpp" #include "ros2_medkit_gateway/plugins/plugin_context.hpp" +#include "ros2_medkit_gateway/plugins/plugin_http_types.hpp" #include "ros2_medkit_gateway/plugins/plugin_types.hpp" #include "ros2_medkit_gateway/providers/introspection_provider.hpp" #include "ros2_medkit_linux_introspection/plugin_config.hpp" #include "ros2_medkit_linux_introspection/proc_reader.hpp" #include "ros2_medkit_linux_introspection/procfs_utils.hpp" -#include #include #include @@ -47,18 +47,17 @@ class ProcfsPlugin : public GatewayPlugin, public IntrospectionProvider { ctx.register_capability(SovdEntityType::COMPONENT, "x-medkit-procfs"); } - void register_routes(httplib::Server & server, const std::string & api_prefix) override { - // App-level endpoint - server.Get((api_prefix + R"(/apps/([^/]+)/x-medkit-procfs)").c_str(), - [this](const httplib::Request & req, httplib::Response & res) { - handle_app_request(req, res); - }); - - // Component-level aggregation endpoint - server.Get((api_prefix + R"(/components/([^/]+)/x-medkit-procfs)").c_str(), - [this](const httplib::Request & req, httplib::Response & res) { - handle_component_request(req, res); - }); + std::vector get_routes() override { + return { + {"GET", R"(apps/([^/]+)/x-medkit-procfs)", + [this](const PluginRequest & req, PluginResponse & res) { + handle_app_request(req, res); + }}, + {"GET", R"(components/([^/]+)/x-medkit-procfs)", + [this](const PluginRequest & req, PluginResponse & res) { + handle_component_request(req, res); + }}, + }; } IntrospectionResult introspect(const IntrospectionInput & input) override { @@ -95,8 +94,8 @@ class ProcfsPlugin : public GatewayPlugin, public IntrospectionProvider { std::make_unique(); std::string proc_root_{"/"}; - void handle_app_request(const httplib::Request & req, httplib::Response & res) { - auto entity_id = req.matches[1].str(); + void handle_app_request(const PluginRequest & req, PluginResponse & res) { + auto entity_id = req.path_param(1); auto entity = ctx_->validate_entity_for_route(req, res, entity_id); if (!entity) { return; @@ -104,24 +103,22 @@ class ProcfsPlugin : public GatewayPlugin, public IntrospectionProvider { auto pid_opt = pid_cache_->lookup(entity->fqn, proc_root_); if (!pid_opt) { - PluginContext::send_error(res, 404, "x-medkit-pid-lookup-failed", "Process not found for entity " + entity_id); + res.send_error(404, "x-medkit-pid-lookup-failed", "Process not found for entity " + entity_id); return; } auto proc_info = ros2_medkit_linux_introspection::read_process_info(*pid_opt, proc_root_); if (!proc_info) { - PluginContext::send_error(res, 503, "x-medkit-proc-read-failed", - "Failed to read process information for entity " + entity_id); + res.send_error(503, "x-medkit-proc-read-failed", "Failed to read process information for entity " + entity_id); return; } auto sys_uptime = ros2_medkit_linux_introspection::read_system_uptime(proc_root_); - PluginContext::send_json( - res, ros2_medkit_linux_introspection::process_info_to_json(*proc_info, sys_uptime ? *sys_uptime : 0.0)); + res.send_json(ros2_medkit_linux_introspection::process_info_to_json(*proc_info, sys_uptime ? *sys_uptime : 0.0)); } - void handle_component_request(const httplib::Request & req, httplib::Response & res) { - auto entity_id = req.matches[1].str(); + void handle_component_request(const PluginRequest & req, PluginResponse & res) { + auto entity_id = req.path_param(1); auto entity = ctx_->validate_entity_for_route(req, res, entity_id); if (!entity) { return; @@ -156,7 +153,7 @@ class ProcfsPlugin : public GatewayPlugin, public IntrospectionProvider { for (auto & [_, proc_json] : processes) { result["processes"].push_back(std::move(proc_json)); } - PluginContext::send_json(res, result); + res.send_json(result); } }; diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/src/systemd_plugin.cpp b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/src/systemd_plugin.cpp index 1849d7afe..0a3fba64e 100644 --- a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/src/systemd_plugin.cpp +++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/src/systemd_plugin.cpp @@ -14,13 +14,13 @@ #include "ros2_medkit_gateway/plugins/gateway_plugin.hpp" #include "ros2_medkit_gateway/plugins/plugin_context.hpp" +#include "ros2_medkit_gateway/plugins/plugin_http_types.hpp" #include "ros2_medkit_gateway/plugins/plugin_types.hpp" #include "ros2_medkit_gateway/providers/introspection_provider.hpp" #include "ros2_medkit_linux_introspection/plugin_config.hpp" #include "ros2_medkit_linux_introspection/proc_reader.hpp" #include "ros2_medkit_linux_introspection/systemd_utils.hpp" -#include #include #include #include @@ -136,15 +136,17 @@ class SystemdPlugin : public GatewayPlugin, public IntrospectionProvider { ctx.register_capability(SovdEntityType::COMPONENT, "x-medkit-systemd"); } - void register_routes(httplib::Server & server, const std::string & api_prefix) override { - server.Get((api_prefix + R"(/apps/([^/]+)/x-medkit-systemd)").c_str(), - [this](const httplib::Request & req, httplib::Response & res) { - handle_app_request(req, res); - }); - server.Get((api_prefix + R"(/components/([^/]+)/x-medkit-systemd)").c_str(), - [this](const httplib::Request & req, httplib::Response & res) { - handle_component_request(req, res); - }); + std::vector get_routes() override { + return { + {"GET", R"(apps/([^/]+)/x-medkit-systemd)", + [this](const PluginRequest & req, PluginResponse & res) { + handle_app_request(req, res); + }}, + {"GET", R"(components/([^/]+)/x-medkit-systemd)", + [this](const PluginRequest & req, PluginResponse & res) { + handle_component_request(req, res); + }}, + }; } IntrospectionResult introspect(const IntrospectionInput & input) override { @@ -197,8 +199,8 @@ class SystemdPlugin : public GatewayPlugin, public IntrospectionProvider { {"sub_state", info.sub_state}, {"restart_count", info.restart_count}, {"watchdog_usec", info.watchdog_usec}}; } - void handle_app_request(const httplib::Request & req, httplib::Response & res) { - auto entity_id = req.matches[1].str(); + void handle_app_request(const PluginRequest & req, PluginResponse & res) { + auto entity_id = req.path_param(1); auto entity = ctx_->validate_entity_for_route(req, res, entity_id); if (!entity) { return; @@ -206,14 +208,13 @@ class SystemdPlugin : public GatewayPlugin, public IntrospectionProvider { auto pid_opt = pid_cache_->lookup(entity->fqn, proc_root_); if (!pid_opt) { - PluginContext::send_error(res, 404, "x-medkit-pid-lookup-failed", "Process not found for entity " + entity_id); + res.send_error(404, "x-medkit-pid-lookup-failed", "Process not found for entity " + entity_id); return; } char * unit_cstr = nullptr; if (sd_pid_get_unit(*pid_opt, &unit_cstr) < 0 || !unit_cstr) { - PluginContext::send_error(res, 404, "x-medkit-not-in-systemd-unit", - "Entity " + entity_id + " is not managed by a systemd unit"); + res.send_error(404, "x-medkit-not-in-systemd-unit", "Entity " + entity_id + " is not managed by a systemd unit"); return; } std::string unit_name(unit_cstr); @@ -221,16 +222,16 @@ class SystemdPlugin : public GatewayPlugin, public IntrospectionProvider { auto unit_info = query_unit_info(unit_name); if (!unit_info) { - PluginContext::send_error(res, 503, "x-medkit-systemd-query-failed", - "Failed to query systemd properties for entity " + entity_id); + res.send_error(503, "x-medkit-systemd-query-failed", + "Failed to query systemd properties for entity " + entity_id); return; } - PluginContext::send_json(res, unit_info_to_json(*unit_info)); + res.send_json(unit_info_to_json(*unit_info)); } - void handle_component_request(const httplib::Request & req, httplib::Response & res) { - auto entity_id = req.matches[1].str(); + void handle_component_request(const PluginRequest & req, PluginResponse & res) { + auto entity_id = req.path_param(1); auto entity = ctx_->validate_entity_for_route(req, res, entity_id); if (!entity) { return; @@ -275,7 +276,7 @@ class SystemdPlugin : public GatewayPlugin, public IntrospectionProvider { for (auto & [_, unit_json] : units) { result["units"].push_back(std::move(unit_json)); } - PluginContext::send_json(res, result); + res.send_json(result); } }; From 0513e5c379d53d7b2215961b81587218a4545c06 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Fri, 3 Apr 2026 10:53:19 +0200 Subject: [PATCH 08/18] docs: update cmake README for VENDORED_DIR parameter --- src/ros2_medkit_cmake/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ros2_medkit_cmake/README.md b/src/ros2_medkit_cmake/README.md index d79623a88..7f5bb5023 100644 --- a/src/ros2_medkit_cmake/README.md +++ b/src/ros2_medkit_cmake/README.md @@ -16,7 +16,7 @@ build acceleration, and centralized linting configuration across all packages. Resolves dependency differences across ROS 2 distributions: - `medkit_find_yaml_cpp()` - Finds yaml-cpp (namespaced targets on Jazzy, manual fallback on Humble) -- `medkit_find_cpp_httplib()` - Finds cpp-httplib >= 0.14 via pkg-config or CMake config +- `medkit_find_cpp_httplib()` - Finds cpp-httplib >= 0.14 via pkg-config, CMake config, or vendored fallback (`VENDORED_DIR` param) - `medkit_target_dependencies()` - Drop-in replacement for `ament_target_dependencies` (removed on Rolling) - `medkit_detect_compat_defs()` / `medkit_apply_compat_defs()` - Compile definitions for version-specific APIs From 8d8809c994d482dfd85c4e6c934572afe3284b1e Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Fri, 3 Apr 2026 11:12:54 +0200 Subject: [PATCH 09/18] fix: route separator bug, stale docs, integration test regression - Fix missing '/' between api_prefix and route pattern in PluginManager::register_routes (produced /api/v1apps/... instead of /api/v1/apps/...) - Fix integration test expecting plaintext 'pong' after demo plugin was changed to return JSON - Update plugin-system.rst, server.rst, troubleshooting.rst, and linux_introspection design doc to reference new get_routes() API instead of removed register_routes() - Update documented API version from 4 to 5 - Remove dead code get_all_route_info() (zero callers) - Add unit test verifying PluginManager route wrapping end-to-end --- docs/config/server.rst | 2 +- docs/troubleshooting.rst | 2 +- docs/tutorials/plugin-system.rst | 72 +- .../design/index.rst | 4 +- src/ros2_medkit_gateway/CMakeLists.txt | 6 +- .../plugins/plugin_manager.hpp | 8 - .../src/plugins/plugin_manager.cpp | 52 +- .../src/vendored/cpp_httplib/httplib.h | 7098 +++++++++++++++++ .../test/test_plugin_manager.cpp | 51 + .../test_plugin_vendor_extensions.test.py | 2 +- .../ros2_medkit_graph_provider/CMakeLists.txt | 7 +- .../test/test_graph_provider_plugin.cpp | 8 +- 12 files changed, 7234 insertions(+), 78 deletions(-) diff --git a/docs/config/server.rst b/docs/config/server.rst index 96ad372a8..71949539c 100644 --- a/docs/config/server.rst +++ b/docs/config/server.rst @@ -564,7 +564,7 @@ Plugin loading lifecycle: 4. Provider interfaces are queried via ``extern "C"`` functions 5. ``configure()`` is called with per-plugin config 6. ``set_context()`` passes the gateway context to the plugin -7. ``register_routes()`` allows the plugin to add custom REST endpoints +7. ``get_routes()`` returns custom REST endpoint definitions as ``vector`` Error isolation: if a plugin throws during any lifecycle call, it is disabled without crashing the gateway. Other plugins continue to operate normally. diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 245cdbef5..5c283c971 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -254,7 +254,7 @@ ros2_medkit is currently suitable for development and testing. For production: **Q: Can I extend ros2_medkit with custom endpoints?** Yes. The gateway plugin framework allows you to add custom REST endpoints via -``GatewayPlugin::register_routes()``. Create a shared library (``.so``) that +``GatewayPlugin::get_routes()``. Create a shared library (``.so``) that implements the ``GatewayPlugin`` base class and configure it in ``gateway_params.yaml``. See :doc:`/config/server` for plugin configuration details. diff --git a/docs/tutorials/plugin-system.rst b/docs/tutorials/plugin-system.rst index 34d2ea378..7d56440ef 100644 --- a/docs/tutorials/plugin-system.rst +++ b/docs/tutorials/plugin-system.rst @@ -157,7 +157,6 @@ A self-contained plugin implementing UpdateProvider (copy-paste starting point): #include "ros2_medkit_gateway/plugins/plugin_types.hpp" #include "ros2_medkit_gateway/providers/update_provider.hpp" - #include #include using namespace ros2_medkit_gateway; @@ -228,7 +227,7 @@ Plugin Lifecycle 4. Provider interfaces are queried via ``get_update_provider()`` / ``get_introspection_provider()`` / ``get_log_provider()`` / ``get_script_provider()`` 5. ``configure()`` is called with per-plugin JSON config 6. ``set_context()`` provides ``PluginContext`` with ROS 2 node, entity cache, faults, and HTTP utilities -7. ``register_routes()`` allows registering custom REST endpoints +7. ``get_routes()`` returns custom REST endpoint definitions as ``vector`` 8. Runtime: subsystem managers call provider methods as needed 9. ``shutdown()`` is called before the plugin is destroyed @@ -242,8 +241,14 @@ providing access to gateway data and utilities: - ``get_entity(id)`` - look up any entity (area, component, app, function) from the discovery cache - ``list_entity_faults(entity_id)`` - query faults for an entity - ``validate_entity_for_route(req, res, entity_id)`` - validate entity exists and matches the route type, auto-sending SOVD errors on failure -- ``send_error()`` / ``send_json()`` - SOVD-compliant HTTP response helpers (static methods) - ``register_capability()`` / ``register_entity_capability()`` - register custom capabilities on entities + +.. note:: + + SOVD-compliant HTTP response helpers (``send_json()``, ``send_error()``) are instance + methods on ``PluginResponse``, not static methods on ``PluginContext``. Use + ``res.send_json(data)`` and ``res.send_error(status, code, msg)`` inside route handlers. + - ``check_lock(entity_id, client_id, collection)`` - verify lock access before mutating operations; returns ``LockAccessResult`` with ``allowed`` flag and denial details - ``acquire_lock()`` / ``release_lock()`` - acquire and release entity locks with optional scope and TTL - ``get_entity_snapshot()`` - returns an ``IntrospectionInput`` populated from the current entity cache @@ -302,13 +307,14 @@ for the lower-level registry API. still required because ``plugin_api_version()`` must return the current version (exact-match check). -PluginContext API (v4) +PluginContext API (v5) ---------------------- -Version 4 of the plugin API introduced several new methods on ``PluginContext``. -These methods have default no-op implementations, so existing plugins continue to -compile without changes (though a rebuild is required to match the new -``PLUGIN_API_VERSION``). +Version 5 of the plugin API replaced ``register_routes()`` with ``get_routes()`` +and moved ``send_json``/``send_error`` from ``PluginContext`` static methods to +``PluginResponse`` instance methods. Plugins that implement custom REST routes +require source changes to adapt to the new API. Plugins that do not implement +routes only need a rebuild to match the new ``PLUGIN_API_VERSION``. **check_lock(entity_id, client_id, collection)** @@ -320,7 +326,7 @@ should call this before proceeding: auto result = ctx_->check_lock(entity_id, client_id, "configurations"); if (!result.allowed) { - PluginContext::send_error(res, 409, result.denied_code, result.denied_reason); + res.send_error(409, result.denied_code, result.denied_reason); return; } @@ -378,30 +384,36 @@ API described in `Cyclic Subscription Extensions`_. Custom REST Endpoints --------------------- -Any plugin can register vendor-specific endpoints via ``register_routes()``. -Use ``PluginContext`` utilities for entity validation and SOVD-compliant responses: +Any plugin can expose vendor-specific endpoints by overriding ``get_routes()``, which +returns a ``vector``. Each route specifies an HTTP method, a URL pattern +relative to the API prefix (no leading slash), and a handler. Use ``PluginRequest`` and +``PluginResponse`` for path parameters and SOVD-compliant responses: .. code-block:: cpp - void register_routes(httplib::Server& server, const std::string& api_prefix) override { - // Global vendor endpoint - server.Get(api_prefix + "/x-myvendor/status", - [this](const httplib::Request&, httplib::Response& res) { - PluginContext::send_json(res, get_status_json()); - }); - - // Entity-scoped endpoint (matches a registered capability) - server.Get((api_prefix + R"(/apps/([^/]+)/x-medkit-traces)").c_str(), - [this](const httplib::Request& req, httplib::Response& res) { - auto entity = ctx_->validate_entity_for_route(req, res, req.matches[1]); - if (!entity) return; // Error already sent - - auto faults = ctx_->list_entity_faults(entity->id); - PluginContext::send_json(res, {{"entity", entity->id}, {"faults", faults}}); - }); + std::vector get_routes() override { + return { + // Global vendor endpoint + {"GET", "x-myvendor/status", + [this](const PluginRequest& /*req*/, PluginResponse& res) { + res.send_json(get_status_json()); + }}, + + // Entity-scoped endpoint (matches a registered capability) + {"GET", R"(apps/([^/]+)/x-medkit-traces)", + [this](const PluginRequest& req, PluginResponse& res) { + auto entity_id = req.path_param(1); + auto entity = ctx_->validate_entity_for_route(req, res, entity_id); + if (!entity) return; // Error already sent + + auto faults = ctx_->list_entity_faults(entity->id); + res.send_json({{"entity", entity->id}, {"faults", faults}}); + }}, + }; } -Use the ``x-`` prefix for vendor-specific endpoints per SOVD convention. +Use the ``x-`` prefix for vendor-specific endpoints per SOVD convention. Patterns are +relative to the API prefix and must not include a leading slash. For entity-scoped endpoints, register a matching capability via ``register_capability()`` or ``register_entity_capability()`` in ``set_context()`` so the endpoint appears in the @@ -750,7 +762,7 @@ Alternatively, simply do not install the ``ros2_medkit_graph_provider`` package Error Handling -------------- -If a plugin throws during any lifecycle method (``configure``, ``set_context``, ``register_routes``, +If a plugin throws during any lifecycle method (``configure``, ``set_context``, ``get_routes``, ``shutdown``), the exception is caught and logged. The plugin is disabled but the gateway continues operating. A failing plugin never crashes the gateway. @@ -761,7 +773,7 @@ Plugins export ``plugin_api_version()`` which must return the gateway's ``PLUGIN If the version does not match, the plugin is rejected with a clear error message suggesting a rebuild against matching gateway headers. -The current API version is **4**. It is incremented when the ``PluginContext`` vtable changes +The current API version is **5**. It is incremented when the ``PluginContext`` vtable changes or breaking changes are made to ``GatewayPlugin`` or provider interfaces. Build Requirements diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/design/index.rst b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/design/index.rst index d8c733cf2..a7def9f45 100644 --- a/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/design/index.rst +++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_linux_introspection/design/index.rst @@ -32,7 +32,7 @@ The following diagram shows the three plugins and their shared infrastructure. +name(): string +configure(config): void +set_context(ctx): void - +register_routes(server, prefix): void + +get_routes(): vector~PluginRoute~ } interface IntrospectionProvider { @@ -43,8 +43,6 @@ The following diagram shows the three plugins and their shared infrastructure. +register_capability() +validate_entity_for_route() +get_child_apps() - +send_json() - +send_error() } } diff --git a/src/ros2_medkit_gateway/CMakeLists.txt b/src/ros2_medkit_gateway/CMakeLists.txt index 2fed3da85..391e51907 100644 --- a/src/ros2_medkit_gateway/CMakeLists.txt +++ b/src/ros2_medkit_gateway/CMakeLists.txt @@ -779,7 +779,11 @@ endif() # Export include directories for downstream packages (plugins) install(DIRECTORY include/ DESTINATION include) install(DIRECTORY src/vendored/tl_expected/include/ DESTINATION include/ros2_medkit_gateway/vendored) -install(FILES src/vendored/cpp_httplib/httplib.h DESTINATION include/ros2_medkit_gateway/vendored) +# Vendored httplib.h installed to share/ (NOT include/) so it does not pollute +# downstream include paths. Used as VENDORED_DIR fallback by packages whose tests +# need httplib (e.g. graph_provider) on platforms where the system package is too old. +install(FILES src/vendored/cpp_httplib/httplib.h + DESTINATION share/${PROJECT_NAME}/vendored/cpp_httplib) ament_export_include_directories(include include/ros2_medkit_gateway/vendored) ament_package() diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_manager.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_manager.hpp index 5b96d4e4f..2e393a49d 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_manager.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_manager.hpp @@ -161,14 +161,6 @@ class PluginManager { return context_; } - // ---- Route descriptions (for handle_root auto-registration) ---- - - /** - * @brief Collect route info (method, pattern) from all active plugins - * @return Combined (method, pattern) pairs from all plugins - */ - std::vector> get_all_route_info() const; - // ---- Info ---- bool has_plugins() const; std::vector plugin_names() const; diff --git a/src/ros2_medkit_gateway/src/plugins/plugin_manager.cpp b/src/ros2_medkit_gateway/src/plugins/plugin_manager.cpp index 5df409f92..0b883eb8e 100644 --- a/src/ros2_medkit_gateway/src/plugins/plugin_manager.cpp +++ b/src/ros2_medkit_gateway/src/plugins/plugin_manager.cpp @@ -273,22 +273,36 @@ void PluginManager::register_routes(void * server_ptr, const std::string & api_p try { auto routes = lp.load_result.plugin->get_routes(); for (auto & route : routes) { - std::string full_pattern = api_prefix + route.pattern; + std::string full_pattern = api_prefix + "/" + route.pattern; auto handler_fn = route.handler; // capture by value for lambda - auto httplib_handler = [handler_fn](const httplib::Request & req, httplib::Response & res) { - PluginRequest plugin_req(&req); - PluginResponse plugin_res(&res); - handler_fn(plugin_req, plugin_res); + auto plugin_name = lp.load_result.plugin->name(); + auto httplib_handler = [handler_fn, plugin_name, &full_pattern](const httplib::Request & req, + httplib::Response & res) { + try { + PluginRequest plugin_req(&req); + PluginResponse plugin_res(&res); + handler_fn(plugin_req, plugin_res); + } catch (const std::exception & e) { + RCLCPP_ERROR(rclcpp::get_logger("plugin_manager"), "Plugin '%s' handler threw on %s: %s", + plugin_name.c_str(), full_pattern.c_str(), e.what()); + PluginResponse plugin_res(&res); + plugin_res.send_error(500, "x-medkit-plugin-error", "Internal plugin error"); + } catch (...) { + RCLCPP_ERROR(rclcpp::get_logger("plugin_manager"), "Plugin '%s' handler threw unknown exception on %s", + plugin_name.c_str(), full_pattern.c_str()); + PluginResponse plugin_res(&res); + plugin_res.send_error(500, "x-medkit-plugin-error", "Internal plugin error"); + } }; if (route.method == "GET") { - server->Get(full_pattern.c_str(), httplib_handler); + server->Get(full_pattern, httplib_handler); } else if (route.method == "POST") { - server->Post(full_pattern.c_str(), httplib_handler); + server->Post(full_pattern, httplib_handler); } else if (route.method == "PUT") { - server->Put(full_pattern.c_str(), httplib_handler); + server->Put(full_pattern, httplib_handler); } else if (route.method == "DELETE") { - server->Delete(full_pattern.c_str(), httplib_handler); + server->Delete(full_pattern, httplib_handler); } else { RCLCPP_WARN(logger(), "Plugin '%s' registered route with unknown method '%s' - skipping", lp.load_result.plugin->name().c_str(), route.method.c_str()); @@ -381,26 +395,6 @@ std::vector> PluginManager::get_ return result; } -std::vector> PluginManager::get_all_route_info() const { - std::shared_lock lock(plugins_mutex_); - std::vector> all; - for (const auto & lp : plugins_) { - if (!lp.load_result.plugin) { - continue; - } - try { - auto routes = lp.load_result.plugin->get_routes(); - for (const auto & route : routes) { - all.emplace_back(route.method, route.pattern); - } - } catch (const std::exception & e) { - RCLCPP_ERROR(rclcpp::get_logger("plugin_manager"), "Plugin '%s' threw in get_routes(): %s", - lp.load_result.plugin->name().c_str(), e.what()); - } - } - return all; -} - bool PluginManager::has_plugins() const { std::shared_lock lock(plugins_mutex_); for (const auto & lp : plugins_) { diff --git a/src/ros2_medkit_gateway/src/vendored/cpp_httplib/httplib.h b/src/ros2_medkit_gateway/src/vendored/cpp_httplib/httplib.h index 9510585a7..d29ff8529 100644 --- a/src/ros2_medkit_gateway/src/vendored/cpp_httplib/httplib.h +++ b/src/ros2_medkit_gateway/src/vendored/cpp_httplib/httplib.h @@ -2262,6 +2262,7104 @@ class mmap { } // namespace detail +// ---------------------------------------------------------------------------- + +/* + * Implementation that will be part of the .cc file if split into .h + .cc. + */ + +namespace detail { + +inline bool is_hex(char c, int &v) { + if (0x20 <= c && 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; +} + +inline 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; +} + +inline 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; +} + +inline 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; +} + +// NOTE: This code came up with the following stackoverflow post: +// https://stackoverflow.com/questions/180947/base64-decode-snippet-in-c +inline 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; +} + +inline bool is_file(const std::string &path) { +#ifdef _WIN32 + return _access_s(path.c_str(), 0) == 0; +#else + struct stat st; + return stat(path.c_str(), &st) >= 0 && S_ISREG(st.st_mode); +#endif +} + +inline bool is_dir(const std::string &path) { + struct stat st; + return stat(path.c_str(), &st) >= 0 && S_ISDIR(st.st_mode); +} + +inline 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] != '/') { + 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; +} + +inline std::string encode_query_param(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(); +} + +inline std::string encode_url(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; +} + +inline std::string decode_url(const std::string &s, + bool convert_plus_to_space) { + std::string result; + + for (size_t i = 0; i < s.size(); i++) { + if (s[i] == '%' && i + 1 < s.size()) { + if (s[i + 1] == 'u') { + auto val = 0; + if (from_hex_to_i(s, i + 2, 4, val)) { + // 4 digits Unicode codes + char buff[4]; + size_t len = to_utf8(val, buff); + if (len > 0) { result.append(buff, len); } + i += 5; // 'u0000' + } else { + result += s[i]; + } + } else { + auto val = 0; + if (from_hex_to_i(s, i + 1, 2, val)) { + // 2 digits hex codes + result += static_cast(val); + i += 2; // '00' + } else { + result += s[i]; + } + } + } else if (convert_plus_to_space && s[i] == '+') { + result += ' '; + } else { + result += s[i]; + } + } + + return result; +} + +inline void read_file(const std::string &path, std::string &out) { + std::ifstream fs(path, std::ios_base::binary); + fs.seekg(0, std::ios_base::end); + auto size = fs.tellg(); + fs.seekg(0); + out.resize(static_cast(size)); + fs.read(&out[0], static_cast(size)); +} + +inline std::string file_extension(const std::string &path) { + std::smatch m; + static auto re = std::regex("\\.([a-zA-Z0-9]+)$"); + if (std::regex_search(path, m, re)) { return m[1].str(); } + return std::string(); +} + +inline bool is_space_or_tab(char c) { return c == ' ' || c == '\t'; } + +inline 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); +} + +inline 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); +} + +inline 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; +} + +inline void split(const char *b, const char *e, char d, + std::function fn) { + return split(b, e, d, std::numeric_limits::max(), fn); +} + +inline 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]); } + } +} + +inline 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) {} + +inline const char *stream_line_reader::ptr() const { + if (glowable_buffer_.empty()) { + return fixed_buffer_; + } else { + return glowable_buffer_.data(); + } +} + +inline size_t stream_line_reader::size() const { + if (glowable_buffer_.empty()) { + return fixed_buffer_used_size_; + } else { + return glowable_buffer_.size(); + } +} + +inline bool stream_line_reader::end_with_crlf() const { + auto end = ptr() + size(); + return size() >= 2 && end[-2] == '\r' && end[-1] == '\n'; +} + +inline bool stream_line_reader::getline() { + fixed_buffer_used_size_ = 0; + glowable_buffer_.clear(); + + for (size_t i = 0;; i++) { + 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); + + if (byte == '\n') { break; } + } + + return true; +} + +inline 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 (glowable_buffer_.empty()) { + assert(fixed_buffer_[fixed_buffer_used_size_] == '\0'); + glowable_buffer_.assign(fixed_buffer_, fixed_buffer_used_size_); + } + glowable_buffer_ += c; + } +} + +inline mmap::mmap(const char *path) +#if defined(_WIN32) + : hFile_(NULL), hMapping_(NULL) +#else + : fd_(-1) +#endif + , + size_(0), addr_(nullptr) { + open(path); +} + +inline mmap::~mmap() { close(); } + +inline bool mmap::open(const char *path) { + close(); + +#if defined(_WIN32) + hFile_ = ::CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + + if (hFile_ == INVALID_HANDLE_VALUE) { return false; } + + size_ = ::GetFileSize(hFile_, NULL); + + hMapping_ = ::CreateFileMapping(hFile_, NULL, PAGE_READONLY, 0, 0, NULL); + + if (hMapping_ == NULL) { + close(); + return false; + } + + addr_ = ::MapViewOfFile(hMapping_, FILE_MAP_READ, 0, 0, 0); +#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); +#endif + + if (addr_ == nullptr) { + close(); + return false; + } + + return true; +} + +inline bool mmap::is_open() const { return addr_ != nullptr; } + +inline size_t mmap::size() const { return size_; } + +inline const char *mmap::data() const { return (const char *)addr_; } + +inline 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; + } +#else + if (addr_ != nullptr) { + munmap(addr_, size_); + addr_ = nullptr; + } + + if (fd_ != -1) { + ::close(fd_); + fd_ = -1; + } +#endif + size_ = 0; +} +inline int close_socket(socket_t sock) { +#ifdef _WIN32 + return closesocket(sock); +#else + return close(sock); +#endif +} + +template inline ssize_t handle_EINTR(T fn) { + ssize_t res = 0; + while (true) { + res = fn(); + if (res < 0 && errno == EINTR) { continue; } + break; + } + return res; +} + +inline 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); + }); +} + +inline 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); + }); +} + +inline ssize_t select_read(socket_t sock, time_t sec, time_t usec) { +#ifdef CPPHTTPLIB_USE_POLL + struct pollfd pfd_read; + pfd_read.fd = sock; + pfd_read.events = POLLIN; + + auto timeout = static_cast(sec * 1000 + usec / 1000); + + return handle_EINTR([&]() { return poll(&pfd_read, 1, timeout); }); +#else +#ifndef _WIN32 + if (sock >= FD_SETSIZE) { return 1; } +#endif + + fd_set fds; + FD_ZERO(&fds); + FD_SET(sock, &fds); + + timeval tv; + tv.tv_sec = static_cast(sec); + tv.tv_usec = static_cast(usec); + + return handle_EINTR([&]() { + return select(static_cast(sock + 1), &fds, nullptr, nullptr, &tv); + }); +#endif +} + +inline ssize_t select_write(socket_t sock, time_t sec, time_t usec) { +#ifdef CPPHTTPLIB_USE_POLL + struct pollfd pfd_read; + pfd_read.fd = sock; + pfd_read.events = POLLOUT; + + auto timeout = static_cast(sec * 1000 + usec / 1000); + + return handle_EINTR([&]() { return poll(&pfd_read, 1, timeout); }); +#else +#ifndef _WIN32 + if (sock >= FD_SETSIZE) { return 1; } +#endif + + fd_set fds; + FD_ZERO(&fds); + FD_SET(sock, &fds); + + timeval tv; + tv.tv_sec = static_cast(sec); + tv.tv_usec = static_cast(usec); + + return handle_EINTR([&]() { + return select(static_cast(sock + 1), nullptr, &fds, nullptr, &tv); + }); +#endif +} + +inline Error wait_until_socket_is_ready(socket_t sock, time_t sec, + time_t usec) { +#ifdef CPPHTTPLIB_USE_POLL + struct pollfd pfd_read; + pfd_read.fd = sock; + pfd_read.events = POLLIN | POLLOUT; + + auto timeout = static_cast(sec * 1000 + usec / 1000); + + auto poll_res = handle_EINTR([&]() { return poll(&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; +#else +#ifndef _WIN32 + if (sock >= FD_SETSIZE) { return Error::Connection; } +#endif + + fd_set fdsr; + FD_ZERO(&fdsr); + FD_SET(sock, &fdsr); + + auto fdsw = fdsr; + auto fdse = fdsr; + + timeval tv; + tv.tv_sec = static_cast(sec); + tv.tv_usec = static_cast(usec); + + auto ret = handle_EINTR([&]() { + return select(static_cast(sock + 1), &fdsr, &fdsw, &fdse, &tv); + }); + + if (ret == 0) { return Error::ConnectionTimeout; } + + if (ret > 0 && (FD_ISSET(sock, &fdsr) || FD_ISSET(sock, &fdsw))) { + 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; +#endif +} + +inline 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 : 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); + ~SocketStream() override; + + bool is_readable() const override; + bool is_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; + +private: + socket_t sock_; + time_t read_timeout_sec_; + time_t read_timeout_usec_; + time_t write_timeout_sec_; + time_t write_timeout_usec_; + + 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; +}; + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +class SSLSocketStream : public Stream { +public: + SSLSocketStream(socket_t sock, SSL *ssl, time_t read_timeout_sec, + time_t read_timeout_usec, time_t write_timeout_sec, + time_t write_timeout_usec); + ~SSLSocketStream() override; + + bool is_readable() const override; + bool is_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; + +private: + socket_t sock_; + SSL *ssl_; + time_t read_timeout_sec_; + time_t read_timeout_usec_; + time_t write_timeout_sec_; + time_t write_timeout_usec_; +}; +#endif + +inline bool keep_alive(socket_t sock, time_t keep_alive_timeout_sec) { + using namespace std::chrono; + auto start = steady_clock::now(); + while (true) { + auto val = select_read(sock, 0, 10000); + if (val < 0) { + return false; + } else if (val == 0) { + auto current = steady_clock::now(); + auto duration = duration_cast(current - start); + auto timeout = keep_alive_timeout_sec * 1000; + if (duration.count() > timeout) { return false; } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } else { + return true; + } + } +} + +template +inline 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 (svr_sock != INVALID_SOCKET && count > 0 && + keep_alive(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 +inline 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); + }); +} + +inline 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, + std::function callback) { + SocketStream strm(sock, read_timeout_sec, read_timeout_usec, + write_timeout_sec, write_timeout_usec); + return callback(strm); +} + +inline int shutdown_socket(socket_t sock) { +#ifdef _WIN32 + return shutdown(sock, SD_BOTH); +#else + return shutdown(sock, SHUT_RDWR); +#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, + SocketOptions socket_options, + BindOrConnect bind_or_connect) { + // 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 = 0; + + 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; + } + +#ifndef _WIN32 + if (hints.ai_family == AF_UNIX) { + const auto addrlen = host.length(); + if (addrlen > sizeof(sockaddr_un::sun_path)) { return INVALID_SOCKET; } + + auto sock = socket(hints.ai_family, hints.ai_socktype, hints.ai_protocol); + if (sock != INVALID_SOCKET) { + sockaddr_un addr{}; + addr.sun_family = AF_UNIX; + std::copy(host.begin(), host.end(), addr.sun_path); + + hints.ai_addr = reinterpret_cast(&addr); + hints.ai_addrlen = static_cast( + sizeof(addr) - sizeof(addr.sun_path) + addrlen); + + fcntl(sock, F_SETFD, FD_CLOEXEC); + if (socket_options) { socket_options(sock); } + + if (!bind_or_connect(sock, hints)) { + close_socket(sock); + sock = INVALID_SOCKET; + } + } + return sock; + } +#endif + + auto service = std::to_string(port); + + if (getaddrinfo(node, service.c_str(), &hints, &result)) { +#if defined __linux__ && !defined __ANDROID__ + res_init(); +#endif + return INVALID_SOCKET; + } + + 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 + auto sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); +#endif + if (sock == INVALID_SOCKET) { continue; } + +#ifndef _WIN32 + if (fcntl(sock, F_SETFD, FD_CLOEXEC) == -1) { + close_socket(sock); + continue; + } +#endif + + if (tcp_nodelay) { + auto yes = 1; +#ifdef _WIN32 + setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, + reinterpret_cast(&yes), sizeof(yes)); +#else + setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, + reinterpret_cast(&yes), sizeof(yes)); +#endif + } + + if (socket_options) { socket_options(sock); } + + if (rp->ai_family == AF_INET6) { + auto no = 0; +#ifdef _WIN32 + setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, + reinterpret_cast(&no), sizeof(no)); +#else + setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, + reinterpret_cast(&no), sizeof(no)); +#endif + } + + // bind or connect + if (bind_or_connect(sock, *rp)) { + freeaddrinfo(result); + return sock; + } + + close_socket(sock); + } + + freeaddrinfo(result); + return INVALID_SOCKET; +} + +inline 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 +} + +inline bool is_connection_error() { +#ifdef _WIN32 + return WSAGetLastError() != WSAEWOULDBLOCK; +#else + return errno != EINPROGRESS; +#endif +} + +inline 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(host.c_str(), "0", &hints, &result)) { return false; } + + 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; + } + } + + freeaddrinfo(result); + return ret; +} + +#if !defined _WIN32 && !defined ANDROID && !defined _AIX && !defined __MVS__ +#define USE_IF2IP +#endif + +#ifdef USE_IF2IP +inline std::string if2ip(int address_family, const std::string &ifn) { + struct ifaddrs *ifap; + getifaddrs(&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)) { + freeifaddrs(ifap); + 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 { + freeifaddrs(ifap); + return std::string(buf, INET6_ADDRSTRLEN); + } + } + } + } + } + } + freeifaddrs(ifap); + return addr_candidate; +} +#endif + +inline socket_t create_client_socket( + const std::string &host, const std::string &ip, int port, + int address_family, bool tcp_nodelay, 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, std::move(socket_options), + [&](socket_t sock2, struct addrinfo &ai) -> 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) { return false; } + } + + set_nonblocking(sock2, false); + + { +#ifdef _WIN32 + auto timeout = static_cast(read_timeout_sec * 1000 + + read_timeout_usec / 1000); + setsockopt(sock2, SOL_SOCKET, SO_RCVTIMEO, + reinterpret_cast(&timeout), sizeof(timeout)); +#else + timeval tv; + tv.tv_sec = static_cast(read_timeout_sec); + tv.tv_usec = static_cast(read_timeout_usec); + setsockopt(sock2, SOL_SOCKET, SO_RCVTIMEO, + reinterpret_cast(&tv), sizeof(tv)); +#endif + } + { + +#ifdef _WIN32 + auto timeout = static_cast(write_timeout_sec * 1000 + + write_timeout_usec / 1000); + setsockopt(sock2, SOL_SOCKET, SO_SNDTIMEO, + reinterpret_cast(&timeout), sizeof(timeout)); +#else + timeval tv; + tv.tv_sec = static_cast(write_timeout_sec); + tv.tv_usec = static_cast(write_timeout_usec); + setsockopt(sock2, SOL_SOCKET, SO_SNDTIMEO, + reinterpret_cast(&tv), sizeof(tv)); +#endif + } + + error = Error::Success; + return true; + }); + + if (sock != INVALID_SOCKET) { + error = Error::Success; + } else { + if (error == Error::Success) { error = Error::Connection; } + } + + return sock; +} + +inline 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; +} + +inline 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); + } +} + +inline 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) // __APPLE__ + 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); + } +} + +inline 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)); +} + +inline unsigned int str2tag(const std::string &s) { + return str2tag_core(s.data(), s.size(), 0); +} + +namespace udl { + +inline constexpr unsigned int operator"" _t(const char *s, size_t l) { + return str2tag_core(s, l, 0); +} + +} // namespace udl + +inline 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"; + } +} + +inline bool can_compress_content_type(const std::string &content_type) { + using udl::operator""_t; + + auto tag = str2tag(content_type); + + switch (tag) { + case "image/svg+xml"_t: + case "application/javascript"_t: + case "application/json"_t: + case "application/xml"_t: + case "application/protobuf"_t: + case "application/xhtml+xml"_t: return true; + + default: + return !content_type.rfind("text/", 0) && tag != "text/event-stream"_t; + } +} + +inline EncodingType encoding_type(const Request &req, const Response &res) { + auto ret = + detail::can_compress_content_type(res.get_header_value("Content-Type")); + if (!ret) { return EncodingType::None; } + + const auto &s = req.get_header_value("Accept-Encoding"); + (void)(s); + +#ifdef CPPHTTPLIB_BROTLI_SUPPORT + // TODO: 'Accept-Encoding' has br, not br;q=0 + ret = s.find("br") != std::string::npos; + if (ret) { return EncodingType::Brotli; } +#endif + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + // TODO: 'Accept-Encoding' has gzip, not gzip;q=0 + ret = s.find("gzip") != std::string::npos; + if (ret) { return EncodingType::Gzip; } +#endif + + return EncodingType::None; +} + +inline 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 +inline 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; +} + +inline gzip_compressor::~gzip_compressor() { deflateEnd(&strm_); } + +inline 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; +} + +inline 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; +} + +inline gzip_decompressor::~gzip_decompressor() { inflateEnd(&strm_); } + +inline bool gzip_decompressor::is_valid() const { return is_valid_; } + +inline 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 +inline brotli_compressor::brotli_compressor() { + state_ = BrotliEncoderCreateInstance(nullptr, nullptr, nullptr); +} + +inline brotli_compressor::~brotli_compressor() { + BrotliEncoderDestroyInstance(state_); +} + +inline 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; +} + +inline brotli_decompressor::brotli_decompressor() { + decoder_s = BrotliDecoderCreateInstance(0, 0, 0); + decoder_r = decoder_s ? BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT + : BROTLI_DECODER_RESULT_ERROR; +} + +inline brotli_decompressor::~brotli_decompressor() { + if (decoder_s) { BrotliDecoderDestroyInstance(decoder_s); } +} + +inline bool brotli_decompressor::is_valid() const { return decoder_s; } + +inline 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 + +inline bool has_header(const Headers &headers, const std::string &key) { + return headers.find(key) != headers.end(); +} + +inline const char *get_header_value(const Headers &headers, + const std::string &key, size_t id, + const char *def) { + 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; +} + +inline bool compare_case_ignore(const std::string &a, const std::string &b) { + if (a.size() != b.size()) { return false; } + for (size_t i = 0; i < b.size(); i++) { + if (::tolower(a[i]) != ::tolower(b[i])) { return false; } + } + return true; +} + +template +inline 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++; + } + + 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 = compare_case_ignore(key, "Location") + ? std::string(p, end) + : decode_url(std::string(p, end), false); + fn(std::move(key), std::move(val)); + return true; + } + + return false; +} + +inline bool read_headers(Stream &strm, Headers &headers) { + const auto bufsiz = 2048; + char buf[bufsiz]; + stream_line_reader line_reader(strm, buf, bufsiz); + + 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; } +#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR + } else { + // Blank line indicates end of headers. + if (line_reader.size() == 1) { break; } + line_terminator_len = 1; + } +#else + } else { + continue; // Skip invalid line. + } +#endif + + if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } + + // Exclude line terminator + auto end = line_reader.ptr() + line_reader.size() - line_terminator_len; + + parse_header(line_reader.ptr(), end, + [&](std::string &&key, std::string &&val) { + headers.emplace(std::move(key), std::move(val)); + }); + } + + return true; +} + +inline bool read_content_with_length(Stream &strm, uint64_t len, + Progress progress, + ContentReceiverWithProgress out) { + char buf[CPPHTTPLIB_RECV_BUFSIZ]; + + uint64_t r = 0; + while (r < len) { + auto read_len = static_cast(len - r); + auto n = strm.read(buf, (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ)); + if (n <= 0) { return false; } + + if (!out(buf, static_cast(n), r, len)) { return false; } + r += static_cast(n); + + if (progress) { + if (!progress(r, len)) { return false; } + } + } + + return true; +} + +inline void skip_content_with_length(Stream &strm, uint64_t len) { + char buf[CPPHTTPLIB_RECV_BUFSIZ]; + uint64_t r = 0; + while (r < len) { + auto read_len = static_cast(len - r); + auto n = strm.read(buf, (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ)); + if (n <= 0) { return; } + r += static_cast(n); + } +} + +inline bool read_content_without_length(Stream &strm, + ContentReceiverWithProgress out) { + char buf[CPPHTTPLIB_RECV_BUFSIZ]; + uint64_t r = 0; + for (;;) { + auto n = strm.read(buf, CPPHTTPLIB_RECV_BUFSIZ); + if (n <= 0) { return true; } + + if (!out(buf, static_cast(n), r, 0)) { return false; } + r += static_cast(n); + } + + return true; +} + +template +inline bool read_content_chunked(Stream &strm, T &x, + ContentReceiverWithProgress out) { + const auto bufsiz = 16; + char buf[bufsiz]; + + stream_line_reader line_reader(strm, buf, bufsiz); + + if (!line_reader.getline()) { return false; } + + unsigned long chunk_len; + while (true) { + char *end_ptr; + + chunk_len = std::strtoul(line_reader.ptr(), &end_ptr, 16); + + if (end_ptr == line_reader.ptr()) { return false; } + if (chunk_len == ULONG_MAX) { return false; } + + if (chunk_len == 0) { break; } + + if (!read_content_with_length(strm, chunk_len, nullptr, out)) { + return false; + } + + if (!line_reader.getline()) { return false; } + + if (strcmp(line_reader.ptr(), "\r\n") != 0) { return false; } + + if (!line_reader.getline()) { return false; } + } + + assert(chunk_len == 0); + + // Trailer + if (!line_reader.getline()) { return false; } + + while (strcmp(line_reader.ptr(), "\r\n") != 0) { + if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } + + // Exclude line terminator + constexpr auto line_terminator_len = 2; + auto end = line_reader.ptr() + line_reader.size() - line_terminator_len; + + parse_header(line_reader.ptr(), end, + [&](std::string &&key, std::string &&val) { + x.headers.emplace(std::move(key), std::move(val)); + }); + + if (!line_reader.getline()) { return false; } + } + + return true; +} + +inline bool is_chunked_transfer_encoding(const Headers &headers) { + return !strcasecmp(get_header_value(headers, "Transfer-Encoding", 0, ""), + "chunked"); +} + +template +bool prepare_content_receiver(T &x, int &status, + ContentReceiverWithProgress receiver, + bool decompress, U callback) { + if (decompress) { + std::string encoding = x.get_header_value("Content-Encoding"); + std::unique_ptr decompressor; + + if (encoding == "gzip" || encoding == "deflate") { +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + decompressor = detail::make_unique(); +#else + status = StatusCode::UnsupportedMediaType_415; + return false; +#endif + } else if (encoding.find("br") != std::string::npos) { +#ifdef CPPHTTPLIB_BROTLI_SUPPORT + decompressor = detail::make_unique(); +#else + status = StatusCode::UnsupportedMediaType_415; + return false; +#endif + } + + if (decompressor) { + if (decompressor->is_valid()) { + ContentReceiverWithProgress out = [&](const char *buf, size_t n, + uint64_t off, uint64_t len) { + return decompressor->decompress(buf, n, + [&](const char *buf2, size_t 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, uint64_t off, + uint64_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, + Progress progress, ContentReceiverWithProgress receiver, + bool decompress) { + return prepare_content_receiver( + x, status, std::move(receiver), decompress, + [&](const ContentReceiverWithProgress &out) { + auto ret = true; + auto exceed_payload_max_length = false; + + if (is_chunked_transfer_encoding(x.headers)) { + ret = read_content_chunked(strm, x, out); + } else if (!has_header(x.headers, "Content-Length")) { + ret = read_content_without_length(strm, out); + } else { + auto len = get_header_value_u64(x.headers, "Content-Length", 0, 0); + if (len > payload_max_length) { + exceed_payload_max_length = true; + skip_content_with_length(strm, len); + ret = false; + } else if (len > 0) { + ret = read_content_with_length(strm, len, std::move(progress), out); + } + } + + if (!ret) { + status = exceed_payload_max_length ? StatusCode::PayloadTooLarge_413 + : StatusCode::BadRequest_400; + } + return ret; + }); +} // namespace detail + +inline ssize_t write_headers(Stream &strm, const Headers &headers) { + ssize_t write_len = 0; + for (const auto &x : headers) { + auto len = + strm.write_format("%s: %s\r\n", x.first.c_str(), x.second.c_str()); + 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; +} + +inline 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 +inline bool write_content(Stream &strm, const ContentProvider &content_provider, + size_t offset, size_t length, T is_shutting_down, + Error &error) { + size_t end_offset = offset + length; + auto ok = true; + DataSink data_sink; + + data_sink.write = [&](const char *d, size_t l) -> bool { + if (ok) { + if (strm.is_writable() && write_data(strm, d, l)) { + offset += l; + } else { + ok = false; + } + } + return ok; + }; + + data_sink.is_writable = [&]() -> bool { return strm.is_writable(); }; + + while (offset < end_offset && !is_shutting_down()) { + if (!strm.is_writable()) { + 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; + } + } + + error = Error::Success; + return true; +} + +template +inline 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 +inline 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 (!strm.is_writable() || !write_data(strm, d, l)) { ok = false; } + } + return ok; + }; + + data_sink.is_writable = [&]() -> bool { return strm.is_writable(); }; + + data_sink.done = [&](void) { data_available = false; }; + + while (data_available && !is_shutting_down()) { + if (!strm.is_writable()) { + return false; + } else if (!content_provider(offset, 0, data_sink)) { + return false; + } else if (!ok) { + return false; + } + } + return true; +} + +template +inline 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 (!strm.is_writable() || + !write_data(strm, chunk.data(), chunk.size())) { + ok = false; + } + } + } else { + ok = false; + } + } + return ok; + }; + + data_sink.is_writable = [&]() -> bool { return strm.is_writable(); }; + + 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 (!strm.is_writable() || + !write_data(strm, chunk.data(), chunk.size())) { + ok = false; + return; + } + } + + static const std::string done_marker("0\r\n"); + if (!write_data(strm, done_marker.data(), done_marker.size())) { + 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; + } + } + } + + static const std::string crlf("\r\n"); + if (!write_data(strm, crlf.data(), crlf.size())) { 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.is_writable()) { + 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; + } + } + + error = Error::Success; + return true; +} + +template +inline 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 +inline 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 = new_req; + res = new_res; + + if (res.location.empty()) { res.location = location; } + } + return ret; +} + +inline 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 += it->first; + query += "="; + query += encode_query_param(it->second); + } + return query; +} + +inline void parse_query_text(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(decode_url(key, true), decode_url(val, true)); + } + }); +} + +inline bool parse_multipart_boundary(const std::string &content_type, + std::string &boundary) { + auto boundary_keyword = "boundary="; + auto pos = content_type.find(boundary_keyword); + if (pos == std::string::npos) { return false; } + auto end = content_type.find(';', pos); + auto beg = pos + strlen(boundary_keyword); + boundary = trim_double_quotes_copy(content_type.substr(beg, end - beg)); + return !boundary.empty(); +} + +inline 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 +inline bool parse_range_header(const std::string &s, Ranges &ranges) { +#else +inline bool parse_range_header(const std::string &s, Ranges &ranges) try { +#endif + static auto re_first_range = std::regex(R"(bytes=(\d*-\d*(?:,\s*\d*-\d*)*))"); + std::smatch m; + if (std::regex_match(s, m, re_first_range)) { + auto pos = static_cast(m.position(1)); + auto len = static_cast(m.length(1)); + auto all_valid_ranges = true; + split(&s[pos], &s[pos + len], ',', [&](const char *b, const char *e) { + if (!all_valid_ranges) { return; } + static auto re_another_range = std::regex(R"(\s*(\d*)-(\d*))"); + std::cmatch cm; + if (std::regex_match(b, e, cm, re_another_range)) { + ssize_t first = -1; + if (!cm.str(1).empty()) { + first = static_cast(std::stoll(cm.str(1))); + } + + ssize_t last = -1; + if (!cm.str(2).empty()) { + last = static_cast(std::stoll(cm.str(2))); + } + + if (first != -1 && last != -1 && first > last) { + all_valid_ranges = false; + return; + } + ranges.emplace_back(std::make_pair(first, last)); + } + }); + return all_valid_ranges; + } + return false; +#ifdef CPPHTTPLIB_NO_EXCEPTIONS +} +#else +} catch (...) { return false; } +#endif + +class MultipartFormDataParser { +public: + MultipartFormDataParser() = default; + + void set_boundary(std::string &&boundary) { + boundary_ = 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 ContentReceiver &content_callback, + const MultipartContentHeader &header_callback) { + + buf_append(buf, n); + + while (buf_size() > 0) { + switch (state_) { + case 0: { // Initial boundary + buf_erase(buf_find(dash_boundary_crlf_)); + if (dash_boundary_crlf_.size() > buf_size()) { return true; } + if (!buf_start_with(dash_boundary_crlf_)) { return false; } + buf_erase(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(), + [&](std::string &&, std::string &&) {})) { + is_valid_ = false; + return false; + } + + static const std::string header_content_type = "Content-Type:"; + + if (start_with_case_ignore(header, header_content_type)) { + file_.content_type = + trim_copy(header.substr(header_content_type.size())); + } else { + static const std::regex re_content_disposition( + R"~(^Content-Disposition:\s*form-data;\s*(.*)$)~", + std::regex_constants::icase); + + std::smatch m; + if (std::regex_match(header, m, re_content_disposition)) { + Params params; + parse_disposition_params(m[1], 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()) { + // Only allow UTF-8 enconnding... + static const std::regex re_rfc5987_encoding( + R"~(^UTF-8''(.+?)$)~", std::regex_constants::icase); + + std::smatch m2; + if (std::regex_match(it->second, m2, re_rfc5987_encoding)) { + file_.filename = decode_url(m2[1], false); // 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(); + } + + bool start_with_case_ignore(const std::string &a, + const std::string &b) const { + if (a.size() < b.size()) { return false; } + for (size_t i = 0; i < b.size(); i++) { + if (::tolower(a[i]) != ::tolower(b[i])) { return false; } + } + 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; + MultipartFormData 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; +}; + +inline std::string to_lower(const char *beg, const char *end) { + std::string out; + auto it = beg; + while (it != end) { + out += static_cast(::tolower(*it)); + it++; + } + return out; +} + +inline std::string make_multipart_data_boundary() { + static const char data[] = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + + // 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()}; + std::mt19937 engine(seed_sequence); + + std::string result = "--cpp-httplib-multipart-data-"; + + for (auto i = 0; i < 16; i++) { + result += data[engine() % (sizeof(data) - 1)]; + } + + return result; +} + +inline 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 +inline 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; +} + +inline std::string serialize_multipart_formdata_item_end() { return "\r\n"; } + +inline std::string +serialize_multipart_formdata_finish(const std::string &boundary) { + return "--" + boundary + "--\r\n"; +} + +inline std::string +serialize_multipart_formdata_get_content_type(const std::string &boundary) { + return "multipart/form-data; boundary=" + boundary; +} + +inline std::string +serialize_multipart_formdata(const MultipartFormDataItems &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; +} + +inline std::pair +get_range_offset_and_length(const Request &req, size_t content_length, + size_t index) { + auto r = req.ranges[index]; + + if (r.first == -1 && r.second == -1) { + return std::make_pair(0, content_length); + } + + auto slen = static_cast(content_length); + + if (r.first == -1) { + r.first = (std::max)(static_cast(0), slen - r.second); + r.second = slen - 1; + } + + if (r.second == -1) { r.second = slen - 1; } + return std::make_pair(r.first, static_cast(r.second - r.first) + 1); +} + +inline std::string +make_content_range_header_field(const std::pair &range, + size_t content_length) { + std::string field = "bytes "; + if (range.first != -1) { field += std::to_string(range.first); } + field += "-"; + if (range.second != -1) { field += std::to_string(range.second); } + field += "/"; + field += std::to_string(content_length); + return field; +} + +template +bool process_multipart_ranges_data(const Request &req, Response &res, + const std::string &boundary, + const std::string &content_type, + 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"); + } + + ctoken("Content-Range: "); + const auto &range = req.ranges[i]; + stoken(make_content_range_header_field(range, res.content_length_)); + ctoken("\r\n"); + ctoken("\r\n"); + + auto offsets = get_range_offset_and_length(req, res.content_length_, i); + auto offset = offsets.first; + auto length = offsets.second; + if (!content(offset, length)) { return false; } + ctoken("\r\n"); + } + + ctoken("--"); + stoken(boundary); + ctoken("--"); + + return true; +} + +inline bool make_multipart_ranges_data(const Request &req, Response &res, + const std::string &boundary, + const std::string &content_type, + std::string &data) { + return process_multipart_ranges_data( + req, res, boundary, content_type, + [&](const std::string &token) { data += token; }, + [&](const std::string &token) { data += token; }, + [&](size_t offset, size_t length) { + if (offset < res.body.size()) { + data += res.body.substr(offset, length); + return true; + } + return false; + }); +} + +inline size_t +get_multipart_ranges_data_length(const Request &req, Response &res, + const std::string &boundary, + const std::string &content_type) { + size_t data_length = 0; + + process_multipart_ranges_data( + req, res, boundary, content_type, + [&](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 +inline bool write_multipart_ranges_data(Stream &strm, const Request &req, + Response &res, + const std::string &boundary, + const std::string &content_type, + const T &is_shutting_down) { + return process_multipart_ranges_data( + req, res, boundary, content_type, + [&](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); + }); +} + +inline std::pair +get_range_offset_and_length(const Request &req, const Response &res, + size_t index) { + auto r = req.ranges[index]; + + if (r.second == -1) { + r.second = static_cast(res.content_length_) - 1; + } + + return std::make_pair(r.first, r.second - r.first + 1); +} + +inline bool expect_content(const Request &req) { + if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH" || + req.method == "PRI" || req.method == "DELETE") { + return true; + } + // TODO: check if Content-Length is set + return false; +} + +inline bool has_crlf(const std::string &s) { + auto p = s.c_str(); + while (*p) { + if (*p == '\r' || *p == '\n') { return true; } + p++; + } + return false; +} + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline 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(); +} + +inline std::string MD5(const std::string &s) { + return message_digest(s, EVP_md5()); +} + +inline std::string SHA_256(const std::string &s) { + return message_digest(s, EVP_sha256()); +} + +inline std::string SHA_512(const std::string &s) { + return message_digest(s, EVP_sha512()); +} +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef _WIN32 +// NOTE: This code came up with the following stackoverflow post: +// https://stackoverflow.com/questions/9507184/can-openssl-on-windows-use-the-system-certificate-store +inline bool load_system_certs_on_windows(X509_STORE *store) { + auto hStore = CertOpenSystemStoreW((HCRYPTPROV_LEGACY)NULL, L"ROOT"); + if (!hStore) { return false; } + + auto result = false; + PCCERT_CONTEXT pContext = NULL; + while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) != + nullptr) { + auto encoded_cert = + static_cast(pContext->pbCertEncoded); + + auto x509 = d2i_X509(NULL, &encoded_cert, pContext->cbCertEncoded); + if (x509) { + X509_STORE_add_cert(store, x509); + X509_free(x509); + result = true; + } + } + + CertFreeCertificateContext(pContext); + CertCloseStore(hStore, 0); + + return result; +} +#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && defined(__APPLE__) +#if TARGET_OS_OSX +template +using CFObjectPtr = + std::unique_ptr::type, void (*)(CFTypeRef)>; + +inline void cf_object_ptr_deleter(CFTypeRef obj) { + if (obj) { CFRelease(obj); } +} + +inline bool retrieve_certs_from_keychain(CFObjectPtr &certs) { + CFStringRef keys[] = {kSecClass, kSecMatchLimit, kSecReturnRef}; + CFTypeRef values[] = {kSecClassCertificate, kSecMatchLimitAll, + kCFBooleanTrue}; + + CFObjectPtr query( + CFDictionaryCreate(nullptr, reinterpret_cast(keys), values, + sizeof(keys) / sizeof(keys[0]), + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks), + cf_object_ptr_deleter); + + if (!query) { return false; } + + CFTypeRef security_items = nullptr; + if (SecItemCopyMatching(query.get(), &security_items) != errSecSuccess || + CFArrayGetTypeID() != CFGetTypeID(security_items)) { + return false; + } + + certs.reset(reinterpret_cast(security_items)); + return true; +} + +inline bool retrieve_root_certs_from_keychain(CFObjectPtr &certs) { + CFArrayRef root_security_items = nullptr; + if (SecTrustCopyAnchorCertificates(&root_security_items) != errSecSuccess) { + return false; + } + + certs.reset(root_security_items); + return true; +} + +inline bool add_certs_to_x509_store(CFArrayRef certs, X509_STORE *store) { + auto result = false; + for (auto i = 0; i < CFArrayGetCount(certs); ++i) { + const auto cert = reinterpret_cast( + CFArrayGetValueAtIndex(certs, i)); + + if (SecCertificateGetTypeID() != CFGetTypeID(cert)) { continue; } + + CFDataRef cert_data = nullptr; + if (SecItemExport(cert, kSecFormatX509Cert, 0, nullptr, &cert_data) != + errSecSuccess) { + continue; + } + + CFObjectPtr cert_data_ptr(cert_data, cf_object_ptr_deleter); + + auto encoded_cert = static_cast( + CFDataGetBytePtr(cert_data_ptr.get())); + + auto x509 = + d2i_X509(NULL, &encoded_cert, CFDataGetLength(cert_data_ptr.get())); + + if (x509) { + X509_STORE_add_cert(store, x509); + X509_free(x509); + result = true; + } + } + + return result; +} + +inline bool load_system_certs_on_macos(X509_STORE *store) { + auto result = false; + CFObjectPtr certs(nullptr, cf_object_ptr_deleter); + if (retrieve_certs_from_keychain(certs) && certs) { + result = add_certs_to_x509_store(certs.get(), store); + } + + if (retrieve_root_certs_from_keychain(certs) && certs) { + result = add_certs_to_x509_store(certs.get(), store) || result; + } + + return result; +} +#endif // TARGET_OS_OSX +#endif // _WIN32 +#endif // CPPHTTPLIB_OPENSSL_SUPPORT + +#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 + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline 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); +} +#endif + +inline 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)) { + static 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[key] = val; + } + return true; + } + } + } + return false; +} + +// https://stackoverflow.com/questions/440133/how-do-i-create-a-random-alpha-numeric-string-in-c/440240#answer-440240 +inline std::string random_string(size_t length) { + auto randchar = []() -> char { + const char charset[] = "0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; + const size_t max_index = (sizeof(charset) - 1); + return charset[static_cast(std::rand()) % max_index]; + }; + std::string str(length, 0); + std::generate_n(str.begin(), length, randchar); + return str; +} + +class ContentProviderAdapter { +public: + explicit ContentProviderAdapter( + ContentProviderWithoutLength &&content_provider) + : content_provider_(content_provider) {} + + bool operator()(size_t offset, size_t, DataSink &sink) { + return content_provider_(offset, sink); + } + +private: + ContentProviderWithoutLength content_provider_; +}; + +} // namespace detail + +inline std::string hosted_at(const std::string &hostname) { + std::vector addrs; + hosted_at(hostname, addrs); + if (addrs.empty()) { return std::string(); } + return addrs[0]; +} + +inline 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 (getaddrinfo(hostname.c_str(), nullptr, &hints, &result)) { +#if defined __linux__ && !defined __ANDROID__ + res_init(); +#endif + return; + } + + 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.push_back(ip); + } + } + + freeaddrinfo(result); +} + +inline std::string append_query_params(const std::string &path, + const Params ¶ms) { + std::string path_with_query = path; + const static 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 +inline std::pair make_range_header(Ranges ranges) { + std::string field = "bytes="; + auto i = 0; + for (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)); +} + +inline 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)); +} + +inline 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 +inline bool Request::has_header(const std::string &key) const { + return detail::has_header(headers, key); +} + +inline std::string Request::get_header_value(const std::string &key, + size_t id) const { + return detail::get_header_value(headers, key, id, ""); +} + +inline size_t Request::get_header_value_count(const std::string &key) const { + auto r = headers.equal_range(key); + return static_cast(std::distance(r.first, r.second)); +} + +inline void Request::set_header(const std::string &key, + const std::string &val) { + if (!detail::has_crlf(key) && !detail::has_crlf(val)) { + headers.emplace(key, val); + } +} + +inline bool Request::has_param(const std::string &key) const { + return params.find(key) != params.end(); +} + +inline std::string Request::get_param_value(const std::string &key, + size_t id) const { + auto rng = params.equal_range(key); + auto it = rng.first; + std::advance(it, static_cast(id)); + if (it != rng.second) { return it->second; } + return std::string(); +} + +inline 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)); +} + +inline bool Request::is_multipart_form_data() const { + const auto &content_type = get_header_value("Content-Type"); + return !content_type.rfind("multipart/form-data", 0); +} + +inline bool Request::has_file(const std::string &key) const { + return files.find(key) != files.end(); +} + +inline MultipartFormData Request::get_file_value(const std::string &key) const { + auto it = files.find(key); + if (it != files.end()) { return it->second; } + return MultipartFormData(); +} + +inline std::vector +Request::get_file_values(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; +} + +// Response implementation +inline bool Response::has_header(const std::string &key) const { + return headers.find(key) != headers.end(); +} + +inline std::string Response::get_header_value(const std::string &key, + size_t id) const { + return detail::get_header_value(headers, key, id, ""); +} + +inline size_t Response::get_header_value_count(const std::string &key) const { + auto r = headers.equal_range(key); + return static_cast(std::distance(r.first, r.second)); +} + +inline void Response::set_header(const std::string &key, + const std::string &val) { + if (!detail::has_crlf(key) && !detail::has_crlf(val)) { + headers.emplace(key, val); + } +} + +inline void Response::set_redirect(const std::string &url, int stat) { + if (!detail::has_crlf(url)) { + set_header("Location", url); + if (300 <= stat && stat < 400) { + this->status = stat; + } else { + this->status = StatusCode::Found_302; + } + } +} + +inline 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); +} + +inline void Response::set_content(const std::string &s, + const std::string &content_type) { + set_content(s.data(), s.size(), content_type); +} + +inline 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_ = resource_releaser; + is_chunked_content_provider_ = false; +} + +inline 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_ = resource_releaser; + is_chunked_content_provider_ = false; +} + +inline 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_ = resource_releaser; + is_chunked_content_provider_ = true; +} + +// Result implementation +inline bool Result::has_request_header(const std::string &key) const { + return request_headers_.find(key) != request_headers_.end(); +} + +inline std::string Result::get_request_header_value(const std::string &key, + size_t id) const { + return detail::get_header_value(request_headers_, key, id, ""); +} + +inline 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 +inline ssize_t Stream::write(const char *ptr) { + return write(ptr, strlen(ptr)); +} + +inline ssize_t Stream::write(const std::string &s) { + return write(s.data(), s.size()); +} + +namespace detail { + +// Socket stream implementation +inline 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) + : 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), read_buff_(read_buff_size_, 0) {} + +inline SocketStream::~SocketStream() = default; + +inline bool SocketStream::is_readable() const { + return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0; +} + +inline bool SocketStream::is_writable() const { + return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0 && + is_socket_alive(sock_); +} + +inline 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 (!is_readable()) { 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) { + 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 { + return read_socket(sock_, ptr, size, CPPHTTPLIB_RECV_FLAGS); + } +} + +inline ssize_t SocketStream::write(const char *ptr, size_t size) { + if (!is_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); +} + +inline void SocketStream::get_remote_ip_and_port(std::string &ip, + int &port) const { + return detail::get_remote_ip_and_port(sock_, ip, port); +} + +inline void SocketStream::get_local_ip_and_port(std::string &ip, + int &port) const { + return detail::get_local_ip_and_port(sock_, ip, port); +} + +inline socket_t SocketStream::socket() const { return sock_; } + +// Buffer stream implementation +inline bool BufferStream::is_readable() const { return true; } + +inline bool BufferStream::is_writable() const { return true; } + +inline 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); +} + +inline ssize_t BufferStream::write(const char *ptr, size_t size) { + buffer.append(ptr, size); + return static_cast(size); +} + +inline void BufferStream::get_remote_ip_and_port(std::string & /*ip*/, + int & /*port*/) const {} + +inline void BufferStream::get_local_ip_and_port(std::string & /*ip*/, + int & /*port*/) const {} + +inline socket_t BufferStream::socket() const { return 0; } + +inline const std::string &BufferStream::get_buffer() const { return buffer; } + +inline PathParamsMatcher::PathParamsMatcher(const std::string &pattern) { + // 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); + if (marker_pos == std::string::npos) { break; } + + static_fragments_.push_back( + pattern.substr(last_param_end, marker_pos - last_param_end)); + + const auto param_name_start = marker_pos + 1; + + 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)); + } +} + +inline 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 everythin 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(); +} + +inline bool RegexMatcher::match(Request &request) const { + request.path_params.clear(); + return std::regex_match(request.path, request.matches, regex_); +} + +} // namespace detail + +// HTTP server implementation +inline Server::Server() + : new_task_queue( + [] { return new ThreadPool(CPPHTTPLIB_THREAD_POOL_COUNT); }) { +#ifndef _WIN32 + signal(SIGPIPE, SIG_IGN); +#endif +} + +inline Server::~Server() = default; + +inline 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); + } +} + +inline Server &Server::Get(const std::string &pattern, Handler handler) { + get_handlers_.emplace_back(make_matcher(pattern), std::move(handler)); + return *this; +} + +inline Server &Server::Post(const std::string &pattern, Handler handler) { + post_handlers_.emplace_back(make_matcher(pattern), std::move(handler)); + return *this; +} + +inline Server &Server::Post(const std::string &pattern, + HandlerWithContentReader handler) { + post_handlers_for_content_reader_.emplace_back(make_matcher(pattern), + std::move(handler)); + return *this; +} + +inline Server &Server::Put(const std::string &pattern, Handler handler) { + put_handlers_.emplace_back(make_matcher(pattern), std::move(handler)); + return *this; +} + +inline Server &Server::Put(const std::string &pattern, + HandlerWithContentReader handler) { + put_handlers_for_content_reader_.emplace_back(make_matcher(pattern), + std::move(handler)); + return *this; +} + +inline Server &Server::Patch(const std::string &pattern, Handler handler) { + patch_handlers_.emplace_back(make_matcher(pattern), std::move(handler)); + return *this; +} + +inline Server &Server::Patch(const std::string &pattern, + HandlerWithContentReader handler) { + patch_handlers_for_content_reader_.emplace_back(make_matcher(pattern), + std::move(handler)); + return *this; +} + +inline Server &Server::Delete(const std::string &pattern, Handler handler) { + delete_handlers_.emplace_back(make_matcher(pattern), std::move(handler)); + return *this; +} + +inline Server &Server::Delete(const std::string &pattern, + HandlerWithContentReader handler) { + delete_handlers_for_content_reader_.emplace_back(make_matcher(pattern), + std::move(handler)); + return *this; +} + +inline Server &Server::Options(const std::string &pattern, Handler handler) { + options_handlers_.emplace_back(make_matcher(pattern), std::move(handler)); + return *this; +} + +inline bool Server::set_base_dir(const std::string &dir, + const std::string &mount_point) { + return set_mount_point(mount_point, dir); +} + +inline bool Server::set_mount_point(const std::string &mount_point, + const std::string &dir, Headers headers) { + if (detail::is_dir(dir)) { + std::string mnt = !mount_point.empty() ? mount_point : "/"; + if (!mnt.empty() && mnt[0] == '/') { + base_dirs_.push_back({mnt, dir, std::move(headers)}); + return true; + } + } + return false; +} + +inline 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; +} + +inline 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; +} + +inline Server &Server::set_default_file_mimetype(const std::string &mime) { + default_file_mimetype_ = mime; + return *this; +} + +inline Server &Server::set_file_request_handler(Handler handler) { + file_request_handler_ = std::move(handler); + return *this; +} + +inline Server &Server::set_error_handler(HandlerWithResponse handler) { + error_handler_ = std::move(handler); + return *this; +} + +inline Server &Server::set_error_handler(Handler handler) { + error_handler_ = [handler](const Request &req, Response &res) { + handler(req, res); + return HandlerResponse::Handled; + }; + return *this; +} + +inline Server &Server::set_exception_handler(ExceptionHandler handler) { + exception_handler_ = std::move(handler); + return *this; +} + +inline Server &Server::set_pre_routing_handler(HandlerWithResponse handler) { + pre_routing_handler_ = std::move(handler); + return *this; +} + +inline Server &Server::set_post_routing_handler(Handler handler) { + post_routing_handler_ = std::move(handler); + return *this; +} + +inline Server &Server::set_logger(Logger logger) { + logger_ = std::move(logger); + return *this; +} + +inline Server & +Server::set_expect_100_continue_handler(Expect100ContinueHandler handler) { + expect_100_continue_handler_ = std::move(handler); + return *this; +} + +inline Server &Server::set_address_family(int family) { + address_family_ = family; + return *this; +} + +inline Server &Server::set_tcp_nodelay(bool on) { + tcp_nodelay_ = on; + return *this; +} + +inline Server &Server::set_socket_options(SocketOptions socket_options) { + socket_options_ = std::move(socket_options); + return *this; +} + +inline Server &Server::set_default_headers(Headers headers) { + default_headers_ = std::move(headers); + return *this; +} + +inline Server &Server::set_header_writer( + std::function const &writer) { + header_writer_ = writer; + return *this; +} + +inline Server &Server::set_keep_alive_max_count(size_t count) { + keep_alive_max_count_ = count; + return *this; +} + +inline Server &Server::set_keep_alive_timeout(time_t sec) { + keep_alive_timeout_sec_ = sec; + return *this; +} + +inline Server &Server::set_read_timeout(time_t sec, time_t usec) { + read_timeout_sec_ = sec; + read_timeout_usec_ = usec; + return *this; +} + +inline Server &Server::set_write_timeout(time_t sec, time_t usec) { + write_timeout_sec_ = sec; + write_timeout_usec_ = usec; + return *this; +} + +inline Server &Server::set_idle_interval(time_t sec, time_t usec) { + idle_interval_sec_ = sec; + idle_interval_usec_ = usec; + return *this; +} + +inline Server &Server::set_payload_max_length(size_t length) { + payload_max_length_ = length; + return *this; +} + +inline bool Server::bind_to_port(const std::string &host, int port, + int socket_flags) { + return bind_internal(host, port, socket_flags) >= 0; +} +inline int Server::bind_to_any_port(const std::string &host, int socket_flags) { + return bind_internal(host, 0, socket_flags); +} + +inline bool Server::listen_after_bind() { + auto se = detail::scope_exit([&]() { done_ = true; }); + return listen_internal(); +} + +inline bool Server::listen(const std::string &host, int port, + int socket_flags) { + auto se = detail::scope_exit([&]() { done_ = true; }); + return bind_to_port(host, port, socket_flags) && listen_internal(); +} + +inline bool Server::is_running() const { return is_running_; } + +inline void Server::wait_until_ready() const { + while (!is_running() && !done_) { + std::this_thread::sleep_for(std::chrono::milliseconds{1}); + } +} + +inline void Server::stop() { + if (is_running_) { + assert(svr_sock_ != INVALID_SOCKET); + std::atomic sock(svr_sock_.exchange(INVALID_SOCKET)); + detail::shutdown_socket(sock); + detail::close_socket(sock); + } +} + +inline 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; } + } + + static const std::set methods{ + "GET", "HEAD", "POST", "PUT", "DELETE", + "CONNECT", "OPTIONS", "TRACE", "PATCH", "PRI"}; + + if (methods.find(req.method) == methods.end()) { return false; } + + if (req.version != "HTTP/1.1" && req.version != "HTTP/1.0") { return false; } + + { + // Skip URL fragment + for (size_t i = 0; i < req.target.size(); i++) { + if (req.target[i] == '#') { + req.target.erase(i); + break; + } + } + + size_t count = 0; + + detail::split(req.target.data(), req.target.data() + req.target.size(), '?', + 2, [&](const char *b, const char *e) { + switch (count) { + case 0: + req.path = detail::decode_url(std::string(b, e), false); + break; + case 1: { + if (e - b > 0) { + detail::parse_query_text(std::string(b, e), req.params); + } + break; + } + default: break; + } + count++; + }); + + if (count > 2) { return false; } + } + + return true; +} + +inline bool Server::write_response(Stream &strm, bool close_connection, + const Request &req, Response &res) { + return write_response_core(strm, close_connection, req, res, false); +} + +inline 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); +} + +inline 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") { + res.set_header("Connection", "close"); + } else { + std::stringstream ss; + ss << "timeout=" << keep_alive_timeout_sec_ + << ", max=" << keep_alive_max_count_; + res.set_header("Keep-Alive", ss.str()); + } + + if (!res.has_header("Content-Type") && + (!res.body.empty() || res.content_length_ > 0 || res.content_provider_)) { + res.set_header("Content-Type", "text/plain"); + } + + if (!res.has_header("Content-Length") && res.body.empty() && + !res.content_length_ && !res.content_provider_) { + res.set_header("Content-Length", "0"); + } + + if (!res.has_header("Accept-Ranges") && req.method == "HEAD") { + res.set_header("Accept-Ranges", "bytes"); + } + + if (post_routing_handler_) { post_routing_handler_(req, res); } + + // Response line and headers + { + detail::BufferStream bstrm; + + if (!bstrm.write_format("HTTP/1.1 %d %s\r\n", res.status, + status_message(res.status))) { + return false; + } + + if (!header_writer_(bstrm, res.headers)) { return false; } + + // Flush buffer + auto &data = bstrm.get_buffer(); + detail::write_data(strm, data.data(), data.size()); + } + + // Body + auto ret = true; + if (req.method != "HEAD") { + if (!res.body.empty()) { + if (!detail::write_data(strm, res.body.data(), res.body.size())) { + ret = false; + } + } else if (res.content_provider_) { + if (write_content_with_provider(strm, req, res, boundary, content_type)) { + res.content_provider_success_ = true; + } else { + res.content_provider_success_ = false; + ret = false; + } + } + } + + // Log + if (logger_) { logger_(req, res); } + + return ret; +} + +inline 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 offsets = + detail::get_range_offset_and_length(req, res.content_length_, 0); + auto offset = offsets.first; + auto length = offsets.second; + return detail::write_content(strm, res.content_provider_, offset, length, + is_shutting_down); + } else { + return detail::write_multipart_ranges_data( + strm, req, res, boundary, content_type, is_shutting_down); + } + } else { + if (res.is_chunked_content_provider_) { + auto type = detail::encoding_type(req, res); + + std::unique_ptr compressor; + if (type == detail::EncodingType::Gzip) { +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + compressor = detail::make_unique(); +#endif + } else if (type == detail::EncodingType::Brotli) { +#ifdef CPPHTTPLIB_BROTLI_SUPPORT + compressor = detail::make_unique(); +#endif + } else { + compressor = detail::make_unique(); + } + assert(compressor != nullptr); + + 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); + } + } +} + +inline bool Server::read_content(Stream &strm, Request &req, Response &res) { + MultipartFormDataMap::iterator cur; + auto file_count = 0; + if (read_content_core( + strm, req, res, + // Regular + [&](const char *buf, size_t n) { + if (req.body.size() + n > req.body.max_size()) { return false; } + req.body.append(buf, n); + return true; + }, + // Multipart + [&](const MultipartFormData &file) { + if (file_count++ == CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT) { + return false; + } + cur = req.files.emplace(file.name, file); + return true; + }, + [&](const char *buf, size_t n) { + auto &content = cur->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 (!content_type.find("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? + return false; + } + detail::parse_query_text(req.body, req.params); + } + return true; + } + return false; +} + +inline bool Server::read_content_with_content_receiver( + Stream &strm, Request &req, Response &res, ContentReceiver receiver, + MultipartContentHeader multipart_header, + ContentReceiver multipart_receiver) { + return read_content_core(strm, req, res, std::move(receiver), + std::move(multipart_header), + std::move(multipart_receiver)); +} + +inline bool +Server::read_content_core(Stream &strm, Request &req, Response &res, + ContentReceiver receiver, + MultipartContentHeader multipart_header, + ContentReceiver multipart_receiver) const { + detail::MultipartFormDataParser 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; + return false; + } + + multipart_form_data_parser.set_boundary(std::move(boundary)); + out = [&](const char *buf, size_t n, uint64_t /*off*/, uint64_t /*len*/) { + /* For debug + size_t pos = 0; + while (pos < n) { + auto read_size = (std::min)(1, n - pos); + auto ret = multipart_form_data_parser.parse( + buf + pos, read_size, multipart_receiver, multipart_header); + if (!ret) { return false; } + pos += read_size; + } + return true; + */ + return multipart_form_data_parser.parse(buf, n, multipart_receiver, + multipart_header); + }; + } else { + out = [receiver](const char *buf, size_t n, uint64_t /*off*/, + uint64_t /*len*/) { return receiver(buf, n); }; + } + + if (req.method == "DELETE" && !req.has_header("Content-Length")) { + return true; + } + + if (!detail::read_content(strm, req, payload_max_length_, res.status, nullptr, + out, true)) { + return false; + } + + if (req.is_multipart_form_data()) { + if (!multipart_form_data_parser.is_valid()) { + res.status = StatusCode::BadRequest_400; + return false; + } + } + + return true; +} + +inline bool Server::handle_file_request(const Request &req, Response &res, + bool head) { + 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"; } + + if (detail::is_file(path)) { + for (const auto &kv : entry.headers) { + res.set_header(kv.first, kv.second); + } + + auto mm = std::make_shared(path.c_str()); + if (!mm->is_open()) { 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 (!head && file_request_handler_) { + file_request_handler_(req, res); + } + + return true; + } + } + } + } + return false; +} + +inline 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_, + std::move(socket_options), + [](socket_t sock, struct addrinfo &ai) -> bool { + if (::bind(sock, ai.ai_addr, static_cast(ai.ai_addrlen))) { + return false; + } + if (::listen(sock, CPPHTTPLIB_LISTEN_BACKLOG)) { return false; } + return true; + }); +} + +inline int Server::bind_internal(const std::string &host, int port, + int socket_flags) { + 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) { + 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 { + return -1; + } + } else { + return port; + } +} + +inline bool Server::listen_internal() { + 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 + socket_t sock = accept(svr_sock_, nullptr, nullptr); + + 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::milliseconds(1)); + continue; + } else if (errno == EINTR || errno == EAGAIN) { + continue; + } + if (svr_sock_ != INVALID_SOCKET) { + detail::close_socket(svr_sock_); + ret = false; + } else { + ; // The server socket was closed by user. + } + break; + } + + { +#ifdef _WIN32 + auto timeout = static_cast(read_timeout_sec_ * 1000 + + read_timeout_usec_ / 1000); + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, + reinterpret_cast(&timeout), sizeof(timeout)); +#else + timeval tv; + tv.tv_sec = static_cast(read_timeout_sec_); + tv.tv_usec = static_cast(read_timeout_usec_); + setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, + reinterpret_cast(&tv), sizeof(tv)); +#endif + } + { + +#ifdef _WIN32 + auto timeout = static_cast(write_timeout_sec_ * 1000 + + write_timeout_usec_ / 1000); + setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, + reinterpret_cast(&timeout), sizeof(timeout)); +#else + timeval tv; + tv.tv_sec = static_cast(write_timeout_sec_); + tv.tv_usec = static_cast(write_timeout_usec_); + setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, + reinterpret_cast(&tv), sizeof(tv)); +#endif + } + + task_queue->enqueue([this, sock]() { process_and_close_socket(sock); }); + } + + task_queue->shutdown(); + } + + return ret; +} + +inline bool Server::routing(Request &req, Response &res, Stream &strm) { + if (pre_routing_handler_ && + pre_routing_handler_(req, res) == HandlerResponse::Handled) { + return true; + } + + // File handler + auto is_head_request = req.method == "HEAD"; + if ((req.method == "GET" || is_head_request) && + handle_file_request(req, res, is_head_request)) { + return true; + } + + if (detail::expect_content(req)) { + // Content reader handler + { + ContentReader reader( + [&](ContentReceiver receiver) { + return read_content_with_content_receiver( + strm, req, res, std::move(receiver), nullptr, nullptr); + }, + [&](MultipartContentHeader header, ContentReceiver receiver) { + return read_content_with_content_receiver(strm, req, res, nullptr, + std::move(header), + std::move(receiver)); + }); + + if (req.method == "POST") { + if (dispatch_request_for_content_reader( + req, res, std::move(reader), + post_handlers_for_content_reader_)) { + return true; + } + } else if (req.method == "PUT") { + if (dispatch_request_for_content_reader( + req, res, std::move(reader), + put_handlers_for_content_reader_)) { + return true; + } + } else if (req.method == "PATCH") { + if (dispatch_request_for_content_reader( + req, res, std::move(reader), + patch_handlers_for_content_reader_)) { + return true; + } + } else if (req.method == "DELETE") { + if (dispatch_request_for_content_reader( + req, res, std::move(reader), + delete_handlers_for_content_reader_)) { + return true; + } + } + } + + // Read content into `req.body` + if (!read_content(strm, req, res)) { 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; +} + +inline 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)) { + handler(req, res); + return true; + } + } + return false; +} + +inline void Server::apply_ranges(const Request &req, Response &res, + std::string &content_type, + std::string &boundary) const { + if (req.ranges.size() > 1) { + boundary = detail::make_multipart_data_boundary(); + + auto it = res.headers.find("Content-Type"); + if (it != res.headers.end()) { + content_type = it->second; + res.headers.erase(it); + } + + 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()) { + length = res.content_length_; + } else if (req.ranges.size() == 1) { + auto offsets = + detail::get_range_offset_and_length(req, res.content_length_, 0); + length = offsets.second; + + auto content_range = detail::make_content_range_header_field( + req.ranges[0], res.content_length_); + res.set_header("Content-Range", content_range); + } else { + length = detail::get_multipart_ranges_data_length(req, res, boundary, + content_type); + } + 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::Gzip) { + res.set_header("Content-Encoding", "gzip"); + } else if (type == detail::EncodingType::Brotli) { + res.set_header("Content-Encoding", "br"); + } + } + } + } + } else { + if (req.ranges.empty()) { + ; + } else if (req.ranges.size() == 1) { + auto content_range = detail::make_content_range_header_field( + req.ranges[0], res.body.size()); + res.set_header("Content-Range", content_range); + + auto offsets = + detail::get_range_offset_and_length(req, res.body.size(), 0); + auto offset = offsets.first; + auto length = offsets.second; + + if (offset < res.body.size()) { + res.body = res.body.substr(offset, length); + } else { + res.body.clear(); + res.status = StatusCode::RangeNotSatisfiable_416; + } + } else { + std::string data; + if (detail::make_multipart_ranges_data(req, res, boundary, content_type, + data)) { + res.body.swap(data); + } else { + res.body.clear(); + res.status = StatusCode::RangeNotSatisfiable_416; + } + } + + if (type != detail::EncodingType::None) { + std::unique_ptr compressor; + std::string content_encoding; + + if (type == detail::EncodingType::Gzip) { +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + compressor = detail::make_unique(); + content_encoding = "gzip"; +#endif + } else if (type == detail::EncodingType::Brotli) { +#ifdef CPPHTTPLIB_BROTLI_SUPPORT + compressor = detail::make_unique(); + content_encoding = "br"; +#endif + } + + if (compressor) { + 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", content_encoding); + } + } + } + + auto length = std::to_string(res.body.size()); + res.set_header("Content-Length", length); + } +} + +inline 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)) { + handler(req, res, content_reader); + return true; + } + } + return false; +} + +inline bool +Server::process_request(Stream &strm, bool close_connection, + bool &connection_closed, + const std::function &setup_request) { + 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; + + Response res; + res.version = "HTTP/1.1"; + res.headers = default_headers_; + +#ifdef _WIN32 + // TODO: Increase FD_SETSIZE statically (libzmq), dynamically (MySQL). +#else +#ifndef CPPHTTPLIB_USE_POLL + // Socket file descriptor exceeded FD_SETSIZE... + if (strm.socket() >= FD_SETSIZE) { + Headers dummy; + detail::read_headers(strm, dummy); + res.status = StatusCode::InternalServerError_500; + return write_response(strm, close_connection, req, res); + } +#endif +#endif + + // Check if the request URI doesn't exceed the limit + if (line_reader.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) { + Headers dummy; + detail::read_headers(strm, dummy); + res.status = StatusCode::UriTooLong_414; + return write_response(strm, close_connection, req, res); + } + + // Request line and headers + if (!parse_request_line(line_reader.ptr(), req) || + !detail::read_headers(strm, req.headers)) { + res.status = StatusCode::BadRequest_400; + 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; + } + + strm.get_remote_ip_and_port(req.remote_addr, req.remote_port); + req.set_header("REMOTE_ADDR", req.remote_addr); + req.set_header("REMOTE_PORT", std::to_string(req.remote_port)); + + strm.get_local_ip_and_port(req.local_addr, req.local_port); + req.set_header("LOCAL_ADDR", req.local_addr); + req.set_header("LOCAL_PORT", std::to_string(req.local_port)); + + 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)) { + res.status = StatusCode::RangeNotSatisfiable_416; + 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: + strm.write_format("HTTP/1.1 %d %s\r\n\r\n", status, + status_message(status)); + break; + default: return write_response(strm, close_connection, req, res); + } + } + + // Rounting + auto routed = false; +#ifdef CPPHTTPLIB_NO_EXCEPTIONS + routed = routing(req, res, strm); +#else + try { + routed = routing(req, res, strm); + } catch (std::exception &e) { + if (exception_handler_) { + auto ep = std::current_exception(); + exception_handler_(req, res, ep); + routed = true; + } else { + res.status = StatusCode::InternalServerError_500; + std::string val; + auto s = e.what(); + for (size_t i = 0; s[i]; i++) { + switch (s[i]) { + case '\r': val += "\\r"; break; + case '\n': val += "\\n"; break; + default: val += s[i]; break; + } + } + res.set_header("EXCEPTION_WHAT", val); + } + } catch (...) { + if (exception_handler_) { + auto ep = std::current_exception(); + exception_handler_(req, res, ep); + routed = true; + } else { + res.status = StatusCode::InternalServerError_500; + res.set_header("EXCEPTION_WHAT", "UNKNOWN"); + } + } +#endif + + if (routed) { + if (res.status == -1) { + res.status = req.ranges.empty() ? StatusCode::OK_200 + : StatusCode::PartialContent_206; + } + return write_response_with_content(strm, close_connection, req, res); + } else { + if (res.status == -1) { res.status = StatusCode::NotFound_404; } + return write_response(strm, close_connection, req, res); + } +} + +inline bool Server::is_valid() const { return true; } + +inline bool Server::process_and_close_socket(socket_t sock) { + 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_, + [this](Stream &strm, bool close_connection, bool &connection_closed) { + return process_request(strm, close_connection, connection_closed, + nullptr); + }); + + detail::shutdown_socket(sock); + detail::close_socket(sock); + return ret; +} + +// HTTP client implementation +inline ClientImpl::ClientImpl(const std::string &host) + : ClientImpl(host, 80, std::string(), std::string()) {} + +inline ClientImpl::ClientImpl(const std::string &host, int port) + : ClientImpl(host, port, std::string(), std::string()) {} + +inline ClientImpl::ClientImpl(const std::string &host, int port, + const std::string &client_cert_path, + const std::string &client_key_path) + : host_(host), port_(port), + host_and_port_(adjust_host_string(host) + ":" + std::to_string(port)), + client_cert_path_(client_cert_path), client_key_path_(client_key_path) {} + +inline ClientImpl::~ClientImpl() { + std::lock_guard guard(socket_mutex_); + shutdown_socket(socket_); + close_socket(socket_); +} + +inline bool ClientImpl::is_valid() const { return true; } + +inline 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_; + basic_auth_username_ = rhs.basic_auth_username_; + basic_auth_password_ = rhs.basic_auth_password_; + bearer_token_auth_token_ = rhs.bearer_token_auth_token_; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + digest_auth_username_ = rhs.digest_auth_username_; + digest_auth_password_ = rhs.digest_auth_password_; +#endif + keep_alive_ = rhs.keep_alive_; + follow_location_ = rhs.follow_location_; + url_encode_ = rhs.url_encode_; + address_family_ = rhs.address_family_; + tcp_nodelay_ = rhs.tcp_nodelay_; + socket_options_ = rhs.socket_options_; + compress_ = rhs.compress_; + decompress_ = rhs.decompress_; + 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_; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + proxy_digest_auth_username_ = rhs.proxy_digest_auth_username_; + proxy_digest_auth_password_ = rhs.proxy_digest_auth_password_; +#endif +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + ca_cert_file_path_ = rhs.ca_cert_file_path_; + ca_cert_dir_path_ = rhs.ca_cert_dir_path_; + ca_cert_store_ = rhs.ca_cert_store_; +#endif +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + server_certificate_verification_ = rhs.server_certificate_verification_; +#endif + logger_ = rhs.logger_; +} + +inline socket_t ClientImpl::create_client_socket(Error &error) const { + if (!proxy_host_.empty() && proxy_port_ != -1) { + return detail::create_client_socket( + proxy_host_, std::string(), proxy_port_, address_family_, tcp_nodelay_, + 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_, socket_options_, + connection_timeout_sec_, connection_timeout_usec_, read_timeout_sec_, + read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, interface_, + error); +} + +inline 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; +} + +inline 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()); +} + +inline void ClientImpl::shutdown_socket(Socket &socket) const { + if (socket.sock == INVALID_SOCKET) { return; } + detail::shutdown_socket(socket.sock); +} + +inline 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_OPENSSL_SUPPORT + assert(socket.ssl == nullptr); +#endif + if (socket.sock == INVALID_SOCKET) { return; } + detail::close_socket(socket.sock); + socket.sock = INVALID_SOCKET; +} + +inline bool ClientImpl::read_response_line(Stream &strm, const Request &req, + Response &res) 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 + const static std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r?\n"); +#else + const static 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' + while (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; +} + +inline 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); + } + return ret; +} + +inline 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); + if (!is_alive) { + // Attempt to avoid sigpipe by shutting down nongracefully if it seems + // like the other side has already closed the connection Also, there + // cannot be any requests in flight from other threads since we locked + // request_mutex_, so safe to close everything immediately + const bool shutdown_gracefully = false; + shutdown_ssl(socket_, shutdown_gracefully); + shutdown_socket(socket_); + close_socket(socket_); + } + } + + if (!is_alive) { + if (!create_and_connect_socket(socket_, error)) { return false; } + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + // TODO: refactoring + if (is_ssl()) { + auto &scli = static_cast(*this); + if (!proxy_host_.empty() && proxy_port_ != -1) { + auto success = false; + if (!scli.connect_with_proxy(socket_, res, success, error)) { + return success; + } + } + + if (!scli.initialize_ssl(socket_, error)) { return false; } + } +#endif + } + + // 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) { + shutdown_ssl(socket_, true); + shutdown_socket(socket_); + close_socket(socket_); + } + }); + + ret = process_socket(socket_, [&](Stream &strm) { + return handle_request(strm, req, res, close_connection, error); + }); + + if (!ret) { + if (error == Error::Success) { error = Error::Unknown; } + } + + return ret; +} + +inline Result ClientImpl::send(const Request &req) { + auto req2 = req; + return send_(std::move(req2)); +} + +inline Result ClientImpl::send_(Request &&req) { + auto res = detail::make_unique(); + auto error = Error::Success; + auto ret = send(req, *res, error); + return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers)}; +} + +inline bool ClientImpl::handle_request(Stream &strm, Request &req, + Response &res, bool close_connection, + Error &error) { + if (req.path.empty()) { + error = Error::Connection; + return false; + } + + auto req_save = req; + + bool ret; + + if (!is_ssl() && !proxy_host_.empty() && proxy_port_ != -1) { + auto req2 = req; + req2.path = "http://" + host_and_port_ + req.path; + ret = process_request(strm, req2, res, close_connection, error); + req = 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")) { + // TODO 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_); + shutdown_ssl(socket_, true); + shutdown_socket(socket_); + close_socket(socket_); + } + + if (300 < res.status && res.status < 400 && follow_location_) { + req = req_save; + ret = redirect(req, res, error); + } + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + if ((res.status == StatusCode::Unauthorized_401 || + res.status == StatusCode::ProxyAuthenticationRequired_407) && + req.authorization_count_ < 5) { + auto is_proxy = res.status == StatusCode::ProxyAuthenticationRequired_407; + 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 = new_res; } + } + } + } +#endif + + return ret; +} + +inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) { + if (req.redirect_count_ == 0) { + error = Error::ExceedRedirectCount; + return false; + } + + auto location = res.get_header_value("location"); + if (location.empty()) { return false; } + + const static std::regex re( + R"((?:(https?):)?(?://(?:\[([\d:]+)\]|([^:/?#]+))(?::(\d+))?)?([^?#]*)(\?[^#]*)?(?:#.*)?)"); + + std::smatch m; + if (!std::regex_match(location, m, re)) { return false; } + + auto scheme = is_ssl() ? "https" : "http"; + + auto next_scheme = m[1].str(); + auto next_host = m[2].str(); + if (next_host.empty()) { next_host = m[3].str(); } + auto port_str = m[4].str(); + auto next_path = m[5].str(); + auto next_query = m[6].str(); + + auto next_port = port_; + if (!port_str.empty()) { + next_port = std::stoi(port_str); + } 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 = detail::decode_url(next_path, true) + next_query; + + if (next_scheme == scheme && next_host == host_ && next_port == port_) { + return detail::redirect(*this, req, res, path, location, error); + } else { + if (next_scheme == "https") { +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + SSLClient cli(next_host, next_port); + cli.copy_settings(*this); + if (ca_cert_store_) { cli.set_ca_cert_store(ca_cert_store_); } + return detail::redirect(cli, req, res, path, location, error); +#else + return false; +#endif + } else { + ClientImpl cli(next_host, next_port); + cli.copy_settings(*this); + return detail::redirect(cli, req, res, path, location, error); + } + } +} + +inline 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_) { + // TODO: Brotli support + std::unique_ptr compressor; +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + if (compress_) { + compressor = detail::make_unique(); + } else +#endif + { + compressor = detail::make_unique(); + } + + return detail::write_content_chunked(strm, req.content_provider_, + is_shutting_down, *compressor, error); + } else { + return detail::write_content(strm, req.content_provider_, 0, + req.content_length_, is_shutting_down, error); + } +} + +inline bool ClientImpl::write_request(Stream &strm, Request &req, + bool close_connection, Error &error) { + // Prepare additional headers + if (close_connection) { + if (!req.has_header("Connection")) { + req.set_header("Connection", "close"); + } + } + + if (!req.has_header("Host")) { + if (is_ssl()) { + if (port_ == 443) { + req.set_header("Host", host_); + } else { + req.set_header("Host", host_and_port_); + } + } else { + if (port_ == 80) { + req.set_header("Host", host_); + } else { + req.set_header("Host", host_and_port_); + } + } + } + + if (!req.has_header("Accept")) { req.set_header("Accept", "*/*"); } + +#ifndef CPPHTTPLIB_NO_DEFAULT_USER_AGENT + if (!req.has_header("User-Agent")) { + auto agent = std::string("cpp-httplib/") + CPPHTTPLIB_VERSION; + req.set_header("User-Agent", agent); + } +#endif + + 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"); + } + } + } else { + if (!req.has_header("Content-Type")) { + req.set_header("Content-Type", "text/plain"); + } + + if (!req.has_header("Content-Length")) { + auto length = std::to_string(req.body.size()); + req.set_header("Content-Length", length); + } + } + + 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 (!proxy_basic_auth_username_.empty() && + !proxy_basic_auth_password_.empty()) { + if (!req.has_header("Proxy-Authorization")) { + req.headers.insert(make_basic_authentication_header( + proxy_basic_auth_username_, proxy_basic_auth_password_, true)); + } + } + + if (!bearer_token_auth_token_.empty()) { + if (!req.has_header("Authorization")) { + req.headers.insert(make_bearer_token_authentication_header( + bearer_token_auth_token_, false)); + } + } + + if (!proxy_bearer_token_auth_token_.empty()) { + if (!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; + + const auto &path = url_encode_ ? detail::encode_url(req.path) : req.path; + bstrm.write_format("%s %s HTTP/1.1\r\n", req.method.c_str(), path.c_str()); + + header_writer_(bstrm, req.headers); + + // Flush buffer + auto &data = bstrm.get_buffer(); + if (!detail::write_data(strm, data.data(), data.size())) { + error = Error::Write; + return false; + } + } + + // Body + if (req.body.empty()) { + return write_content_with_provider(strm, req, error); + } + + if (!detail::write_data(strm, req.body.data(), req.body.size())) { + error = Error::Write; + return false; + } + + return true; +} + +inline std::unique_ptr ClientImpl::send_with_content_provider( + Request &req, const char *body, size_t content_length, + ContentProvider content_provider, + ContentProviderWithoutLength content_provider_without_length, + const std::string &content_type, Error &error) { + if (!content_type.empty()) { req.set_header("Content-Type", content_type); } + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + if (compress_) { req.set_header("Content-Encoding", "gzip"); } +#endif + +#ifdef CPPHTTPLIB_ZLIB_SUPPORT + if (compress_ && !content_provider_without_length) { + // TODO: Brotli support + detail::gzip_compressor compressor; + + 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; + 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; + return nullptr; + } + } + } else +#endif + { + 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); + } + } + + auto res = detail::make_unique(); + return send(req, *res, error) ? std::move(res) : nullptr; +} + +inline Result ClientImpl::send_with_content_provider( + 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) { + Request req; + req.method = method; + req.headers = headers; + req.path = path; + + auto error = Error::Success; + + auto res = send_with_content_provider( + req, body, content_length, std::move(content_provider), + std::move(content_provider_without_length), content_type, error); + + return Result{std::move(res), error, std::move(req.headers)}; +} + +inline std::string +ClientImpl::adjust_host_string(const std::string &host) const { + if (host.find(':') != std::string::npos) { return "[" + host + "]"; } + return host; +} + +inline bool ClientImpl::process_request(Stream &strm, Request &req, + Response &res, bool close_connection, + Error &error) { + // Send request + if (!write_request(strm, req, close_connection, error)) { return false; } + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + if (is_ssl()) { + auto is_proxy_enabled = !proxy_host_.empty() && proxy_port_ != -1; + if (!is_proxy_enabled) { + char buf[1]; + if (SSL_peek(socket_.ssl, buf, 1) == 0 && + SSL_get_error(socket_.ssl, 0) == SSL_ERROR_ZERO_RETURN) { + error = Error::SSLPeerCouldBeClosed_; + return false; + } + } + } +#endif + + // Receive response and headers + if (!read_response_line(strm, req, res) || + !detail::read_headers(strm, res.headers)) { + error = Error::Read; + return false; + } + + // Body + if ((res.status != StatusCode::NoContent_204) && req.method != "HEAD" && + req.method != "CONNECT") { + auto redirect = 300 < res.status && res.status < 400 && follow_location_; + + if (req.response_handler && !redirect) { + if (!req.response_handler(res)) { + error = Error::Canceled; + return false; + } + } + + auto out = + req.content_receiver + ? static_cast( + [&](const char *buf, size_t n, uint64_t off, uint64_t len) { + if (redirect) { return true; } + auto ret = req.content_receiver(buf, n, off, len); + if (!ret) { error = Error::Canceled; } + return ret; + }) + : static_cast( + [&](const char *buf, size_t n, uint64_t /*off*/, + uint64_t /*len*/) { + if (res.body.size() + n > res.body.max_size()) { + return false; + } + res.body.append(buf, n); + return true; + }); + + auto progress = [&](uint64_t current, uint64_t total) { + if (!req.progress || redirect) { return true; } + auto ret = req.progress(current, total); + if (!ret) { error = Error::Canceled; } + return ret; + }; + + int dummy_status; + if (!detail::read_content(strm, res, (std::numeric_limits::max)(), + dummy_status, std::move(progress), std::move(out), + decompress_)) { + if (error != Error::Canceled) { error = Error::Read; } + return false; + } + } + + // Log + if (logger_) { logger_(req, res); } + + return true; +} + +inline ContentProviderWithoutLength ClientImpl::get_multipart_content_provider( + const std::string &boundary, const MultipartFormDataItems &items, + const MultipartFormDataProviderItems &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; + } + }; +} + +inline bool +ClientImpl::process_socket(const Socket &socket, + std::function callback) { + return detail::process_client_socket( + socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, + write_timeout_usec_, std::move(callback)); +} + +inline bool ClientImpl::is_ssl() const { return false; } + +inline Result ClientImpl::Get(const std::string &path) { + return Get(path, Headers(), Progress()); +} + +inline Result ClientImpl::Get(const std::string &path, Progress progress) { + return Get(path, Headers(), std::move(progress)); +} + +inline Result ClientImpl::Get(const std::string &path, const Headers &headers) { + return Get(path, headers, Progress()); +} + +inline Result ClientImpl::Get(const std::string &path, const Headers &headers, + Progress progress) { + Request req; + req.method = "GET"; + req.path = path; + req.headers = headers; + req.progress = std::move(progress); + + return send_(std::move(req)); +} + +inline Result ClientImpl::Get(const std::string &path, + ContentReceiver content_receiver) { + return Get(path, Headers(), nullptr, std::move(content_receiver), nullptr); +} + +inline Result ClientImpl::Get(const std::string &path, + ContentReceiver content_receiver, + Progress progress) { + return Get(path, Headers(), nullptr, std::move(content_receiver), + std::move(progress)); +} + +inline Result ClientImpl::Get(const std::string &path, const Headers &headers, + ContentReceiver content_receiver) { + return Get(path, headers, nullptr, std::move(content_receiver), nullptr); +} + +inline Result ClientImpl::Get(const std::string &path, const Headers &headers, + ContentReceiver content_receiver, + Progress progress) { + return Get(path, headers, nullptr, std::move(content_receiver), + std::move(progress)); +} + +inline Result ClientImpl::Get(const std::string &path, + ResponseHandler response_handler, + ContentReceiver content_receiver) { + return Get(path, Headers(), std::move(response_handler), + std::move(content_receiver), nullptr); +} + +inline Result ClientImpl::Get(const std::string &path, const Headers &headers, + ResponseHandler response_handler, + ContentReceiver content_receiver) { + return Get(path, headers, std::move(response_handler), + std::move(content_receiver), nullptr); +} + +inline Result ClientImpl::Get(const std::string &path, + ResponseHandler response_handler, + ContentReceiver content_receiver, + Progress progress) { + return Get(path, Headers(), std::move(response_handler), + std::move(content_receiver), std::move(progress)); +} + +inline Result ClientImpl::Get(const std::string &path, const Headers &headers, + ResponseHandler response_handler, + ContentReceiver content_receiver, + Progress 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, + uint64_t /*offset*/, uint64_t /*total_length*/) { + return content_receiver(data, data_length); + }; + req.progress = std::move(progress); + + return send_(std::move(req)); +} + +inline Result ClientImpl::Get(const std::string &path, const Params ¶ms, + const Headers &headers, Progress progress) { + if (params.empty()) { return Get(path, headers); } + + std::string path_with_query = append_query_params(path, params); + return Get(path_with_query, headers, progress); +} + +inline Result ClientImpl::Get(const std::string &path, const Params ¶ms, + const Headers &headers, + ContentReceiver content_receiver, + Progress progress) { + return Get(path, params, headers, nullptr, content_receiver, progress); +} + +inline Result ClientImpl::Get(const std::string &path, const Params ¶ms, + const Headers &headers, + ResponseHandler response_handler, + ContentReceiver content_receiver, + Progress progress) { + if (params.empty()) { + return Get(path, headers, response_handler, content_receiver, progress); + } + + std::string path_with_query = append_query_params(path, params); + return Get(path_with_query, headers, response_handler, content_receiver, + progress); +} + +inline Result ClientImpl::Head(const std::string &path) { + return Head(path, Headers()); +} + +inline Result ClientImpl::Head(const std::string &path, + const Headers &headers) { + Request req; + req.method = "HEAD"; + req.headers = headers; + req.path = path; + + return send_(std::move(req)); +} + +inline Result ClientImpl::Post(const std::string &path) { + return Post(path, std::string(), std::string()); +} + +inline Result ClientImpl::Post(const std::string &path, + const Headers &headers) { + return Post(path, headers, nullptr, 0, std::string()); +} + +inline Result ClientImpl::Post(const std::string &path, const char *body, + size_t content_length, + const std::string &content_type) { + return Post(path, Headers(), body, content_length, content_type); +} + +inline Result ClientImpl::Post(const std::string &path, const Headers &headers, + const char *body, size_t content_length, + const std::string &content_type) { + return send_with_content_provider("POST", path, headers, body, content_length, + nullptr, nullptr, content_type); +} + +inline Result ClientImpl::Post(const std::string &path, const std::string &body, + const std::string &content_type) { + return Post(path, Headers(), body, content_type); +} + +inline Result ClientImpl::Post(const std::string &path, const Headers &headers, + const std::string &body, + const std::string &content_type) { + return send_with_content_provider("POST", path, headers, body.data(), + body.size(), nullptr, nullptr, + content_type); +} + +inline Result ClientImpl::Post(const std::string &path, const Params ¶ms) { + return Post(path, Headers(), params); +} + +inline Result ClientImpl::Post(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type) { + return Post(path, Headers(), content_length, std::move(content_provider), + content_type); +} + +inline Result ClientImpl::Post(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type) { + return Post(path, Headers(), std::move(content_provider), content_type); +} + +inline Result ClientImpl::Post(const std::string &path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const std::string &content_type) { + return send_with_content_provider("POST", path, headers, nullptr, + content_length, std::move(content_provider), + nullptr, content_type); +} + +inline Result ClientImpl::Post(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type) { + return send_with_content_provider("POST", path, headers, nullptr, 0, nullptr, + std::move(content_provider), content_type); +} + +inline 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"); +} + +inline Result ClientImpl::Post(const std::string &path, + const MultipartFormDataItems &items) { + return Post(path, Headers(), items); +} + +inline Result ClientImpl::Post(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items) { + const auto &boundary = detail::make_multipart_data_boundary(); + const auto &content_type = + detail::serialize_multipart_formdata_get_content_type(boundary); + const auto &body = detail::serialize_multipart_formdata(items, boundary); + return Post(path, headers, body, content_type); +} + +inline Result ClientImpl::Post(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items, + const std::string &boundary) { + 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); + const auto &body = detail::serialize_multipart_formdata(items, boundary); + return Post(path, headers, body, content_type); +} + +inline Result +ClientImpl::Post(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items, + const MultipartFormDataProviderItems &provider_items) { + 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( + "POST", path, headers, nullptr, 0, nullptr, + get_multipart_content_provider(boundary, items, provider_items), + content_type); +} + +inline Result ClientImpl::Put(const std::string &path) { + return Put(path, std::string(), std::string()); +} + +inline Result ClientImpl::Put(const std::string &path, const char *body, + size_t content_length, + const std::string &content_type) { + return Put(path, Headers(), body, content_length, content_type); +} + +inline Result ClientImpl::Put(const std::string &path, const Headers &headers, + const char *body, size_t content_length, + const std::string &content_type) { + return send_with_content_provider("PUT", path, headers, body, content_length, + nullptr, nullptr, content_type); +} + +inline Result ClientImpl::Put(const std::string &path, const std::string &body, + const std::string &content_type) { + return Put(path, Headers(), body, content_type); +} + +inline Result ClientImpl::Put(const std::string &path, const Headers &headers, + const std::string &body, + const std::string &content_type) { + return send_with_content_provider("PUT", path, headers, body.data(), + body.size(), nullptr, nullptr, + content_type); +} + +inline Result ClientImpl::Put(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type) { + return Put(path, Headers(), content_length, std::move(content_provider), + content_type); +} + +inline Result ClientImpl::Put(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type) { + return Put(path, Headers(), std::move(content_provider), content_type); +} + +inline Result ClientImpl::Put(const std::string &path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const std::string &content_type) { + return send_with_content_provider("PUT", path, headers, nullptr, + content_length, std::move(content_provider), + nullptr, content_type); +} + +inline Result ClientImpl::Put(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type) { + return send_with_content_provider("PUT", path, headers, nullptr, 0, nullptr, + std::move(content_provider), content_type); +} + +inline Result ClientImpl::Put(const std::string &path, const Params ¶ms) { + return Put(path, Headers(), params); +} + +inline 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"); +} + +inline Result ClientImpl::Put(const std::string &path, + const MultipartFormDataItems &items) { + return Put(path, Headers(), items); +} + +inline Result ClientImpl::Put(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items) { + const auto &boundary = detail::make_multipart_data_boundary(); + const auto &content_type = + detail::serialize_multipart_formdata_get_content_type(boundary); + const auto &body = detail::serialize_multipart_formdata(items, boundary); + return Put(path, headers, body, content_type); +} + +inline Result ClientImpl::Put(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items, + const std::string &boundary) { + 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); + const auto &body = detail::serialize_multipart_formdata(items, boundary); + return Put(path, headers, body, content_type); +} + +inline Result +ClientImpl::Put(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items, + const MultipartFormDataProviderItems &provider_items) { + 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( + "PUT", path, headers, nullptr, 0, nullptr, + get_multipart_content_provider(boundary, items, provider_items), + content_type); +} +inline Result ClientImpl::Patch(const std::string &path) { + return Patch(path, std::string(), std::string()); +} + +inline Result ClientImpl::Patch(const std::string &path, const char *body, + size_t content_length, + const std::string &content_type) { + return Patch(path, Headers(), body, content_length, content_type); +} + +inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, + const char *body, size_t content_length, + const std::string &content_type) { + return send_with_content_provider("PATCH", path, headers, body, + content_length, nullptr, nullptr, + content_type); +} + +inline Result ClientImpl::Patch(const std::string &path, + const std::string &body, + const std::string &content_type) { + return Patch(path, Headers(), body, content_type); +} + +inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, + const std::string &body, + const std::string &content_type) { + return send_with_content_provider("PATCH", path, headers, body.data(), + body.size(), nullptr, nullptr, + content_type); +} + +inline Result ClientImpl::Patch(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type) { + return Patch(path, Headers(), content_length, std::move(content_provider), + content_type); +} + +inline Result ClientImpl::Patch(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type) { + return Patch(path, Headers(), std::move(content_provider), content_type); +} + +inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const std::string &content_type) { + return send_with_content_provider("PATCH", path, headers, nullptr, + content_length, std::move(content_provider), + nullptr, content_type); +} + +inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type) { + return send_with_content_provider("PATCH", path, headers, nullptr, 0, nullptr, + std::move(content_provider), content_type); +} + +inline Result ClientImpl::Delete(const std::string &path) { + return Delete(path, Headers(), std::string(), std::string()); +} + +inline Result ClientImpl::Delete(const std::string &path, + const Headers &headers) { + return Delete(path, headers, std::string(), std::string()); +} + +inline Result ClientImpl::Delete(const std::string &path, const char *body, + size_t content_length, + const std::string &content_type) { + return Delete(path, Headers(), body, content_length, content_type); +} + +inline Result ClientImpl::Delete(const std::string &path, + const Headers &headers, const char *body, + size_t content_length, + const std::string &content_type) { + Request req; + req.method = "DELETE"; + req.headers = headers; + req.path = path; + + if (!content_type.empty()) { req.set_header("Content-Type", content_type); } + req.body.assign(body, content_length); + + return send_(std::move(req)); +} + +inline Result ClientImpl::Delete(const std::string &path, + const std::string &body, + const std::string &content_type) { + return Delete(path, Headers(), body.data(), body.size(), content_type); +} + +inline Result ClientImpl::Delete(const std::string &path, + const Headers &headers, + const std::string &body, + const std::string &content_type) { + return Delete(path, headers, body.data(), body.size(), content_type); +} + +inline Result ClientImpl::Options(const std::string &path) { + return Options(path, Headers()); +} + +inline Result ClientImpl::Options(const std::string &path, + const Headers &headers) { + Request req; + req.method = "OPTIONS"; + req.headers = headers; + req.path = path; + + return send_(std::move(req)); +} + +inline 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; + } + + // Otherwise, still holding the mutex, we can shut everything down ourselves + shutdown_ssl(socket_, true); + shutdown_socket(socket_); + close_socket(socket_); +} + +inline std::string ClientImpl::host() const { return host_; } + +inline int ClientImpl::port() const { return port_; } + +inline size_t ClientImpl::is_socket_open() const { + std::lock_guard guard(socket_mutex_); + return socket_.is_open(); +} + +inline socket_t ClientImpl::socket() const { return socket_.sock; } + +inline void ClientImpl::set_connection_timeout(time_t sec, time_t usec) { + connection_timeout_sec_ = sec; + connection_timeout_usec_ = usec; +} + +inline void ClientImpl::set_read_timeout(time_t sec, time_t usec) { + read_timeout_sec_ = sec; + read_timeout_usec_ = usec; +} + +inline void ClientImpl::set_write_timeout(time_t sec, time_t usec) { + write_timeout_sec_ = sec; + write_timeout_usec_ = usec; +} + +inline void ClientImpl::set_basic_auth(const std::string &username, + const std::string &password) { + basic_auth_username_ = username; + basic_auth_password_ = password; +} + +inline void ClientImpl::set_bearer_token_auth(const std::string &token) { + bearer_token_auth_token_ = token; +} + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline void ClientImpl::set_digest_auth(const std::string &username, + const std::string &password) { + digest_auth_username_ = username; + digest_auth_password_ = password; +} +#endif + +inline void ClientImpl::set_keep_alive(bool on) { keep_alive_ = on; } + +inline void ClientImpl::set_follow_location(bool on) { follow_location_ = on; } + +inline void ClientImpl::set_url_encode(bool on) { url_encode_ = on; } + +inline void +ClientImpl::set_hostname_addr_map(std::map addr_map) { + addr_map_ = std::move(addr_map); +} + +inline void ClientImpl::set_default_headers(Headers headers) { + default_headers_ = std::move(headers); +} + +inline void ClientImpl::set_header_writer( + std::function const &writer) { + header_writer_ = writer; +} + +inline void ClientImpl::set_address_family(int family) { + address_family_ = family; +} + +inline void ClientImpl::set_tcp_nodelay(bool on) { tcp_nodelay_ = on; } + +inline void ClientImpl::set_socket_options(SocketOptions socket_options) { + socket_options_ = std::move(socket_options); +} + +inline void ClientImpl::set_compress(bool on) { compress_ = on; } + +inline void ClientImpl::set_decompress(bool on) { decompress_ = on; } + +inline void ClientImpl::set_interface(const std::string &intf) { + interface_ = intf; +} + +inline void ClientImpl::set_proxy(const std::string &host, int port) { + proxy_host_ = host; + proxy_port_ = port; +} + +inline void ClientImpl::set_proxy_basic_auth(const std::string &username, + const std::string &password) { + proxy_basic_auth_username_ = username; + proxy_basic_auth_password_ = password; +} + +inline void ClientImpl::set_proxy_bearer_token_auth(const std::string &token) { + proxy_bearer_token_auth_token_ = token; +} + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline void ClientImpl::set_proxy_digest_auth(const std::string &username, + const std::string &password) { + proxy_digest_auth_username_ = username; + proxy_digest_auth_password_ = password; +} + +inline 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; +} + +inline void ClientImpl::set_ca_cert_store(X509_STORE *ca_cert_store) { + if (ca_cert_store && ca_cert_store != ca_cert_store_) { + ca_cert_store_ = ca_cert_store; + } +} + +inline X509_STORE *ClientImpl::create_ca_cert_store(const char *ca_cert, + std::size_t size) const { + auto mem = BIO_new_mem_buf(ca_cert, static_cast(size)); + if (!mem) { return nullptr; } + + auto inf = PEM_X509_INFO_read_bio(mem, nullptr, nullptr, nullptr); + if (!inf) { + BIO_free_all(mem); + return nullptr; + } + + auto cts = X509_STORE_new(); + if (cts) { + 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(cts, itmp->x509); } + if (itmp->crl) { X509_STORE_add_crl(cts, itmp->crl); } + } + } + + sk_X509_INFO_pop_free(inf, X509_INFO_free); + BIO_free_all(mem); + return cts; +} + +inline void ClientImpl::enable_server_certificate_verification(bool enabled) { + server_certificate_verification_ = enabled; +} +#endif + +inline void ClientImpl::set_logger(Logger logger) { + logger_ = std::move(logger); +} + +/* + * SSL Implementation + */ +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +namespace detail { + +template +inline SSL *ssl_new(socket_t sock, SSL_CTX *ctx, std::mutex &ctx_mutex, + U SSL_connect_or_accept, V setup) { + SSL *ssl = nullptr; + { + std::lock_guard guard(ctx_mutex); + ssl = SSL_new(ctx); + } + + if (ssl) { + set_nonblocking(sock, true); + auto bio = BIO_new_socket(static_cast(sock), BIO_NOCLOSE); + BIO_set_nbio(bio, 1); + SSL_set_bio(ssl, bio, bio); + + if (!setup(ssl) || SSL_connect_or_accept(ssl) != 1) { + SSL_shutdown(ssl); + { + std::lock_guard guard(ctx_mutex); + SSL_free(ssl); + } + set_nonblocking(sock, false); + return nullptr; + } + BIO_set_nbio(bio, 0); + set_nonblocking(sock, false); + } + + return ssl; +} + +inline void ssl_delete(std::mutex &ctx_mutex, SSL *ssl, + bool shutdown_gracefully) { + // sometimes we may want to skip this to try to avoid SIGPIPE if we know + // the remote has closed the network connection + // Note that it is not always possible to avoid SIGPIPE, this is merely a + // best-efforts. + if (shutdown_gracefully) { SSL_shutdown(ssl); } + + std::lock_guard guard(ctx_mutex); + SSL_free(ssl); +} + +template +bool ssl_connect_or_accept_nonblocking(socket_t sock, SSL *ssl, + U ssl_connect_or_accept, + time_t timeout_sec, + time_t timeout_usec) { + auto res = 0; + while ((res = ssl_connect_or_accept(ssl)) != 1) { + auto err = SSL_get_error(ssl, res); + switch (err) { + case SSL_ERROR_WANT_READ: + if (select_read(sock, timeout_sec, timeout_usec) > 0) { continue; } + break; + case SSL_ERROR_WANT_WRITE: + if (select_write(sock, timeout_sec, timeout_usec) > 0) { continue; } + break; + default: break; + } + return false; + } + return true; +} + +template +inline bool process_server_socket_ssl( + const std::atomic &svr_sock, SSL *ssl, 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, ssl, read_timeout_sec, read_timeout_usec, + write_timeout_sec, write_timeout_usec); + return callback(strm, close_connection, connection_closed); + }); +} + +template +inline bool +process_client_socket_ssl(SSL *ssl, socket_t sock, time_t read_timeout_sec, + time_t read_timeout_usec, time_t write_timeout_sec, + time_t write_timeout_usec, T callback) { + SSLSocketStream strm(sock, ssl, read_timeout_sec, read_timeout_usec, + write_timeout_sec, write_timeout_usec); + return callback(strm); +} + +class SSLInit { +public: + SSLInit() { + OPENSSL_init_ssl( + OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL); + } +}; + +// SSL socket stream implementation +inline SSLSocketStream::SSLSocketStream(socket_t sock, SSL *ssl, + time_t read_timeout_sec, + time_t read_timeout_usec, + time_t write_timeout_sec, + time_t write_timeout_usec) + : sock_(sock), ssl_(ssl), read_timeout_sec_(read_timeout_sec), + read_timeout_usec_(read_timeout_usec), + write_timeout_sec_(write_timeout_sec), + write_timeout_usec_(write_timeout_usec) { + SSL_clear_mode(ssl, SSL_MODE_AUTO_RETRY); +} + +inline SSLSocketStream::~SSLSocketStream() = default; + +inline bool SSLSocketStream::is_readable() const { + return detail::select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0; +} + +inline bool SSLSocketStream::is_writable() const { + return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0 && + is_socket_alive(sock_); +} + +inline ssize_t SSLSocketStream::read(char *ptr, size_t size) { + if (SSL_pending(ssl_) > 0) { + return SSL_read(ssl_, ptr, static_cast(size)); + } else if (is_readable()) { + auto ret = SSL_read(ssl_, ptr, static_cast(size)); + if (ret < 0) { + auto err = SSL_get_error(ssl_, ret); + auto n = 1000; +#ifdef _WIN32 + while (--n >= 0 && (err == SSL_ERROR_WANT_READ || + (err == SSL_ERROR_SYSCALL && + WSAGetLastError() == WSAETIMEDOUT))) { +#else + while (--n >= 0 && err == SSL_ERROR_WANT_READ) { +#endif + if (SSL_pending(ssl_) > 0) { + return SSL_read(ssl_, ptr, static_cast(size)); + } else if (is_readable()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + ret = SSL_read(ssl_, ptr, static_cast(size)); + if (ret >= 0) { return ret; } + err = SSL_get_error(ssl_, ret); + } else { + return -1; + } + } + } + return ret; + } + return -1; +} + +inline ssize_t SSLSocketStream::write(const char *ptr, size_t size) { + if (is_writable()) { + auto handle_size = static_cast( + std::min(size, (std::numeric_limits::max)())); + + auto ret = SSL_write(ssl_, ptr, static_cast(handle_size)); + if (ret < 0) { + auto err = SSL_get_error(ssl_, ret); + auto n = 1000; +#ifdef _WIN32 + while (--n >= 0 && (err == SSL_ERROR_WANT_WRITE || + (err == SSL_ERROR_SYSCALL && + WSAGetLastError() == WSAETIMEDOUT))) { +#else + while (--n >= 0 && err == SSL_ERROR_WANT_WRITE) { +#endif + if (is_writable()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + ret = SSL_write(ssl_, ptr, static_cast(handle_size)); + if (ret >= 0) { return ret; } + err = SSL_get_error(ssl_, ret); + } else { + return -1; + } + } + } + return ret; + } + return -1; +} + +inline void SSLSocketStream::get_remote_ip_and_port(std::string &ip, + int &port) const { + detail::get_remote_ip_and_port(sock_, ip, port); +} + +inline void SSLSocketStream::get_local_ip_and_port(std::string &ip, + int &port) const { + detail::get_local_ip_and_port(sock_, ip, port); +} + +inline socket_t SSLSocketStream::socket() const { return sock_; } + +static SSLInit sslinit_; + +} // namespace detail + +// SSL HTTP server implementation +inline 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) { + 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_1_VERSION); + + // add default password callback before opening encrypted private key + if (private_key_password != nullptr && (private_key_password[0] != '\0')) { + SSL_CTX_set_default_passwd_cb_userdata( + ctx_, + reinterpret_cast(const_cast(private_key_password))); + } + + if (SSL_CTX_use_certificate_chain_file(ctx_, cert_path) != 1 || + SSL_CTX_use_PrivateKey_file(ctx_, private_key_path, SSL_FILETYPE_PEM) != + 1) { + SSL_CTX_free(ctx_); + ctx_ = nullptr; + } else if (client_ca_cert_file_path || client_ca_cert_dir_path) { + SSL_CTX_load_verify_locations(ctx_, client_ca_cert_file_path, + client_ca_cert_dir_path); + + SSL_CTX_set_verify( + ctx_, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr); + } + } +} + +inline SSLServer::SSLServer(X509 *cert, EVP_PKEY *private_key, + X509_STORE *client_ca_cert_store) { + 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_1_VERSION); + + if (SSL_CTX_use_certificate(ctx_, cert) != 1 || + SSL_CTX_use_PrivateKey(ctx_, private_key) != 1) { + SSL_CTX_free(ctx_); + ctx_ = nullptr; + } else if (client_ca_cert_store) { + SSL_CTX_set_cert_store(ctx_, client_ca_cert_store); + + SSL_CTX_set_verify( + ctx_, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr); + } + } +} + +inline SSLServer::SSLServer( + const std::function &setup_ssl_ctx_callback) { + ctx_ = SSL_CTX_new(TLS_method()); + if (ctx_) { + if (!setup_ssl_ctx_callback(*ctx_)) { + SSL_CTX_free(ctx_); + ctx_ = nullptr; + } + } +} + +inline SSLServer::~SSLServer() { + if (ctx_) { SSL_CTX_free(ctx_); } +} + +inline bool SSLServer::is_valid() const { return ctx_; } + +inline SSL_CTX *SSLServer::ssl_context() const { return ctx_; } + +inline bool SSLServer::process_and_close_socket(socket_t sock) { + auto ssl = detail::ssl_new( + sock, ctx_, ctx_mutex_, + [&](SSL *ssl2) { + return detail::ssl_connect_or_accept_nonblocking( + sock, ssl2, SSL_accept, read_timeout_sec_, read_timeout_usec_); + }, + [](SSL * /*ssl2*/) { return true; }); + + auto ret = false; + if (ssl) { + ret = detail::process_server_socket_ssl( + svr_sock_, ssl, sock, keep_alive_max_count_, keep_alive_timeout_sec_, + read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, + write_timeout_usec_, + [this, ssl](Stream &strm, bool close_connection, + bool &connection_closed) { + return process_request(strm, close_connection, connection_closed, + [&](Request &req) { req.ssl = ssl; }); + }); + + // Shutdown gracefully if the result seemed successful, non-gracefully if + // the connection appeared to be closed. + const bool shutdown_gracefully = ret; + detail::ssl_delete(ctx_mutex_, ssl, shutdown_gracefully); + } + + detail::shutdown_socket(sock); + detail::close_socket(sock); + return ret; +} + +// SSL HTTP client implementation +inline SSLClient::SSLClient(const std::string &host) + : SSLClient(host, 443, std::string(), std::string()) {} + +inline SSLClient::SSLClient(const std::string &host, int port) + : SSLClient(host, port, std::string(), std::string()) {} + +inline SSLClient::SSLClient(const std::string &host, int port, + const std::string &client_cert_path, + const std::string &client_key_path) + : ClientImpl(host, port, client_cert_path, client_key_path) { + ctx_ = SSL_CTX_new(TLS_client_method()); + + detail::split(&host_[0], &host_[host_.size()], '.', + [&](const char *b, const char *e) { + host_components_.emplace_back(b, e); + }); + + if (!client_cert_path.empty() && !client_key_path.empty()) { + if (SSL_CTX_use_certificate_file(ctx_, client_cert_path.c_str(), + SSL_FILETYPE_PEM) != 1 || + SSL_CTX_use_PrivateKey_file(ctx_, client_key_path.c_str(), + SSL_FILETYPE_PEM) != 1) { + SSL_CTX_free(ctx_); + ctx_ = nullptr; + } + } +} + +inline SSLClient::SSLClient(const std::string &host, int port, + X509 *client_cert, EVP_PKEY *client_key) + : ClientImpl(host, port) { + ctx_ = SSL_CTX_new(TLS_client_method()); + + detail::split(&host_[0], &host_[host_.size()], '.', + [&](const char *b, const char *e) { + host_components_.emplace_back(b, e); + }); + + if (client_cert != nullptr && client_key != nullptr) { + if (SSL_CTX_use_certificate(ctx_, client_cert) != 1 || + SSL_CTX_use_PrivateKey(ctx_, client_key) != 1) { + SSL_CTX_free(ctx_); + ctx_ = nullptr; + } + } +} + +inline SSLClient::~SSLClient() { + if (ctx_) { SSL_CTX_free(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); +} + +inline bool SSLClient::is_valid() const { return ctx_; } + +inline void SSLClient::set_ca_cert_store(X509_STORE *ca_cert_store) { + if (ca_cert_store) { + if (ctx_) { + if (SSL_CTX_get_cert_store(ctx_) != ca_cert_store) { + // Free memory allocated for old cert and use new store `ca_cert_store` + SSL_CTX_set_cert_store(ctx_, ca_cert_store); + } + } else { + X509_STORE_free(ca_cert_store); + } + } +} + +inline void SSLClient::load_ca_cert_store(const char *ca_cert, + std::size_t size) { + set_ca_cert_store(ClientImpl::create_ca_cert_store(ca_cert, size)); +} + +inline long SSLClient::get_openssl_verify_result() const { + return verify_result_; +} + +inline SSL_CTX *SSLClient::ssl_context() const { return ctx_; } + +inline bool SSLClient::create_and_connect_socket(Socket &socket, Error &error) { + return is_valid() && ClientImpl::create_and_connect_socket(socket, error); +} + +// Assumes that socket_mutex_ is locked and that there are no requests in flight +inline bool SSLClient::connect_with_proxy(Socket &socket, 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_, [&](Stream &strm) { + Request req2; + req2.method = "CONNECT"; + req2.path = host_and_port_; + 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)) { + proxy_res = Response(); + if (!detail::process_client_socket( + socket.sock, read_timeout_sec_, read_timeout_usec_, + write_timeout_sec_, write_timeout_usec_, [&](Stream &strm) { + Request req3; + req3.method = "CONNECT"; + req3.path = host_and_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)); + 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; + 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; +} + +inline 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 (!SSL_CTX_load_verify_locations(ctx_, ca_cert_file_path_.c_str(), + nullptr)) { + ret = false; + } + } else if (!ca_cert_dir_path_.empty()) { + if (!SSL_CTX_load_verify_locations(ctx_, nullptr, + ca_cert_dir_path_.c_str())) { + ret = false; + } + } else { + auto loaded = false; +#ifdef _WIN32 + loaded = + detail::load_system_certs_on_windows(SSL_CTX_get_cert_store(ctx_)); +#elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && defined(__APPLE__) +#if TARGET_OS_OSX + loaded = detail::load_system_certs_on_macos(SSL_CTX_get_cert_store(ctx_)); +#endif // TARGET_OS_OSX +#endif // _WIN32 + if (!loaded) { SSL_CTX_set_default_verify_paths(ctx_); } + } + }); + + return ret; +} + +inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { + auto ssl = detail::ssl_new( + socket.sock, ctx_, ctx_mutex_, + [&](SSL *ssl2) { + if (server_certificate_verification_) { + if (!load_certs()) { + error = Error::SSLLoadingCerts; + return false; + } + SSL_set_verify(ssl2, SSL_VERIFY_NONE, nullptr); + } + + if (!detail::ssl_connect_or_accept_nonblocking( + socket.sock, ssl2, SSL_connect, connection_timeout_sec_, + connection_timeout_usec_)) { + error = Error::SSLConnection; + return false; + } + + if (server_certificate_verification_) { + verify_result_ = SSL_get_verify_result(ssl2); + + if (verify_result_ != X509_V_OK) { + error = Error::SSLServerVerification; + return false; + } + + auto server_cert = SSL_get1_peer_certificate(ssl2); + + if (server_cert == nullptr) { + error = Error::SSLServerVerification; + return false; + } + + if (!verify_host(server_cert)) { + X509_free(server_cert); + error = Error::SSLServerVerification; + return false; + } + X509_free(server_cert); + } + + return true; + }, + [&](SSL *ssl2) { + // NOTE: With -Wold-style-cast, this can produce a warning, since + // SSL_set_tlsext_host_name is a macro (in OpenSSL), which contains + // an old style cast. Short of doing compiler specific pragma's + // here, we can't get rid of this warning. :'( + SSL_set_tlsext_host_name(ssl2, host_.c_str()); + return true; + }); + + if (ssl) { + socket.ssl = ssl; + return true; + } + + shutdown_socket(socket); + close_socket(socket); + return false; +} + +inline void SSLClient::shutdown_ssl(Socket &socket, bool shutdown_gracefully) { + shutdown_ssl_impl(socket, shutdown_gracefully); +} + +inline void SSLClient::shutdown_ssl_impl(Socket &socket, + bool shutdown_gracefully) { + if (socket.sock == INVALID_SOCKET) { + assert(socket.ssl == nullptr); + return; + } + if (socket.ssl) { + detail::ssl_delete(ctx_mutex_, socket.ssl, shutdown_gracefully); + socket.ssl = nullptr; + } + assert(socket.ssl == nullptr); +} + +inline bool +SSLClient::process_socket(const Socket &socket, + 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_, std::move(callback)); +} + +inline bool SSLClient::is_ssl() const { return true; } + +inline 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); +} + +inline 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->type == type) { + auto name = + reinterpret_cast(ASN1_STRING_get0_data(val->d.ia5)); + auto name_len = static_cast(ASN1_STRING_length(val->d.ia5)); + + switch (type) { + case GEN_DNS: dsn_matched = check_host_name(name, name_len); 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; +} + +inline 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 check_host_name(name, static_cast(name_len)); + } + } + + return false; +} + +inline bool SSLClient::check_host_name(const char *pattern, + size_t pattern_len) const { + if (host_.size() == pattern_len && host_ == pattern) { return true; } + + // Wildcard match + // https://bugs.launchpad.net/ubuntu/+source/firefox-3.0/+bug/376484 + std::vector pattern_components; + detail::split(&pattern[0], &pattern[pattern_len], '.', + [&](const char *b, const char *e) { + pattern_components.emplace_back(b, e); + }); + + if (host_components_.size() != pattern_components.size()) { return false; } + + auto itr = pattern_components.begin(); + for (const auto &h : host_components_) { + auto &p = *itr; + if (p != h && p != "*") { + auto partial_match = (p.size() > 0 && p[p.size() - 1] == '*' && + !p.compare(0, p.size() - 1, h)); + if (!partial_match) { return false; } + } + ++itr; + } + + return true; +} +#endif + +// Universal client implementation +inline Client::Client(const std::string &scheme_host_port) + : Client(scheme_host_port, std::string(), std::string()) {} + +inline Client::Client(const std::string &scheme_host_port, + const std::string &client_cert_path, + const std::string &client_key_path) { + const static std::regex re( + R"((?:([a-z]+):\/\/)?(?:\[([\d:]+)\]|([^:/?#]+))(?::(\d+))?)"); + + std::smatch m; + if (std::regex_match(scheme_host_port, m, re)) { + auto scheme = m[1].str(); + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + 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 = m[2].str(); + if (host.empty()) { host = m[3].str(); } + + auto port_str = m[4].str(); + auto port = !port_str.empty() ? std::stoi(port_str) : (is_ssl ? 443 : 80); + + if (is_ssl) { +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + 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 { + cli_ = detail::make_unique(scheme_host_port, 80, + client_cert_path, client_key_path); + } +} + +inline Client::Client(const std::string &host, int port) + : cli_(detail::make_unique(host, port)) {} + +inline 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)) {} + +inline Client::~Client() = default; + +inline bool Client::is_valid() const { + return cli_ != nullptr && cli_->is_valid(); +} + +inline Result Client::Get(const std::string &path) { return cli_->Get(path); } +inline Result Client::Get(const std::string &path, const Headers &headers) { + return cli_->Get(path, headers); +} +inline Result Client::Get(const std::string &path, Progress progress) { + return cli_->Get(path, std::move(progress)); +} +inline Result Client::Get(const std::string &path, const Headers &headers, + Progress progress) { + return cli_->Get(path, headers, std::move(progress)); +} +inline Result Client::Get(const std::string &path, + ContentReceiver content_receiver) { + return cli_->Get(path, std::move(content_receiver)); +} +inline Result Client::Get(const std::string &path, const Headers &headers, + ContentReceiver content_receiver) { + return cli_->Get(path, headers, std::move(content_receiver)); +} +inline Result Client::Get(const std::string &path, + ContentReceiver content_receiver, Progress progress) { + return cli_->Get(path, std::move(content_receiver), std::move(progress)); +} +inline Result Client::Get(const std::string &path, const Headers &headers, + ContentReceiver content_receiver, Progress progress) { + return cli_->Get(path, headers, std::move(content_receiver), + std::move(progress)); +} +inline Result Client::Get(const std::string &path, + ResponseHandler response_handler, + ContentReceiver content_receiver) { + return cli_->Get(path, std::move(response_handler), + std::move(content_receiver)); +} +inline Result Client::Get(const std::string &path, const Headers &headers, + ResponseHandler response_handler, + ContentReceiver content_receiver) { + return cli_->Get(path, headers, std::move(response_handler), + std::move(content_receiver)); +} +inline Result Client::Get(const std::string &path, + ResponseHandler response_handler, + ContentReceiver content_receiver, Progress progress) { + return cli_->Get(path, std::move(response_handler), + std::move(content_receiver), std::move(progress)); +} +inline Result Client::Get(const std::string &path, const Headers &headers, + ResponseHandler response_handler, + ContentReceiver content_receiver, Progress progress) { + return cli_->Get(path, headers, std::move(response_handler), + std::move(content_receiver), std::move(progress)); +} +inline Result Client::Get(const std::string &path, const Params ¶ms, + const Headers &headers, Progress progress) { + return cli_->Get(path, params, headers, progress); +} +inline Result Client::Get(const std::string &path, const Params ¶ms, + const Headers &headers, + ContentReceiver content_receiver, Progress progress) { + return cli_->Get(path, params, headers, content_receiver, progress); +} +inline Result Client::Get(const std::string &path, const Params ¶ms, + const Headers &headers, + ResponseHandler response_handler, + ContentReceiver content_receiver, Progress progress) { + return cli_->Get(path, params, headers, response_handler, content_receiver, + progress); +} + +inline Result Client::Head(const std::string &path) { return cli_->Head(path); } +inline Result Client::Head(const std::string &path, const Headers &headers) { + return cli_->Head(path, headers); +} + +inline Result Client::Post(const std::string &path) { return cli_->Post(path); } +inline Result Client::Post(const std::string &path, const Headers &headers) { + return cli_->Post(path, headers); +} +inline Result Client::Post(const std::string &path, const char *body, + size_t content_length, + const std::string &content_type) { + return cli_->Post(path, body, content_length, content_type); +} +inline Result Client::Post(const std::string &path, const Headers &headers, + const char *body, size_t content_length, + const std::string &content_type) { + return cli_->Post(path, headers, body, content_length, content_type); +} +inline Result Client::Post(const std::string &path, const std::string &body, + const std::string &content_type) { + return cli_->Post(path, body, content_type); +} +inline Result Client::Post(const std::string &path, const Headers &headers, + const std::string &body, + const std::string &content_type) { + return cli_->Post(path, headers, body, content_type); +} +inline Result Client::Post(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type) { + return cli_->Post(path, content_length, std::move(content_provider), + content_type); +} +inline Result Client::Post(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type) { + return cli_->Post(path, std::move(content_provider), content_type); +} +inline Result Client::Post(const std::string &path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const std::string &content_type) { + return cli_->Post(path, headers, content_length, std::move(content_provider), + content_type); +} +inline Result Client::Post(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type) { + return cli_->Post(path, headers, std::move(content_provider), content_type); +} +inline Result Client::Post(const std::string &path, const Params ¶ms) { + return cli_->Post(path, params); +} +inline Result Client::Post(const std::string &path, const Headers &headers, + const Params ¶ms) { + return cli_->Post(path, headers, params); +} +inline Result Client::Post(const std::string &path, + const MultipartFormDataItems &items) { + return cli_->Post(path, items); +} +inline Result Client::Post(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items) { + return cli_->Post(path, headers, items); +} +inline Result Client::Post(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items, + const std::string &boundary) { + return cli_->Post(path, headers, items, boundary); +} +inline Result +Client::Post(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items, + const MultipartFormDataProviderItems &provider_items) { + return cli_->Post(path, headers, items, provider_items); +} +inline Result Client::Put(const std::string &path) { return cli_->Put(path); } +inline Result Client::Put(const std::string &path, const char *body, + size_t content_length, + const std::string &content_type) { + return cli_->Put(path, body, content_length, content_type); +} +inline Result Client::Put(const std::string &path, const Headers &headers, + const char *body, size_t content_length, + const std::string &content_type) { + return cli_->Put(path, headers, body, content_length, content_type); +} +inline Result Client::Put(const std::string &path, const std::string &body, + const std::string &content_type) { + return cli_->Put(path, body, content_type); +} +inline Result Client::Put(const std::string &path, const Headers &headers, + const std::string &body, + const std::string &content_type) { + return cli_->Put(path, headers, body, content_type); +} +inline Result Client::Put(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type) { + return cli_->Put(path, content_length, std::move(content_provider), + content_type); +} +inline Result Client::Put(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type) { + return cli_->Put(path, std::move(content_provider), content_type); +} +inline Result Client::Put(const std::string &path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const std::string &content_type) { + return cli_->Put(path, headers, content_length, std::move(content_provider), + content_type); +} +inline Result Client::Put(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type) { + return cli_->Put(path, headers, std::move(content_provider), content_type); +} +inline Result Client::Put(const std::string &path, const Params ¶ms) { + return cli_->Put(path, params); +} +inline Result Client::Put(const std::string &path, const Headers &headers, + const Params ¶ms) { + return cli_->Put(path, headers, params); +} +inline Result Client::Put(const std::string &path, + const MultipartFormDataItems &items) { + return cli_->Put(path, items); +} +inline Result Client::Put(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items) { + return cli_->Put(path, headers, items); +} +inline Result Client::Put(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items, + const std::string &boundary) { + return cli_->Put(path, headers, items, boundary); +} +inline Result +Client::Put(const std::string &path, const Headers &headers, + const MultipartFormDataItems &items, + const MultipartFormDataProviderItems &provider_items) { + return cli_->Put(path, headers, items, provider_items); +} +inline Result Client::Patch(const std::string &path) { + return cli_->Patch(path); +} +inline Result Client::Patch(const std::string &path, const char *body, + size_t content_length, + const std::string &content_type) { + return cli_->Patch(path, body, content_length, content_type); +} +inline Result Client::Patch(const std::string &path, const Headers &headers, + const char *body, size_t content_length, + const std::string &content_type) { + return cli_->Patch(path, headers, body, content_length, content_type); +} +inline Result Client::Patch(const std::string &path, const std::string &body, + const std::string &content_type) { + return cli_->Patch(path, body, content_type); +} +inline Result Client::Patch(const std::string &path, const Headers &headers, + const std::string &body, + const std::string &content_type) { + return cli_->Patch(path, headers, body, content_type); +} +inline Result Client::Patch(const std::string &path, size_t content_length, + ContentProvider content_provider, + const std::string &content_type) { + return cli_->Patch(path, content_length, std::move(content_provider), + content_type); +} +inline Result Client::Patch(const std::string &path, + ContentProviderWithoutLength content_provider, + const std::string &content_type) { + return cli_->Patch(path, std::move(content_provider), content_type); +} +inline Result Client::Patch(const std::string &path, const Headers &headers, + size_t content_length, + ContentProvider content_provider, + const std::string &content_type) { + return cli_->Patch(path, headers, content_length, std::move(content_provider), + content_type); +} +inline Result Client::Patch(const std::string &path, const Headers &headers, + ContentProviderWithoutLength content_provider, + const std::string &content_type) { + return cli_->Patch(path, headers, std::move(content_provider), content_type); +} +inline Result Client::Delete(const std::string &path) { + return cli_->Delete(path); +} +inline Result Client::Delete(const std::string &path, const Headers &headers) { + return cli_->Delete(path, headers); +} +inline Result Client::Delete(const std::string &path, const char *body, + size_t content_length, + const std::string &content_type) { + return cli_->Delete(path, body, content_length, content_type); +} +inline Result Client::Delete(const std::string &path, const Headers &headers, + const char *body, size_t content_length, + const std::string &content_type) { + return cli_->Delete(path, headers, body, content_length, content_type); +} +inline Result Client::Delete(const std::string &path, const std::string &body, + const std::string &content_type) { + return cli_->Delete(path, body, content_type); +} +inline Result Client::Delete(const std::string &path, const Headers &headers, + const std::string &body, + const std::string &content_type) { + return cli_->Delete(path, headers, body, content_type); +} +inline Result Client::Options(const std::string &path) { + return cli_->Options(path); +} +inline Result Client::Options(const std::string &path, const Headers &headers) { + return cli_->Options(path, headers); +} + +inline bool Client::send(Request &req, Response &res, Error &error) { + return cli_->send(req, res, error); +} + +inline Result Client::send(const Request &req) { return cli_->send(req); } + +inline void Client::stop() { cli_->stop(); } + +inline std::string Client::host() const { return cli_->host(); } + +inline int Client::port() const { return cli_->port(); } + +inline size_t Client::is_socket_open() const { return cli_->is_socket_open(); } + +inline socket_t Client::socket() const { return cli_->socket(); } + +inline void +Client::set_hostname_addr_map(std::map addr_map) { + cli_->set_hostname_addr_map(std::move(addr_map)); +} + +inline void Client::set_default_headers(Headers headers) { + cli_->set_default_headers(std::move(headers)); +} + +inline void Client::set_header_writer( + std::function const &writer) { + cli_->set_header_writer(writer); +} + +inline void Client::set_address_family(int family) { + cli_->set_address_family(family); +} + +inline void Client::set_tcp_nodelay(bool on) { cli_->set_tcp_nodelay(on); } + +inline void Client::set_socket_options(SocketOptions socket_options) { + cli_->set_socket_options(std::move(socket_options)); +} + +inline void Client::set_connection_timeout(time_t sec, time_t usec) { + cli_->set_connection_timeout(sec, usec); +} + +inline void Client::set_read_timeout(time_t sec, time_t usec) { + cli_->set_read_timeout(sec, usec); +} + +inline void Client::set_write_timeout(time_t sec, time_t usec) { + cli_->set_write_timeout(sec, usec); +} + +inline void Client::set_basic_auth(const std::string &username, + const std::string &password) { + cli_->set_basic_auth(username, password); +} +inline void Client::set_bearer_token_auth(const std::string &token) { + cli_->set_bearer_token_auth(token); +} +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline void Client::set_digest_auth(const std::string &username, + const std::string &password) { + cli_->set_digest_auth(username, password); +} +#endif + +inline void Client::set_keep_alive(bool on) { cli_->set_keep_alive(on); } +inline void Client::set_follow_location(bool on) { + cli_->set_follow_location(on); +} + +inline void Client::set_url_encode(bool on) { cli_->set_url_encode(on); } + +inline void Client::set_compress(bool on) { cli_->set_compress(on); } + +inline void Client::set_decompress(bool on) { cli_->set_decompress(on); } + +inline void Client::set_interface(const std::string &intf) { + cli_->set_interface(intf); +} + +inline void Client::set_proxy(const std::string &host, int port) { + cli_->set_proxy(host, port); +} +inline void Client::set_proxy_basic_auth(const std::string &username, + const std::string &password) { + cli_->set_proxy_basic_auth(username, password); +} +inline void Client::set_proxy_bearer_token_auth(const std::string &token) { + cli_->set_proxy_bearer_token_auth(token); +} +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline void Client::set_proxy_digest_auth(const std::string &username, + const std::string &password) { + cli_->set_proxy_digest_auth(username, password); +} +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline void Client::enable_server_certificate_verification(bool enabled) { + cli_->enable_server_certificate_verification(enabled); +} +#endif + +inline void Client::set_logger(Logger logger) { + cli_->set_logger(std::move(logger)); +} + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline 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); +} + +inline void Client::set_ca_cert_store(X509_STORE *ca_cert_store) { + if (is_ssl_) { + static_cast(*cli_).set_ca_cert_store(ca_cert_store); + } else { + cli_->set_ca_cert_store(ca_cert_store); + } +} + +inline void Client::load_ca_cert_store(const char *ca_cert, std::size_t size) { + set_ca_cert_store(cli_->create_ca_cert_store(ca_cert, size)); +} + +inline long Client::get_openssl_verify_result() const { + if (is_ssl_) { + return static_cast(*cli_).get_openssl_verify_result(); + } + return -1; // NOTE: -1 doesn't match any of X509_V_ERR_??? +} + +inline SSL_CTX *Client::ssl_context() const { + if (is_ssl_) { return static_cast(*cli_).ssl_context(); } + return nullptr; +} +#endif + +// ---------------------------------------------------------------------------- } // namespace httplib diff --git a/src/ros2_medkit_gateway/test/test_plugin_manager.cpp b/src/ros2_medkit_gateway/test/test_plugin_manager.cpp index d82c528bb..dc131ed78 100644 --- a/src/ros2_medkit_gateway/test/test_plugin_manager.cpp +++ b/src/ros2_medkit_gateway/test/test_plugin_manager.cpp @@ -132,6 +132,26 @@ class MockThrowOnSetContext : public GatewayPlugin, public UpdateProvider { } }; +/// Plugin that returns a test route for verifying wrapping logic +class MockRoutePlugin : public GatewayPlugin { + public: + std::string name() const override { + return "mock_route"; + } + void configure(const json &) override { + } + std::vector get_routes() override { + return { + {"GET", R"(apps/([^/]+)/x-test-route)", + [this](const PluginRequest & req, PluginResponse & res) { + last_path_param_ = req.path_param(1); + res.send_json({{"handled", true}, {"entity", last_path_param_}}); + }}, + }; + } + std::string last_path_param_; +}; + /// Plugin that throws during get_routes class MockThrowOnGetRoutes : public GatewayPlugin, public IntrospectionProvider { public: @@ -312,6 +332,37 @@ TEST(PluginManagerTest, ThrowOnGetRoutesDisablesPlugin) { EXPECT_EQ(mgr.plugin_names()[0], "introspection_only"); } +TEST(PluginManagerTest, RegisterRoutesWrapsPluginHandlers) { + PluginManager mgr; + auto plugin = std::make_unique(); + auto * raw = plugin.get(); + mgr.add_plugin(std::move(plugin)); + mgr.configure_plugins(); + + httplib::Server srv; + mgr.register_routes(&srv, "/api/v1"); + + // Bind to ephemeral port to avoid conflicts in parallel CTest runs + auto port = srv.bind_to_any_port("127.0.0.1"); + std::thread server_thread([&srv]() { + srv.listen_after_bind(); + }); + srv.wait_until_ready(); + + httplib::Client cli("127.0.0.1", port); + auto res = cli.Get("/api/v1/apps/test_entity/x-test-route"); + + srv.stop(); + server_thread.join(); + + ASSERT_TRUE(res); + EXPECT_EQ(res->status, 200); + auto body = json::parse(res->body); + EXPECT_EQ(body["handled"], true); + EXPECT_EQ(body["entity"], "test_entity"); + EXPECT_EQ(raw->last_path_param_, "test_entity"); +} + TEST(PluginManagerTest, ShutdownAllIdempotent) { PluginManager mgr; auto plugin = std::make_unique(); diff --git a/src/ros2_medkit_integration_tests/test/features/test_plugin_vendor_extensions.test.py b/src/ros2_medkit_integration_tests/test/features/test_plugin_vendor_extensions.test.py index e0a0d260c..ef7b19023 100644 --- a/src/ros2_medkit_integration_tests/test/features/test_plugin_vendor_extensions.test.py +++ b/src/ros2_medkit_integration_tests/test/features/test_plugin_vendor_extensions.test.py @@ -113,7 +113,7 @@ def test_04_global_vendor_endpoint_still_works(self): """GET /x-test/ping global endpoint still responds.""" r = requests.get(f'{self.BASE_URL}/x-test/ping', timeout=5) self.assertEqual(r.status_code, 200) - self.assertEqual(r.text, 'pong') + self.assertEqual(r.json(), {'response': 'pong'}) @launch_testing.post_shutdown_test() diff --git a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/CMakeLists.txt b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/CMakeLists.txt index 5bf93cec9..08670b473 100644 --- a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/CMakeLists.txt +++ b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/CMakeLists.txt @@ -86,9 +86,12 @@ if(BUILD_TESTING) find_package(ament_cmake_gtest REQUIRED) # The route test uses httplib::Server/Client directly for end-to-end HTTP testing. - # Find cpp-httplib so the test binary can link against the compiled implementation. + # On Humble/Jammy the system package is too old (0.10.x); fall back to gateway's + # vendored copy which is installed alongside the gateway package. find_package(OpenSSL REQUIRED) - medkit_find_cpp_httplib() + set(_gw_vendored "${ros2_medkit_gateway_DIR}/../vendored/cpp_httplib") + medkit_find_cpp_httplib(VENDORED_DIR "${_gw_vendored}") + unset(_gw_vendored) include(ROS2MedkitTestDomain) medkit_init_test_domains(START 120 END 129) diff --git a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/test/test_graph_provider_plugin.cpp b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/test/test_graph_provider_plugin.cpp index 8ba767543..26306766b 100644 --- a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/test/test_graph_provider_plugin.cpp +++ b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/test/test_graph_provider_plugin.cpp @@ -65,14 +65,18 @@ std::string PluginRequest::body() const { PluginResponse::PluginResponse(void * impl) : impl_(impl) { } void PluginResponse::send_json(const nlohmann::json & data) { - static_cast(impl_)->set_content(data.dump(), "application/json"); + auto & res = *static_cast(impl_); + res.status = 200; + res.body = data.dump(); + res.headers.emplace("Content-Type", "application/json"); } void PluginResponse::send_error(int status, const std::string & /*error_code*/, const std::string & message, const nlohmann::json & /*parameters*/) { auto & res = *static_cast(impl_); res.status = status; nlohmann::json err = {{"error", message}}; - res.set_content(err.dump(), "application/json"); + res.body = err.dump(); + res.headers.emplace("Content-Type", "application/json"); } } // namespace ros2_medkit_gateway From 56cddd081461ac9bfe8b74e239a7bf30c54f35dd Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Fri, 3 Apr 2026 17:43:01 +0200 Subject: [PATCH 10/18] fix: use creation order for execution history eviction Eviction sorted by completed_at timestamps with millisecond precision, causing non-deterministic eviction order when fast executions complete within the same millisecond. Use monotonic creation_seq counter instead to guarantee FIFO eviction regardless of completion timing. --- .../default_script_provider.hpp | 4 ++++ .../src/default_script_provider.cpp | 14 +++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/default_script_provider.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/default_script_provider.hpp index 38db19486..e19af07c6 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/default_script_provider.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/default_script_provider.hpp @@ -40,6 +40,10 @@ struct ExecutionState { std::optional output_parameters; std::optional error; + // Eviction ordering - monotonic counter set at creation time. + // Used instead of completed_at timestamps to guarantee FIFO eviction. + uint64_t creation_seq = 0; + // Subprocess I/O std::string stdout_data; std::string stderr_data; diff --git a/src/ros2_medkit_gateway/src/default_script_provider.cpp b/src/ros2_medkit_gateway/src/default_script_provider.cpp index 492e245e6..d3485b0d1 100644 --- a/src/ros2_medkit_gateway/src/default_script_provider.cpp +++ b/src/ros2_medkit_gateway/src/default_script_provider.cpp @@ -557,6 +557,7 @@ DefaultScriptProvider::start_execution(const std::string & entity_id, const std: state->status = "running"; state->pid.store(child_pid); state->started_at = now_iso8601(); + state->creation_seq = static_cast(exec_id_counter_.load()); int timeout_sec = resolved->timeout_sec; @@ -1097,16 +1098,15 @@ void DefaultScriptProvider::evict_old_executions(std::vectorstatus != "running" && state->completed_at.has_value()) { - if (oldest_id.empty() || *state->completed_at < oldest_time || - (*state->completed_at == oldest_time && id < oldest_id)) { - oldest_time = *state->completed_at; + if (state->creation_seq < oldest_seq) { + oldest_seq = state->creation_seq; oldest_id = id; } } From 977ce14e85416d95754e2f877be40441e61cd5d8 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Sat, 4 Apr 2026 12:06:02 +0200 Subject: [PATCH 11/18] fix: increase test_resource_change_notifier timeout to 300s for TSAN --- src/ros2_medkit_gateway/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ros2_medkit_gateway/CMakeLists.txt b/src/ros2_medkit_gateway/CMakeLists.txt index 391e51907..be933dc86 100644 --- a/src/ros2_medkit_gateway/CMakeLists.txt +++ b/src/ros2_medkit_gateway/CMakeLists.txt @@ -655,7 +655,7 @@ if(BUILD_TESTING) target_link_libraries(test_condition_evaluator gateway_lib) # Resource change notifier tests (async notification hub for triggers) - ament_add_gtest(test_resource_change_notifier test/test_resource_change_notifier.cpp) + ament_add_gtest(test_resource_change_notifier test/test_resource_change_notifier.cpp TIMEOUT 300) target_link_libraries(test_resource_change_notifier gateway_lib) # Trigger store tests (SQLite persistence for triggers) From 16d097d53e4b9112f9241f25a1090ac09a80b42c Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Sat, 4 Apr 2026 12:32:38 +0200 Subject: [PATCH 12/18] ci: remove manual cpp-httplib source install for Humble No longer needed - the gateway vendors cpp-httplib 0.14.3 as a fallback when the system package is too old. The medkit_find_cpp_httplib() macro with VENDORED_DIR handles this automatically. --- .github/workflows/ci.yml | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d05f06a9..e833c959c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,32 +50,13 @@ jobs: restore-keys: | ccache-${{ matrix.ros_distro }}- - - name: Install cpp-httplib from source (Humble) - if: matrix.ros_distro == 'humble' - run: | - apt-get update - apt-get install -y cmake g++ libssl-dev pkg-config - git clone --depth 1 --branch v0.14.3 https://github.com/yhirose/cpp-httplib.git /tmp/cpp-httplib - cd /tmp/cpp-httplib - mkdir build && cd build - cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DHTTPLIB_REQUIRE_OPENSSL=ON - make install - # Verify installation — cpp-httplib from source installs cmake config (not pkg-config .pc) - test -f /usr/include/httplib.h && echo "cpp-httplib installed successfully" || exit 1 - - name: Install dependencies run: | apt-get update apt-get install -y ros-${{ matrix.ros_distro }}-test-msgs source /opt/ros/${{ matrix.ros_distro }}/setup.bash rosdep update - # On Humble, skip the libcpp-httplib-dev rosdep key - the apt version (0.10.3) - # is too old; cpp-httplib v0.14.3 is built from source in an earlier step. - if [ "${{ matrix.ros_distro }}" = "humble" ]; then - rosdep install --from-paths src --ignore-src -r -y --skip-keys="libcpp-httplib-dev" - else - rosdep install --from-paths src --ignore-src -y - fi + rosdep install --from-paths src --ignore-src -y - name: Build packages env: From 871fde631dce0dd4e602bc26b7aefc1292a440d5 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Sat, 4 Apr 2026 12:34:23 +0200 Subject: [PATCH 13/18] docs: update installation guide for vendored cpp-httplib fallback Remove manual source install instructions for Humble - the vendored fallback handles this automatically. Update troubleshooting section and cmake error message accordingly. --- docs/installation.rst | 39 ++++--------------- scripts/pixi-install-cpp-httplib.sh | 3 +- .../cmake/ROS2MedkitCompat.cmake | 23 +++++------ src/ros2_medkit_cmake/design/index.rst | 2 +- 4 files changed, 20 insertions(+), 47 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 79a19b9c3..d5993934d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -36,25 +36,10 @@ for your distribution: .. note:: - On Ubuntu 22.04 (Humble), the ``libcpp-httplib-dev`` system package is either not - available or too old (0.10.x). ros2_medkit requires cpp-httplib >= 0.14 for the - ``httplib::StatusCode`` enum and ``std::string`` API overloads. - - If ``libcpp-httplib-dev`` is installed, **remove it first** to avoid version conflicts: - - .. code-block:: bash - - sudo apt remove libcpp-httplib-dev - - Then install cpp-httplib >= 0.14 from source: - - .. code-block:: bash - - sudo apt install cmake g++ libssl-dev - git clone --depth 1 --branch v0.14.3 https://github.com/yhirose/cpp-httplib.git /tmp/cpp-httplib - cd /tmp/cpp-httplib && mkdir build && cd build - cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DHTTPLIB_REQUIRE_OPENSSL=ON - sudo make install + On Ubuntu 22.04 (Humble), the system ``libcpp-httplib-dev`` package provides + cpp-httplib 0.10.x which is too old. ros2_medkit requires >= 0.14 but ships a + vendored copy as a fallback - no manual installation is needed. The build system + automatically uses the vendored header when the system package is insufficient. Installation from Source ------------------------ @@ -198,19 +183,11 @@ Troubleshooting gcc --version # Should show 13.x or higher -**Build fails on Humble with** ``httplib::StatusCode has not been declared`` - - The system ``libcpp-httplib-dev`` package on Ubuntu 22.04 provides cpp-httplib 0.10.x, - which is too old. ros2_medkit requires cpp-httplib >= 0.14. Remove the system package - and install from source: - - .. code-block:: bash +**Build fails on Humble with** ``Could not find cpp-httplib >= 0.14`` - sudo apt remove libcpp-httplib-dev - git clone --depth 1 --branch v0.14.3 https://github.com/yhirose/cpp-httplib.git /tmp/cpp-httplib - cd /tmp/cpp-httplib && mkdir build && cd build - cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DHTTPLIB_REQUIRE_OPENSSL=ON - sudo make install + This should not happen with current builds - a vendored copy of cpp-httplib 0.14.3 + is included as an automatic fallback. If you see this error, ensure ``ros2_medkit_cmake`` + is built before the gateway (``colcon build`` handles this automatically). **Cannot find ros2_medkit packages after build** diff --git a/scripts/pixi-install-cpp-httplib.sh b/scripts/pixi-install-cpp-httplib.sh index 3f27d15ad..730ee1f10 100755 --- a/scripts/pixi-install-cpp-httplib.sh +++ b/scripts/pixi-install-cpp-httplib.sh @@ -15,7 +15,8 @@ # Install cpp-httplib v0.14.3 into the Pixi prefix ($CONDA_PREFIX). # cpp-httplib is not available on conda-forge, so we build from source. -# This is the same approach used in CI for ROS 2 Humble. +# Standard CI uses the vendored copy in ros2_medkit_gateway; this script +# is only needed for Pixi environments where conda-forge is the package source. set -euo pipefail VERSION="v0.14.3" diff --git a/src/ros2_medkit_cmake/cmake/ROS2MedkitCompat.cmake b/src/ros2_medkit_cmake/cmake/ROS2MedkitCompat.cmake index 550f74800..d28794a33 100644 --- a/src/ros2_medkit_cmake/cmake/ROS2MedkitCompat.cmake +++ b/src/ros2_medkit_cmake/cmake/ROS2MedkitCompat.cmake @@ -72,13 +72,13 @@ endmacro() # --------------------------------------------------------------------------- # medkit_find_cpp_httplib() # --------------------------------------------------------------------------- -# On Jazzy/Noble, libcpp-httplib-dev is available via apt and provides a -# pkg-config .pc file. On Humble/Jammy, cpp-httplib must be built from -# source, which installs a CMake config file (httplibConfig.cmake). +# Finds cpp-httplib >= 0.14 through a multi-tier fallback chain: +# 1. pkg-config (Jazzy/Noble system package) +# 2. cmake find_package(httplib) (source builds, Pixi) +# 3. VENDORED_DIR parameter (bundled header-only copy) # -# Requires cpp-httplib >= 0.14 (StatusCode enum, std::string overloads). -# Older system packages (e.g. 0.10.x on Jammy) are rejected by pkg-config -# so the fallback cmake/source path is used instead. +# On Humble/Jammy the system package is 0.10.x (too old); the vendored +# fallback in ros2_medkit_gateway handles this automatically. # # Creates a unified alias target `cpp_httplib_target` for consumers. # --------------------------------------------------------------------------- @@ -104,14 +104,9 @@ macro(medkit_find_cpp_httplib) else() message(FATAL_ERROR "[MedkitCompat] Could not find cpp-httplib >= 0.14.\n" - " The system libcpp-httplib-dev package on Ubuntu 22.04 provides 0.10.x which is too old.\n" - " ros2_medkit requires cpp-httplib >= 0.14 for httplib::StatusCode and std::string overloads.\n" - " Fix: remove the old system package and install from source:\n" - " sudo apt remove libcpp-httplib-dev\n" - " git clone --depth 1 --branch v0.14.3 https://github.com/yhirose/cpp-httplib.git /tmp/cpp-httplib\n" - " cd /tmp/cpp-httplib && mkdir build && cd build\n" - " cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DHTTPLIB_REQUIRE_OPENSSL=ON\n" - " sudo make install\n" + " Tried: pkg-config, cmake find_package(httplib), VENDORED_DIR.\n" + " ros2_medkit_gateway vendors cpp-httplib 0.14.3 - ensure ros2_medkit_gateway\n" + " is built first, or pass VENDORED_DIR to medkit_find_cpp_httplib().\n" " See: https://selfpatch.github.io/ros2_medkit/installation.html") endif() endif() diff --git a/src/ros2_medkit_cmake/design/index.rst b/src/ros2_medkit_cmake/design/index.rst index f18367d8b..90741d3dd 100644 --- a/src/ros2_medkit_cmake/design/index.rst +++ b/src/ros2_medkit_cmake/design/index.rst @@ -38,7 +38,7 @@ The package provides four CMake modules installed to the ament index: 4. **ROS2MedkitCompat.cmake** - Multi-distro compatibility layer - ``medkit_find_yaml_cpp()`` - Resolves yaml-cpp across Humble (no cmake target) and Jazzy (namespaced target) - - ``medkit_find_cpp_httplib()`` - Finds cpp-httplib via pkg-config (Jazzy/Noble) or cmake config (source build on Humble) + - ``medkit_find_cpp_httplib()`` - Finds cpp-httplib >= 0.14 via pkg-config, cmake config, or vendored fallback (``VENDORED_DIR`` param) - ``medkit_detect_compat_defs()`` - Detects rclcpp and rosbag2 versions, sets ``MEDKIT_RCLCPP_VERSION_MAJOR`` and ``MEDKIT_ROSBAG2_OLD_TIMESTAMP`` - ``medkit_apply_compat_defs(target)`` - Applies compile definitions based on detected versions - ``medkit_target_dependencies(target ...)`` - Drop-in replacement for ``ament_target_dependencies`` that also works on Rolling (where ``ament_target_dependencies`` was removed) From 4300c1a6a1111abc48cf6a31f7999152f5e0bdd1 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Sat, 4 Apr 2026 12:40:52 +0200 Subject: [PATCH 14/18] ci: remove cpp-httplib source install from Dockerfile No longer needed - the gateway vendors cpp-httplib 0.14.3 as a fallback when the system package is too old. Removes the conditional git clone step and the libcpp-httplib-dev skip-key from rosdep. --- Dockerfile | 20 +++---------------- .../src/graph_provider_plugin.cpp | 17 +++++++++++++++- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Dockerfile b/Dockerfile index ae6cc5dc9..79ce6d377 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,20 +50,6 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ git \ && rm -rf /var/lib/apt/lists/* -# cpp-httplib: use system package on jazzy/rolling, build from source on humble -# (Ubuntu 22.04 either lacks the package or provides 0.10.x, we need >= 0.14) -RUN apt-get update && \ - if apt-cache show libcpp-httplib-dev 2>/dev/null | grep -q "^Version: 0\.1[4-9]\|^Version: 0\.[2-9]"; then \ - apt-get install -y --no-install-recommends libcpp-httplib-dev; \ - else \ - git clone --depth 1 --branch v0.14.3 https://github.com/yhirose/cpp-httplib.git /tmp/cpp-httplib && \ - cd /tmp/cpp-httplib && mkdir build && cd build && \ - cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DHTTPLIB_REQUIRE_OPENSSL=ON && \ - make install && \ - rm -rf /tmp/cpp-httplib; \ - fi && \ - rm -rf /var/lib/apt/lists/* - WORKDIR ${COLCON_WS} # Copy shared cmake modules first (depended on by all packages) @@ -85,7 +71,7 @@ COPY src/ros2_medkit_plugins/ ${COLCON_WS}/src/ros2_medkit_plugins/ RUN bash -c "source /opt/ros/${ROS_DISTRO}/setup.bash && \ rosdep update && \ rosdep install --from-paths src --ignore-src -r -y \ - --skip-keys='ament_cmake_clang_format ament_cmake_clang_tidy test_msgs sqlite3 libcpp-httplib-dev rosbag2_storage_mcap' && \ + --skip-keys='ament_cmake_clang_format ament_cmake_clang_tidy test_msgs sqlite3 rosbag2_storage_mcap' && \ colcon build --cmake-args -DBUILD_TESTING=OFF" # ============================================================================ @@ -99,8 +85,8 @@ ENV DEBIAN_FRONTEND=noninteractive ENV ROS_DISTRO=${ROS_DISTRO} ENV COLCON_WS=/home/medkit/ws -# Runtime dependencies only (header-only libs like nlohmann-json and cpp-httplib -# are already compiled into the binaries, no need to install here) +# Runtime dependencies only (nlohmann-json and cpp-httplib are compiled into +# the binaries at build time, no need to install here) RUN apt-get update && apt-get install -y --no-install-recommends \ ros-${ROS_DISTRO}-yaml-cpp-vendor \ ros-${ROS_DISTRO}-example-interfaces \ diff --git a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/src/graph_provider_plugin.cpp b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/src/graph_provider_plugin.cpp index a759018a4..cffae3280 100644 --- a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/src/graph_provider_plugin.cpp +++ b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/src/graph_provider_plugin.cpp @@ -25,13 +25,28 @@ #include #include "ros2_medkit_gateway/http/error_codes.hpp" -#include "ros2_medkit_gateway/http/http_utils.hpp" #include "ros2_medkit_gateway/plugins/plugin_context.hpp" namespace ros2_medkit_gateway { namespace { +std::string format_timestamp_ns(int64_t ns) { + auto seconds = ns / 1'000'000'000; + auto nanos = ns % 1'000'000'000; + std::time_t time = static_cast(seconds); + std::tm tm_buf{}; + std::tm * tm = gmtime_r(&time, &tm_buf); + if (!tm) { + return "1970-01-01T00:00:00.000Z"; + } + char buf[64]; + std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", tm); + char result[80]; + std::snprintf(result, sizeof(result), "%s.%03dZ", buf, static_cast(nanos / 1'000'000)); + return result; +} + constexpr size_t kMaxCachedTopicMetrics = 512; bool is_filtered_topic_name(const std::string & topic_name) { From 26a1b0d86641b1505f78593a658cbbf6494162f4 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Sat, 4 Apr 2026 21:18:42 +0200 Subject: [PATCH 15/18] ci: improve TSan job reliability - Use RelWithDebInfo instead of Debug (2-3x faster under TSan) - Set halt_on_error=0 so TSan reports races without killing the process - Add history_size=4 for better race reports - Run ctest with -j1 to avoid parallel test interference under TSan --- .github/workflows/quality.yml | 6 +++--- src/ros2_medkit_gateway/CMakeLists.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 94ba7bcbd..92c13c6ef 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -350,7 +350,7 @@ jobs: run: | source /opt/ros/jazzy/setup.bash colcon build --symlink-install \ - --cmake-args -DCMAKE_BUILD_TYPE=Debug -DSANITIZER=tsan \ + --cmake-args -DCMAKE_BUILD_TYPE=RelWithDebInfo -DSANITIZER=tsan \ --event-handlers console_direct+ ccache -s @@ -362,14 +362,14 @@ jobs: - name: Run unit tests with TSan timeout-minutes: 15 run: | - export TSAN_OPTIONS="halt_on_error=1:suppressions=$(pwd)/tsan_suppressions.txt" + export TSAN_OPTIONS="halt_on_error=0:history_size=4:suppressions=$(pwd)/tsan_suppressions.txt" source /opt/ros/jazzy/setup.bash source install/setup.bash failed=0 for pkg_dir in build/ros2_medkit_*/; do pkg=$(basename "$pkg_dir") echo "::group::Testing $pkg" - (cd "$pkg_dir" && ctest -LE "linter|integration" --output-on-failure) || failed=1 + (cd "$pkg_dir" && ctest -j1 -LE "linter|integration" --output-on-failure) || failed=1 echo "::endgroup::" done exit $failed diff --git a/src/ros2_medkit_gateway/CMakeLists.txt b/src/ros2_medkit_gateway/CMakeLists.txt index be933dc86..eb0756f8c 100644 --- a/src/ros2_medkit_gateway/CMakeLists.txt +++ b/src/ros2_medkit_gateway/CMakeLists.txt @@ -655,7 +655,7 @@ if(BUILD_TESTING) target_link_libraries(test_condition_evaluator gateway_lib) # Resource change notifier tests (async notification hub for triggers) - ament_add_gtest(test_resource_change_notifier test/test_resource_change_notifier.cpp TIMEOUT 300) + ament_add_gtest(test_resource_change_notifier test/test_resource_change_notifier.cpp TIMEOUT 600) target_link_libraries(test_resource_change_notifier gateway_lib) # Trigger store tests (SQLite persistence for triggers) From 2131865d3e02b18e53f52fe27e6a9f34ca0baaac Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Sun, 5 Apr 2026 08:27:51 +0200 Subject: [PATCH 16/18] fix: address review findings from human review - Fix dangling reference: capture full_pattern by value in httplib handler lambda (was captured by ref to loop-scoped local) - Restore type safety: register_routes takes httplib::Server& with forward declaration instead of void* - Return const string refs from PluginRequest::path()/body() - Fix doxygen: remove httplib type names from opaque pointer docs - Fix stale register_routes references in gateway_params.yaml, introspection_provider.hpp, merge_pipeline.cpp --- .../test/test_param_beacon_plugin.cpp | 10 ++++++---- .../test/test_topic_beacon_plugin.cpp | 10 ++++++---- .../config/gateway_params.yaml | 2 +- .../plugins/plugin_http_types.hpp | 10 +++++----- .../plugins/plugin_manager.hpp | 8 ++++++-- .../providers/introspection_provider.hpp | 2 +- .../src/discovery/merge_pipeline.cpp | 2 +- src/ros2_medkit_gateway/src/http/rest_server.cpp | 2 +- .../src/plugins/plugin_http_types.cpp | 4 ++-- .../src/plugins/plugin_manager.cpp | 15 +++++++-------- .../test/test_plugin_manager.cpp | 4 ++-- .../test/test_graph_provider_plugin.cpp | 4 ++-- 12 files changed, 40 insertions(+), 33 deletions(-) diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/test/test_param_beacon_plugin.cpp b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/test/test_param_beacon_plugin.cpp index 5d84e065c..265f643af 100644 --- a/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/test/test_param_beacon_plugin.cpp +++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_param_beacon/test/test_param_beacon_plugin.cpp @@ -57,11 +57,13 @@ std::string PluginRequest::path_param(size_t /*index*/) const { std::string PluginRequest::header(const std::string & /*name*/) const { return {}; } -std::string PluginRequest::path() const { - return {}; +const std::string & PluginRequest::path() const { + static const std::string empty; + return empty; } -std::string PluginRequest::body() const { - return {}; +const std::string & PluginRequest::body() const { + static const std::string empty; + return empty; } PluginResponse::PluginResponse(void * impl) : impl_(impl) { } diff --git a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/test/test_topic_beacon_plugin.cpp b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/test/test_topic_beacon_plugin.cpp index a28845624..256e3f640 100644 --- a/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/test/test_topic_beacon_plugin.cpp +++ b/src/ros2_medkit_discovery_plugins/ros2_medkit_topic_beacon/test/test_topic_beacon_plugin.cpp @@ -54,11 +54,13 @@ std::string PluginRequest::path_param(size_t /*index*/) const { std::string PluginRequest::header(const std::string & /*name*/) const { return {}; } -std::string PluginRequest::path() const { - return {}; +const std::string & PluginRequest::path() const { + static const std::string empty; + return empty; } -std::string PluginRequest::body() const { - return {}; +const std::string & PluginRequest::body() const { + static const std::string empty; + return empty; } PluginResponse::PluginResponse(void * impl) : impl_(impl) { } diff --git a/src/ros2_medkit_gateway/config/gateway_params.yaml b/src/ros2_medkit_gateway/config/gateway_params.yaml index 1accbccdb..7bf2bfc0f 100644 --- a/src/ros2_medkit_gateway/config/gateway_params.yaml +++ b/src/ros2_medkit_gateway/config/gateway_params.yaml @@ -316,7 +316,7 @@ ros2_medkit_gateway: # plugins.my_ota_plugin.server_url: "https://updates.example.com" # plugins.my_ota_plugin.timeout_ms: 5000 # - # Plugin lifecycle: load -> configure -> set_context -> register_routes + # Plugin lifecycle: load -> configure -> set_context -> get_routes # Plugins that throw during any lifecycle call are disabled (not crashed). # Default: [] (no plugins) plugins: [""] # Default: no plugins. gateway.launch.py adds graph_provider automatically. diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_http_types.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_http_types.hpp index ab23cc5cb..a6392ae67 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_http_types.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_http_types.hpp @@ -24,7 +24,7 @@ namespace ros2_medkit_gateway { /// Constructed by the gateway per-request; plugins receive it by const reference. class PluginRequest { public: - /// Construct from opaque pointer to httplib::Request. + /// Construct from opaque HTTP request pointer (gateway-internal). explicit PluginRequest(const void * impl); /// Extract a path parameter by regex capture group index. @@ -35,10 +35,10 @@ class PluginRequest { std::string header(const std::string & name) const; /// Full request path (e.g. "/api/v1/apps/my_app/data"). - std::string path() const; + const std::string & path() const; - /// Request body as string. - std::string body() const; + /// Request body (by reference - avoids copying large payloads). + const std::string & body() const; private: const void * impl_; @@ -49,7 +49,7 @@ class PluginRequest { /// Constructed by the gateway per-request; plugins receive it by reference. class PluginResponse { public: - /// Construct from opaque pointer to httplib::Response. + /// Construct from opaque HTTP response pointer (gateway-internal). explicit PluginResponse(void * impl); /// Send a JSON success response (HTTP 200). diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_manager.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_manager.hpp index 2e393a49d..d1649e699 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_manager.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/plugins/plugin_manager.hpp @@ -32,6 +32,10 @@ #include #include +namespace httplib { +class Server; +} + namespace ros2_medkit_gateway { /** @@ -96,10 +100,10 @@ class PluginManager { /** * @brief Register custom REST routes from all plugins - * @param server Opaque pointer to httplib::Server (avoids httplib in header) + * @param server httplib::Server instance * @param api_prefix API path prefix (e.g., "/api/v1") */ - void register_routes(void * server, const std::string & api_prefix); + void register_routes(httplib::Server & server, const std::string & api_prefix); /// Register a resource sampler for a vendor collection (must start with "x-") void register_resource_sampler(const std::string & collection, ResourceSamplerFn fn); diff --git a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/providers/introspection_provider.hpp b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/providers/introspection_provider.hpp index 23c104c7d..9bc41f10c 100644 --- a/src/ros2_medkit_gateway/include/ros2_medkit_gateway/providers/introspection_provider.hpp +++ b/src/ros2_medkit_gateway/include/ros2_medkit_gateway/providers/introspection_provider.hpp @@ -52,7 +52,7 @@ struct NewEntities { struct IntrospectionResult { /// Per-entity metadata for plugin-internal use. Key = entity_id. /// Plugins serve this data as SOVD vendor extension resources - /// via register_routes() and register_capability(). + /// via get_routes() and register_capability(). std::unordered_map metadata; /// New entities discovered by this provider diff --git a/src/ros2_medkit_gateway/src/discovery/merge_pipeline.cpp b/src/ros2_medkit_gateway/src/discovery/merge_pipeline.cpp index cabe5dc59..64830570a 100644 --- a/src/ros2_medkit_gateway/src/discovery/merge_pipeline.cpp +++ b/src/ros2_medkit_gateway/src/discovery/merge_pipeline.cpp @@ -441,7 +441,7 @@ MergeResult MergePipeline::execute() { function_layers.emplace_back(i, std::move(output.functions)); } // entity_metadata is not consumed here - plugins serve their metadata - // as SOVD vendor extension resources via register_routes() and register_capability(). + // as SOVD vendor extension resources via get_routes() and register_capability(). } MergeResult result; diff --git a/src/ros2_medkit_gateway/src/http/rest_server.cpp b/src/ros2_medkit_gateway/src/http/rest_server.cpp index 7f64be19f..a84df2be9 100644 --- a/src/ros2_medkit_gateway/src/http/rest_server.cpp +++ b/src/ros2_medkit_gateway/src/http/rest_server.cpp @@ -165,7 +165,7 @@ RESTServer::RESTServer(GatewayNode * node, const std::string & host, int port, c if (node_->get_plugin_manager()) { auto * plugin_srv = http_server_->get_server(); if (plugin_srv) { - node_->get_plugin_manager()->register_routes(plugin_srv, API_BASE_PATH); + node_->get_plugin_manager()->register_routes(*plugin_srv, API_BASE_PATH); } } } diff --git a/src/ros2_medkit_gateway/src/plugins/plugin_http_types.cpp b/src/ros2_medkit_gateway/src/plugins/plugin_http_types.cpp index 3d6f824da..a4310b653 100644 --- a/src/ros2_medkit_gateway/src/plugins/plugin_http_types.cpp +++ b/src/ros2_medkit_gateway/src/plugins/plugin_http_types.cpp @@ -38,11 +38,11 @@ std::string PluginRequest::header(const std::string & name) const { return req.get_header_value(name); } -std::string PluginRequest::path() const { +const std::string & PluginRequest::path() const { return static_cast(impl_)->path; } -std::string PluginRequest::body() const { +const std::string & PluginRequest::body() const { return static_cast(impl_)->body; } diff --git a/src/ros2_medkit_gateway/src/plugins/plugin_manager.cpp b/src/ros2_medkit_gateway/src/plugins/plugin_manager.cpp index 0b883eb8e..0192f8159 100644 --- a/src/ros2_medkit_gateway/src/plugins/plugin_manager.cpp +++ b/src/ros2_medkit_gateway/src/plugins/plugin_manager.cpp @@ -263,8 +263,7 @@ void PluginManager::register_transport(std::unique_ptrregister_transport(std::move(provider)); } -void PluginManager::register_routes(void * server_ptr, const std::string & api_prefix) { - auto * server = static_cast(server_ptr); +void PluginManager::register_routes(httplib::Server & server, const std::string & api_prefix) { std::unique_lock lock(plugins_mutex_); for (auto & lp : plugins_) { if (!lp.load_result.plugin) { @@ -276,8 +275,8 @@ void PluginManager::register_routes(void * server_ptr, const std::string & api_p std::string full_pattern = api_prefix + "/" + route.pattern; auto handler_fn = route.handler; // capture by value for lambda auto plugin_name = lp.load_result.plugin->name(); - auto httplib_handler = [handler_fn, plugin_name, &full_pattern](const httplib::Request & req, - httplib::Response & res) { + auto httplib_handler = [handler_fn, plugin_name, full_pattern](const httplib::Request & req, + httplib::Response & res) { try { PluginRequest plugin_req(&req); PluginResponse plugin_res(&res); @@ -296,13 +295,13 @@ void PluginManager::register_routes(void * server_ptr, const std::string & api_p }; if (route.method == "GET") { - server->Get(full_pattern, httplib_handler); + server.Get(full_pattern, httplib_handler); } else if (route.method == "POST") { - server->Post(full_pattern, httplib_handler); + server.Post(full_pattern, httplib_handler); } else if (route.method == "PUT") { - server->Put(full_pattern, httplib_handler); + server.Put(full_pattern, httplib_handler); } else if (route.method == "DELETE") { - server->Delete(full_pattern, httplib_handler); + server.Delete(full_pattern, httplib_handler); } else { RCLCPP_WARN(logger(), "Plugin '%s' registered route with unknown method '%s' - skipping", lp.load_result.plugin->name().c_str(), route.method.c_str()); diff --git a/src/ros2_medkit_gateway/test/test_plugin_manager.cpp b/src/ros2_medkit_gateway/test/test_plugin_manager.cpp index dc131ed78..4eba9dfc4 100644 --- a/src/ros2_medkit_gateway/test/test_plugin_manager.cpp +++ b/src/ros2_medkit_gateway/test/test_plugin_manager.cpp @@ -324,7 +324,7 @@ TEST(PluginManagerTest, ThrowOnGetRoutesDisablesPlugin) { mgr.configure_plugins(); httplib::Server srv; - mgr.register_routes(&srv, "/api/v1"); + mgr.register_routes(srv, "/api/v1"); // Throwing plugin disabled, good plugin's IntrospectionProvider still works EXPECT_EQ(mgr.get_introspection_providers().size(), 1u); @@ -340,7 +340,7 @@ TEST(PluginManagerTest, RegisterRoutesWrapsPluginHandlers) { mgr.configure_plugins(); httplib::Server srv; - mgr.register_routes(&srv, "/api/v1"); + mgr.register_routes(srv, "/api/v1"); // Bind to ephemeral port to avoid conflicts in parallel CTest runs auto port = srv.bind_to_any_port("127.0.0.1"); diff --git a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/test/test_graph_provider_plugin.cpp b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/test/test_graph_provider_plugin.cpp index 26306766b..b8cce6421 100644 --- a/src/ros2_medkit_plugins/ros2_medkit_graph_provider/test/test_graph_provider_plugin.cpp +++ b/src/ros2_medkit_plugins/ros2_medkit_graph_provider/test/test_graph_provider_plugin.cpp @@ -55,10 +55,10 @@ std::string PluginRequest::path_param(size_t index) const { std::string PluginRequest::header(const std::string & name) const { return static_cast(impl_)->get_header_value(name); } -std::string PluginRequest::path() const { +const std::string & PluginRequest::path() const { return static_cast(impl_)->path; } -std::string PluginRequest::body() const { +const std::string & PluginRequest::body() const { return static_cast(impl_)->body; } From 32a660a4189cef010b971fb65f508723d9e8a408 Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Sun, 5 Apr 2026 10:20:14 +0200 Subject: [PATCH 17/18] fix: synchronize demo node destructor with timer callback Timer cancel() does not wait for an in-flight callback to finish on Humble. If the callback is running when the destructor resets the publisher, the callback dereferences a null SharedPtr -> SIGSEGV. Add a mutex that the callback locks during execution. The destructor acquires it after cancel() to wait for any in-flight callback before resetting members. Callback also checks for null publisher defensively. Applied to rpm_sensor as an experiment; other demo nodes will follow if this resolves the recurring Humble SIGSEGV failures. --- .../demo_nodes/rpm_sensor.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/ros2_medkit_integration_tests/demo_nodes/rpm_sensor.cpp b/src/ros2_medkit_integration_tests/demo_nodes/rpm_sensor.cpp index 434b7e306..4ac34a469 100644 --- a/src/ros2_medkit_integration_tests/demo_nodes/rpm_sensor.cpp +++ b/src/ros2_medkit_integration_tests/demo_nodes/rpm_sensor.cpp @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include + #include #include @@ -27,12 +29,19 @@ class RPMSensor : public rclcpp::Node { ~RPMSensor() { timer_->cancel(); + // Wait for any in-flight callback to finish before destroying members. + // Humble's timer cancel() does not wait for running callbacks. + std::lock_guard lock(callback_mutex_); timer_.reset(); rpm_pub_.reset(); } private: void publish_data() { + std::lock_guard lock(callback_mutex_); + if (!rpm_pub_) { + return; + } current_rpm_ += 50.0; if (current_rpm_ > 3000.0) { current_rpm_ = 1000.0; @@ -46,6 +55,7 @@ class RPMSensor : public rclcpp::Node { RCLCPP_INFO(this->get_logger(), "RPM: %.0f", current_rpm_); } + std::mutex callback_mutex_; rclcpp::Publisher::SharedPtr rpm_pub_; rclcpp::TimerBase::SharedPtr timer_; double current_rpm_ = 1000.0; From 9f67f09f01b4ac61430c9e3861d1156a0cc6eaed Mon Sep 17 00:00:00 2001 From: Bartosz Burda Date: Sun, 5 Apr 2026 10:43:39 +0200 Subject: [PATCH 18/18] fix(gateway): synchronize with worker before notify in ResourceChangeNotifier::shutdown Classical lost-wakeup race between shutdown() and worker_loop(): 1. worker locks queue_mutex_ 2. worker checks predicate (flag=false, queue empty) -> false 3. shutdown() CAS flag -> true (NOT holding queue_mutex_) 4. shutdown() notify_one() - worker NOT yet enqueued on CV 5. worker enters wait(lock): atomic unlock+enqueue+sleep 6. worker sleeps forever; main blocks in worker_thread_.join() Even though shutdown_flag_ uses seq_cst atomics, the worker's predicate check and entry into wait() are not atomic with respect to the flag modification. The notify can arrive before the worker is enqueued. Fix: briefly acquire queue_mutex_ between setting the flag and notifying. This guarantees the worker is either still outside its critical section (will observe the new flag on the next lock) or already enqueued on the CV (notify_one will wake it). Manifested as a TSan-specific hang in DoubleShutdownIsSafe (worker spawn + immediate shutdown hits the race window). --- src/ros2_medkit_gateway/CMakeLists.txt | 2 +- src/ros2_medkit_gateway/src/resource_change_notifier.cpp | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ros2_medkit_gateway/CMakeLists.txt b/src/ros2_medkit_gateway/CMakeLists.txt index eb0756f8c..3094221ea 100644 --- a/src/ros2_medkit_gateway/CMakeLists.txt +++ b/src/ros2_medkit_gateway/CMakeLists.txt @@ -655,7 +655,7 @@ if(BUILD_TESTING) target_link_libraries(test_condition_evaluator gateway_lib) # Resource change notifier tests (async notification hub for triggers) - ament_add_gtest(test_resource_change_notifier test/test_resource_change_notifier.cpp TIMEOUT 600) + ament_add_gtest(test_resource_change_notifier test/test_resource_change_notifier.cpp TIMEOUT 180) target_link_libraries(test_resource_change_notifier gateway_lib) # Trigger store tests (SQLite persistence for triggers) diff --git a/src/ros2_medkit_gateway/src/resource_change_notifier.cpp b/src/ros2_medkit_gateway/src/resource_change_notifier.cpp index eb4086d7b..c2b33b605 100644 --- a/src/ros2_medkit_gateway/src/resource_change_notifier.cpp +++ b/src/ros2_medkit_gateway/src/resource_change_notifier.cpp @@ -105,6 +105,13 @@ void ResourceChangeNotifier::shutdown() { }); } + // Synchronize with worker_loop()'s predicate check. Without this, the flag + // store above can land between the worker's predicate evaluation and its + // wait() call, losing the subsequent notify_one(). Acquiring queue_mutex_ + // here guarantees the worker is either still outside the critical section + // (will observe the new flag) or already enqueued on queue_cv_ (notify will + // wake it). + { std::lock_guard sync(queue_mutex_); } queue_cv_.notify_one(); if (worker_thread_.joinable()) { worker_thread_.join();