diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 36e3610..418cac5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -75,21 +75,32 @@ jobs: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) || (github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true') + strategy: + matrix: + tls_backend: [openssl, mbedtls] + name: ubuntu (${{ matrix.tls_backend }}) steps: - name: checkout uses: actions/checkout@v4 - - name: install libraries + - name: install common libraries run: | - sudo dpkg --add-architecture i386 sudo apt-get update - sudo apt-get install -y libc6-dev${{ matrix.config.arch_suffix }} libstdc++-13-dev${{ matrix.config.arch_suffix }} \ - libssl-dev${{ matrix.config.arch_suffix }} libcurl4-openssl-dev${{ matrix.config.arch_suffix }} \ - zlib1g-dev${{ matrix.config.arch_suffix }} libbrotli-dev${{ matrix.config.arch_suffix }} \ - libzstd-dev${{ matrix.config.arch_suffix }} - - name: build and run tests - run: cd test && make EXTRA_CXXFLAGS="${{ matrix.config.arch_flags }}" + sudo apt-get install -y libcurl4-openssl-dev zlib1g-dev libbrotli-dev libzstd-dev + - name: install OpenSSL + if: matrix.tls_backend == 'openssl' + run: sudo apt-get install -y libssl-dev + - name: install Mbed TLS + if: matrix.tls_backend == 'mbedtls' + run: sudo apt-get install -y libmbedtls-dev + - name: build and run tests (OpenSSL) + if: matrix.tls_backend == 'openssl' + run: cd test && make + - name: build and run tests (Mbed TLS) + if: matrix.tls_backend == 'mbedtls' + run: cd test && make test_split_mbedtls && make test_mbedtls && ./test_mbedtls - name: run fuzz test target - run: cd test && make EXTRA_CXXFLAGS="${{ matrix.config.arch_flags }}" fuzz_test + if: matrix.tls_backend == 'openssl' + run: cd test && make fuzz_test macos: runs-on: macos-latest @@ -98,12 +109,24 @@ jobs: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) || (github.event_name == 'workflow_dispatch' && github.event.inputs.test_macos == 'true') + strategy: + matrix: + tls_backend: [openssl, mbedtls] + name: macos (${{ matrix.tls_backend }}) steps: - name: checkout uses: actions/checkout@v4 - - name: build and run tests + - name: install Mbed TLS + if: matrix.tls_backend == 'mbedtls' + run: brew install mbedtls@3 + - name: build and run tests (OpenSSL) + if: matrix.tls_backend == 'openssl' run: cd test && make + - name: build and run tests (Mbed TLS) + if: matrix.tls_backend == 'mbedtls' + run: cd test && make test_split_mbedtls && make test_mbedtls && ./test_mbedtls - name: run fuzz test target + if: matrix.tls_backend == 'openssl' run: cd test && make fuzz_test windows: diff --git a/.github/workflows/test_proxy.yaml b/.github/workflows/test_proxy.yaml index 571dc96..f21b148 100644 --- a/.github/workflows/test_proxy.yaml +++ b/.github/workflows/test_proxy.yaml @@ -6,15 +6,28 @@ jobs: test-proxy: runs-on: ubuntu-latest if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name - + strategy: + matrix: + tls_backend: [openssl, mbedtls] + name: proxy (${{ matrix.tls_backend }}) + steps: - - uses: actions/checkout@v3 - - - name: Install dependencies + - uses: actions/checkout@v4 + + - name: Install common dependencies run: | sudo apt-get update - sudo apt-get install -y build-essential libssl-dev zlib1g-dev libcurl4-openssl-dev libbrotli-dev libzstd-dev netcat-openbsd - - - name: Run proxy tests - run: | - cd test && make proxy \ No newline at end of file + sudo apt-get install -y build-essential zlib1g-dev libcurl4-openssl-dev libbrotli-dev libzstd-dev netcat-openbsd + - name: Install OpenSSL + if: matrix.tls_backend == 'openssl' + run: sudo apt-get install -y libssl-dev + - name: Install Mbed TLS + if: matrix.tls_backend == 'mbedtls' + run: sudo apt-get install -y libmbedtls-dev + + - name: Run proxy tests (OpenSSL) + if: matrix.tls_backend == 'openssl' + run: cd test && make proxy + - name: Run proxy tests (Mbed TLS) + if: matrix.tls_backend == 'mbedtls' + run: cd test && make proxy_mbedtls \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9e2bf24..f9ba834 100644 --- a/.gitignore +++ b/.gitignore @@ -33,9 +33,12 @@ example/*.pem test/httplib.cc test/httplib.h test/test +test/test_mbedtls test/server_fuzzer test/test_proxy +test/test_proxy_mbedtls test/test_split +test/test_split_mbedtls test/test.xcodeproj/xcuser* test/test.xcodeproj/*/xcuser* test/*.o diff --git a/README.md b/README.md index aa94afe..49c9729 100644 --- a/README.md +++ b/README.md @@ -50,19 +50,28 @@ if (auto res = cli.Get("/hi")) { } ``` -SSL Support ------------ +SSL/TLS Support +--------------- -SSL support is available with `CPPHTTPLIB_OPENSSL_SUPPORT`. `libssl` and `libcrypto` should be linked. +cpp-httplib supports multiple TLS backends through an abstraction layer: + +| Backend | Define | Libraries | +| :------ | :----- | :-------- | +| OpenSSL | `CPPHTTPLIB_OPENSSL_SUPPORT` | `libssl`, `libcrypto` | +| Mbed TLS | `CPPHTTPLIB_MBEDTLS_SUPPORT` | `libmbedtls`, `libmbedx509`, `libmbedcrypto` | > [!NOTE] -> cpp-httplib currently supports only version 3.0 or later. Please see [this page](https://www.openssl.org/policies/releasestrat.html) to get more information. +> OpenSSL 3.0 or later is required. Please see [this page](https://www.openssl.org/policies/releasestrat.html) for more information. + +> [!NOTE] +> Mbed TLS 2.x and 3.x are supported. The library automatically detects the version and uses the appropriate API. > [!TIP] -> For macOS: cpp-httplib now can use system certs with `CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN`. `CoreFoundation` and `Security` should be linked with `-framework`. +> For macOS: cpp-httplib can use system certs with `CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN`. `CoreFoundation` and `Security` should be linked with `-framework`. ```c++ -#define CPPHTTPLIB_OPENSSL_SUPPORT +// Use either OpenSSL or Mbed TLS +#define CPPHTTPLIB_OPENSSL_SUPPORT // or CPPHTTPLIB_MBEDTLS_SUPPORT #include "path/to/httplib.h" // Server diff --git a/httplib.h b/httplib.h index 4193a4f..2c9dcb1 100644 --- a/httplib.h +++ b/httplib.h @@ -381,6 +381,39 @@ using socket_t = int; #endif // CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#ifdef _MSC_VER +#pragma comment(lib, "crypt32.lib") +#endif +#endif // _WIN32 +#if defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) +#if TARGET_OS_MAC +#include +#endif +#endif // CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN +#endif // CPPHTTPLIB_MBEDTLS_SUPPORT + +// Define CPPHTTPLIB_SSL_ENABLED if any SSL backend is available +// This simplifies conditional compilation when adding new backends (e.g., +// wolfSSL) +#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) || defined(CPPHTTPLIB_MBEDTLS_SUPPORT) +#define CPPHTTPLIB_SSL_ENABLED +#endif + #ifdef CPPHTTPLIB_ZLIB_SUPPORT #include #endif @@ -734,6 +767,64 @@ public: using Range = std::pair; using Ranges = std::vector; +#ifdef CPPHTTPLIB_SSL_ENABLED +// TLS abstraction layer - type definitions +// (Function declarations are in the full namespace tls block below) +namespace tls { + +// Opaque handles (defined as void* for abstraction) +using tls_ctx_t = void *; +using tls_session_t = void *; +using const_tls_session_t = const void *; // For read-only session access +using tls_cert_t = void *; +using tls_ca_store_t = void *; + +// Subject Alternative Names (SAN) entry types +enum class SanType { DNS, IP, EMAIL, URI, OTHER }; + +// SAN entry structure +struct TlsSanEntry { + SanType type; + std::string value; +}; + +// Verification context for certificate verification callback +struct TlsVerifyContext { + tls_session_t session; // TLS session handle + tls_cert_t cert; // Current certificate being verified + int depth; // Certificate chain depth (0 = leaf) + bool preverify_ok; // OpenSSL/Mbed TLS pre-verification result + long error_code; // Backend-specific error code (0 = no error) + const char *error_string; // Human-readable error description +}; + +using TlsVerifyCallback = std::function; + +// Error codes for TLS operations (backend-independent) +enum class ErrorCode : int { + Success = 0, + WantRead, // Non-blocking: need to wait for read + WantWrite, // Non-blocking: need to wait for write + PeerClosed, // Peer closed the connection + Fatal, // Unrecoverable error + SyscallError, // System call error (check sys_errno) + CertVerifyFailed, // Certificate verification failed + HostnameMismatch, // Hostname verification failed +}; + +// TLS error information +struct TlsError { + ErrorCode code = ErrorCode::Fatal; + uint64_t backend_code = 0; // OpenSSL: ERR_get_error(), mbedTLS: return value + int sys_errno = 0; // errno when SyscallError +}; + +// Callback for TLS context setup (used by SSLServer constructor) +using TlsContextSetupCallback = std::function; + +} // namespace tls +#endif + struct Request { std::string method; std::string path; @@ -763,9 +854,6 @@ struct Request { ContentReceiverWithProgress content_receiver; DownloadProgress download_progress; UploadProgress upload_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, const char *def = "", @@ -793,6 +881,10 @@ struct Request { size_t authorization_count_ = 0; std::chrono::time_point start_time_ = (std::chrono::steady_clock::time_point::min)(); + +#ifdef CPPHTTPLIB_SSL_ENABLED + tls::const_tls_session_t ssl = nullptr; +#endif }; struct Response { @@ -1364,17 +1456,6 @@ public: Headers &&request_headers = Headers{}) : res_(std::move(res)), err_(err), request_headers_(std::move(request_headers)) {} -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - Result(std::unique_ptr &&res, Error err, Headers &&request_headers, - int ssl_error) - : res_(std::move(res)), err_(err), - request_headers_(std::move(request_headers)), ssl_error_(ssl_error) {} - Result(std::unique_ptr &&res, Error err, Headers &&request_headers, - int ssl_error, unsigned long ssl_openssl_error) - : res_(std::move(res)), err_(err), - request_headers_(std::move(request_headers)), ssl_error_(ssl_error), - ssl_openssl_error_(ssl_openssl_error) {} -#endif // Response operator bool() const { return res_ != nullptr; } bool operator==(std::nullptr_t) const { return res_ == nullptr; } @@ -1389,13 +1470,6 @@ public: // Error Error error() const { return err_; } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - // SSL Error - int ssl_error() const { return ssl_error_; } - // OpenSSL Error - unsigned long ssl_openssl_error() const { return ssl_openssl_error_; } -#endif - // Request Headers bool has_request_header(const std::string &key) const; std::string get_request_header_value(const std::string &key, @@ -1409,64 +1483,82 @@ private: std::unique_ptr res_; Error err_ = Error::Unknown; Headers request_headers_; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + +#ifdef CPPHTTPLIB_SSL_ENABLED +public: + Result(std::unique_ptr &&res, Error err, Headers &&request_headers, + int ssl_error) + : res_(std::move(res)), err_(err), + request_headers_(std::move(request_headers)), ssl_error_(ssl_error) {} + Result(std::unique_ptr &&res, Error err, Headers &&request_headers, + int ssl_error, unsigned long ssl_backend_error) + : res_(std::move(res)), err_(err), + request_headers_(std::move(request_headers)), ssl_error_(ssl_error), + ssl_backend_error_(ssl_backend_error) {} + + // SSL Error (backend-specific error code from handshake) + int ssl_error() const { return ssl_error_; } + + // Backend-specific error code (OpenSSL: ERR_get_error(), Mbed TLS: mbedtls + // error code) + unsigned long ssl_backend_error() const { return ssl_backend_error_; } + +private: int ssl_error_ = 0; - unsigned long ssl_openssl_error_ = 0; + unsigned long ssl_backend_error_ = 0; +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +public: + // OpenSSL Error (alias for ssl_backend_error for backward compatibility) + unsigned long ssl_openssl_error() const { return ssl_backend_error_; } #endif }; struct ClientConnection { socket_t sock = INVALID_SOCKET; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - SSL *ssl = nullptr; -#endif bool is_open() const { return sock != INVALID_SOCKET; } ClientConnection() = default; - ~ClientConnection() { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - if (ssl) { - SSL_free(ssl); - ssl = nullptr; - } -#endif - if (sock != INVALID_SOCKET) { - detail::close_socket(sock); - sock = INVALID_SOCKET; - } - } + ~ClientConnection(); ClientConnection(const ClientConnection &) = delete; ClientConnection &operator=(const ClientConnection &) = delete; - ClientConnection(ClientConnection &&other) noexcept - : sock(other.sock) -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - , - ssl(other.ssl) -#endif - { +#ifndef CPPHTTPLIB_SSL_ENABLED + ClientConnection(ClientConnection &&other) noexcept : sock(other.sock) { other.sock = INVALID_SOCKET; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - other.ssl = nullptr; -#endif } ClientConnection &operator=(ClientConnection &&other) noexcept { if (this != &other) { sock = other.sock; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - ssl = other.ssl; -#endif other.sock = INVALID_SOCKET; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - other.ssl = nullptr; -#endif } return *this; } +#else + ClientConnection(ClientConnection &&other) noexcept + : sock(other.sock), session(other.session) { + other.sock = INVALID_SOCKET; + other.session = nullptr; + } + + ClientConnection &operator=(ClientConnection &&other) noexcept { + if (this != &other) { + sock = other.sock; + other.sock = INVALID_SOCKET; + + session = other.session; + other.session = nullptr; + } + return *this; + } + + tls::tls_session_t session = nullptr; +#endif }; namespace detail { @@ -1691,10 +1783,6 @@ public: 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); @@ -1711,24 +1799,6 @@ public: 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); - void enable_server_hostname_verification(bool enabled); - void set_server_certificate_verifier( - std::function verifier); -#endif void set_logger(Logger logger); void set_error_logger(ErrorLogger error_logger); @@ -1736,11 +1806,15 @@ public: protected: struct Socket { socket_t sock = INVALID_SOCKET; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - SSL *ssl = nullptr; -#endif + + // For Mbed TLS compatibility: start_time for request timeout tracking + std::chrono::time_point start_time_; bool is_open() const { return sock != INVALID_SOCKET; } + +#ifdef CPPHTTPLIB_SSL_ENABLED + tls::tls_session_t ssl = nullptr; +#endif }; virtual bool create_and_connect_socket(Socket &socket, Error &error); @@ -1807,10 +1881,6 @@ protected: 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; @@ -1833,33 +1903,11 @@ protected: 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; - bool server_hostname_verification_ = true; - std::function server_certificate_verifier_; -#endif mutable std::mutex logger_mutex_; Logger logger_; ErrorLogger error_logger_; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - int last_ssl_error_ = 0; - unsigned long last_openssl_error_ = 0; -#endif - private: bool send_(Request &req, Response &res, Error &error); Result send_(Request &&req); @@ -1904,6 +1952,45 @@ private: virtual bool is_ssl() const; void transfer_socket_ownership_to_handle(StreamHandle &handle); + +#ifdef CPPHTTPLIB_SSL_ENABLED +public: + void set_digest_auth(const std::string &username, + const std::string &password); + void set_proxy_digest_auth(const std::string &username, + const std::string &password); + void set_ca_cert_path(const std::string &ca_cert_file_path, + const std::string &ca_cert_dir_path = std::string()); + void enable_server_certificate_verification(bool enabled); + void enable_server_hostname_verification(bool enabled); + +protected: + std::string digest_auth_username_; + std::string digest_auth_password_; + std::string proxy_digest_auth_username_; + std::string proxy_digest_auth_password_; + std::string ca_cert_file_path_; + std::string ca_cert_dir_path_; + bool server_certificate_verification_ = true; + bool server_hostname_verification_ = true; + std::string ca_cert_pem_; // Store CA cert PEM for redirect transfer + int last_ssl_error_ = 0; + unsigned long last_backend_error_ = 0; +#endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +public: + [[deprecated("Use load_ca_cert_store() instead")]] + void set_ca_cert_store(X509_STORE *ca_cert_store); + + [[deprecated("Use tls::tls_create_ca_store() instead")]] + X509_STORE *create_ca_cert_store(const char *ca_cert, std::size_t size) const; + + [[deprecated( + "Use set_server_certificate_verifier(TlsVerifyCallback) instead")]] + virtual void set_server_certificate_verifier( + std::function verifier); +#endif }; class Client { @@ -2073,10 +2160,6 @@ public: 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); @@ -2094,43 +2177,75 @@ public: 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); - void enable_server_hostname_verification(bool enabled); - void set_server_certificate_verifier( - std::function verifier); -#endif - void set_logger(Logger logger); void set_error_logger(ErrorLogger error_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 +#ifdef CPPHTTPLIB_SSL_ENABLED +public: + void set_digest_auth(const std::string &username, + const std::string &password); + void set_proxy_digest_auth(const std::string &username, + const std::string &password); + void enable_server_certificate_verification(bool enabled); + void enable_server_hostname_verification(bool enabled); + void set_ca_cert_path(const std::string &ca_cert_file_path, + const std::string &ca_cert_dir_path = std::string()); + + void set_ca_cert_store(tls::tls_ca_store_t ca_cert_store); + void load_ca_cert_store(const char *ca_cert, std::size_t size); + + // Custom certificate verification callback (works with all TLS backends) + void set_server_certificate_verifier(tls::TlsVerifyCallback verifier); + + // Backend-agnostic TLS context accessor + tls::tls_ctx_t tls_context() const; + +private: bool is_ssl_ = false; #endif + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +public: + [[deprecated("Use tls_context() instead")]] + SSL_CTX *ssl_context() const; + + [[deprecated( + "Use set_server_certificate_verifier(TlsVerifyCallback) instead")]] + void set_server_certificate_verifier( + std::function verifier); + + [[deprecated("Use Result::ssl_backend_error() instead")]] + long get_openssl_verify_result() const; +#endif }; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT +// Mbed TLS context wrapper (holds config, entropy, DRBG, CA chain, own +// cert/key). This struct is exposed for use in SSL context setup callbacks. +struct MbedTlsContext { + mbedtls_ssl_config conf; + mbedtls_entropy_context entropy; + mbedtls_ctr_drbg_context ctr_drbg; + mbedtls_x509_crt ca_chain; + mbedtls_x509_crt own_cert; + mbedtls_pk_context own_key; + bool is_server = false; + bool verify_client = false; + bool has_verify_callback = false; + + MbedTlsContext(); + ~MbedTlsContext(); + + // Non-copyable + MbedTlsContext(const MbedTlsContext &) = delete; + MbedTlsContext &operator=(const MbedTlsContext &) = delete; +}; +#endif + +#ifdef CPPHTTPLIB_SSL_ENABLED class SSLServer : public Server { public: SSLServer(const char *cert_path, const char *private_key_path, @@ -2138,32 +2253,64 @@ public: 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); + // PEM memory-based constructor (works with all TLS backends) + struct PemMemory { + const char *cert_pem; + size_t cert_pem_len; + const char *key_pem; + size_t key_pem_len; + const char *client_ca_pem; + size_t client_ca_pem_len; + const char *private_key_password; + }; + explicit SSLServer(const PemMemory &pem); - SSLServer( - const std::function &setup_ssl_ctx_callback); + // Backend-agnostic callback constructor + // The callback receives the tls_ctx_t handle which can be cast to the + // appropriate backend type (SSL_CTX* for OpenSSL, MbedTlsContext* for + // mbedTLS) + explicit SSLServer(const tls::TlsContextSetupCallback &setup_callback); ~SSLServer() override; bool is_valid() const override; - SSL_CTX *ssl_context() const; + // Update certificates from PEM strings (works with all TLS backends) + bool update_certs_pem(const char *cert_pem, const char *key_pem, + const char *client_ca_pem = nullptr, + const char *password = nullptr); - void update_certs(X509 *cert, EVP_PKEY *private_key, - X509_STORE *client_ca_cert_store = nullptr); + // Backend-agnostic TLS context accessor + tls::tls_ctx_t tls_context() const { return ctx_; } int ssl_last_error() const { return last_ssl_error_; } private: bool process_and_close_socket(socket_t sock) override; - STACK_OF(X509_NAME) * extract_ca_names_from_x509_store(X509_STORE *store); - - SSL_CTX *ctx_; + tls::tls_ctx_t ctx_ = nullptr; std::mutex ctx_mutex_; int last_ssl_error_ = 0; + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +public: + [[deprecated("Use SSLServer(PemMemory) or " + "SSLServer(TlsContextSetupCallback) instead")]] + SSLServer(X509 *cert, EVP_PKEY *private_key, + X509_STORE *client_ca_cert_store = nullptr); + + [[deprecated("Use SSLServer(TlsContextSetupCallback) instead")]] + SSLServer( + const std::function &setup_ssl_ctx_callback); + + [[deprecated("Use tls_context() instead")]] + SSL_CTX *ssl_context() const; + + [[deprecated("Use update_certs_pem() instead")]] + void update_certs(X509 *cert, EVP_PKEY *private_key, + X509_STORE *client_ca_cert_store = nullptr); +#endif }; class SSLClient final : public ClientImpl { @@ -2177,20 +2324,28 @@ public: const std::string &client_key_path, const std::string &private_key_password = std::string()); - explicit SSLClient(const std::string &host, int port, X509 *client_cert, - EVP_PKEY *client_key, - const std::string &private_key_password = std::string()); + // PEM memory-based constructor (works with all TLS backends) + struct PemMemory { + const char *cert_pem; + size_t cert_pem_len; + const char *key_pem; + size_t key_pem_len; + const char *private_key_password; + }; + explicit SSLClient(const std::string &host, int port, const PemMemory &pem); ~SSLClient() override; bool is_valid() const override; - void set_ca_cert_store(X509_STORE *ca_cert_store); + void set_ca_cert_store(tls::tls_ca_store_t ca_cert_store); void load_ca_cert_store(const char *ca_cert, std::size_t size); - long get_openssl_verify_result() const; + // Custom certificate verification callback (works with all TLS backends) + void set_server_certificate_verifier(tls::TlsVerifyCallback verifier); - SSL_CTX *ssl_context() const; + // Backend-agnostic TLS context accessor + tls::tls_ctx_t tls_context() const { return ctx_; } private: bool create_and_connect_socket(Socket &socket, Error &error) override; @@ -2212,12 +2367,9 @@ private: 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_; + tls::tls_ctx_t ctx_ = nullptr; std::mutex ctx_mutex_; std::once_flag initialize_cert_; @@ -2226,12 +2378,33 @@ private: long verify_result_ = 0; friend class ClientImpl; -}; -#endif -/* - * Implementation of template methods. - */ +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +public: + [[deprecated("Use SSLClient(host, port, PemMemory) instead")]] + explicit SSLClient(const std::string &host, int port, X509 *client_cert, + EVP_PKEY *client_key, + const std::string &private_key_password = std::string()); + + [[deprecated("Use Result::ssl_backend_error() instead")]] + long get_openssl_verify_result() const; + + [[deprecated("Use tls_context() instead")]] + SSL_CTX *ssl_context() const; + + [[deprecated( + "Use set_server_certificate_verifier(TlsVerifyCallback) instead")]] + void set_server_certificate_verifier( + std::function verifier) override; + +private: + bool verify_host(X509 *server_cert) const; + bool verify_host_with_subject_alt_name(X509 *server_cert) const; + bool verify_host_with_common_name(X509 *server_cert) const; + std::function legacy_server_cert_verifier_; +#endif +}; +#endif // CPPHTTPLIB_SSL_ENABLED namespace detail { @@ -2780,6 +2953,163 @@ bool is_field_value(const std::string &s); } // namespace detail +/* + * TLS Abstraction Layer Declarations + */ + +#ifdef CPPHTTPLIB_SSL_ENABLED +// Crypto abstraction layer +namespace crypto { + +// Hash algorithm enumeration +enum class HashAlgorithm { MD5, SHA1, SHA256, SHA384, SHA512 }; + +// Compute hash of data, returns hex-encoded string +std::string hash(HashAlgorithm algo, const void *data, size_t len); +std::string hash(HashAlgorithm algo, const std::string &data); + +// Compute hash of data, returns raw bytes +bool hash_raw(HashAlgorithm algo, const void *data, size_t len, + std::vector &digest); + +// Get digest size in bytes for algorithm +size_t hash_size(HashAlgorithm algo); + +} // namespace crypto + +// TLS abstraction layer - function declarations +// (Type definitions are in the earlier namespace tls block above struct +// Request) +namespace tls { + +// Global initialization +bool tls_global_init(); +void tls_global_cleanup(); + +// Client context +tls_ctx_t tls_create_client_context(); +void tls_free_context(tls_ctx_t ctx); +bool tls_set_min_version(tls_ctx_t ctx, int version); +bool tls_load_ca_pem(tls_ctx_t ctx, const char *pem, size_t len); +bool tls_load_ca_file(tls_ctx_t ctx, const char *file_path); +bool tls_load_ca_dir(tls_ctx_t ctx, const char *dir_path); +bool tls_load_system_certs(tls_ctx_t ctx); +bool tls_set_client_cert_pem(tls_ctx_t ctx, const char *cert, const char *key, + const char *password); +bool tls_set_client_cert_file(tls_ctx_t ctx, const char *cert_path, + const char *key_path, const char *password); + +// Server context +tls_ctx_t tls_create_server_context(); +bool tls_set_server_cert_pem(tls_ctx_t ctx, const char *cert, const char *key, + const char *password); +bool tls_set_server_cert_file(tls_ctx_t ctx, const char *cert_path, + const char *key_path, const char *password); +bool tls_set_client_ca_file(tls_ctx_t ctx, const char *ca_file, + const char *ca_dir); +void tls_set_verify_client(tls_ctx_t ctx, bool require); + +// Session management +tls_session_t tls_create_session(tls_ctx_t ctx, socket_t sock); +void tls_free_session(tls_session_t session); +bool tls_set_sni(tls_session_t session, const char *hostname); +bool tls_set_hostname(tls_session_t session, const char *hostname); + +// Handshake (non-blocking capable) +TlsError tls_connect(tls_session_t session); +TlsError tls_accept(tls_session_t session); + +// Handshake with timeout (blocking until timeout) +bool tls_connect_nonblocking(tls_session_t session, socket_t sock, + time_t timeout_sec, time_t timeout_usec, + TlsError *err); +bool tls_accept_nonblocking(tls_session_t session, socket_t sock, + time_t timeout_sec, time_t timeout_usec, + TlsError *err); + +// I/O (non-blocking capable) +ssize_t tls_read(tls_session_t session, void *buf, size_t len, TlsError &err); +ssize_t tls_write(tls_session_t session, const void *buf, size_t len, + TlsError &err); +int tls_pending(const_tls_session_t session); +void tls_shutdown(tls_session_t session, bool graceful); + +// Connection state +bool tls_is_peer_closed(tls_session_t session, socket_t sock); + +// Certificate verification +// NOTE: Certificate ownership differs between backends: +// - OpenSSL: Returns a new reference (caller must call tls_free_cert()) +// - Mbed TLS: Returns internal pointer (valid only while session is active) +// Always call tls_free_cert() for portability; it's a no-op on Mbed TLS. +tls_cert_t tls_get_peer_cert(const_tls_session_t session); +void tls_free_cert(tls_cert_t cert); +bool tls_verify_hostname(tls_cert_t cert, const char *hostname); +long tls_get_verify_result(const_tls_session_t session); + +// Certificate introspection +std::string tls_get_cert_subject_cn(tls_cert_t cert); +std::string tls_get_cert_issuer_name(tls_cert_t cert); + +// Get Subject Alternative Names from certificate +bool tls_get_cert_sans(tls_cert_t cert, std::vector &sans); + +// Get certificate validity period (Unix timestamps) +bool tls_get_cert_validity(tls_cert_t cert, time_t ¬_before, + time_t ¬_after); + +// Get certificate serial number as hex string +std::string tls_get_cert_serial(tls_cert_t cert); + +// SNI (Server Name Indication) +// Returns the SNI hostname from the TLS session, or empty string if not set +const char *tls_get_sni(const_tls_session_t session); + +// CA store management +tls_ca_store_t tls_create_ca_store(const char *pem, size_t len); +void tls_free_ca_store(tls_ca_store_t store); +bool tls_set_ca_store(tls_ctx_t ctx, tls_ca_store_t store); + +// Get list of CA certificates from store (returns count, fills certs vector) +// Caller must call tls_free_cert() on each certificate when done +size_t tls_get_ca_certs(tls_ctx_t ctx, std::vector &certs); + +// Get list of CA subject names from context (for client certificate request) +// Returns vector of subject name strings +std::vector tls_get_ca_names(tls_ctx_t ctx); + +// Dynamic certificate update (for servers) +bool tls_update_server_cert(tls_ctx_t ctx, const char *cert_pem, + const char *key_pem, const char *password); +bool tls_update_server_client_ca(tls_ctx_t ctx, const char *ca_pem); + +// Certificate verification callback +bool tls_set_verify_callback(tls_ctx_t ctx, TlsVerifyCallback callback); + +// Get verification error code from session (after handshake) +long tls_get_verify_error(const_tls_session_t session); + +// Convert verification error code to string +std::string tls_verify_error_string(long error_code); + +// Error information +uint64_t tls_peek_error(); +uint64_t tls_get_error(); +std::string tls_error_string(uint64_t code); + +} // namespace tls + +#endif // CPPHTTPLIB_SSL_ENABLED + +namespace detail { + +#ifdef CPPHTTPLIB_SSL_ENABLED +// Utility function for SSL implementation +bool is_ip_address(const std::string &host); +#endif // CPPHTTPLIB_SSL_ENABLED + +} // namespace detail + namespace stream { class Result { @@ -4368,11 +4698,11 @@ private: static const size_t read_buff_size_ = 1024l * 4; }; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED class SSLSocketStream final : public Stream { public: SSLSocketStream( - socket_t sock, SSL *ssl, time_t read_timeout_sec, + socket_t sock, tls::tls_session_t session, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, time_t max_timeout_msec = 0, std::chrono::time_point start_time = @@ -4391,7 +4721,7 @@ public: private: socket_t sock_; - SSL *ssl_; + tls::tls_session_t session_; time_t read_timeout_sec_; time_t read_timeout_usec_; time_t write_timeout_sec_; @@ -7103,7 +7433,63 @@ inline std::string SHA_256(const std::string &s) { inline std::string SHA_512(const std::string &s) { return message_digest(s, EVP_sha512()); } +#elif defined(CPPHTTPLIB_MBEDTLS_SUPPORT) +inline std::string MD5(const std::string &s) { + unsigned char hash[16]; +#if MBEDTLS_VERSION_MAJOR >= 3 + mbedtls_md5(reinterpret_cast(s.c_str()), s.size(), + hash); +#else + mbedtls_md5_ret(reinterpret_cast(s.c_str()), s.size(), + hash); +#endif + std::stringstream ss; + for (auto i = 0u; i < 16; ++i) { + ss << std::hex << std::setw(2) << std::setfill('0') + << static_cast(hash[i]); + } + return ss.str(); +} + +inline std::string SHA_256(const std::string &s) { + unsigned char hash[32]; +#if MBEDTLS_VERSION_MAJOR >= 3 + mbedtls_sha256(reinterpret_cast(s.c_str()), s.size(), + hash, 0); +#else + mbedtls_sha256_ret(reinterpret_cast(s.c_str()), + s.size(), hash, 0); +#endif + + std::stringstream ss; + for (auto i = 0u; i < 32; ++i) { + ss << std::hex << std::setw(2) << std::setfill('0') + << static_cast(hash[i]); + } + return ss.str(); +} + +inline std::string SHA_512(const std::string &s) { + unsigned char hash[64]; +#if MBEDTLS_VERSION_MAJOR >= 3 + mbedtls_sha512(reinterpret_cast(s.c_str()), s.size(), + hash, 0); +#else + mbedtls_sha512_ret(reinterpret_cast(s.c_str()), + s.size(), hash, 0); +#endif + + std::stringstream ss; + for (auto i = 0u; i < 64; ++i) { + ss << std::hex << std::setw(2) << std::setfill('0') + << static_cast(hash[i]); + } + return ss.str(); +} +#endif + +#ifdef CPPHTTPLIB_SSL_ENABLED 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, @@ -7164,132 +7550,7 @@ inline std::pair make_digest_authentication_header( return std::make_pair(key, field); } -inline bool is_ssl_peer_could_be_closed(SSL *ssl, socket_t sock) { - detail::set_nonblocking(sock, true); - auto se = detail::scope_exit([&]() { detail::set_nonblocking(sock, false); }); - - char buf[1]; - return !SSL_peek(ssl, buf, 1) && - SSL_get_error(ssl, 0) == SSL_ERROR_ZERO_RETURN; -} - -#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) && TARGET_OS_MAC -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 // _WIN32 -#endif // CPPHTTPLIB_OPENSSL_SUPPORT +#endif // CPPHTTPLIB_SSL_ENABLED #ifdef _WIN32 class WSInit { @@ -9987,10 +10248,6 @@ inline void ClientImpl::copy_settings(const ClientImpl &rhs) { 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_; path_encode_ = rhs.path_encode_; @@ -10006,22 +10263,19 @@ inline void ClientImpl::copy_settings(const ClientImpl &rhs) { 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_; - server_hostname_verification_ = rhs.server_hostname_verification_; - server_certificate_verifier_ = rhs.server_certificate_verifier_; -#endif logger_ = rhs.logger_; error_logger_ = rhs.error_logger_; + +#ifdef CPPHTTPLIB_SSL_ENABLED + digest_auth_username_ = rhs.digest_auth_username_; + digest_auth_password_ = rhs.digest_auth_password_; + proxy_digest_auth_username_ = rhs.proxy_digest_auth_username_; + proxy_digest_auth_password_ = rhs.proxy_digest_auth_password_; + ca_cert_file_path_ = rhs.ca_cert_file_path_; + ca_cert_dir_path_ = rhs.ca_cert_dir_path_; + server_certificate_verification_ = rhs.server_certificate_verification_; + server_hostname_verification_ = rhs.server_hostname_verification_; +#endif } inline socket_t ClientImpl::create_client_socket(Error &error) const { @@ -10057,7 +10311,7 @@ inline bool ClientImpl::ensure_socket_connection(Socket &socket, Error &error) { return create_and_connect_socket(socket, error); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED inline bool SSLClient::ensure_socket_connection(Socket &socket, Error &error) { if (!ClientImpl::ensure_socket_connection(socket, error)) { return false; } @@ -10097,9 +10351,10 @@ inline void ClientImpl::close_socket(Socket &socket) { 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 +#ifdef CPPHTTPLIB_SSL_ENABLED assert(socket.ssl == nullptr); #endif + if (socket.sock == INVALID_SOCKET) { return; } detail::close_socket(socket.sock); socket.sock = INVALID_SOCKET; @@ -10148,6 +10403,8 @@ inline bool ClientImpl::send(Request &req, Response &res, Error &error) { if (error == Error::SSLPeerCouldBeClosed_) { assert(!ret); ret = send_(req, res, error); + // If still failing with SSLPeerCouldBeClosed_, convert to Read error + if (error == Error::SSLPeerCouldBeClosed_) { error = Error::Read; } } return ret; } @@ -10165,9 +10422,9 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) { if (socket_.is_open()) { is_alive = detail::is_socket_alive(socket_.sock); -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED if (is_alive && is_ssl()) { - if (detail::is_ssl_peer_could_be_closed(socket_.ssl, socket_.sock)) { + if (tls::tls_is_peer_closed(socket_.ssl, socket_.sock)) { is_alive = false; } } @@ -10191,7 +10448,7 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) { return false; } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED // TODO: refactoring if (is_ssl()) { auto &scli = static_cast(*this); @@ -10273,9 +10530,9 @@ inline Result ClientImpl::send_(Request &&req) { auto res = detail::make_unique(); auto error = Error::Success; auto ret = send(req, *res, error); -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers), - last_ssl_error_, last_openssl_error_}; + last_ssl_error_, last_backend_error_}; #else return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers)}; #endif @@ -10352,9 +10609,9 @@ ClientImpl::open_stream(const std::string &method, const std::string &path, auto is_alive = false; if (socket_.is_open()) { is_alive = detail::is_socket_alive(socket_.sock); -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED if (is_alive && is_ssl()) { - if (detail::is_ssl_peer_could_be_closed(socket_.ssl, socket_.sock)) { + if (tls::tls_is_peer_closed(socket_.ssl, socket_.sock)) { is_alive = false; } } @@ -10372,7 +10629,7 @@ ClientImpl::open_stream(const std::string &method, const std::string &path, return handle; } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED if (is_ssl()) { auto &scli = static_cast(*this); if (!proxy_host_.empty() && proxy_port_ != -1) { @@ -10388,11 +10645,12 @@ ClientImpl::open_stream(const std::string &method, const std::string &path, transfer_socket_ownership_to_handle(handle); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - if (is_ssl() && handle.connection_->ssl) { +#ifdef CPPHTTPLIB_SSL_ENABLED + if (is_ssl() && handle.connection_->session) { handle.socket_stream_ = detail::make_unique( - handle.connection_->sock, handle.connection_->ssl, read_timeout_sec_, - read_timeout_usec_, write_timeout_sec_, write_timeout_usec_); + handle.connection_->sock, handle.connection_->session, + read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, + write_timeout_usec_); } else { handle.socket_stream_ = detail::make_unique( handle.connection_->sock, read_timeout_sec_, read_timeout_usec_, @@ -10611,8 +10869,8 @@ inline bool ChunkedDecoder::parse_trailers_into(Headers &dest, inline void ClientImpl::transfer_socket_ownership_to_handle(StreamHandle &handle) { handle.connection_->sock = socket_.sock; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT - handle.connection_->ssl = socket_.ssl; +#ifdef CPPHTTPLIB_SSL_ENABLED + handle.connection_->session = socket_.ssl; socket_.ssl = nullptr; #endif socket_.sock = INVALID_SOCKET; @@ -10665,7 +10923,7 @@ inline bool ClientImpl::handle_request(Stream &strm, Request &req, ret = redirect(req, res, error); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED if ((res.status == StatusCode::Unauthorized_401 || res.status == StatusCode::ProxyAuthenticationRequired_407) && req.authorization_count_ < 5) { @@ -10769,7 +11027,7 @@ inline bool ClientImpl::create_redirect_client( // Create appropriate client type and handle redirect if (need_ssl) { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED // Create SSL client for HTTPS redirect SSLClient redirect_client(host, port); @@ -10789,9 +11047,10 @@ inline bool ClientImpl::create_redirect_client( server_hostname_verification_); } - // Handle CA certificate store and paths if available - if (ca_cert_store_ && X509_STORE_up_ref(ca_cert_store_)) { - redirect_client.set_ca_cert_store(ca_cert_store_); + // Transfer CA certificate to redirect client + if (!ca_cert_pem_.empty()) { + redirect_client.load_ca_cert_store(ca_cert_pem_.c_str(), + ca_cert_pem_.size()); } if (!ca_cert_file_path_.empty()) { redirect_client.set_ca_cert_path(ca_cert_file_path_, ca_cert_dir_path_); @@ -10844,7 +11103,7 @@ inline void ClientImpl::setup_redirect_client(ClientType &client) { if (!bearer_token_auth_token_.empty()) { client.set_bearer_token_auth(bearer_token_auth_token_); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED if (!digest_auth_username_.empty()) { client.set_digest_auth(digest_auth_username_, digest_auth_password_); } @@ -10864,7 +11123,7 @@ inline void ClientImpl::setup_redirect_client(ClientType &client) { if (!proxy_bearer_token_auth_token_.empty()) { client.set_proxy_bearer_token_auth(proxy_bearer_token_auth_token_); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED if (!proxy_digest_auth_username_.empty()) { client.set_proxy_digest_auth(proxy_digest_auth_username_, proxy_digest_auth_password_); @@ -11235,9 +11494,9 @@ inline Result ClientImpl::send_with_content_provider_and_receiver( std::move(content_provider_without_length), content_type, std::move(content_receiver), error); -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED return Result{std::move(res), error, std::move(req.headers), last_ssl_error_, - last_openssl_error_}; + last_backend_error_}; #else return Result{std::move(res), error, std::move(req.headers)}; #endif @@ -11277,11 +11536,11 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req, auto write_request_success = write_request(strm, req, close_connection, error, expect_100_continue); -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED if (is_ssl()) { auto is_proxy_enabled = !proxy_host_.empty() && proxy_port_ != -1; if (!is_proxy_enabled) { - if (detail::is_ssl_peer_could_be_closed(socket_.ssl, socket_.sock)) { + if (tls::tls_is_peer_closed(socket_.ssl, socket_.sock)) { error = Error::SSLPeerCouldBeClosed_; output_error_log(error, &req); return false; @@ -12304,14 +12563,6 @@ 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; } @@ -12367,11 +12618,11 @@ 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; +#ifdef CPPHTTPLIB_SSL_ENABLED +inline void ClientImpl::set_digest_auth(const std::string &username, + const std::string &password) { + digest_auth_username_ = username; + digest_auth_password_ = password; } inline void ClientImpl::set_ca_cert_path(const std::string &ca_cert_file_path, @@ -12380,12 +12631,23 @@ inline void ClientImpl::set_ca_cert_path(const std::string &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 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::enable_server_certificate_verification(bool enabled) { + server_certificate_verification_ = enabled; +} + +inline void ClientImpl::enable_server_hostname_verification(bool enabled) { + server_hostname_verification_ = enabled; +} +#endif + +// ClientImpl::set_ca_cert_store is defined after TLS namespace (uses helpers) +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT 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)); @@ -12410,17 +12672,9 @@ inline X509_STORE *ClientImpl::create_ca_cert_store(const char *ca_cert, return cts; } -inline void ClientImpl::enable_server_certificate_verification(bool enabled) { - server_certificate_verification_ = enabled; -} - -inline void ClientImpl::enable_server_hostname_verification(bool enabled) { - server_hostname_verification_ = enabled; -} - inline void ClientImpl::set_server_certificate_verifier( - std::function verifier) { - server_certificate_verifier_ = verifier; + std::function /*verifier*/) { + // Base implementation does nothing - SSLClient overrides this } #endif @@ -12433,420 +12687,997 @@ inline void ClientImpl::set_error_logger(ErrorLogger error_logger) { } /* - * SSL Implementation + * OpenSSL Backend Implementation */ + #ifdef CPPHTTPLIB_OPENSSL_SUPPORT -namespace detail { +namespace crypto { -inline bool is_ip_address(const std::string &host) { - struct in_addr addr4; - struct in6_addr addr6; - return inet_pton(AF_INET, host.c_str(), &addr4) == 1 || - inet_pton(AF_INET6, host.c_str(), &addr6) == 1; +inline size_t hash_size(HashAlgorithm algo) { + switch (algo) { + case HashAlgorithm::MD5: return 16; + case HashAlgorithm::SHA1: return 20; + case HashAlgorithm::SHA256: return 32; + case HashAlgorithm::SHA384: return 48; + case HashAlgorithm::SHA512: return 64; + default: return 0; + } } -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); +inline const EVP_MD *get_evp_md(HashAlgorithm algo) { + switch (algo) { + case HashAlgorithm::MD5: return EVP_md5(); + case HashAlgorithm::SHA1: return EVP_sha1(); + case HashAlgorithm::SHA256: return EVP_sha256(); + case HashAlgorithm::SHA384: return EVP_sha384(); + case HashAlgorithm::SHA512: return EVP_sha512(); + default: return nullptr; } - - 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, socket_t sock, - 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) { - (void)(sock); - // SSL_shutdown() returns 0 on first call (indicating close_notify alert - // sent) and 1 on subsequent call (indicating close_notify alert received) - if (SSL_shutdown(ssl) == 0) { - // Expected to return 1, but even if it doesn't, we free ssl - SSL_shutdown(ssl); - } - } +inline bool hash_raw(HashAlgorithm algo, const void *data, size_t len, + std::vector &digest) { + auto md = get_evp_md(algo); + if (!md) { return false; } - std::lock_guard guard(ctx_mutex); - SSL_free(ssl); -} + auto ctx = std::unique_ptr( + EVP_MD_CTX_new(), EVP_MD_CTX_free); + if (!ctx) { return false; } -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, - int *ssl_error) { - 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; - } - if (ssl_error) { *ssl_error = err; } + unsigned int hash_len = 0; + digest.resize(EVP_MAX_MD_SIZE); + + if (EVP_DigestInit_ex(ctx.get(), md, nullptr) != 1) { return false; } + if (EVP_DigestUpdate(ctx.get(), data, len) != 1) { return false; } + if (EVP_DigestFinal_ex(ctx.get(), digest.data(), &hash_len) != 1) { return false; } + + digest.resize(hash_len); 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); - }); +inline std::string hash(HashAlgorithm algo, const void *data, size_t len) { + std::vector digest; + if (!hash_raw(algo, data, len, digest)) { return ""; } + + std::stringstream ss; + for (auto byte : digest) { + ss << std::hex << std::setw(2) << std::setfill('0') + << static_cast(byte); + } + return ss.str(); } -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, - time_t max_timeout_msec, - std::chrono::time_point start_time, T callback) { - SSLSocketStream strm(sock, ssl, read_timeout_sec, read_timeout_usec, - write_timeout_sec, write_timeout_usec, max_timeout_msec, - start_time); - return callback(strm); +inline std::string hash(HashAlgorithm algo, const std::string &data) { + return hash(algo, data.c_str(), data.size()); } -// 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, - time_t max_timeout_msec, - std::chrono::time_point start_time) - : 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), - max_timeout_msec_(max_timeout_msec), start_time_(start_time) { - SSL_clear_mode(ssl, SSL_MODE_AUTO_RETRY); +} // namespace crypto + +namespace tls { + +// OpenSSL-specific helpers for converting native types to PEM +inline std::string x509_to_pem(X509 *cert) { + if (!cert) return {}; + BIO *bio = BIO_new(BIO_s_mem()); + if (!bio) return {}; + if (PEM_write_bio_X509(bio, cert) != 1) { + BIO_free(bio); + return {}; + } + char *data = nullptr; + long len = BIO_get_mem_data(bio, &data); + std::string pem(data, static_cast(len)); + BIO_free(bio); + return pem; } -inline SSLSocketStream::~SSLSocketStream() = default; - -inline bool SSLSocketStream::is_readable() const { - return SSL_pending(ssl_) > 0; +inline std::string evp_pkey_to_pem(EVP_PKEY *key) { + if (!key) return {}; + BIO *bio = BIO_new(BIO_s_mem()); + if (!bio) return {}; + if (PEM_write_bio_PrivateKey(bio, key, nullptr, nullptr, 0, nullptr, + nullptr) != 1) { + BIO_free(bio); + return {}; + } + char *data = nullptr; + long len = BIO_get_mem_data(bio, &data); + std::string pem(data, static_cast(len)); + BIO_free(bio); + return pem; } -inline bool SSLSocketStream::wait_readable() const { - if (max_timeout_msec_ <= 0) { - return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0; +inline std::string x509_store_to_pem(X509_STORE *store) { + if (!store) return {}; + std::string pem; + auto objs = X509_STORE_get0_objects(store); + if (!objs) return {}; + for (int i = 0; i < sk_X509_OBJECT_num(objs); i++) { + auto obj = sk_X509_OBJECT_value(objs, i); + if (X509_OBJECT_get_type(obj) == X509_LU_X509) { + auto cert = X509_OBJECT_get0_X509(obj); + if (cert) { pem += x509_to_pem(cert); } + } + } + return pem; +} + +// Helper to map OpenSSL SSL_get_error to ErrorCode +inline ErrorCode map_ssl_error(int ssl_error, int &out_errno) { + switch (ssl_error) { + case SSL_ERROR_NONE: return ErrorCode::Success; + case SSL_ERROR_WANT_READ: return ErrorCode::WantRead; + case SSL_ERROR_WANT_WRITE: return ErrorCode::WantWrite; + case SSL_ERROR_ZERO_RETURN: return ErrorCode::PeerClosed; + case SSL_ERROR_SYSCALL: out_errno = errno; return ErrorCode::SyscallError; + case SSL_ERROR_SSL: + default: return ErrorCode::Fatal; + } +} + +inline bool tls_global_init() { + // OpenSSL 3.0+: OPENSSL_init_ssl() is called automatically + return OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | + OPENSSL_INIT_LOAD_CRYPTO_STRINGS, + nullptr) == 1; +} + +inline void tls_global_cleanup() { + // OpenSSL 3.0+: cleanup is automatic +} + +inline tls_ctx_t tls_create_client_context() { + SSL_CTX *ctx = SSL_CTX_new(TLS_client_method()); + if (ctx) { + // Disable auto-retry to properly handle non-blocking I/O + SSL_CTX_clear_mode(ctx, SSL_MODE_AUTO_RETRY); + // Set minimum TLS version + SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); + } + return static_cast(ctx); +} + +inline void tls_free_context(tls_ctx_t ctx) { + if (ctx) { SSL_CTX_free(static_cast(ctx)); } +} + +inline bool tls_set_min_version(tls_ctx_t ctx, int version) { + if (!ctx) return false; + return SSL_CTX_set_min_proto_version(static_cast(ctx), version) == + 1; +} + +inline bool tls_load_ca_pem(tls_ctx_t ctx, const char *pem, size_t len) { + if (!ctx || !pem || len == 0) return false; + + auto ssl_ctx = static_cast(ctx); + auto store = SSL_CTX_get_cert_store(ssl_ctx); + if (!store) return false; + + auto bio = BIO_new_mem_buf(pem, static_cast(len)); + if (!bio) return false; + + bool ok = true; + X509 *cert = nullptr; + while ((cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) != + nullptr) { + if (X509_STORE_add_cert(store, cert) != 1) { + // Ignore duplicate errors + auto err = ERR_peek_last_error(); + if (ERR_GET_REASON(err) != X509_R_CERT_ALREADY_IN_HASH_TABLE) { + ok = false; + } + } + X509_free(cert); + if (!ok) break; + } + BIO_free(bio); + + // Clear any "no more certificates" errors + ERR_clear_error(); + return ok; +} + +inline bool tls_load_ca_file(tls_ctx_t ctx, const char *file_path) { + if (!ctx || !file_path) return false; + return SSL_CTX_load_verify_locations(static_cast(ctx), file_path, + nullptr) == 1; +} + +inline bool tls_load_ca_dir(tls_ctx_t ctx, const char *dir_path) { + if (!ctx || !dir_path) return false; + return SSL_CTX_load_verify_locations(static_cast(ctx), nullptr, + dir_path) == 1; +} + +inline bool tls_load_system_certs(tls_ctx_t ctx) { + if (!ctx) return false; + auto ssl_ctx = static_cast(ctx); + +#ifdef _WIN32 + // Windows: Load from system certificate store + auto store = SSL_CTX_get_cert_store(ssl_ctx); + if (!store) return false; + + auto hStore = CertOpenSystemStoreW(NULL, L"ROOT"); + if (!hStore) return false; + + bool loaded_any = false; + PCCERT_CONTEXT pContext = nullptr; + while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) != + nullptr) { + const unsigned char *data = pContext->pbCertEncoded; + auto x509 = d2i_X509(nullptr, &data, pContext->cbCertEncoded); + if (x509) { + if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; } + X509_free(x509); + } + } + CertCloseStore(hStore, 0); + return loaded_any; + +#elif defined(__APPLE__) +#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN + // macOS: Load from Keychain + auto store = SSL_CTX_get_cert_store(ssl_ctx); + if (!store) return false; + + CFArrayRef certs = nullptr; + if (SecTrustCopyAnchorCertificates(&certs) != errSecSuccess || !certs) { + return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1; } - time_t read_timeout_sec; - time_t read_timeout_usec; - calc_actual_timeout(max_timeout_msec_, duration(), read_timeout_sec_, - read_timeout_usec_, read_timeout_sec, read_timeout_usec); - - return select_read(sock_, read_timeout_sec, read_timeout_usec) > 0; -} - -inline bool SSLSocketStream::wait_writable() const { - return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0 && - is_socket_alive(sock_) && !is_ssl_peer_could_be_closed(ssl_, sock_); -} - -inline ssize_t SSLSocketStream::read(char *ptr, size_t size) { - if (SSL_pending(ssl_) > 0) { - auto ret = SSL_read(ssl_, ptr, static_cast(size)); - if (ret == 0) { error_ = Error::ConnectionClosed; } - return ret; - } else if (wait_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 (wait_readable()) { - std::this_thread::sleep_for(std::chrono::microseconds{10}); - ret = SSL_read(ssl_, ptr, static_cast(size)); - if (ret >= 0) { return ret; } - err = SSL_get_error(ssl_, ret); - } else { - break; - } + bool loaded_any = false; + auto count = CFArrayGetCount(certs); + for (CFIndex i = 0; i < count; i++) { + auto cert = reinterpret_cast( + const_cast(CFArrayGetValueAtIndex(certs, i))); + CFDataRef der = SecCertificateCopyData(cert); + if (der) { + const unsigned char *data = CFDataGetBytePtr(der); + auto x509 = d2i_X509(nullptr, &data, CFDataGetLength(der)); + if (x509) { + if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; } + X509_free(x509); } - assert(ret < 0); - } else if (ret == 0) { - error_ = Error::ConnectionClosed; + CFRelease(der); } - return ret; + } + CFRelease(certs); + return loaded_any || SSL_CTX_set_default_verify_paths(ssl_ctx) == 1; +#else + return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1; +#endif + +#else + // Other Unix: use default verify paths + return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1; +#endif +} + +inline bool tls_set_client_cert_pem(tls_ctx_t ctx, const char *cert, + const char *key, const char *password) { + if (!ctx || !cert || !key) return false; + + auto ssl_ctx = static_cast(ctx); + + // Load certificate + auto cert_bio = BIO_new_mem_buf(cert, -1); + if (!cert_bio) return false; + + auto x509 = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr); + BIO_free(cert_bio); + if (!x509) return false; + + auto cert_ok = SSL_CTX_use_certificate(ssl_ctx, x509) == 1; + X509_free(x509); + if (!cert_ok) return false; + + // Load private key + auto key_bio = BIO_new_mem_buf(key, -1); + if (!key_bio) return false; + + auto pkey = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, + password ? const_cast(password) + : nullptr); + BIO_free(key_bio); + if (!pkey) return false; + + auto key_ok = SSL_CTX_use_PrivateKey(ssl_ctx, pkey) == 1; + EVP_PKEY_free(pkey); + + return key_ok && SSL_CTX_check_private_key(ssl_ctx) == 1; +} + +inline bool tls_set_client_cert_file(tls_ctx_t ctx, const char *cert_path, + const char *key_path, + const char *password) { + if (!ctx || !cert_path || !key_path) return false; + + auto ssl_ctx = static_cast(ctx); + + if (password && password[0] != '\0') { + SSL_CTX_set_default_passwd_cb_userdata( + ssl_ctx, reinterpret_cast(const_cast(password))); + } + + return SSL_CTX_use_certificate_chain_file(ssl_ctx, cert_path) == 1 && + SSL_CTX_use_PrivateKey_file(ssl_ctx, key_path, SSL_FILETYPE_PEM) == 1; +} + +inline tls_ctx_t tls_create_server_context() { + SSL_CTX *ctx = SSL_CTX_new(TLS_server_method()); + if (ctx) { + SSL_CTX_set_options(ctx, SSL_OP_NO_COMPRESSION | + SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); + SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION); + } + return static_cast(ctx); +} + +inline bool tls_set_server_cert_pem(tls_ctx_t ctx, const char *cert, + const char *key, const char *password) { + // Same implementation as client cert + return tls_set_client_cert_pem(ctx, cert, key, password); +} + +inline bool tls_set_server_cert_file(tls_ctx_t ctx, const char *cert_path, + const char *key_path, + const char *password) { + // Same implementation as client cert file + return tls_set_client_cert_file(ctx, cert_path, key_path, password); +} + +inline bool tls_set_client_ca_file(tls_ctx_t ctx, const char *ca_file, + const char *ca_dir) { + if (!ctx) return false; + auto ssl_ctx = static_cast(ctx); + + if (ca_file || ca_dir) { + if (SSL_CTX_load_verify_locations(ssl_ctx, ca_file, ca_dir) != 1) { + return false; + } + } + + // Set CA list for client certificate request + if (ca_file) { + auto list = SSL_load_client_CA_file(ca_file); + if (list) { SSL_CTX_set_client_CA_list(ssl_ctx, list); } + } + + return true; +} + +inline void tls_set_verify_client(tls_ctx_t ctx, bool require) { + if (!ctx) return; + SSL_CTX_set_verify(static_cast(ctx), + require + ? (SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT) + : SSL_VERIFY_NONE, + nullptr); +} + +inline tls_session_t tls_create_session(tls_ctx_t ctx, socket_t sock) { + if (!ctx || sock == INVALID_SOCKET) return nullptr; + + auto ssl_ctx = static_cast(ctx); + SSL *ssl = SSL_new(ssl_ctx); + if (!ssl) return nullptr; + + // Disable auto-retry for proper non-blocking I/O handling + SSL_clear_mode(ssl, SSL_MODE_AUTO_RETRY); + + auto bio = BIO_new_socket(static_cast(sock), BIO_NOCLOSE); + if (!bio) { + SSL_free(ssl); + return nullptr; + } + + SSL_set_bio(ssl, bio, bio); + return static_cast(ssl); +} + +inline void tls_free_session(tls_session_t session) { + if (session) { SSL_free(static_cast(session)); } +} + +inline bool tls_set_sni(tls_session_t session, const char *hostname) { + if (!session || !hostname) return false; + + auto ssl = static_cast(session); + + // Set SNI (Server Name Indication) only - does not enable verification +#if defined(OPENSSL_IS_BORINGSSL) + return SSL_set_tlsext_host_name(ssl, hostname) == 1; +#else + // Direct call instead of macro to suppress -Wold-style-cast warning + return SSL_ctrl(ssl, SSL_CTRL_SET_TLSEXT_HOSTNAME, TLSEXT_NAMETYPE_host_name, + static_cast(const_cast(hostname))) == 1; +#endif +} + +inline bool tls_set_hostname(tls_session_t session, const char *hostname) { + if (!session || !hostname) return false; + + auto ssl = static_cast(session); + + // Set SNI (Server Name Indication) + if (!tls_set_sni(session, hostname)) { return false; } + + // Enable hostname verification + auto param = SSL_get0_param(ssl); + if (!param) return false; + + X509_VERIFY_PARAM_set_hostflags(param, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS); + if (X509_VERIFY_PARAM_set1_host(param, hostname, 0) != 1) { return false; } + + SSL_set_verify(ssl, SSL_VERIFY_PEER, nullptr); + return true; +} + +inline TlsError tls_connect(tls_session_t session) { + if (!session) { return TlsError(); } + + auto ssl = static_cast(session); + auto ret = SSL_connect(ssl); + + TlsError err; + if (ret == 1) { + err.code = ErrorCode::Success; } else { - error_ = Error::Timeout; + auto ssl_err = SSL_get_error(ssl, ret); + err.code = map_ssl_error(ssl_err, err.sys_errno); + if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); } + } + return err; +} + +inline TlsError tls_accept(tls_session_t session) { + if (!session) { return TlsError(); } + + auto ssl = static_cast(session); + auto ret = SSL_accept(ssl); + + TlsError err; + if (ret == 1) { + err.code = ErrorCode::Success; + } else { + auto ssl_err = SSL_get_error(ssl, ret); + err.code = map_ssl_error(ssl_err, err.sys_errno); + if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); } + } + return err; +} + +inline bool tls_connect_nonblocking(tls_session_t session, socket_t sock, + time_t timeout_sec, time_t timeout_usec, + TlsError *err) { + if (!session) { + if (err) { err->code = ErrorCode::Fatal; } + return false; + } + + auto ssl = static_cast(session); + auto bio = SSL_get_rbio(ssl); + + // Set non-blocking mode for handshake + detail::set_nonblocking(sock, true); + if (bio) { BIO_set_nbio(bio, 1); } + + auto cleanup = detail::scope_exit([&]() { + // Restore blocking mode after handshake + if (bio) { BIO_set_nbio(bio, 0); } + detail::set_nonblocking(sock, false); + }); + + auto res = 0; + while ((res = SSL_connect(ssl)) != 1) { + auto ssl_err = SSL_get_error(ssl, res); + switch (ssl_err) { + case SSL_ERROR_WANT_READ: + if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) { + continue; + } + break; + case SSL_ERROR_WANT_WRITE: + if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) { + continue; + } + break; + default: break; + } + if (err) { + err->code = map_ssl_error(ssl_err, err->sys_errno); + if (err->code == ErrorCode::Fatal) { + err->backend_code = ERR_get_error(); + } + } + return false; + } + if (err) { err->code = ErrorCode::Success; } + return true; +} + +inline bool tls_accept_nonblocking(tls_session_t session, socket_t sock, + time_t timeout_sec, time_t timeout_usec, + TlsError *err) { + if (!session) { + if (err) { err->code = ErrorCode::Fatal; } + return false; + } + + auto ssl = static_cast(session); + auto bio = SSL_get_rbio(ssl); + + // Set non-blocking mode for handshake + detail::set_nonblocking(sock, true); + if (bio) { BIO_set_nbio(bio, 1); } + + auto cleanup = detail::scope_exit([&]() { + // Restore blocking mode after handshake + if (bio) { BIO_set_nbio(bio, 0); } + detail::set_nonblocking(sock, false); + }); + + auto res = 0; + while ((res = SSL_accept(ssl)) != 1) { + auto ssl_err = SSL_get_error(ssl, res); + switch (ssl_err) { + case SSL_ERROR_WANT_READ: + if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) { + continue; + } + break; + case SSL_ERROR_WANT_WRITE: + if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) { + continue; + } + break; + default: break; + } + if (err) { + err->code = map_ssl_error(ssl_err, err->sys_errno); + if (err->code == ErrorCode::Fatal) { + err->backend_code = ERR_get_error(); + } + } + return false; + } + if (err) { err->code = ErrorCode::Success; } + return true; +} + +inline ssize_t tls_read(tls_session_t session, void *buf, size_t len, + TlsError &err) { + if (!session || !buf) { + err.code = ErrorCode::Fatal; return -1; } -} -inline ssize_t SSLSocketStream::write(const char *ptr, size_t size) { - if (wait_writable()) { - auto handle_size = static_cast( - std::min(size, (std::numeric_limits::max)())); + auto ssl = static_cast(session); + auto ret = SSL_read(ssl, buf, static_cast(len)); - 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 (wait_writable()) { - std::this_thread::sleep_for(std::chrono::microseconds{10}); - ret = SSL_write(ssl_, ptr, static_cast(handle_size)); - if (ret >= 0) { return ret; } - err = SSL_get_error(ssl_, ret); - } else { - break; - } - } - assert(ret < 0); - } + if (ret > 0) { + err.code = ErrorCode::Success; return ret; } + + auto ssl_err = SSL_get_error(ssl, ret); + err.code = map_ssl_error(ssl_err, err.sys_errno); + if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); } 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 ssize_t tls_write(tls_session_t session, const void *buf, size_t len, + TlsError &err) { + if (!session || !buf) { + err.code = ErrorCode::Fatal; + return -1; + } + + auto ssl = static_cast(session); + auto ret = SSL_write(ssl, buf, static_cast(len)); + + if (ret > 0) { + err.code = ErrorCode::Success; + return ret; + } + + auto ssl_err = SSL_get_error(ssl, ret); + err.code = map_ssl_error(ssl_err, err.sys_errno); + if (err.code == ErrorCode::Fatal) { err.backend_code = ERR_get_error(); } + return -1; } -inline void SSLSocketStream::get_local_ip_and_port(std::string &ip, - int &port) const { - detail::get_local_ip_and_port(sock_, ip, port); +inline int tls_pending(const_tls_session_t session) { + if (!session) return 0; + return SSL_pending(static_cast(const_cast(session))); } -inline socket_t SSLSocketStream::socket() const { return sock_; } +inline void tls_shutdown(tls_session_t session, bool graceful) { + if (!session) return; -inline time_t SSLSocketStream::duration() const { - return std::chrono::duration_cast( - std::chrono::steady_clock::now() - start_time_) - .count(); -} - -} // 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_2_VERSION); - - 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))); + auto ssl = static_cast(session); + if (graceful) { + // First call sends close_notify + if (SSL_shutdown(ssl) == 0) { + // Second call waits for peer's close_notify + SSL_shutdown(ssl); } + } +} - 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_check_private_key(ctx_) != 1) { - last_ssl_error_ = static_cast(ERR_get_error()); - 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); +inline bool tls_is_peer_closed(tls_session_t session, socket_t sock) { + if (!session) return true; - // Set client CA list to be sent to clients during TLS handshake - if (client_ca_cert_file_path) { - auto ca_list = SSL_load_client_CA_file(client_ca_cert_file_path); - if (ca_list != nullptr) { - SSL_CTX_set_client_CA_list(ctx_, ca_list); - } else { - // Failed to load client CA list, but we continue since - // SSL_CTX_load_verify_locations already succeeded and - // certificate verification will still work - last_ssl_error_ = static_cast(ERR_get_error()); + // Temporarily set socket to non-blocking to avoid blocking on SSL_peek + detail::set_nonblocking(sock, true); + auto se = detail::scope_exit([&]() { detail::set_nonblocking(sock, false); }); + + auto ssl = static_cast(session); + char buf; + auto ret = SSL_peek(ssl, &buf, 1); + if (ret > 0) return false; + + auto err = SSL_get_error(ssl, ret); + return err == SSL_ERROR_ZERO_RETURN; +} + +inline tls_cert_t tls_get_peer_cert(const_tls_session_t session) { + if (!session) return nullptr; + return static_cast(SSL_get1_peer_certificate( + static_cast(const_cast(session)))); +} + +inline void tls_free_cert(tls_cert_t cert) { + if (cert) { X509_free(static_cast(cert)); } +} + +inline bool tls_verify_hostname(tls_cert_t cert, const char *hostname) { + if (!cert || !hostname) return false; + + auto x509 = static_cast(cert); + return X509_check_host(x509, hostname, strlen(hostname), 0, nullptr) == 1; +} + +inline long tls_get_verify_result(const_tls_session_t session) { + if (!session) return X509_V_ERR_UNSPECIFIED; + return SSL_get_verify_result(static_cast(const_cast(session))); +} + +inline std::string tls_get_cert_subject_cn(tls_cert_t cert) { + if (!cert) return ""; + auto x509 = static_cast(cert); + auto subject_name = X509_get_subject_name(x509); + if (!subject_name) return ""; + + char buf[256]; + auto len = + X509_NAME_get_text_by_NID(subject_name, NID_commonName, buf, sizeof(buf)); + if (len < 0) return ""; + return std::string(buf, static_cast(len)); +} + +inline std::string tls_get_cert_issuer_name(tls_cert_t cert) { + if (!cert) return ""; + auto x509 = static_cast(cert); + auto issuer_name = X509_get_issuer_name(x509); + if (!issuer_name) return ""; + + char buf[256]; + X509_NAME_oneline(issuer_name, buf, sizeof(buf)); + return std::string(buf); +} + +inline bool tls_get_cert_sans(tls_cert_t cert, std::vector &sans) { + sans.clear(); + if (!cert) return false; + auto x509 = static_cast(cert); + + auto names = static_cast( + X509_get_ext_d2i(x509, NID_subject_alt_name, nullptr, nullptr)); + if (!names) return true; // No SANs is valid + + auto count = sk_GENERAL_NAME_num(names); + for (int i = 0; i < count; i++) { + auto gen = sk_GENERAL_NAME_value(names, i); + if (!gen) continue; + + TlsSanEntry entry; + switch (gen->type) { + case GEN_DNS: + entry.type = SanType::DNS; + if (gen->d.dNSName) { + entry.value = std::string( + reinterpret_cast( + ASN1_STRING_get0_data(gen->d.dNSName)), + static_cast(ASN1_STRING_length(gen->d.dNSName))); + } + break; + case GEN_IPADD: + entry.type = SanType::IP; + if (gen->d.iPAddress) { + auto data = ASN1_STRING_get0_data(gen->d.iPAddress); + auto len = ASN1_STRING_length(gen->d.iPAddress); + if (len == 4) { + // IPv4 + char buf[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, data, buf, sizeof(buf)); + entry.value = buf; + } else if (len == 16) { + // IPv6 + char buf[INET6_ADDRSTRLEN]; + inet_ntop(AF_INET6, data, buf, sizeof(buf)); + entry.value = buf; } } - - 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_2_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); - - // Extract CA names from the store and set them as the client CA list - auto ca_list = extract_ca_names_from_x509_store(client_ca_cert_store); - if (ca_list) { - SSL_CTX_set_client_CA_list(ctx_, ca_list); - } else { - // Failed to extract CA names, record the error - last_ssl_error_ = static_cast(ERR_get_error()); + break; + case GEN_EMAIL: + entry.type = SanType::EMAIL; + if (gen->d.rfc822Name) { + entry.value = std::string( + reinterpret_cast( + ASN1_STRING_get0_data(gen->d.rfc822Name)), + static_cast(ASN1_STRING_length(gen->d.rfc822Name))); } + break; + case GEN_URI: + entry.type = SanType::URI; + if (gen->d.uniformResourceIdentifier) { + entry.value = std::string( + reinterpret_cast( + ASN1_STRING_get0_data(gen->d.uniformResourceIdentifier)), + static_cast( + ASN1_STRING_length(gen->d.uniformResourceIdentifier))); + } + break; + default: entry.type = SanType::OTHER; break; + } - SSL_CTX_set_verify( - ctx_, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr); + if (!entry.value.empty()) { sans.push_back(std::move(entry)); } + } + + GENERAL_NAMES_free(names); + return true; +} + +inline bool tls_get_cert_validity(tls_cert_t cert, time_t ¬_before, + time_t ¬_after) { + if (!cert) return false; + auto x509 = static_cast(cert); + + auto nb = X509_get0_notBefore(x509); + auto na = X509_get0_notAfter(x509); + if (!nb || !na) return false; + + // Convert ASN1_TIME to time_t + struct tm tm_nb = {}, tm_na = {}; + if (ASN1_TIME_to_tm(nb, &tm_nb) != 1) return false; + if (ASN1_TIME_to_tm(na, &tm_na) != 1) return false; + +#ifdef _WIN32 + not_before = _mkgmtime(&tm_nb); + not_after = _mkgmtime(&tm_na); +#else + not_before = timegm(&tm_nb); + not_after = timegm(&tm_na); +#endif + return true; +} + +inline std::string tls_get_cert_serial(tls_cert_t cert) { + if (!cert) return ""; + auto x509 = static_cast(cert); + + auto serial = X509_get_serialNumber(x509); + if (!serial) return ""; + + auto bn = ASN1_INTEGER_to_BN(serial, nullptr); + if (!bn) return ""; + + auto hex = BN_bn2hex(bn); + BN_free(bn); + if (!hex) return ""; + + std::string result(hex); + OPENSSL_free(hex); + return result; +} + +inline const char *tls_get_sni(const_tls_session_t session) { + if (!session) return nullptr; + auto ssl = static_cast(const_cast(session)); + return SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); +} + +inline uint64_t tls_peek_error() { return ERR_peek_last_error(); } + +inline uint64_t tls_get_error() { return ERR_get_error(); } + +inline std::string tls_error_string(uint64_t code) { + char buf[256]; + ERR_error_string_n(static_cast(code), buf, sizeof(buf)); + return std::string(buf); +} + +inline tls_ca_store_t tls_create_ca_store(const char *pem, size_t len) { + auto mem = BIO_new_mem_buf(pem, static_cast(len)); + if (!mem) { return nullptr; } + auto mem_guard = detail::scope_exit([&] { BIO_free_all(mem); }); + + auto inf = PEM_X509_INFO_read_bio(mem, nullptr, nullptr, nullptr); + if (!inf) { return nullptr; } + + auto store = X509_STORE_new(); + if (store) { + for (auto i = 0; i < static_cast(sk_X509_INFO_num(inf)); i++) { + auto itmp = sk_X509_INFO_value(inf, i); + if (!itmp) { continue; } + if (itmp->x509) { X509_STORE_add_cert(store, itmp->x509); } + if (itmp->crl) { X509_STORE_add_crl(store, itmp->crl); } } } + + sk_X509_INFO_pop_free(inf, X509_INFO_free); + return static_cast(store); } -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 void tls_free_ca_store(tls_ca_store_t store) { + if (store) { X509_STORE_free(static_cast(store)); } +} + +inline bool tls_set_ca_store(tls_ctx_t ctx, tls_ca_store_t store) { + if (!ctx || !store) { return false; } + auto ssl_ctx = static_cast(ctx); + auto x509_store = static_cast(store); + + // Check if same store is already set + if (SSL_CTX_get_cert_store(ssl_ctx) == x509_store) { return true; } + + // SSL_CTX_set_cert_store takes ownership and frees the old store + SSL_CTX_set_cert_store(ssl_ctx, x509_store); + return true; +} + +inline size_t tls_get_ca_certs(tls_ctx_t ctx, std::vector &certs) { + certs.clear(); + if (!ctx) { return 0; } + auto ssl_ctx = static_cast(ctx); + + auto store = SSL_CTX_get_cert_store(ssl_ctx); + if (!store) { return 0; } + + auto objs = X509_STORE_get0_objects(store); + if (!objs) { return 0; } + + int count = sk_X509_OBJECT_num(objs); + for (int i = 0; i < count; i++) { + auto obj = sk_X509_OBJECT_value(objs, i); + if (!obj) { continue; } + if (X509_OBJECT_get_type(obj) == X509_LU_X509) { + auto x509 = X509_OBJECT_get0_X509(obj); + if (x509) { + // Increment reference count so caller can free it + X509_up_ref(x509); + certs.push_back(static_cast(x509)); + } } } + return certs.size(); } -inline SSLServer::~SSLServer() { - if (ctx_) { SSL_CTX_free(ctx_); } -} +inline std::vector tls_get_ca_names(tls_ctx_t ctx) { + std::vector names; + if (!ctx) { return names; } + auto ssl_ctx = static_cast(ctx); -inline bool SSLServer::is_valid() const { return ctx_; } + auto store = SSL_CTX_get_cert_store(ssl_ctx); + if (!store) { return names; } -inline SSL_CTX *SSLServer::ssl_context() const { return ctx_; } + auto objs = X509_STORE_get0_objects(store); + if (!objs) { return names; } -inline void SSLServer::update_certs(X509 *cert, EVP_PKEY *private_key, - X509_STORE *client_ca_cert_store) { - - std::lock_guard guard(ctx_mutex_); - - SSL_CTX_use_certificate(ctx_, cert); - SSL_CTX_use_PrivateKey(ctx_, private_key); - - if (client_ca_cert_store != nullptr) { - SSL_CTX_set_cert_store(ctx_, client_ca_cert_store); + int count = sk_X509_OBJECT_num(objs); + for (int i = 0; i < count; i++) { + auto obj = sk_X509_OBJECT_value(objs, i); + if (!obj) { continue; } + if (X509_OBJECT_get_type(obj) == X509_LU_X509) { + auto x509 = X509_OBJECT_get0_X509(obj); + if (x509) { + auto subject = X509_get_subject_name(x509); + if (subject) { + char buf[512]; + X509_NAME_oneline(subject, buf, sizeof(buf)); + names.push_back(buf); + } + } + } } + return names; } -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_, - &last_ssl_error_); - }, - [](SSL * /*ssl2*/) { return true; }); +inline bool tls_update_server_cert(tls_ctx_t ctx, const char *cert_pem, + const char *key_pem, const char *password) { + if (!ctx || !cert_pem || !key_pem) { return false; } + auto ssl_ctx = static_cast(ctx); - auto ret = false; - if (ssl) { - std::string remote_addr; - int remote_port = 0; - detail::get_remote_ip_and_port(sock, remote_addr, remote_port); + // Load certificate from PEM + auto cert_bio = BIO_new_mem_buf(cert_pem, -1); + if (!cert_bio) { return false; } + auto cert = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr); + BIO_free(cert_bio); + if (!cert) { return false; } - std::string local_addr; - int local_port = 0; - detail::get_local_ip_and_port(sock, local_addr, local_port); - - ret = detail::process_server_socket_ssl( - svr_sock_, ssl, sock, keep_alive_max_count_, keep_alive_timeout_sec_, - read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, - write_timeout_usec_, - [&](Stream &strm, bool close_connection, bool &connection_closed) { - return process_request(strm, remote_addr, remote_port, local_addr, - local_port, close_connection, - connection_closed, - [&](Request &req) { req.ssl = 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, sock, shutdown_gracefully); + // Load private key from PEM + auto key_bio = BIO_new_mem_buf(key_pem, -1); + if (!key_bio) { + X509_free(cert); + return false; + } + auto key = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, + password ? const_cast(password) + : nullptr); + BIO_free(key_bio); + if (!key) { + X509_free(cert); + return false; } - detail::shutdown_socket(sock); - detail::close_socket(sock); + // Update certificate and key + auto ret = SSL_CTX_use_certificate(ssl_ctx, cert) == 1 && + SSL_CTX_use_PrivateKey(ssl_ctx, key) == 1; + + X509_free(cert); + EVP_PKEY_free(key); return ret; } -inline STACK_OF(X509_NAME) * SSLServer::extract_ca_names_from_x509_store( - X509_STORE *store) { +// Helper: Create client CA list from PEM string +// Returns a new STACK_OF(X509_NAME)* or nullptr on failure +// Caller takes ownership of returned list +inline STACK_OF(X509_NAME) * + create_client_ca_list_from_pem(const char *ca_pem) { + if (!ca_pem) { return nullptr; } + + auto ca_list = sk_X509_NAME_new_null(); + if (!ca_list) { return nullptr; } + + BIO *bio = BIO_new_mem_buf(ca_pem, -1); + if (!bio) { + sk_X509_NAME_pop_free(ca_list, X509_NAME_free); + return nullptr; + } + + X509 *cert = nullptr; + while ((cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr)) != + nullptr) { + X509_NAME *name = X509_get_subject_name(cert); + if (name) { sk_X509_NAME_push(ca_list, X509_NAME_dup(name)); } + X509_free(cert); + } + BIO_free(bio); + + return ca_list; +} + +// Helper: Extract CA names from X509_STORE +// Returns a new STACK_OF(X509_NAME)* or nullptr on failure +// Caller takes ownership of returned list +inline STACK_OF(X509_NAME) * + extract_client_ca_list_from_store(X509_STORE *store) { if (!store) { return nullptr; } auto ca_list = sk_X509_NAME_new_null(); if (!ca_list) { return nullptr; } - // Get all objects from the store auto objs = X509_STORE_get0_objects(store); if (!objs) { sk_X509_NAME_free(ca_list); return nullptr; } - // Iterate through objects and extract certificate subject names for (int i = 0; i < sk_X509_OBJECT_num(objs); i++) { auto obj = sk_X509_OBJECT_value(objs, i); if (X509_OBJECT_get_type(obj) == X509_LU_X509) { @@ -12861,7 +13692,6 @@ inline STACK_OF(X509_NAME) * SSLServer::extract_ca_names_from_x509_store( } } - // If no names were extracted, free the list and return nullptr if (sk_X509_NAME_num(ca_list) == 0) { sk_X509_NAME_free(ca_list); return nullptr; @@ -12870,6 +13700,242 @@ inline STACK_OF(X509_NAME) * SSLServer::extract_ca_names_from_x509_store( return ca_list; } +inline bool tls_update_server_client_ca(tls_ctx_t ctx, const char *ca_pem) { + if (!ctx || !ca_pem) { return false; } + auto ssl_ctx = static_cast(ctx); + + // Create new X509_STORE from PEM + auto store = tls_create_ca_store(ca_pem, strlen(ca_pem)); + if (!store) { return false; } + + // SSL_CTX_set_cert_store takes ownership + SSL_CTX_set_cert_store(ssl_ctx, static_cast(store)); + + // Set client CA list for client certificate request + auto ca_list = create_client_ca_list_from_pem(ca_pem); + if (ca_list) { + // SSL_CTX_set_client_CA_list takes ownership of ca_list + SSL_CTX_set_client_CA_list(ssl_ctx, ca_list); + } + + return true; +} + +// Thread-local storage for verify callback +inline TlsVerifyCallback &get_verify_callback() { + static thread_local TlsVerifyCallback callback; + return callback; +} + +// OpenSSL verify callback wrapper +inline int openssl_verify_callback(int preverify_ok, X509_STORE_CTX *ctx) { + auto &callback = get_verify_callback(); + if (!callback) { return preverify_ok; } + + // Get SSL object from X509_STORE_CTX + auto ssl = static_cast( + X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx())); + if (!ssl) { return preverify_ok; } + + // Get current certificate and depth + auto cert = X509_STORE_CTX_get_current_cert(ctx); + int depth = X509_STORE_CTX_get_error_depth(ctx); + int error = X509_STORE_CTX_get_error(ctx); + + // Build context + TlsVerifyContext verify_ctx; + verify_ctx.session = static_cast(ssl); + verify_ctx.cert = static_cast(cert); + verify_ctx.depth = depth; + verify_ctx.preverify_ok = (preverify_ok != 0); + verify_ctx.error_code = error; + verify_ctx.error_string = + (error != X509_V_OK) ? X509_verify_cert_error_string(error) : nullptr; + + return callback(verify_ctx) ? 1 : 0; +} + +inline bool tls_set_verify_callback(tls_ctx_t ctx, TlsVerifyCallback callback) { + if (!ctx) { return false; } + auto ssl_ctx = static_cast(ctx); + + get_verify_callback() = std::move(callback); + + if (get_verify_callback()) { + SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, openssl_verify_callback); + } else { + SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, nullptr); + } + return true; +} + +inline long tls_get_verify_error(const_tls_session_t session) { + if (!session) { return -1; } + auto ssl = static_cast(const_cast(session)); + return SSL_get_verify_result(ssl); +} + +inline std::string tls_verify_error_string(long error_code) { + if (error_code == X509_V_OK) { return ""; } + const char *str = X509_verify_cert_error_string(static_cast(error_code)); + return str ? str : "unknown error"; +} + +// OpenSSL-specific helpers for public API wrappers +inline tls_ctx_t create_server_context_from_x509(X509 *cert, EVP_PKEY *key, + X509_STORE *client_ca_store, + int &out_error) { + out_error = 0; + auto cert_pem = x509_to_pem(cert); + auto key_pem = evp_pkey_to_pem(key); + if (cert_pem.empty() || key_pem.empty()) { + out_error = static_cast(ERR_get_error()); + return nullptr; + } + + auto ctx = tls_create_server_context(); + if (!ctx) { + out_error = static_cast(tls_get_error()); + return nullptr; + } + + if (!tls_set_server_cert_pem(ctx, cert_pem.c_str(), key_pem.c_str(), + nullptr)) { + out_error = static_cast(tls_get_error()); + tls_free_context(ctx); + return nullptr; + } + + if (client_ca_store) { + // Set cert store for verification (SSL_CTX_set_cert_store takes ownership) + SSL_CTX_set_cert_store(static_cast(ctx), client_ca_store); + + // Extract and set client CA list directly from store (more efficient than + // PEM conversion) + auto ca_list = extract_client_ca_list_from_store(client_ca_store); + if (ca_list) { + SSL_CTX_set_client_CA_list(static_cast(ctx), ca_list); + } + + tls_set_verify_client(ctx, true); + } + + return ctx; +} + +inline void update_server_certs_from_x509(tls_ctx_t ctx, X509 *cert, + EVP_PKEY *key, + X509_STORE *client_ca_store) { + auto cert_pem = x509_to_pem(cert); + auto key_pem = evp_pkey_to_pem(key); + + if (!cert_pem.empty() && !key_pem.empty()) { + tls_update_server_cert(ctx, cert_pem.c_str(), key_pem.c_str(), nullptr); + } + + if (client_ca_store) { + auto ca_pem = x509_store_to_pem(client_ca_store); + if (!ca_pem.empty()) { tls_update_server_client_ca(ctx, ca_pem.c_str()); } + X509_STORE_free(client_ca_store); + } +} + +inline tls_ctx_t create_client_context_from_x509(X509 *cert, EVP_PKEY *key, + const char *password, + unsigned long &out_error) { + out_error = 0; + auto ctx = tls_create_client_context(); + if (!ctx) { + out_error = static_cast(tls_get_error()); + return nullptr; + } + + if (cert && key) { + auto cert_pem = x509_to_pem(cert); + auto key_pem = evp_pkey_to_pem(key); + if (cert_pem.empty() || key_pem.empty()) { + out_error = ERR_get_error(); + tls_free_context(ctx); + return nullptr; + } + if (!tls_set_client_cert_pem(ctx, cert_pem.c_str(), key_pem.c_str(), + password)) { + out_error = static_cast(tls_get_error()); + tls_free_context(ctx); + return nullptr; + } + } + + return ctx; +} + +} // namespace tls + +// ClientImpl::set_ca_cert_store - defined here to use tls::x509_store_to_pem +// Deprecated: converts X509_STORE to PEM and stores for redirect transfer +inline void ClientImpl::set_ca_cert_store(X509_STORE *ca_cert_store) { + if (ca_cert_store) { ca_cert_pem_ = tls::x509_store_to_pem(ca_cert_store); } +} + +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; +} + +inline SSLServer::SSLServer(X509 *cert, EVP_PKEY *private_key, + X509_STORE *client_ca_cert_store) { + ctx_ = tls::create_server_context_from_x509( + cert, private_key, client_ca_cert_store, last_ssl_error_); +} + +inline SSLServer::SSLServer( + const std::function &setup_ssl_ctx_callback) { + // Use abstract API to create context + ctx_ = tls::tls_create_server_context(); + if (ctx_) { + // Pass to OpenSSL-specific callback (ctx_ is SSL_CTX* internally) + auto ssl_ctx = static_cast(ctx_); + if (!setup_ssl_ctx_callback(*ssl_ctx)) { + tls::tls_free_context(ctx_); + ctx_ = nullptr; + } + } +} + +inline SSL_CTX *SSLServer::ssl_context() const { + return static_cast(ctx_); +} + +inline void SSLServer::update_certs(X509 *cert, EVP_PKEY *private_key, + X509_STORE *client_ca_cert_store) { + std::lock_guard guard(ctx_mutex_); + tls::update_server_certs_from_x509(ctx_, cert, private_key, + client_ca_cert_store); +} + // SSL HTTP client implementation inline SSLClient::SSLClient(const std::string &host) : SSLClient(host, 443, std::string(), std::string()) {} @@ -12882,9 +13948,10 @@ inline SSLClient::SSLClient(const std::string &host, int port, const std::string &client_key_path, const std::string &private_key_password) : ClientImpl(host, port, client_cert_path, client_key_path) { - ctx_ = SSL_CTX_new(TLS_client_method()); + ctx_ = tls::tls_create_client_context(); - SSL_CTX_set_min_proto_version(ctx_, TLS1_2_VERSION); + // TODO: Add tls_set_min_protocol_version() to TLS abstraction API + // SSL_CTX_set_min_proto_version(ctx_, TLS1_2_VERSION); detail::split(&host_[0], &host_[host_.size()], '.', [&](const char *b, const char *e) { @@ -12892,18 +13959,12 @@ inline SSLClient::SSLClient(const std::string &host, int port, }); if (!client_cert_path.empty() && !client_key_path.empty()) { - if (!private_key_password.empty()) { - SSL_CTX_set_default_passwd_cb_userdata( - ctx_, reinterpret_cast( - const_cast(private_key_password.c_str()))); - } - - 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) { - last_openssl_error_ = ERR_get_error(); - SSL_CTX_free(ctx_); + const char *password = + private_key_password.empty() ? nullptr : private_key_password.c_str(); + if (!tls::tls_set_client_cert_file(ctx_, client_cert_path.c_str(), + client_key_path.c_str(), password)) { + last_backend_error_ = ERR_get_error(); + tls::tls_free_context(ctx_); ctx_ = nullptr; } } @@ -12913,167 +13974,69 @@ inline SSLClient::SSLClient(const std::string &host, int port, X509 *client_cert, EVP_PKEY *client_key, const std::string &private_key_password) : ClientImpl(host, port) { - ctx_ = SSL_CTX_new(TLS_client_method()); + const char *password = + private_key_password.empty() ? nullptr : private_key_password.c_str(); + ctx_ = tls::create_client_context_from_x509(client_cert, client_key, password, + last_backend_error_); + detail::split(&host_[0], &host_[host_.size()], '.', + [&](const char *b, const char *e) { + host_components_.emplace_back(b, e); + }); +} + +inline SSLClient::SSLClient(const std::string &host, int port, + const PemMemory &pem) + : ClientImpl(host, port) { + ctx_ = tls::tls_create_client_context(); 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 (!private_key_password.empty()) { - SSL_CTX_set_default_passwd_cb_userdata( - ctx_, reinterpret_cast( - const_cast(private_key_password.c_str()))); - } - - if (SSL_CTX_use_certificate(ctx_, client_cert) != 1 || - SSL_CTX_use_PrivateKey(ctx_, client_key) != 1) { - last_openssl_error_ = ERR_get_error(); - SSL_CTX_free(ctx_); + if (ctx_ && pem.cert_pem && pem.key_pem) { + if (!tls::tls_set_client_cert_pem(ctx_, pem.cert_pem, pem.key_pem, + pem.private_key_password)) { + last_backend_error_ = tls::tls_get_error(); + tls::tls_free_context(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); - ca_cert_store_ = ca_cert_store; - } - } else { - X509_STORE_free(ca_cert_store); - } +inline void SSLClient::set_ca_cert_store(tls::tls_ca_store_t ca_cert_store) { + if (ca_cert_store && ctx_) { + // tls_set_ca_store takes ownership of ca_cert_store + tls::tls_set_ca_store(ctx_, ca_cert_store); + } else if (ca_cert_store) { + tls::tls_free_ca_store(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)); + ca_cert_pem_.assign(ca_cert, size); // Store for redirect transfer + set_ca_cert_store(tls::tls_create_ca_store(ca_cert, size)); +} + +inline void +SSLClient::set_server_certificate_verifier(tls::TlsVerifyCallback verifier) { + if (!ctx_) { return; } + tls::tls_set_verify_callback(ctx_, verifier); } 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) { - if (!is_valid()) { - error = Error::SSLConnection; - return false; - } - return ClientImpl::create_and_connect_socket(socket, error); +inline void SSLClient::set_server_certificate_verifier( + std::function verifier) { + // Store for use in initialize_ssl (legacy behavior: called after handshake) + legacy_server_cert_verifier_ = std::move(verifier); } -// Assumes that socket_mutex_ is locked and that there are no requests in -// flight -inline bool SSLClient::connect_with_proxy( - Socket &socket, - std::chrono::time_point start_time, - Response &res, bool &success, Error &error) { - success = true; - Response proxy_res; - if (!detail::process_client_socket( - socket.sock, read_timeout_sec_, read_timeout_usec_, - write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, - start_time, [&](Stream &strm) { - Request req2; - req2.method = "CONNECT"; - req2.path = - detail::make_host_and_port_string_always_port(host_, port_); - if (max_timeout_msec_ > 0) { - req2.start_time_ = std::chrono::steady_clock::now(); - } - return process_request(strm, req2, proxy_res, false, error); - })) { - // Thread-safe to close everything because we are assuming there are no - // requests in flight - shutdown_ssl(socket, true); - shutdown_socket(socket); - close_socket(socket); - success = false; - return false; - } - - if (proxy_res.status == StatusCode::ProxyAuthenticationRequired_407) { - if (!proxy_digest_auth_username_.empty() && - !proxy_digest_auth_password_.empty()) { - std::map auth; - if (detail::parse_www_authenticate(proxy_res, auth, true)) { - // Close the current socket and create a new one for the authenticated - // request - shutdown_ssl(socket, true); - shutdown_socket(socket); - close_socket(socket); - - // Create a new socket for the authenticated CONNECT request - if (!ensure_socket_connection(socket, error)) { - success = false; - output_error_log(error, nullptr); - return false; - } - - proxy_res = Response(); - if (!detail::process_client_socket( - socket.sock, read_timeout_sec_, read_timeout_usec_, - write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, - start_time, [&](Stream &strm) { - Request req3; - req3.method = "CONNECT"; - req3.path = detail::make_host_and_port_string_always_port( - host_, port_); - req3.headers.insert(detail::make_digest_authentication_header( - req3, auth, 1, detail::random_string(10), - proxy_digest_auth_username_, proxy_digest_auth_password_, - true)); - if (max_timeout_msec_ > 0) { - req3.start_time_ = std::chrono::steady_clock::now(); - } - return process_request(strm, req3, proxy_res, false, error); - })) { - // Thread-safe to close everything because we are assuming there are - // no requests in flight - shutdown_ssl(socket, true); - shutdown_socket(socket); - close_socket(socket); - success = false; - return false; - } - } - } - } - - // If status code is not 200, proxy request is failed. - // Set error to ProxyConnection and return proxy response - // as the response of the request - if (proxy_res.status != StatusCode::OK_200) { - error = Error::ProxyConnection; - output_error_log(error, nullptr); - res = std::move(proxy_res); - // Thread-safe to close everything because we are assuming there are - // no requests in flight - shutdown_ssl(socket, true); - shutdown_socket(socket); - close_socket(socket); - return false; - } - - return true; +inline SSL_CTX *SSLClient::ssl_context() const { + return static_cast(ctx_); } inline bool SSLClient::load_certs() { @@ -13081,27 +14044,23 @@ inline bool SSLClient::load_certs() { 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)) { - last_openssl_error_ = ERR_get_error(); + if (!tls::tls_load_ca_file(ctx_, ca_cert_file_path_.c_str())) { + last_backend_error_ = ERR_get_error(); ret = false; } } else if (!ca_cert_dir_path_.empty()) { - if (!SSL_CTX_load_verify_locations(ctx_, nullptr, - ca_cert_dir_path_.c_str())) { - last_openssl_error_ = ERR_get_error(); + if (!tls::tls_load_ca_dir(ctx_, ca_cert_dir_path_.c_str())) { + last_backend_error_ = ERR_get_error(); 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) && TARGET_OS_MAC - loaded = detail::load_system_certs_on_macos(SSL_CTX_get_cert_store(ctx_)); -#endif // _WIN32 - if (!loaded) { SSL_CTX_set_default_verify_paths(ctx_); } + // Load system certificates + if (!tls::tls_load_system_certs(ctx_)) { + last_backend_error_ = ERR_get_error(); + // Ignore error and continue - some systems may not have certs + } } }); @@ -13109,135 +14068,115 @@ inline bool SSLClient::load_certs() { } 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; - output_error_log(error, nullptr); - return false; - } - SSL_set_verify(ssl2, SSL_VERIFY_NONE, nullptr); - } + using namespace tls; - if (!detail::ssl_connect_or_accept_nonblocking( - socket.sock, ssl2, SSL_connect, connection_timeout_sec_, - connection_timeout_usec_, &last_ssl_error_)) { - error = Error::SSLConnection; + // Load CA certificates if server verification is enabled + if (server_certificate_verification_) { + if (!load_certs()) { + error = Error::SSLLoadingCerts; + output_error_log(error, nullptr); + return false; + } + } + + // Create TLS session (uses ctx_ which has SSL_VERIFY_NONE by default) + tls_session_t session = nullptr; + { + std::lock_guard guard(ctx_mutex_); + session = tls_create_session(ctx_, socket.sock); + } + + if (!session) { + error = Error::SSLConnection; + last_backend_error_ = ERR_get_error(); + return false; + } + + // Use scope_exit to ensure session is freed on error paths + bool success = false; + auto session_guard = detail::scope_exit([&] { + if (!success) { tls_free_session(session); } + }); + + // Set SNI before handshake (only if host is not IP address) + if (!detail::is_ip_address(host_)) { + if (!tls_set_sni(session, host_.c_str())) { + error = Error::SSLConnection; + last_backend_error_ = ERR_get_error(); + return false; + } + } + + // Perform non-blocking TLS handshake with timeout + TlsError tls_err; + if (!tls_connect_nonblocking(session, socket.sock, connection_timeout_sec_, + connection_timeout_usec_, &tls_err)) { + // Map TlsError to legacy ssl_error for backward compatibility + if (tls_err.code == ErrorCode::WantRead) { + last_ssl_error_ = SSL_ERROR_WANT_READ; + } else if (tls_err.code == ErrorCode::WantWrite) { + last_ssl_error_ = SSL_ERROR_WANT_WRITE; + } else { + last_ssl_error_ = SSL_ERROR_SSL; + } + error = Error::SSLConnection; + output_error_log(error, nullptr); + return false; + } + + // Server certificate verification + if (server_certificate_verification_) { + // Cast to SSL* for backward compatibility with verifier callback + auto ssl = static_cast(session); + + auto verification_status = SSLVerifierResponse::NoDecisionMade; + if (legacy_server_cert_verifier_) { + verification_status = legacy_server_cert_verifier_(ssl); + } + + if (verification_status == SSLVerifierResponse::CertificateRejected) { + last_backend_error_ = ERR_get_error(); + error = Error::SSLServerVerification; + output_error_log(error, nullptr); + return false; + } + + if (verification_status == SSLVerifierResponse::NoDecisionMade) { + verify_result_ = tls_get_verify_result(session); + + if (verify_result_ != X509_V_OK) { + last_backend_error_ = static_cast(verify_result_); + error = Error::SSLServerVerification; + output_error_log(error, nullptr); + return false; + } + + auto server_cert = tls_get_peer_cert(session); + if (!server_cert) { + last_backend_error_ = ERR_get_error(); + error = Error::SSLServerVerification; + output_error_log(error, nullptr); + return false; + } + auto cert_guard = detail::scope_exit([&] { tls_free_cert(server_cert); }); + + if (server_hostname_verification_) { + // verify_host() expects X509*, so cast from tls_cert_t + if (!verify_host(static_cast(server_cert))) { + last_backend_error_ = X509_V_ERR_HOSTNAME_MISMATCH; + error = Error::SSLServerHostnameVerification; output_error_log(error, nullptr); return false; } - - if (server_certificate_verification_) { - auto verification_status = SSLVerifierResponse::NoDecisionMade; - - if (server_certificate_verifier_) { - verification_status = server_certificate_verifier_(ssl2); - } - - if (verification_status == SSLVerifierResponse::CertificateRejected) { - last_openssl_error_ = ERR_get_error(); - error = Error::SSLServerVerification; - output_error_log(error, nullptr); - return false; - } - - if (verification_status == SSLVerifierResponse::NoDecisionMade) { - verify_result_ = SSL_get_verify_result(ssl2); - - if (verify_result_ != X509_V_OK) { - last_openssl_error_ = static_cast(verify_result_); - error = Error::SSLServerVerification; - output_error_log(error, nullptr); - return false; - } - - auto server_cert = SSL_get1_peer_certificate(ssl2); - auto se = detail::scope_exit([&] { X509_free(server_cert); }); - - if (server_cert == nullptr) { - last_openssl_error_ = ERR_get_error(); - error = Error::SSLServerVerification; - output_error_log(error, nullptr); - return false; - } - - if (server_hostname_verification_) { - if (!verify_host(server_cert)) { - last_openssl_error_ = X509_V_ERR_HOSTNAME_MISMATCH; - error = Error::SSLServerHostnameVerification; - output_error_log(error, nullptr); - return false; - } - } - } - } - - return true; - }, - [&](SSL *ssl2) { - // Set SNI only if host is not IP address - if (!detail::is_ip_address(host_)) { -#if defined(OPENSSL_IS_BORINGSSL) - SSL_set_tlsext_host_name(ssl2, host_.c_str()); -#else - // NOTE: Direct call instead of using the OpenSSL macro to suppress - // -Wold-style-cast warning - SSL_ctrl(ssl2, SSL_CTRL_SET_TLSEXT_HOSTNAME, - TLSEXT_NAMETYPE_host_name, - static_cast(const_cast(host_.c_str()))); -#endif - } - return true; - }); - - if (ssl) { - socket.ssl = ssl; - return true; + } + } } - if (ctx_ == nullptr) { - error = Error::SSLConnection; - last_openssl_error_ = ERR_get_error(); - } - - shutdown_socket(socket); - close_socket(socket); - return false; + success = true; + socket.ssl = session; + return true; } -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, socket.sock, - shutdown_gracefully); - socket.ssl = nullptr; - } - assert(socket.ssl == nullptr); -} - -inline bool SSLClient::process_socket( - const Socket &socket, - std::chrono::time_point start_time, - std::function callback) { - assert(socket.ssl); - return detail::process_client_socket_ssl( - socket.ssl, socket.sock, read_timeout_sec_, read_timeout_usec_, - write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, start_time, - std::move(callback)); -} - -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" @@ -13338,34 +14277,2126 @@ inline bool SSLClient::verify_host_with_common_name(X509 *server_cert) const { 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; } +#endif // CPPHTTPLIB_OPENSSL_SUPPORT - // 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); - }); +/* + * Mbed TLS Backend Implementation + */ - if (host_components_.size() != pattern_components.size()) { return false; } +#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT +namespace crypto { - 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; } +inline size_t hash_size(HashAlgorithm algo) { + switch (algo) { + case HashAlgorithm::MD5: return 16; + case HashAlgorithm::SHA1: return 20; + case HashAlgorithm::SHA256: return 32; + case HashAlgorithm::SHA384: return 48; + case HashAlgorithm::SHA512: return 64; + default: return 0; + } +} + +inline bool hash_raw(HashAlgorithm algo, const void *data, size_t len, + std::vector &digest) { + size_t dsize = hash_size(algo); + if (dsize == 0) { return false; } + digest.resize(dsize); + + int ret = 0; + switch (algo) { + case HashAlgorithm::MD5: +#if MBEDTLS_VERSION_MAJOR >= 3 + ret = mbedtls_md5(static_cast(data), len, + digest.data()); +#else + ret = mbedtls_md5_ret(static_cast(data), len, + digest.data()); +#endif + break; + case HashAlgorithm::SHA1: +#if MBEDTLS_VERSION_MAJOR >= 3 + ret = mbedtls_sha1(static_cast(data), len, + digest.data()); +#else + ret = mbedtls_sha1_ret(static_cast(data), len, + digest.data()); +#endif + break; + case HashAlgorithm::SHA256: +#if MBEDTLS_VERSION_MAJOR >= 3 + ret = mbedtls_sha256(static_cast(data), len, + digest.data(), 0); +#else + ret = mbedtls_sha256_ret(static_cast(data), len, + digest.data(), 0); +#endif + break; + case HashAlgorithm::SHA384: +#if MBEDTLS_VERSION_MAJOR >= 3 + ret = mbedtls_sha512(static_cast(data), len, + digest.data(), 1); // is384 = 1 +#else + ret = mbedtls_sha512_ret(static_cast(data), len, + digest.data(), 1); // is384 = 1 +#endif + break; + case HashAlgorithm::SHA512: +#if MBEDTLS_VERSION_MAJOR >= 3 + ret = mbedtls_sha512(static_cast(data), len, + digest.data(), 0); +#else + ret = mbedtls_sha512_ret(static_cast(data), len, + digest.data(), 0); +#endif + break; + } + return ret == 0; +} + +inline std::string hash(HashAlgorithm algo, const void *data, size_t len) { + std::vector digest; + if (!hash_raw(algo, data, len, digest)) { return ""; } + + std::stringstream ss; + for (auto byte : digest) { + ss << std::hex << std::setw(2) << std::setfill('0') + << static_cast(byte); + } + return ss.str(); +} + +inline std::string hash(HashAlgorithm algo, const std::string &data) { + return hash(algo, data.c_str(), data.size()); +} + +} // namespace crypto + +namespace tls { + +// Mbed TLS session wrapper +struct MbedTlsSession { + mbedtls_ssl_context ssl; + socket_t sock = INVALID_SOCKET; + std::string hostname; // For client: set via tls_set_sni + std::string sni_hostname; // For server: received from client via SNI callback + + MbedTlsSession() { mbedtls_ssl_init(&ssl); } + + ~MbedTlsSession() { mbedtls_ssl_free(&ssl); } + + // Non-copyable + MbedTlsSession(const MbedTlsSession &) = delete; + MbedTlsSession &operator=(const MbedTlsSession &) = delete; +}; + +// Thread-local error code accessor for Mbed TLS (since it doesn't have an error +// queue) +inline int &mbedtls_last_error() { + static thread_local int err = 0; + return err; +} + +// Helper to map Mbed TLS error to ErrorCode +inline ErrorCode map_mbedtls_error(int ret, int &out_errno) { + if (ret == 0) { return ErrorCode::Success; } + if (ret == MBEDTLS_ERR_SSL_WANT_READ) { return ErrorCode::WantRead; } + if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) { return ErrorCode::WantWrite; } + if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) { + return ErrorCode::PeerClosed; + } + if (ret == MBEDTLS_ERR_NET_CONN_RESET || ret == MBEDTLS_ERR_NET_SEND_FAILED || + ret == MBEDTLS_ERR_NET_RECV_FAILED) { + out_errno = errno; + return ErrorCode::SyscallError; + } + if (ret == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) { + return ErrorCode::CertVerifyFailed; + } + return ErrorCode::Fatal; +} + +inline bool tls_global_init() { + // Mbed TLS doesn't require global initialization + return true; +} + +inline void tls_global_cleanup() { + // Mbed TLS doesn't require global cleanup +} + +// BIO-like send callback for Mbed TLS +inline int mbedtls_net_send_cb(void *ctx, const unsigned char *buf, + size_t len) { + auto sock = *static_cast(ctx); +#ifdef _WIN32 + auto ret = + send(sock, reinterpret_cast(buf), static_cast(len), 0); + if (ret == SOCKET_ERROR) { + int err = WSAGetLastError(); + if (err == WSAEWOULDBLOCK) { return MBEDTLS_ERR_SSL_WANT_WRITE; } + return MBEDTLS_ERR_NET_SEND_FAILED; + } +#else + auto ret = send(sock, buf, len, 0); + if (ret < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return MBEDTLS_ERR_SSL_WANT_WRITE; } - ++itr; + return MBEDTLS_ERR_NET_SEND_FAILED; + } +#endif + return static_cast(ret); +} + +// BIO-like recv callback for Mbed TLS +inline int mbedtls_net_recv_cb(void *ctx, unsigned char *buf, size_t len) { + auto sock = *static_cast(ctx); +#ifdef _WIN32 + auto ret = + recv(sock, reinterpret_cast(buf), static_cast(len), 0); + if (ret == SOCKET_ERROR) { + int err = WSAGetLastError(); + if (err == WSAEWOULDBLOCK) { return MBEDTLS_ERR_SSL_WANT_READ; } + return MBEDTLS_ERR_NET_RECV_FAILED; + } +#else + auto ret = recv(sock, buf, len, 0); + if (ret < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return MBEDTLS_ERR_SSL_WANT_READ; + } + return MBEDTLS_ERR_NET_RECV_FAILED; + } +#endif + if (ret == 0) { return MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY; } + return static_cast(ret); +} + +inline tls_ctx_t tls_create_client_context() { + auto ctx = new (std::nothrow) httplib::MbedTlsContext(); + if (!ctx) { return nullptr; } + + ctx->is_server = false; + + // Seed the random number generator + const char *pers = "httplib_client"; + int ret = mbedtls_ctr_drbg_seed( + &ctx->ctr_drbg, mbedtls_entropy_func, &ctx->entropy, + reinterpret_cast(pers), strlen(pers)); + if (ret != 0) { + mbedtls_last_error() = ret; + delete ctx; + return nullptr; + } + + // Set up SSL config for client + ret = mbedtls_ssl_config_defaults(&ctx->conf, MBEDTLS_SSL_IS_CLIENT, + MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT); + if (ret != 0) { + mbedtls_last_error() = ret; + delete ctx; + return nullptr; + } + + // Set random number generator + mbedtls_ssl_conf_rng(&ctx->conf, mbedtls_ctr_drbg_random, &ctx->ctr_drbg); + + // Default: verify peer certificate + mbedtls_ssl_conf_authmode(&ctx->conf, MBEDTLS_SSL_VERIFY_REQUIRED); + + // Set minimum TLS version to 1.2 +#if MBEDTLS_VERSION_MAJOR >= 3 + mbedtls_ssl_conf_min_tls_version(&ctx->conf, MBEDTLS_SSL_VERSION_TLS1_2); +#else + mbedtls_ssl_conf_min_version(&ctx->conf, MBEDTLS_SSL_MAJOR_VERSION_3, + MBEDTLS_SSL_MINOR_VERSION_3); +#endif + + return static_cast(ctx); +} + +// Forward declaration for SNI callback (defined later) +inline int mbedtls_sni_callback(void *p_ctx, mbedtls_ssl_context *ssl, + const unsigned char *name, size_t name_len); + +inline tls_ctx_t tls_create_server_context() { + auto ctx = new (std::nothrow) httplib::MbedTlsContext(); + if (!ctx) { return nullptr; } + + ctx->is_server = true; + + // Seed the random number generator + const char *pers = "httplib_server"; + int ret = mbedtls_ctr_drbg_seed( + &ctx->ctr_drbg, mbedtls_entropy_func, &ctx->entropy, + reinterpret_cast(pers), strlen(pers)); + if (ret != 0) { + mbedtls_last_error() = ret; + delete ctx; + return nullptr; + } + + // Set up SSL config for server + ret = mbedtls_ssl_config_defaults(&ctx->conf, MBEDTLS_SSL_IS_SERVER, + MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT); + if (ret != 0) { + mbedtls_last_error() = ret; + delete ctx; + return nullptr; + } + + // Set random number generator + mbedtls_ssl_conf_rng(&ctx->conf, mbedtls_ctr_drbg_random, &ctx->ctr_drbg); + + // Default: don't verify client + mbedtls_ssl_conf_authmode(&ctx->conf, MBEDTLS_SSL_VERIFY_NONE); + + // Set minimum TLS version to 1.2 +#if MBEDTLS_VERSION_MAJOR >= 3 + mbedtls_ssl_conf_min_tls_version(&ctx->conf, MBEDTLS_SSL_VERSION_TLS1_2); +#else + mbedtls_ssl_conf_min_version(&ctx->conf, MBEDTLS_SSL_MAJOR_VERSION_3, + MBEDTLS_SSL_MINOR_VERSION_3); +#endif + + // Set SNI callback to capture client's SNI hostname + mbedtls_ssl_conf_sni(&ctx->conf, mbedtls_sni_callback, nullptr); + + return static_cast(ctx); +} + +inline void tls_free_context(tls_ctx_t ctx) { + if (ctx) { delete static_cast(ctx); } +} + +inline bool tls_set_min_version(tls_ctx_t ctx, int version) { + if (!ctx) { return false; } + auto mctx = static_cast(ctx); + + // Map OpenSSL-style version constants to Mbed TLS + // TLS1_2_VERSION = 0x0303, TLS1_3_VERSION = 0x0304 +#if MBEDTLS_VERSION_MAJOR >= 3 + // Mbed TLS 3.x uses mbedtls_ssl_protocol_version enum + mbedtls_ssl_protocol_version min_ver = MBEDTLS_SSL_VERSION_TLS1_2; + if (version >= 0x0304) { +#if defined(MBEDTLS_SSL_PROTO_TLS1_3) + min_ver = MBEDTLS_SSL_VERSION_TLS1_3; +#endif + } + mbedtls_ssl_conf_min_tls_version(&mctx->conf, min_ver); +#else + // Mbed TLS 2.x uses major/minor version numbers + int major = MBEDTLS_SSL_MAJOR_VERSION_3; + int minor = MBEDTLS_SSL_MINOR_VERSION_3; // TLS 1.2 + if (version >= 0x0304) { +#if defined(MBEDTLS_SSL_PROTO_TLS1_3) + minor = MBEDTLS_SSL_MINOR_VERSION_4; // TLS 1.3 +#else + minor = MBEDTLS_SSL_MINOR_VERSION_3; // Fall back to TLS 1.2 +#endif + } + mbedtls_ssl_conf_min_version(&mctx->conf, major, minor); +#endif + return true; +} + +inline bool tls_load_ca_pem(tls_ctx_t ctx, const char *pem, size_t len) { + if (!ctx || !pem) { return false; } + auto mctx = static_cast(ctx); + + // mbedtls_x509_crt_parse expects null-terminated string for PEM + // Add null terminator if not present + std::string pem_str(pem, len); + int ret = mbedtls_x509_crt_parse( + &mctx->ca_chain, reinterpret_cast(pem_str.c_str()), + pem_str.size() + 1); + if (ret != 0) { + mbedtls_last_error() = ret; + return false; + } + + mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr); + return true; +} + +inline bool tls_load_ca_file(tls_ctx_t ctx, const char *file_path) { + if (!ctx || !file_path) { return false; } + auto mctx = static_cast(ctx); + + int ret = mbedtls_x509_crt_parse_file(&mctx->ca_chain, file_path); + if (ret != 0) { + mbedtls_last_error() = ret; + return false; + } + + mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr); + return true; +} + +inline bool tls_load_ca_dir(tls_ctx_t ctx, const char *dir_path) { + if (!ctx || !dir_path) { return false; } + auto mctx = static_cast(ctx); + + int ret = mbedtls_x509_crt_parse_path(&mctx->ca_chain, dir_path); + if (ret < 0) { // Returns number of certs on success, negative on error + mbedtls_last_error() = ret; + return false; + } + + mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr); + return true; +} + +inline bool tls_load_system_certs(tls_ctx_t ctx) { + if (!ctx) { return false; } + auto mctx = static_cast(ctx); + bool loaded = false; + +#ifdef _WIN32 + // Load from Windows certificate store + HCERTSTORE hStore = CertOpenSystemStoreW(0, L"ROOT"); + if (hStore) { + PCCERT_CONTEXT pContext = nullptr; + while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) != + nullptr) { + int ret = mbedtls_x509_crt_parse_der( + &mctx->ca_chain, pContext->pbCertEncoded, pContext->cbCertEncoded); + if (ret == 0) { loaded = true; } + } + CertCloseStore(hStore, 0); + } +#elif defined(__APPLE__) && defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) + // Load from macOS Keychain + CFArrayRef certs = nullptr; + OSStatus status = SecTrustCopyAnchorCertificates(&certs); + if (status == errSecSuccess && certs) { + CFIndex count = CFArrayGetCount(certs); + for (CFIndex i = 0; i < count; i++) { + SecCertificateRef cert = + (SecCertificateRef)CFArrayGetValueAtIndex(certs, i); + CFDataRef data = SecCertificateCopyData(cert); + if (data) { + int ret = mbedtls_x509_crt_parse_der( + &mctx->ca_chain, CFDataGetBytePtr(data), + static_cast(CFDataGetLength(data))); + if (ret == 0) { loaded = true; } + CFRelease(data); + } + } + CFRelease(certs); + } +#else + // Try common CA certificate locations on Linux/Unix + static const char *ca_paths[] = { + "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu + "/etc/pki/tls/certs/ca-bundle.crt", // RHEL/CentOS + "/etc/ssl/ca-bundle.pem", // OpenSUSE + "/etc/pki/tls/cacert.pem", // OpenELEC + "/etc/ssl/cert.pem", // Alpine, FreeBSD + nullptr}; + + for (const char **path = ca_paths; *path; ++path) { + int ret = mbedtls_x509_crt_parse_file(&mctx->ca_chain, *path); + if (ret >= 0) { + loaded = true; + break; + } + } + + // Also try the CA directory + if (!loaded) { + static const char *ca_dirs[] = {"/etc/ssl/certs", // Debian/Ubuntu + "/etc/pki/tls/certs", // RHEL/CentOS + "/usr/share/ca-certificates", nullptr}; + + for (const char **dir = ca_dirs; *dir; ++dir) { + int ret = mbedtls_x509_crt_parse_path(&mctx->ca_chain, *dir); + if (ret >= 0) { + loaded = true; + break; + } + } + } +#endif + + if (loaded) { + mbedtls_ssl_conf_ca_chain(&mctx->conf, &mctx->ca_chain, nullptr); + } + return loaded; +} + +inline bool tls_set_client_cert_pem(tls_ctx_t ctx, const char *cert, + const char *key, const char *password) { + if (!ctx || !cert || !key) { return false; } + auto mctx = static_cast(ctx); + + // Parse certificate + std::string cert_str(cert); + int ret = mbedtls_x509_crt_parse( + &mctx->own_cert, + reinterpret_cast(cert_str.c_str()), + cert_str.size() + 1); + if (ret != 0) { + mbedtls_last_error() = ret; + return false; + } + + // Parse private key + std::string key_str(key); + const unsigned char *pwd = + password ? reinterpret_cast(password) : nullptr; + size_t pwd_len = password ? strlen(password) : 0; + +#if MBEDTLS_VERSION_MAJOR >= 3 + ret = mbedtls_pk_parse_key( + &mctx->own_key, reinterpret_cast(key_str.c_str()), + key_str.size() + 1, pwd, pwd_len, mbedtls_ctr_drbg_random, + &mctx->ctr_drbg); +#else + ret = mbedtls_pk_parse_key( + &mctx->own_key, reinterpret_cast(key_str.c_str()), + key_str.size() + 1, pwd, pwd_len); +#endif + if (ret != 0) { + mbedtls_last_error() = ret; + return false; + } + + ret = mbedtls_ssl_conf_own_cert(&mctx->conf, &mctx->own_cert, &mctx->own_key); + if (ret != 0) { + mbedtls_last_error() = ret; + return false; } return true; } + +inline bool tls_set_client_cert_file(tls_ctx_t ctx, const char *cert_path, + const char *key_path, + const char *password) { + if (!ctx || !cert_path || !key_path) { return false; } + auto mctx = static_cast(ctx); + + // Parse certificate file + int ret = mbedtls_x509_crt_parse_file(&mctx->own_cert, cert_path); + if (ret != 0) { + mbedtls_last_error() = ret; + return false; + } + + // Parse private key file +#if MBEDTLS_VERSION_MAJOR >= 3 + ret = mbedtls_pk_parse_keyfile(&mctx->own_key, key_path, password, + mbedtls_ctr_drbg_random, &mctx->ctr_drbg); +#else + ret = mbedtls_pk_parse_keyfile(&mctx->own_key, key_path, password); #endif + if (ret != 0) { + mbedtls_last_error() = ret; + return false; + } + + ret = mbedtls_ssl_conf_own_cert(&mctx->conf, &mctx->own_cert, &mctx->own_key); + if (ret != 0) { + mbedtls_last_error() = ret; + return false; + } + + return true; +} + +inline bool tls_set_server_cert_pem(tls_ctx_t ctx, const char *cert, + const char *key, const char *password) { + // Same as client cert for Mbed TLS + return tls_set_client_cert_pem(ctx, cert, key, password); +} + +inline bool tls_set_server_cert_file(tls_ctx_t ctx, const char *cert_path, + const char *key_path, + const char *password) { + // Same as client cert for Mbed TLS + return tls_set_client_cert_file(ctx, cert_path, key_path, password); +} + +inline bool tls_set_client_ca_file(tls_ctx_t ctx, const char *ca_file, + const char *ca_dir) { + if (!ctx) { return false; } + + bool success = true; + if (ca_file && *ca_file) { + if (!tls_load_ca_file(ctx, ca_file)) { success = false; } + } + if (ca_dir && *ca_dir) { + if (!tls_load_ca_dir(ctx, ca_dir)) { success = false; } + } + + return success; +} + +inline void tls_set_verify_client(tls_ctx_t ctx, bool require) { + if (!ctx) { return; } + auto mctx = static_cast(ctx); + mctx->verify_client = require; + if (require) { + mbedtls_ssl_conf_authmode(&mctx->conf, MBEDTLS_SSL_VERIFY_REQUIRED); + } else { + // If a verify callback is set, use OPTIONAL mode to ensure the callback + // is called (matching OpenSSL behavior). Otherwise use NONE. + mbedtls_ssl_conf_authmode(&mctx->conf, mctx->has_verify_callback + ? MBEDTLS_SSL_VERIFY_OPTIONAL + : MBEDTLS_SSL_VERIFY_NONE); + } +} + +// Thread-local storage for SNI captured during handshake +// This is needed because the SNI callback doesn't have a way to pass +// session-specific data before the session is fully set up +inline std::string &mbedtls_pending_sni() { + static thread_local std::string sni; + return sni; +} + +// SNI callback for Mbed TLS server to capture client's SNI hostname +inline int mbedtls_sni_callback(void *p_ctx, mbedtls_ssl_context *ssl, + const unsigned char *name, size_t name_len) { + (void)p_ctx; + (void)ssl; + + // Store SNI name in thread-local storage + // It will be retrieved and stored in the session after handshake + if (name && name_len > 0) { + mbedtls_pending_sni().assign(reinterpret_cast(name), + name_len); + } else { + mbedtls_pending_sni().clear(); + } + return 0; // Accept any SNI +} + +inline int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt, + int cert_depth, uint32_t *flags); + +inline tls_session_t tls_create_session(tls_ctx_t ctx, socket_t sock) { + if (!ctx || sock == INVALID_SOCKET) { return nullptr; } + auto mctx = static_cast(ctx); + + auto session = new (std::nothrow) MbedTlsSession(); + if (!session) { return nullptr; } + + session->sock = sock; + + int ret = mbedtls_ssl_setup(&session->ssl, &mctx->conf); + if (ret != 0) { + mbedtls_last_error() = ret; + delete session; + return nullptr; + } + + // Set BIO callbacks + mbedtls_ssl_set_bio(&session->ssl, &session->sock, mbedtls_net_send_cb, + mbedtls_net_recv_cb, nullptr); + + // Set per-session verify callback with session pointer if callback is + // registered + if (mctx->has_verify_callback) { + mbedtls_ssl_set_verify(&session->ssl, mbedtls_verify_callback, session); + } + + return static_cast(session); +} + +inline void tls_free_session(tls_session_t session) { + if (session) { delete static_cast(session); } +} + +inline bool tls_set_sni(tls_session_t session, const char *hostname) { + if (!session || !hostname) { return false; } + auto msession = static_cast(session); + + int ret = mbedtls_ssl_set_hostname(&msession->ssl, hostname); + if (ret != 0) { + mbedtls_last_error() = ret; + return false; + } + + msession->hostname = hostname; + return true; +} + +inline bool tls_set_hostname(tls_session_t session, const char *hostname) { + // In Mbed TLS, set_hostname also sets up hostname verification + return tls_set_sni(session, hostname); +} + +inline TlsError tls_connect(tls_session_t session) { + TlsError err; + if (!session) { + err.code = ErrorCode::Fatal; + return err; + } + + auto msession = static_cast(session); + int ret = mbedtls_ssl_handshake(&msession->ssl); + + if (ret == 0) { + err.code = ErrorCode::Success; + } else { + err.code = map_mbedtls_error(ret, err.sys_errno); + err.backend_code = static_cast(-ret); + mbedtls_last_error() = ret; + } + + return err; +} + +inline TlsError tls_accept(tls_session_t session) { + // Same as connect for Mbed TLS - handshake works for both client and server + auto result = tls_connect(session); + + // After successful handshake, capture SNI from thread-local storage + if (result.code == ErrorCode::Success && session) { + auto msession = static_cast(session); + msession->sni_hostname = std::move(mbedtls_pending_sni()); + mbedtls_pending_sni().clear(); + } + + return result; +} + +inline bool tls_connect_nonblocking(tls_session_t session, socket_t sock, + time_t timeout_sec, time_t timeout_usec, + TlsError *err) { + if (!session) { + if (err) { err->code = ErrorCode::Fatal; } + return false; + } + + auto msession = static_cast(session); + + // Set socket to non-blocking mode + detail::set_nonblocking(sock, true); + auto cleanup = + detail::scope_exit([&]() { detail::set_nonblocking(sock, false); }); + + int ret; + while ((ret = mbedtls_ssl_handshake(&msession->ssl)) != 0) { + if (ret == MBEDTLS_ERR_SSL_WANT_READ) { + if (detail::select_read(sock, timeout_sec, timeout_usec) > 0) { + continue; + } + } else if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) { + if (detail::select_write(sock, timeout_sec, timeout_usec) > 0) { + continue; + } + } + + // Error or timeout + if (err) { + err->code = map_mbedtls_error(ret, err->sys_errno); + err->backend_code = static_cast(-ret); + } + mbedtls_last_error() = ret; + return false; + } + + if (err) { err->code = ErrorCode::Success; } + return true; +} + +inline bool tls_accept_nonblocking(tls_session_t session, socket_t sock, + time_t timeout_sec, time_t timeout_usec, + TlsError *err) { + // Same implementation as connect for Mbed TLS + bool result = + tls_connect_nonblocking(session, sock, timeout_sec, timeout_usec, err); + + // After successful handshake, capture SNI from thread-local storage + if (result && session) { + auto msession = static_cast(session); + msession->sni_hostname = std::move(mbedtls_pending_sni()); + mbedtls_pending_sni().clear(); + } + + return result; +} + +inline ssize_t tls_read(tls_session_t session, void *buf, size_t len, + TlsError &err) { + if (!session || !buf) { + err.code = ErrorCode::Fatal; + return -1; + } + + auto msession = static_cast(session); + int ret = + mbedtls_ssl_read(&msession->ssl, static_cast(buf), len); + + if (ret > 0) { + err.code = ErrorCode::Success; + return static_cast(ret); + } + + if (ret == 0) { + err.code = ErrorCode::PeerClosed; + return 0; + } + + err.code = map_mbedtls_error(ret, err.sys_errno); + err.backend_code = static_cast(-ret); + mbedtls_last_error() = ret; + return -1; +} + +inline ssize_t tls_write(tls_session_t session, const void *buf, size_t len, + TlsError &err) { + if (!session || !buf) { + err.code = ErrorCode::Fatal; + return -1; + } + + auto msession = static_cast(session); + int ret = mbedtls_ssl_write(&msession->ssl, + static_cast(buf), len); + + if (ret > 0) { + err.code = ErrorCode::Success; + return static_cast(ret); + } + + if (ret == 0) { + err.code = ErrorCode::PeerClosed; + return 0; + } + + err.code = map_mbedtls_error(ret, err.sys_errno); + err.backend_code = static_cast(-ret); + mbedtls_last_error() = ret; + return -1; +} + +inline int tls_pending(const_tls_session_t session) { + if (!session) { return 0; } + auto msession = static_cast(const_cast(session)); + return static_cast(mbedtls_ssl_get_bytes_avail(&msession->ssl)); +} + +inline void tls_shutdown(tls_session_t session, bool graceful) { + if (!session) { return; } + auto msession = static_cast(session); + + if (graceful) { + // Try to send close_notify, but don't block forever + int ret; + int attempts = 0; + while ((ret = mbedtls_ssl_close_notify(&msession->ssl)) != 0 && + attempts < 3) { + if (ret != MBEDTLS_ERR_SSL_WANT_READ && + ret != MBEDTLS_ERR_SSL_WANT_WRITE) { + break; + } + attempts++; + } + } +} + +inline bool tls_is_peer_closed(tls_session_t session, socket_t sock) { + if (!session || sock == INVALID_SOCKET) { return true; } + auto msession = static_cast(session); + + // Check if there's already decrypted data available in the TLS buffer + // If so, the connection is definitely alive + if (mbedtls_ssl_get_bytes_avail(&msession->ssl) > 0) { return false; } + + // Set socket to non-blocking to avoid blocking on read + detail::set_nonblocking(sock, true); + auto cleanup = + detail::scope_exit([&]() { detail::set_nonblocking(sock, false); }); + + // Try a 1-byte read to check connection status + // Note: This will consume the byte if data is available, but for the + // purpose of checking if peer is closed, this should be acceptable + // since we're only called when we expect the connection might be closing + unsigned char buf; + int ret = mbedtls_ssl_read(&msession->ssl, &buf, 1); + + // If we got data or WANT_READ (would block), connection is alive + if (ret > 0 || ret == MBEDTLS_ERR_SSL_WANT_READ) { return false; } + + // If we get a peer close notify or a connection reset, the peer is closed + return ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY || + ret == MBEDTLS_ERR_NET_CONN_RESET || ret == 0; +} + +inline tls_cert_t tls_get_peer_cert(const_tls_session_t session) { + if (!session) { return nullptr; } + auto msession = static_cast(const_cast(session)); + + // Mbed TLS returns a pointer to the internal peer cert chain. + // WARNING: This pointer is only valid while the session is active. + // Do not use the certificate after calling tls_free_session(). + const mbedtls_x509_crt *cert = mbedtls_ssl_get_peer_cert(&msession->ssl); + return const_cast(cert); +} + +inline void tls_free_cert(tls_cert_t cert) { + // Mbed TLS: peer certificate is owned by the SSL context. + // No-op here, but callers should still call this for cross-backend + // portability. + (void)cert; +} + +namespace { +// Helper function to match hostname with pattern (supports wildcards) +inline bool match_hostname(const std::string &pattern, + const std::string &hostname) { + if (pattern == hostname) { return true; } + + // Wildcard matching: *.example.com matches foo.example.com + if (pattern.size() > 2 && pattern[0] == '*' && pattern[1] == '.') { + std::string suffix = pattern.substr(1); // .example.com + size_t dot_pos = hostname.find('.'); + if (dot_pos != std::string::npos) { + std::string host_suffix = hostname.substr(dot_pos); + if (host_suffix == suffix) { return true; } + } + } + return false; +} +} // namespace + +// Check if a string is an IPv4 address +inline bool is_ipv4_address(const std::string &str) { + int dots = 0; + for (char c : str) { + if (c == '.') { + dots++; + } else if (!isdigit(static_cast(c))) { + return false; + } + } + return dots == 3; +} + +// Parse IPv4 address string to bytes +inline bool parse_ipv4(const std::string &str, unsigned char *out) { + int parts[4]; + if (sscanf(str.c_str(), "%d.%d.%d.%d", &parts[0], &parts[1], &parts[2], + &parts[3]) != 4) { + return false; + } + for (int i = 0; i < 4; i++) { + if (parts[i] < 0 || parts[i] > 255) return false; + out[i] = static_cast(parts[i]); + } + return true; +} + +inline bool tls_verify_hostname(tls_cert_t cert, const char *hostname) { + if (!cert || !hostname) { return false; } + auto mcert = static_cast(cert); + std::string host_str(hostname); + + // Check if hostname is an IP address + bool is_ip = is_ipv4_address(host_str); + unsigned char ip_bytes[4]; + if (is_ip) { parse_ipv4(host_str, ip_bytes); } + + // Check Subject Alternative Names (SAN) + // In Mbed TLS 3.x, subject_alt_names contains raw values without ASN.1 tags + // - DNS names: raw string bytes + // - IP addresses: raw IP bytes (4 for IPv4, 16 for IPv6) + const mbedtls_x509_sequence *san = &mcert->subject_alt_names; + while (san != nullptr && san->buf.p != nullptr && san->buf.len > 0) { + const unsigned char *p = san->buf.p; + size_t len = san->buf.len; + + if (is_ip) { + // Check if this SAN is an IPv4 address (4 bytes) + if (len == 4 && memcmp(p, ip_bytes, 4) == 0) { return true; } + // Check if this SAN is an IPv6 address (16 bytes) - skip for now + } else { + // Check if this SAN is a DNS name (printable ASCII string) + bool is_dns = len > 0; + for (size_t i = 0; i < len && is_dns; i++) { + if (p[i] < 32 || p[i] > 126) { is_dns = false; } + } + if (is_dns) { + std::string san_name(reinterpret_cast(p), len); + if (match_hostname(san_name, host_str)) { return true; } + } + } + san = san->next; + } + + // Fallback: Check Common Name (CN) in subject + char cn[256]; + int ret = mbedtls_x509_dn_gets(cn, sizeof(cn), &mcert->subject); + if (ret > 0) { + std::string cn_str(cn); + + // Look for "CN=" in the DN string + size_t cn_pos = cn_str.find("CN="); + if (cn_pos != std::string::npos) { + size_t start = cn_pos + 3; + size_t end = cn_str.find(',', start); + std::string cn_value = + cn_str.substr(start, end == std::string::npos ? end : end - start); + + if (match_hostname(cn_value, host_str)) { return true; } + } + } + + return false; +} + +inline long tls_get_verify_result(const_tls_session_t session) { + if (!session) { return -1; } + auto msession = static_cast(const_cast(session)); + uint32_t flags = mbedtls_ssl_get_verify_result(&msession->ssl); + // Return 0 (X509_V_OK equivalent) if verification passed + return flags == 0 ? 0 : static_cast(flags); +} + +inline std::string tls_get_cert_subject_cn(tls_cert_t cert) { + if (!cert) return ""; + auto x509 = static_cast(cert); + + // Find the CN in the subject + const mbedtls_x509_name *name = &x509->subject; + while (name != nullptr) { + if (MBEDTLS_OID_CMP(MBEDTLS_OID_AT_CN, &name->oid) == 0) { + return std::string(reinterpret_cast(name->val.p), + name->val.len); + } + name = name->next; + } + return ""; +} + +inline std::string tls_get_cert_issuer_name(tls_cert_t cert) { + if (!cert) return ""; + auto x509 = static_cast(cert); + + // Build a human-readable issuer name string + char buf[512]; + int ret = mbedtls_x509_dn_gets(buf, sizeof(buf), &x509->issuer); + if (ret < 0) return ""; + return std::string(buf); +} + +inline bool tls_get_cert_sans(tls_cert_t cert, std::vector &sans) { + sans.clear(); + if (!cert) return false; + auto x509 = static_cast(cert); + + // Parse the Subject Alternative Name extension + const mbedtls_x509_sequence *cur = &x509->subject_alt_names; + while (cur != nullptr) { + if (cur->buf.len > 0) { + // Mbed TLS stores SAN as ASN.1 sequences + // The tag byte indicates the type + const unsigned char *p = cur->buf.p; + size_t len = cur->buf.len; + + // First byte is the tag + unsigned char tag = *p; + p++; + len--; + + // Parse length (simple single-byte length assumed) + if (len > 0 && *p < 0x80) { + size_t value_len = *p; + p++; + len--; + + if (value_len <= len) { + TlsSanEntry entry; + // ASN.1 context tags for GeneralName + switch (tag & 0x1F) { + case 2: // dNSName + entry.type = SanType::DNS; + entry.value = + std::string(reinterpret_cast(p), value_len); + break; + case 7: // iPAddress + entry.type = SanType::IP; + if (value_len == 4) { + // IPv4 + char buf[16]; + snprintf(buf, sizeof(buf), "%d.%d.%d.%d", p[0], p[1], p[2], p[3]); + entry.value = buf; + } else if (value_len == 16) { + // IPv6 + char buf[64]; + snprintf(buf, sizeof(buf), + "%02x%02x:%02x%02x:%02x%02x:%02x%02x:" + "%02x%02x:%02x%02x:%02x%02x:%02x%02x", + p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8], + p[9], p[10], p[11], p[12], p[13], p[14], p[15]); + entry.value = buf; + } + break; + case 1: // rfc822Name (email) + entry.type = SanType::EMAIL; + entry.value = + std::string(reinterpret_cast(p), value_len); + break; + case 6: // uniformResourceIdentifier + entry.type = SanType::URI; + entry.value = + std::string(reinterpret_cast(p), value_len); + break; + default: entry.type = SanType::OTHER; break; + } + + if (!entry.value.empty()) { sans.push_back(std::move(entry)); } + } + } + } + cur = cur->next; + } + return true; +} + +inline bool tls_get_cert_validity(tls_cert_t cert, time_t ¬_before, + time_t ¬_after) { + if (!cert) return false; + auto x509 = static_cast(cert); + + // Convert mbedtls_x509_time to time_t + auto to_time_t = [](const mbedtls_x509_time &t) -> time_t { + struct tm tm_time = {}; + tm_time.tm_year = t.year - 1900; + tm_time.tm_mon = t.mon - 1; + tm_time.tm_mday = t.day; + tm_time.tm_hour = t.hour; + tm_time.tm_min = t.min; + tm_time.tm_sec = t.sec; +#ifdef _WIN32 + return _mkgmtime(&tm_time); +#else + return timegm(&tm_time); +#endif + }; + + not_before = to_time_t(x509->valid_from); + not_after = to_time_t(x509->valid_to); + return true; +} + +inline std::string tls_get_cert_serial(tls_cert_t cert) { + if (!cert) return ""; + auto x509 = static_cast(cert); + + // Convert serial number to hex string + std::string result; + result.reserve(x509->serial.len * 2); + for (size_t i = 0; i < x509->serial.len; i++) { + char hex[3]; + snprintf(hex, sizeof(hex), "%02X", x509->serial.p[i]); + result += hex; + } + return result; +} + +inline const char *tls_get_sni(const_tls_session_t session) { + if (!session) return nullptr; + auto msession = static_cast(session); + + // For server: return SNI received from client during handshake + if (!msession->sni_hostname.empty()) { + return msession->sni_hostname.c_str(); + } + + // For client: return the hostname set via tls_set_sni + if (!msession->hostname.empty()) { return msession->hostname.c_str(); } + + return nullptr; +} + +inline uint64_t tls_peek_error() { + // Mbed TLS doesn't have an error queue, return the last error + return static_cast(-mbedtls_last_error()); +} + +inline uint64_t tls_get_error() { + // Mbed TLS doesn't have an error queue, return and clear the last error + uint64_t err = static_cast(-mbedtls_last_error()); + mbedtls_last_error() = 0; + return err; +} + +inline std::string tls_error_string(uint64_t code) { + char buf[256]; + mbedtls_strerror(-static_cast(code), buf, sizeof(buf)); + return std::string(buf); +} + +inline tls_ca_store_t tls_create_ca_store(const char *pem, size_t len) { + auto *ca_chain = new (std::nothrow) mbedtls_x509_crt; + if (!ca_chain) { return nullptr; } + + mbedtls_x509_crt_init(ca_chain); + + // mbedtls_x509_crt_parse expects null-terminated PEM + int ret = mbedtls_x509_crt_parse(ca_chain, + reinterpret_cast(pem), + len + 1); // +1 for null terminator + if (ret != 0) { + // Try without +1 in case PEM is already null-terminated + ret = mbedtls_x509_crt_parse( + ca_chain, reinterpret_cast(pem), len); + if (ret != 0) { + mbedtls_x509_crt_free(ca_chain); + delete ca_chain; + return nullptr; + } + } + + return static_cast(ca_chain); +} + +inline void tls_free_ca_store(tls_ca_store_t store) { + if (store) { + auto *ca_chain = static_cast(store); + mbedtls_x509_crt_free(ca_chain); + delete ca_chain; + } +} + +inline bool tls_set_ca_store(tls_ctx_t ctx, tls_ca_store_t store) { + if (!ctx || !store) { return false; } + auto *mbed_ctx = static_cast(ctx); + auto *ca_chain = static_cast(store); + + // Free existing CA chain + mbedtls_x509_crt_free(&mbed_ctx->ca_chain); + mbedtls_x509_crt_init(&mbed_ctx->ca_chain); + + // Copy the CA chain (deep copy) + // Parse from the raw data of the source cert + mbedtls_x509_crt *src = ca_chain; + while (src != nullptr) { + int ret = mbedtls_x509_crt_parse_der(&mbed_ctx->ca_chain, src->raw.p, + src->raw.len); + if (ret != 0) { return false; } + src = src->next; + } + + // Update the SSL config to use the new CA chain + mbedtls_ssl_conf_ca_chain(&mbed_ctx->conf, &mbed_ctx->ca_chain, nullptr); + return true; +} + +inline size_t tls_get_ca_certs(tls_ctx_t ctx, std::vector &certs) { + certs.clear(); + if (!ctx) { return 0; } + auto *mbed_ctx = static_cast(ctx); + + // Iterate through the CA chain + mbedtls_x509_crt *cert = &mbed_ctx->ca_chain; + while (cert != nullptr && cert->raw.len > 0) { + // Create a copy of the certificate for the caller + auto *copy = new mbedtls_x509_crt; + mbedtls_x509_crt_init(copy); + int ret = mbedtls_x509_crt_parse_der(copy, cert->raw.p, cert->raw.len); + if (ret == 0) { + certs.push_back(static_cast(copy)); + } else { + mbedtls_x509_crt_free(copy); + delete copy; + } + cert = cert->next; + } + return certs.size(); +} + +inline std::vector tls_get_ca_names(tls_ctx_t ctx) { + std::vector names; + if (!ctx) { return names; } + auto *mbed_ctx = static_cast(ctx); + + // Iterate through the CA chain + mbedtls_x509_crt *cert = &mbed_ctx->ca_chain; + while (cert != nullptr && cert->raw.len > 0) { + char buf[512]; + int ret = mbedtls_x509_dn_gets(buf, sizeof(buf), &cert->subject); + if (ret > 0) { names.push_back(buf); } + cert = cert->next; + } + return names; +} + +inline bool tls_update_server_cert(tls_ctx_t ctx, const char *cert_pem, + const char *key_pem, const char *password) { + if (!ctx || !cert_pem || !key_pem) { return false; } + auto *mbed_ctx = static_cast(ctx); + + // Free existing certificate and key + mbedtls_x509_crt_free(&mbed_ctx->own_cert); + mbedtls_pk_free(&mbed_ctx->own_key); + mbedtls_x509_crt_init(&mbed_ctx->own_cert); + mbedtls_pk_init(&mbed_ctx->own_key); + + // Parse certificate PEM + int ret = mbedtls_x509_crt_parse( + &mbed_ctx->own_cert, reinterpret_cast(cert_pem), + strlen(cert_pem) + 1); + if (ret != 0) { + mbedtls_last_error() = ret; + return false; + } + + // Parse private key PEM +#if MBEDTLS_VERSION_MAJOR >= 3 + ret = mbedtls_pk_parse_key( + &mbed_ctx->own_key, reinterpret_cast(key_pem), + strlen(key_pem) + 1, + password ? reinterpret_cast(password) : nullptr, + password ? strlen(password) : 0, mbedtls_ctr_drbg_random, + &mbed_ctx->ctr_drbg); +#else + ret = mbedtls_pk_parse_key( + &mbed_ctx->own_key, reinterpret_cast(key_pem), + strlen(key_pem) + 1, + password ? reinterpret_cast(password) : nullptr, + password ? strlen(password) : 0); +#endif + if (ret != 0) { + mbedtls_last_error() = ret; + return false; + } + + // Configure SSL to use the new certificate and key + ret = mbedtls_ssl_conf_own_cert(&mbed_ctx->conf, &mbed_ctx->own_cert, + &mbed_ctx->own_key); + if (ret != 0) { + mbedtls_last_error() = ret; + return false; + } + + return true; +} + +inline bool tls_update_server_client_ca(tls_ctx_t ctx, const char *ca_pem) { + if (!ctx || !ca_pem) { return false; } + auto *mbed_ctx = static_cast(ctx); + + // Free existing CA chain + mbedtls_x509_crt_free(&mbed_ctx->ca_chain); + mbedtls_x509_crt_init(&mbed_ctx->ca_chain); + + // Parse CA PEM + int ret = mbedtls_x509_crt_parse( + &mbed_ctx->ca_chain, reinterpret_cast(ca_pem), + strlen(ca_pem) + 1); + if (ret != 0) { + mbedtls_last_error() = ret; + return false; + } + + // Update SSL config to use new CA chain + mbedtls_ssl_conf_ca_chain(&mbed_ctx->conf, &mbed_ctx->ca_chain, nullptr); + return true; +} + +// Thread-local storage for verify callback (MbedTLS) +inline TlsVerifyCallback &get_mbedtls_verify_callback() { + static thread_local TlsVerifyCallback callback; + return callback; +} + +// MbedTLS verify callback wrapper +inline int mbedtls_verify_callback(void *data, mbedtls_x509_crt *crt, + int cert_depth, uint32_t *flags) { + auto &callback = get_mbedtls_verify_callback(); + if (!callback) { return 0; } // Continue with default verification + + // data points to the MbedTlsSession + auto *session = static_cast(data); + + // Build context + TlsVerifyContext verify_ctx; + verify_ctx.session = static_cast(session); + verify_ctx.cert = static_cast(crt); + verify_ctx.depth = cert_depth; + verify_ctx.preverify_ok = (*flags == 0); + verify_ctx.error_code = static_cast(*flags); + + // Convert Mbed TLS flags to error string + static thread_local char error_buf[256]; + if (*flags != 0) { + mbedtls_x509_crt_verify_info(error_buf, sizeof(error_buf), "", *flags); + verify_ctx.error_string = error_buf; + } else { + verify_ctx.error_string = nullptr; + } + + bool accepted = callback(verify_ctx); + + if (accepted) { + *flags = 0; // Clear all error flags + return 0; + } + return MBEDTLS_ERR_X509_CERT_VERIFY_FAILED; +} + +inline bool tls_set_verify_callback(tls_ctx_t ctx, TlsVerifyCallback callback) { + if (!ctx) { return false; } + auto *mbed_ctx = static_cast(ctx); + + get_mbedtls_verify_callback() = std::move(callback); + mbed_ctx->has_verify_callback = + static_cast(get_mbedtls_verify_callback()); + + if (mbed_ctx->has_verify_callback) { + // Set OPTIONAL mode to ensure callback is called even when verification + // is disabled (matching OpenSSL behavior where SSL_VERIFY_PEER is set) + mbedtls_ssl_conf_authmode(&mbed_ctx->conf, MBEDTLS_SSL_VERIFY_OPTIONAL); + mbedtls_ssl_conf_verify(&mbed_ctx->conf, mbedtls_verify_callback, nullptr); + } else { + mbedtls_ssl_conf_verify(&mbed_ctx->conf, nullptr, nullptr); + } + return true; +} + +inline long tls_get_verify_error(const_tls_session_t session) { + if (!session) { return -1; } + auto *msession = static_cast(const_cast(session)); + return static_cast(mbedtls_ssl_get_verify_result(&msession->ssl)); +} + +inline std::string tls_verify_error_string(long error_code) { + if (error_code == 0) { return ""; } + char buf[256]; + mbedtls_x509_crt_verify_info(buf, sizeof(buf), "", + static_cast(error_code)); + // Remove trailing newline if present + std::string result(buf); + while (!result.empty() && (result.back() == '\n' || result.back() == ' ')) { + result.pop_back(); + } + return result; +} + +} // namespace tls + +// MbedTlsContext constructor/destructor implementations +inline MbedTlsContext::MbedTlsContext() { + mbedtls_ssl_config_init(&conf); + mbedtls_entropy_init(&entropy); + mbedtls_ctr_drbg_init(&ctr_drbg); + mbedtls_x509_crt_init(&ca_chain); + mbedtls_x509_crt_init(&own_cert); + mbedtls_pk_init(&own_key); +} + +inline MbedTlsContext::~MbedTlsContext() { + mbedtls_pk_free(&own_key); + mbedtls_x509_crt_free(&own_cert); + mbedtls_x509_crt_free(&ca_chain); + mbedtls_ctr_drbg_free(&ctr_drbg); + mbedtls_entropy_free(&entropy); + mbedtls_ssl_config_free(&conf); +} + +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, + const std::string &private_key_password) + : ClientImpl(host, port, client_cert_path, client_key_path) { + using namespace tls; + + ctx_ = tls_create_client_context(); + if (!ctx_) { return; } + + // Set minimum TLS version to 1.2 + tls_set_min_version(ctx_, 0x0303); // TLS 1.2 + + 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()) { + const char *password = + private_key_password.empty() ? nullptr : private_key_password.c_str(); + if (!tls_set_client_cert_file(ctx_, client_cert_path.c_str(), + client_key_path.c_str(), password)) { + last_backend_error_ = tls_get_error(); + tls_free_context(ctx_); + ctx_ = nullptr; + } + } +} + +inline SSLClient::SSLClient(const std::string &host, int port, + const PemMemory &pem) + : ClientImpl(host, port) { + using namespace tls; + + ctx_ = tls_create_client_context(); + if (!ctx_) { return; } + + // Set minimum TLS version to 1.2 + tls_set_min_version(ctx_, 0x0303); // TLS 1.2 + + detail::split(&host_[0], &host_[host_.size()], '.', + [&](const char *b, const char *e) { + host_components_.emplace_back(b, e); + }); + + if (pem.cert_pem && pem.key_pem) { + if (!tls_set_client_cert_pem(ctx_, pem.cert_pem, pem.key_pem, + pem.private_key_password)) { + last_backend_error_ = tls_get_error(); + tls_free_context(ctx_); + ctx_ = nullptr; + } + } +} + +inline void SSLClient::set_ca_cert_store(tls::tls_ca_store_t ca_cert_store) { + if (ca_cert_store && ctx_) { + // tls_set_ca_store takes ownership of ca_cert_store + tls::tls_set_ca_store(ctx_, ca_cert_store); + } else if (ca_cert_store) { + tls::tls_free_ca_store(ca_cert_store); + } +} + +inline void SSLClient::load_ca_cert_store(const char *ca_cert, + std::size_t size) { + if (ctx_ && ca_cert && size > 0) { + ca_cert_pem_.assign(ca_cert, size); // Store for redirect transfer + tls::tls_load_ca_pem(ctx_, ca_cert, size); + } +} + +inline void +SSLClient::set_server_certificate_verifier(tls::TlsVerifyCallback verifier) { + if (!ctx_) { return; } + tls::tls_set_verify_callback(ctx_, verifier); +} + +inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { + using namespace tls; + + // Load system certificates if no CA certs were explicitly set + if (ca_cert_file_path_.empty() && ca_cert_dir_path_.empty()) { + tls_load_system_certs(ctx_); + } + + // Load CA certificates from file/dir if specified + if (!ca_cert_file_path_.empty()) { + if (!tls_load_ca_file(ctx_, ca_cert_file_path_.c_str())) { + last_backend_error_ = tls_get_error(); + error = Error::SSLLoadingCerts; + return false; + } + } + if (!ca_cert_dir_path_.empty()) { + if (!tls_load_ca_dir(ctx_, ca_cert_dir_path_.c_str())) { + last_backend_error_ = tls_get_error(); + error = Error::SSLLoadingCerts; + return false; + } + } + + // For IP addresses, we need special handling in mbedTLS: + // - RFC 6066 prohibits sending IP addresses in SNI + // - mbedTLS 3.x requires hostname for cert verification (VERIFY_REQUIRED) + // - Solution: Use VERIFY_OPTIONAL for IP addresses and verify manually + bool is_ip = detail::is_ip_address(host_); + if (is_ip && server_certificate_verification_) { + // Use OPTIONAL mode so mbedTLS doesn't require hostname + tls_set_verify_client(ctx_, false); + } else { + tls_set_verify_client(ctx_, server_certificate_verification_); + } + + auto session = tls_create_session(ctx_, socket.sock); + if (!session) { + last_backend_error_ = tls_get_error(); + error = Error::SSLConnection; + return false; + } + + // Use scope_exit to ensure session is freed on error paths + bool success = false; + auto session_guard = detail::scope_exit([&] { + if (!success) { tls_free_session(session); } + }); + + // Set hostname for SNI (only for non-IP addresses per RFC 6066) + if (!is_ip) { + if (!tls_set_hostname(session, host_.c_str())) { + last_backend_error_ = tls_get_error(); + error = Error::SSLConnection; + return false; + } + } + + // Perform TLS handshake + TlsError tls_err; + if (!tls_connect_nonblocking(session, socket.sock, connection_timeout_sec_, + connection_timeout_usec_, &tls_err)) { + last_ssl_error_ = static_cast(tls_err.code); + last_backend_error_ = tls_err.backend_code; + // Map TLS error codes to appropriate Error types + if (tls_err.code == ErrorCode::CertVerifyFailed) { + error = Error::SSLServerVerification; + } else if (tls_err.code == ErrorCode::HostnameMismatch) { + error = Error::SSLServerHostnameVerification; + } else { + error = Error::SSLConnection; + } + return false; + } + + // Verify server certificate + if (server_certificate_verification_) { + auto verify_result = tls_get_verify_result(session); + if (verify_result != 0) { + last_backend_error_ = static_cast(verify_result); + error = Error::SSLServerVerification; + return false; + } + + auto cert = tls_get_peer_cert(session); + if (!cert) { + last_backend_error_ = tls_get_error(); + error = Error::SSLServerVerification; + return false; + } + auto cert_guard = detail::scope_exit([&] { tls_free_cert(cert); }); + + if (server_hostname_verification_) { + if (!tls_verify_hostname(cert, host_.c_str())) { + // Use a well-known error code for hostname mismatch + last_backend_error_ = 0x4000A000; // Custom code for hostname mismatch + error = Error::SSLServerHostnameVerification; + return false; + } + } + } + + success = true; + socket.ssl = session; + return true; +} + +#endif // CPPHTTPLIB_MBEDTLS_SUPPORT + +/* + * SSL/TLS Common Implementation + */ + +inline ClientConnection::~ClientConnection() { +#ifdef CPPHTTPLIB_SSL_ENABLED + if (session) { + tls::tls_shutdown(session, true); + tls::tls_free_session(session); + session = nullptr; + } +#endif + + if (sock != INVALID_SOCKET) { + detail::close_socket(sock); + sock = INVALID_SOCKET; + } +} + +#ifdef CPPHTTPLIB_SSL_ENABLED +namespace detail { + +inline bool is_ip_address(const std::string &host) { + struct in_addr addr4; + struct in6_addr addr6; + return inet_pton(AF_INET, host.c_str(), &addr4) == 1 || + inet_pton(AF_INET6, host.c_str(), &addr6) == 1; +} + +template +inline bool process_server_socket_ssl( + const std::atomic &svr_sock, tls::tls_session_t session, + socket_t sock, size_t keep_alive_max_count, time_t keep_alive_timeout_sec, + time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, + time_t write_timeout_usec, T callback) { + return process_server_socket_core( + svr_sock, sock, keep_alive_max_count, keep_alive_timeout_sec, + [&](bool close_connection, bool &connection_closed) { + SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec, + write_timeout_sec, write_timeout_usec); + return callback(strm, close_connection, connection_closed); + }); +} + +template +inline bool process_client_socket_ssl( + tls::tls_session_t session, socket_t sock, time_t read_timeout_sec, + time_t read_timeout_usec, time_t write_timeout_sec, + time_t write_timeout_usec, time_t max_timeout_msec, + std::chrono::time_point start_time, T callback) { + SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec, + write_timeout_sec, write_timeout_usec, max_timeout_msec, + start_time); + return callback(strm); +} + +// SSL socket stream implementation +inline SSLSocketStream::SSLSocketStream( + socket_t sock, tls::tls_session_t session, time_t read_timeout_sec, + time_t read_timeout_usec, time_t write_timeout_sec, + time_t write_timeout_usec, time_t max_timeout_msec, + std::chrono::time_point start_time) + : sock_(sock), session_(session), read_timeout_sec_(read_timeout_sec), + read_timeout_usec_(read_timeout_usec), + write_timeout_sec_(write_timeout_sec), + write_timeout_usec_(write_timeout_usec), + max_timeout_msec_(max_timeout_msec), start_time_(start_time) { +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + // Clear AUTO_RETRY for proper non-blocking I/O timeout handling + // Note: tls_create_session() also clears this, but SSLClient currently + // uses ssl_new() which does not. Until full TLS API migration is complete, + // we need to ensure AUTO_RETRY is cleared here regardless of how the + // SSL session was created. + SSL_clear_mode(static_cast(session), SSL_MODE_AUTO_RETRY); +#endif +} + +inline SSLSocketStream::~SSLSocketStream() = default; + +inline bool SSLSocketStream::is_readable() const { + return tls::tls_pending(session_) > 0; +} + +inline bool SSLSocketStream::wait_readable() const { + if (max_timeout_msec_ <= 0) { + return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0; + } + + time_t read_timeout_sec; + time_t read_timeout_usec; + calc_actual_timeout(max_timeout_msec_, duration(), read_timeout_sec_, + read_timeout_usec_, read_timeout_sec, read_timeout_usec); + + return select_read(sock_, read_timeout_sec, read_timeout_usec) > 0; +} + +inline bool SSLSocketStream::wait_writable() const { + return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0 && + is_socket_alive(sock_) && !tls::tls_is_peer_closed(session_, sock_); +} + +inline ssize_t SSLSocketStream::read(char *ptr, size_t size) { + if (tls::tls_pending(session_) > 0) { + tls::TlsError err; + auto ret = tls::tls_read(session_, ptr, size, err); + if (ret == 0 || err.code == tls::ErrorCode::PeerClosed) { + error_ = Error::ConnectionClosed; + } + return ret; + } else if (wait_readable()) { + tls::TlsError err; + auto ret = tls::tls_read(session_, ptr, size, err); + if (ret < 0) { + auto n = 1000; +#ifdef _WIN32 + while (--n >= 0 && (err.code == tls::ErrorCode::WantRead || + (err.code == tls::ErrorCode::SyscallError && + WSAGetLastError() == WSAETIMEDOUT))) { +#else + while (--n >= 0 && err.code == tls::ErrorCode::WantRead) { +#endif + if (tls::tls_pending(session_) > 0) { + return tls::tls_read(session_, ptr, size, err); + } else if (wait_readable()) { + std::this_thread::sleep_for(std::chrono::microseconds{10}); + ret = tls::tls_read(session_, ptr, size, err); + if (ret >= 0) { return ret; } + } else { + break; + } + } + assert(ret < 0); + } else if (ret == 0 || err.code == tls::ErrorCode::PeerClosed) { + error_ = Error::ConnectionClosed; + } + return ret; + } else { + error_ = Error::Timeout; + return -1; + } +} + +inline ssize_t SSLSocketStream::write(const char *ptr, size_t size) { + if (wait_writable()) { + auto handle_size = + std::min(size, (std::numeric_limits::max)()); + + tls::TlsError err; + auto ret = tls::tls_write(session_, ptr, handle_size, err); + if (ret < 0) { + auto n = 1000; +#ifdef _WIN32 + while (--n >= 0 && (err.code == tls::ErrorCode::WantWrite || + (err.code == tls::ErrorCode::SyscallError && + WSAGetLastError() == WSAETIMEDOUT))) { +#else + while (--n >= 0 && err.code == tls::ErrorCode::WantWrite) { +#endif + if (wait_writable()) { + std::this_thread::sleep_for(std::chrono::microseconds{10}); + ret = tls::tls_write(session_, ptr, handle_size, err); + if (ret >= 0) { return ret; } + } else { + break; + } + } + assert(ret < 0); + } + 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_; } + +inline time_t SSLSocketStream::duration() const { + return std::chrono::duration_cast( + std::chrono::steady_clock::now() - start_time_) + .count(); +} + +} // namespace detail + +// SSL HTTP server implementation (common for OpenSSL and Mbed TLS) +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) { + using namespace tls; + + ctx_ = tls_create_server_context(); + if (!ctx_) { return; } + + // Load server certificate and private key + if (!tls_set_server_cert_file(ctx_, cert_path, private_key_path, + private_key_password)) { + last_ssl_error_ = static_cast(tls_get_error()); + tls_free_context(ctx_); + ctx_ = nullptr; + return; + } + + // Load client CA certificates for client authentication + if (client_ca_cert_file_path || client_ca_cert_dir_path) { + if (!tls_set_client_ca_file(ctx_, client_ca_cert_file_path, + client_ca_cert_dir_path)) { + last_ssl_error_ = static_cast(tls_get_error()); + tls_free_context(ctx_); + ctx_ = nullptr; + return; + } + // Enable client certificate verification + tls_set_verify_client(ctx_, true); + } +} + +inline SSLServer::SSLServer(const PemMemory &pem) { + using namespace tls; + ctx_ = tls_create_server_context(); + if (ctx_) { + if (!tls_set_server_cert_pem(ctx_, pem.cert_pem, pem.key_pem, + pem.private_key_password)) { + last_ssl_error_ = static_cast(tls_get_error()); + tls_free_context(ctx_); + ctx_ = nullptr; + } else if (pem.client_ca_pem && pem.client_ca_pem_len > 0) { + if (!tls_load_ca_pem(ctx_, pem.client_ca_pem, pem.client_ca_pem_len)) { + last_ssl_error_ = static_cast(tls_get_error()); + tls_free_context(ctx_); + ctx_ = nullptr; + } else { + tls_set_verify_client(ctx_, true); + } + } + } +} + +inline SSLServer::SSLServer( + const tls::TlsContextSetupCallback &setup_callback) { + using namespace tls; + ctx_ = tls_create_server_context(); + if (ctx_) { + if (!setup_callback(ctx_)) { + tls_free_context(ctx_); + ctx_ = nullptr; + } + } +} + +inline SSLServer::~SSLServer() { + if (ctx_) { tls::tls_free_context(ctx_); } +} + +inline bool SSLServer::is_valid() const { return ctx_ != nullptr; } + +inline bool SSLServer::process_and_close_socket(socket_t sock) { + using namespace tls; + + // Create TLS session with mutex protection + tls_session_t session = nullptr; + { + std::lock_guard guard(ctx_mutex_); + session = tls_create_session(static_cast(ctx_), sock); + } + + if (!session) { + last_ssl_error_ = static_cast(tls_get_error()); + detail::shutdown_socket(sock); + detail::close_socket(sock); + return false; + } + + // Use scope_exit to ensure cleanup on all paths (including exceptions) + bool handshake_done = false; + bool ret = false; + auto cleanup = detail::scope_exit([&] { + // Shutdown gracefully if handshake succeeded and processing was successful + if (handshake_done) { tls_shutdown(session, ret); } + tls_free_session(session); + detail::shutdown_socket(sock); + detail::close_socket(sock); + }); + + // Perform TLS accept handshake with timeout + TlsError tls_err; + if (!tls_accept_nonblocking(session, sock, read_timeout_sec_, + read_timeout_usec_, &tls_err)) { +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + // Map TlsError to legacy ssl_error for backward compatibility + if (tls_err.code == ErrorCode::WantRead) { + last_ssl_error_ = SSL_ERROR_WANT_READ; + } else if (tls_err.code == ErrorCode::WantWrite) { + last_ssl_error_ = SSL_ERROR_WANT_WRITE; + } else { + last_ssl_error_ = SSL_ERROR_SSL; + } +#else + last_ssl_error_ = static_cast(tls_get_error()); +#endif + return false; + } + + handshake_done = true; + + std::string remote_addr; + int remote_port = 0; + detail::get_remote_ip_and_port(sock, remote_addr, remote_port); + + std::string local_addr; + int local_port = 0; + detail::get_local_ip_and_port(sock, local_addr, local_port); + + ret = detail::process_server_socket_ssl( + svr_sock_, session, sock, keep_alive_max_count_, keep_alive_timeout_sec_, + read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, + write_timeout_usec_, + [&](Stream &strm, bool close_connection, bool &connection_closed) { + return process_request(strm, remote_addr, remote_port, local_addr, + local_port, close_connection, connection_closed, + [&](Request &req) { req.ssl = session; }); + }); + + return ret; +} + +inline SSLClient::~SSLClient() { + if (ctx_) { tls::tls_free_context(ctx_); } + // Make sure to shut down SSL since shutdown_ssl will resolve to the + // base function rather than the derived function once we get to the + // base class destructor, and won't free the SSL (causing a leak). + shutdown_ssl_impl(socket_, true); +} + +inline bool SSLClient::is_valid() const { return ctx_ != nullptr; } + +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) { + tls::tls_shutdown(socket.ssl, shutdown_gracefully); + { + std::lock_guard guard(ctx_mutex_); + tls::tls_free_session(socket.ssl); + } + socket.ssl = nullptr; + } + assert(socket.ssl == nullptr); +} + +inline bool SSLClient::process_socket( + const Socket &socket, + std::chrono::time_point start_time, + std::function callback) { + assert(socket.ssl); + return detail::process_client_socket_ssl( + socket.ssl, socket.sock, read_timeout_sec_, read_timeout_usec_, + write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, start_time, + std::move(callback)); +} + +inline bool SSLClient::is_ssl() const { return true; } + +inline bool SSLClient::create_and_connect_socket(Socket &socket, Error &error) { + if (!is_valid()) { + error = Error::SSLConnection; + return false; + } + return ClientImpl::create_and_connect_socket(socket, error); +} + +// Assumes that socket_mutex_ is locked and that there are no requests in +// flight +inline bool SSLClient::connect_with_proxy( + Socket &socket, + std::chrono::time_point start_time, + Response &res, bool &success, Error &error) { + success = true; + Response proxy_res; + if (!detail::process_client_socket( + socket.sock, read_timeout_sec_, read_timeout_usec_, + write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, + start_time, [&](Stream &strm) { + Request req2; + req2.method = "CONNECT"; + req2.path = + detail::make_host_and_port_string_always_port(host_, port_); + if (max_timeout_msec_ > 0) { + req2.start_time_ = std::chrono::steady_clock::now(); + } + return process_request(strm, req2, proxy_res, false, error); + })) { + // Thread-safe to close everything because we are assuming there are no + // requests in flight + shutdown_ssl(socket, true); + shutdown_socket(socket); + close_socket(socket); + success = false; + return false; + } + +#ifdef CPPHTTPLIB_SSL_ENABLED + if (proxy_res.status == StatusCode::ProxyAuthenticationRequired_407) { + if (!proxy_digest_auth_username_.empty() && + !proxy_digest_auth_password_.empty()) { + std::map auth; + if (detail::parse_www_authenticate(proxy_res, auth, true)) { + // Close the current socket and create a new one for the authenticated + // request + shutdown_ssl(socket, true); + shutdown_socket(socket); + close_socket(socket); + + // Create a new socket for the authenticated CONNECT request + if (!ensure_socket_connection(socket, error)) { + success = false; + output_error_log(error, nullptr); + return false; + } + + proxy_res = Response(); + if (!detail::process_client_socket( + socket.sock, read_timeout_sec_, read_timeout_usec_, + write_timeout_sec_, write_timeout_usec_, max_timeout_msec_, + start_time, [&](Stream &strm) { + Request req3; + req3.method = "CONNECT"; + req3.path = detail::make_host_and_port_string_always_port( + host_, port_); + req3.headers.insert(detail::make_digest_authentication_header( + req3, auth, 1, detail::random_string(10), + proxy_digest_auth_username_, proxy_digest_auth_password_, + true)); + if (max_timeout_msec_ > 0) { + req3.start_time_ = std::chrono::steady_clock::now(); + } + return process_request(strm, req3, proxy_res, false, error); + })) { + // Thread-safe to close everything because we are assuming there are + // no requests in flight + shutdown_ssl(socket, true); + shutdown_socket(socket); + close_socket(socket); + success = false; + return false; + } + } + } + } +#endif + + // If status code is not 200, proxy request is failed. + // Set error to ProxyConnection and return proxy response + // as the response of the request + if (proxy_res.status != StatusCode::OK_200) { + error = Error::ProxyConnection; + output_error_log(error, nullptr); + res = std::move(proxy_res); + // Thread-safe to close everything because we are assuming there are + // no requests in flight + shutdown_ssl(socket, true); + shutdown_socket(socket); + close_socket(socket); + return false; + } + + return true; +} + +inline bool SSLServer::update_certs_pem(const char *cert_pem, + const char *key_pem, + const char *client_ca_pem, + const char *password) { + if (!ctx_) { return false; } + std::lock_guard guard(ctx_mutex_); + if (!tls::tls_update_server_cert(ctx_, cert_pem, key_pem, password)) { + return false; + } + if (client_ca_pem) { + return tls::tls_update_server_client_ca(ctx_, client_ca_pem); + } + return true; +} +#endif // CPPHTTPLIB_SSL_ENABLED // Universal client implementation inline Client::Client(const std::string &scheme_host_port) @@ -13381,7 +16412,7 @@ inline Client::Client(const std::string &scheme_host_port, if (std::regex_match(scheme_host_port, m, re)) { auto scheme = m[1].str(); -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED if (!scheme.empty() && (scheme != "http" && scheme != "https")) { #else if (!scheme.empty() && scheme != "http") { @@ -13402,7 +16433,7 @@ inline Client::Client(const std::string &scheme_host_port, auto port = !port_str.empty() ? std::stoi(port_str) : (is_ssl ? 443 : 80); if (is_ssl) { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED cli_ = detail::make_unique(host, port, client_cert_path, client_key_path); is_ssl_ = is_ssl; @@ -13987,12 +17018,6 @@ inline void Client::set_basic_auth(const std::string &username, 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) { @@ -14024,27 +17049,6 @@ inline void Client::set_proxy_basic_auth(const std::string &username, 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); -} - -inline void Client::enable_server_hostname_verification(bool enabled) { - cli_->enable_server_hostname_verification(enabled); -} - -inline void Client::set_server_certificate_verifier( - std::function verifier) { - cli_->set_server_certificate_verifier(verifier); -} -#endif inline void Client::set_logger(Logger logger) { cli_->set_logger(std::move(logger)); @@ -14054,22 +17058,65 @@ inline void Client::set_error_logger(ErrorLogger error_logger) { cli_->set_error_logger(std::move(error_logger)); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED +inline void Client::set_digest_auth(const std::string &username, + const std::string &password) { + cli_->set_digest_auth(username, password); +} + +inline void Client::set_proxy_digest_auth(const std::string &username, + const std::string &password) { + cli_->set_proxy_digest_auth(username, password); +} + +inline void Client::enable_server_certificate_verification(bool enabled) { + cli_->enable_server_certificate_verification(enabled); +} + +inline void Client::enable_server_hostname_verification(bool enabled) { + cli_->enable_server_hostname_verification(enabled); +} + 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) { +inline void Client::set_ca_cert_store(tls::tls_ca_store_t 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); + } else if (ca_cert_store) { + tls::tls_free_ca_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)); + set_ca_cert_store(tls::tls_create_ca_store(ca_cert, size)); +} + +inline void +Client::set_server_certificate_verifier(tls::TlsVerifyCallback verifier) { + if (is_ssl_) { + static_cast(*cli_).set_server_certificate_verifier( + std::move(verifier)); + } +} + +inline tls::tls_ctx_t Client::tls_context() const { + if (is_ssl_) { return static_cast(*cli_).tls_context(); } + return nullptr; +} +#endif // CPPHTTPLIB_SSL_ENABLED + +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +inline SSL_CTX *Client::ssl_context() const { + if (is_ssl_) { return static_cast(*cli_).ssl_context(); } + return nullptr; +} + +inline void Client::set_server_certificate_verifier( + std::function verifier) { + cli_->set_server_certificate_verifier(verifier); } inline long Client::get_openssl_verify_result() const { @@ -14078,12 +17125,7 @@ inline long Client::get_openssl_verify_result() const { } 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 +#endif // CPPHTTPLIB_OPENSSL_SUPPORT // ---------------------------------------------------------------------------- diff --git a/justfile b/justfile new file mode 100644 index 0000000..4ff3e42 --- /dev/null +++ b/justfile @@ -0,0 +1,21 @@ +set shell := ["bash", "-c"] + +default: list + +list: + @just --list --unsorted + +openssl: + @(cd test && make test && ./test) + @(cd test && make proxy) + +mbedtls: + @(cd test && make test_mbedtls && ./test_mbedtls) + @(cd test && make proxy_mbedtls) + +fuzz: + @(cd test && make fuzz_test) + +build: + @(cd test && make test_split) + @(cd test && make test_split_mbedtls) diff --git a/test/Makefile b/test/Makefile index 900cb56..e365463 100644 --- a/test/Makefile +++ b/test/Makefile @@ -1,38 +1,43 @@ CXX = clang++ CXXFLAGS = -g -std=c++11 -I. -Wall -Wextra -Wtype-limits -Wconversion -Wshadow $(EXTRA_CXXFLAGS) -DCPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO # -fno-exceptions -DCPPHTTPLIB_NO_EXCEPTIONS -fsanitize=address -PREFIX ?= $(shell brew --prefix) - -OPENSSL_DIR = $(PREFIX)/opt/openssl@3 -OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -I$(OPENSSL_DIR)/include -L$(OPENSSL_DIR)/lib -lssl -lcrypto - ifneq ($(OS), Windows_NT) UNAME_S := $(shell uname -s) ifeq ($(UNAME_S), Darwin) + PREFIX ?= $(shell brew --prefix) + OPENSSL_DIR = $(PREFIX)/opt/openssl@3 + OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -I$(OPENSSL_DIR)/include -L$(OPENSSL_DIR)/lib -lssl -lcrypto OPENSSL_SUPPORT += -DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -framework Security + MBEDTLS_DIR ?= $(shell brew --prefix mbedtls@3) + MBEDTLS_SUPPORT = -DCPPHTTPLIB_MBEDTLS_SUPPORT -I$(MBEDTLS_DIR)/include -L$(MBEDTLS_DIR)/lib -lmbedtls -lmbedx509 -lmbedcrypto + MBEDTLS_SUPPORT += -DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -framework Security + else + OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -lssl -lcrypto + MBEDTLS_SUPPORT = -DCPPHTTPLIB_MBEDTLS_SUPPORT -lmbedtls -lmbedx509 -lmbedcrypto endif endif ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz -BROTLI_DIR = $(PREFIX)/opt/brotli -BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec - -ZSTD_DIR = $(PREFIX)/opt/zstd -ZSTD_SUPPORT = -DCPPHTTPLIB_ZSTD_SUPPORT -I$(ZSTD_DIR)/include -L$(ZSTD_DIR)/lib -lzstd - -LIBS = -lpthread -lcurl ifneq ($(OS), Windows_NT) UNAME_S := $(shell uname -s) ifeq ($(UNAME_S), Darwin) - LIBS += -framework CoreFoundation -framework CFNetwork - endif - ifneq ($(UNAME_S), Darwin) - LIBS += -lanl + # macOS: use Homebrew paths for brotli and zstd + BROTLI_DIR = $(PREFIX)/opt/brotli + BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec + ZSTD_DIR = $(PREFIX)/opt/zstd + ZSTD_SUPPORT = -DCPPHTTPLIB_ZSTD_SUPPORT -I$(ZSTD_DIR)/include -L$(ZSTD_DIR)/lib -lzstd + LIBS = -lpthread -lcurl -framework CoreFoundation -framework CFNetwork + else + # Linux: use system paths + BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -lbrotlicommon -lbrotlienc -lbrotlidec + ZSTD_SUPPORT = -DCPPHTTPLIB_ZSTD_SUPPORT -lzstd + LIBS = -lpthread -lcurl -lanl endif endif TEST_ARGS = gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(ZSTD_SUPPORT) $(LIBS) +TEST_ARGS_MBEDTLS = gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include $(MBEDTLS_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(ZSTD_SUPPORT) $(LIBS) # By default, use standalone_fuzz_target_runner. # This runner does no fuzzing, but simply executes the inputs @@ -69,6 +74,25 @@ proxy : test_proxy cd proxy && docker compose down; \ exit $$exit_code +proxy_mbedtls : test_proxy_mbedtls + @echo "Starting proxy server..." + cd proxy && \ + docker compose up -d + @echo "Waiting for proxy to be ready..." + @until nc -z localhost 3128 && nc -z localhost 3129; do sleep 1; done + @echo "Proxy servers are ready, waiting additional 5 seconds for full startup..." + @sleep 5 + @echo "Checking proxy server status..." + @cd proxy && docker compose ps + @echo "Checking proxy server logs..." + @cd proxy && docker compose logs --tail=20 + @echo "Running proxy tests (Mbed TLS)..." + ./test_proxy_mbedtls; \ + exit_code=$$?; \ + echo "Stopping proxy server..."; \ + cd proxy && docker compose down; \ + exit $$exit_code + test : test.cc include_httplib.cc ../httplib.h Makefile cert.pem $(CXX) -o $@ -I.. $(CXXFLAGS) test.cc include_httplib.cc $(TEST_ARGS) @file $@ @@ -78,6 +102,14 @@ test : test.cc include_httplib.cc ../httplib.h Makefile cert.pem test_split : test.cc ../httplib.h httplib.cc Makefile cert.pem $(CXX) -o $@ $(CXXFLAGS) test.cc httplib.cc $(TEST_ARGS) +# Mbed TLS backend targets +test_mbedtls : test.cc include_httplib.cc ../httplib.h Makefile cert.pem + $(CXX) -o $@ -I.. $(CXXFLAGS) test.cc include_httplib.cc $(TEST_ARGS_MBEDTLS) + @file $@ + +test_split_mbedtls : test.cc ../httplib.h httplib.cc Makefile cert.pem + $(CXX) -o $@ $(CXXFLAGS) test.cc httplib.cc $(TEST_ARGS_MBEDTLS) + check_abi: @./check-shared-library-abi-compatibility.sh @@ -106,6 +138,9 @@ style_check: $(STYLE_CHECK_FILES) test_proxy : test_proxy.cc ../httplib.h Makefile cert.pem $(CXX) -o $@ -I.. $(CXXFLAGS) test_proxy.cc $(TEST_ARGS) +test_proxy_mbedtls : test_proxy.cc ../httplib.h Makefile cert.pem + $(CXX) -o $@ -I.. $(CXXFLAGS) test_proxy.cc $(TEST_ARGS_MBEDTLS) + # Runs server_fuzzer.cc based on value of $(LIB_FUZZING_ENGINE). # Usage: make fuzz_test LIB_FUZZING_ENGINE=/path/to/libFuzzer fuzz_test: server_fuzzer @@ -128,5 +163,5 @@ cert.pem: ./gen-certs.sh clean: - rm -rf test test_split test_proxy server_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM + rm -rf test test_split test_mbedtls test_split_mbedtls test_proxy test_proxy_mbedtls server_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM diff --git a/test/gen-certs.sh b/test/gen-certs.sh index ee2a2cf..bc91f84 100755 --- a/test/gen-certs.sh +++ b/test/gen-certs.sh @@ -14,5 +14,5 @@ openssl genrsa 2048 > client.key.pem openssl req -new -batch -config test.conf -key client.key.pem | openssl x509 -days 370 -req -CA rootCA.cert.pem -CAkey rootCA.key.pem -CAcreateserial > client.cert.pem openssl genrsa -passout pass:test123! 2048 > key_encrypted.pem openssl req -new -batch -config test.conf -key key_encrypted.pem | openssl x509 -days 3650 -req -signkey key_encrypted.pem > cert_encrypted.pem -openssl genrsa -aes256 -passout pass:test012! 2048 > client_encrypted.key.pem +openssl genrsa 2048 | openssl pkcs8 -topk8 -v1 PBE-SHA1-3DES -passout pass:test012! -out client_encrypted.key.pem openssl req -new -batch -config test.conf -key client_encrypted.key.pem -passin pass:test012! | openssl x509 -days 370 -req -CA rootCA.cert.pem -CAkey rootCA.key.pem -CAcreateserial > client_encrypted.cert.pem diff --git a/test/test.cc b/test/test.cc index b0f64da..dbe1e23 100644 --- a/test/test.cc +++ b/test/test.cc @@ -1265,13 +1265,13 @@ class ChunkedEncodingTest : public ::testing::Test { protected: ChunkedEncodingTest() : cli_(HOST, PORT) -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED , svr_(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE) #endif { cli_.set_connection_timeout(2); -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED cli_.enable_server_certificate_verification(false); #endif } @@ -1317,7 +1317,7 @@ protected: t_.join(); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED SSLClient cli_; SSLServer svr_; #else @@ -1385,7 +1385,7 @@ TEST(RangeTest, FromHTTPBin_Online) { auto path = std::string{"/httpbin/range/32"}; #endif -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED auto port = 443; SSLClient cli(host, port); #else @@ -1445,7 +1445,7 @@ TEST(GetAddrInfoDanglingRefTest, LongTimeout) { auto host = "unresolvableaddress.local"; auto path = std::string{"/"}; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED auto port = 443; SSLClient cli(host, port); #else @@ -1465,7 +1465,7 @@ TEST(GetAddrInfoDanglingRefTest, LongTimeout) { TEST(ConnectionErrorTest, InvalidHost) { auto host = "-abcde.com"; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED auto port = 443; SSLClient cli(host, port); #else @@ -1482,7 +1482,7 @@ TEST(ConnectionErrorTest, InvalidHost) { TEST(ConnectionErrorTest, InvalidHost2) { auto host = "httpcan.org/"; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED SSLClient cli(host); #else Client cli(host); @@ -1497,7 +1497,7 @@ TEST(ConnectionErrorTest, InvalidHost2) { TEST(ConnectionErrorTest, InvalidHostCheckResultErrorToString) { auto host = "httpcan.org/"; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED SSLClient cli(host); #else Client cli(host); @@ -1515,7 +1515,7 @@ TEST(ConnectionErrorTest, InvalidPort) { auto host = "localhost"; auto port = 44380; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED SSLClient cli(host, port); #else Client cli(host, port); @@ -1531,7 +1531,7 @@ TEST(ConnectionErrorTest, InvalidPort) { TEST(ConnectionErrorTest, Timeout_Online) { auto host = "google.com"; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED auto port = 44380; SSLClient cli(host, port); #else @@ -1559,7 +1559,7 @@ TEST(CancelTest, NoCancel_Online) { auto path = std::string{"/httpbin/range/32"}; #endif -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED auto port = 443; SSLClient cli(host, port); #else @@ -1583,7 +1583,7 @@ TEST(CancelTest, WithCancelSmallPayload_Online) { auto path = std::string{"/httpbin/range/32"}; #endif -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED auto port = 443; SSLClient cli(host, port); #else @@ -1606,7 +1606,7 @@ TEST(CancelTest, WithCancelLargePayload_Online) { auto path = std::string{"/httpbin/range/65536"}; #endif -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED auto port = 443; SSLClient cli(host, port); #else @@ -1955,7 +1955,7 @@ TEST(BaseAuthTest, FromHTTPWatch_Online) { auto path = std::string{"/httpbin/basic-auth/hello/world"}; #endif -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED auto port = 443; SSLClient cli(host, port); #else @@ -2002,7 +2002,7 @@ TEST(BaseAuthTest, FromHTTPWatch_Online) { } } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(DigestAuthTest, FromHTTPWatch_Online) { #ifdef CPPHTTPLIB_DEFAULT_HTTPBIN auto host = "httpcan.org"; @@ -2069,7 +2069,7 @@ TEST(SpecifyServerIPAddressTest, AnotherHostname_Online) { auto another_host = "example.com"; auto wrong_ip = "0.0.0.0"; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED SSLClient cli(host); #else Client cli(host); @@ -2085,7 +2085,7 @@ TEST(SpecifyServerIPAddressTest, RealHostname_Online) { auto host = "google.com"; auto wrong_ip = "0.0.0.0"; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED SSLClient cli(host); #else Client cli(host); @@ -2100,7 +2100,7 @@ TEST(SpecifyServerIPAddressTest, RealHostname_Online) { TEST(AbsoluteRedirectTest, Redirect_Online) { auto host = "nghttp2.org"; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED SSLClient cli(host); #else Client cli(host); @@ -2115,7 +2115,7 @@ TEST(AbsoluteRedirectTest, Redirect_Online) { TEST(RedirectTest, Redirect_Online) { auto host = "nghttp2.org"; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED SSLClient cli(host); #else Client cli(host); @@ -2130,7 +2130,7 @@ TEST(RedirectTest, Redirect_Online) { TEST(RelativeRedirectTest, Redirect_Online) { auto host = "nghttp2.org"; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED SSLClient cli(host); #else Client cli(host); @@ -2145,7 +2145,7 @@ TEST(RelativeRedirectTest, Redirect_Online) { TEST(TooManyRedirectTest, Redirect_Online) { auto host = "nghttp2.org"; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED SSLClient cli(host); #else Client cli(host); @@ -2157,7 +2157,7 @@ TEST(TooManyRedirectTest, Redirect_Online) { EXPECT_EQ(Error::ExceedRedirectCount, res.error()); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(YahooRedirectTest, Redirect_Online) { Client cli("yahoo.com"); @@ -2171,7 +2171,9 @@ TEST(YahooRedirectTest, Redirect_Online) { EXPECT_EQ(StatusCode::OK_200, res->status); EXPECT_EQ("https://www.yahoo.com/", res->location); } +#endif +#ifdef CPPHTTPLIB_SSL_ENABLED // Previously "nghttp2.org" "/httpbin/redirect-to" #define REDIR_HOST "httpbingo.org" #define REDIR_PATH "/redirect-to" @@ -2209,7 +2211,9 @@ TEST(HttpsToHttpRedirectTest3, Redirect_Online) { ASSERT_TRUE(res); EXPECT_EQ(StatusCode::OK_200, res->status); } +#endif +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(UrlWithSpace, Redirect_Online) { SSLClient cli("edge.forgecdn.net"); cli.set_follow_location(true); @@ -2219,7 +2223,6 @@ TEST(UrlWithSpace, Redirect_Online) { EXPECT_EQ(StatusCode::OK_200, res->status); EXPECT_EQ(18527U, res->get_header_value_u64("Content-Length")); } - #endif #if !defined(_WIN32) && !defined(_WIN64) @@ -2523,7 +2526,7 @@ TEST(BindServerTest, BindAndListenSeparately) { svr.stop(); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(BindServerTest, BindAndListenSeparatelySSL) { SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE, CLIENT_CA_CERT_DIR); @@ -2534,7 +2537,7 @@ TEST(BindServerTest, BindAndListenSeparatelySSL) { } #endif -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(BindServerTest, BindAndListenSeparatelySSLEncryptedKey) { SSLServer svr(SERVER_ENCRYPTED_CERT_FILE, SERVER_ENCRYPTED_PRIVATE_KEY_FILE, nullptr, nullptr, SERVER_ENCRYPTED_PRIVATE_KEY_PASS); @@ -2603,6 +2606,153 @@ TEST(BindServerTest, UpdateCerts) { X509_free(ca_cert); EVP_PKEY_free(key); } + +// Test that SSLServer(X509*, EVP_PKEY*, X509_STORE*) constructor sets +// client CA list correctly for TLS handshake +TEST(SSLClientServerTest, X509ConstructorSetsClientCAList) { + X509 *cert = readCertificate(SERVER_CERT_FILE); + X509 *ca_cert = readCertificate(CLIENT_CA_CERT_FILE); + EVP_PKEY *key = readPrivateKey(SERVER_PRIVATE_KEY_FILE); + + ASSERT_TRUE(cert != nullptr); + ASSERT_TRUE(ca_cert != nullptr); + ASSERT_TRUE(key != nullptr); + + X509_STORE *cert_store = X509_STORE_new(); + X509_STORE_add_cert(cert_store, ca_cert); + + // Use X509-based constructor (deprecated but should still work correctly) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + SSLServer svr(cert, key, cert_store); +#pragma GCC diagnostic pop + + ASSERT_TRUE(svr.is_valid()); + + // Verify that client CA list is set in SSL_CTX + auto ssl_ctx = static_cast(svr.tls_context()); + ASSERT_TRUE(ssl_ctx != nullptr); + + STACK_OF(X509_NAME) *ca_list = SSL_CTX_get_client_CA_list(ssl_ctx); + ASSERT_TRUE(ca_list != nullptr); + EXPECT_GT(sk_X509_NAME_num(ca_list), 0); + + X509_free(cert); + X509_free(ca_cert); + EVP_PKEY_free(key); +} + +// Test that update_certs() updates client CA list correctly +TEST(SSLClientServerTest, UpdateCertsSetsClientCAList) { + // Start with file-based constructor + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); + ASSERT_TRUE(svr.is_valid()); + + // Initially no client CA list should be set + auto ssl_ctx = static_cast(svr.tls_context()); + ASSERT_TRUE(ssl_ctx != nullptr); + + STACK_OF(X509_NAME) *ca_list_before = SSL_CTX_get_client_CA_list(ssl_ctx); + int count_before = ca_list_before ? sk_X509_NAME_num(ca_list_before) : 0; + EXPECT_EQ(0, count_before); + + // Now update with client CA + X509 *cert = readCertificate(SERVER_CERT_FILE); + X509 *ca_cert = readCertificate(CLIENT_CA_CERT_FILE); + EVP_PKEY *key = readPrivateKey(SERVER_PRIVATE_KEY_FILE); + + ASSERT_TRUE(cert != nullptr); + ASSERT_TRUE(ca_cert != nullptr); + ASSERT_TRUE(key != nullptr); + + X509_STORE *cert_store = X509_STORE_new(); + X509_STORE_add_cert(cert_store, ca_cert); + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + svr.update_certs(cert, key, cert_store); +#pragma GCC diagnostic pop + + ASSERT_TRUE(svr.is_valid()); + + // Now client CA list should be set + STACK_OF(X509_NAME) *ca_list_after = SSL_CTX_get_client_CA_list(ssl_ctx); + ASSERT_TRUE(ca_list_after != nullptr); + EXPECT_GT(sk_X509_NAME_num(ca_list_after), 0); + + X509_free(cert); + X509_free(ca_cert); + EVP_PKEY_free(key); +} +#endif + +// Backend-agnostic tests for update_certs_pem() +#ifdef CPPHTTPLIB_SSL_ENABLED +TEST(BindServerTest, UpdateCertsPem) { + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE); + int port = svr.bind_to_any_port("0.0.0.0"); + ASSERT_TRUE(svr.is_valid()); + ASSERT_TRUE(port > 0); + + // Read PEM files + std::string cert_pem, key_pem, ca_pem; + read_file(SERVER_CERT_FILE, cert_pem); + read_file(SERVER_PRIVATE_KEY_FILE, key_pem); + read_file(CLIENT_CA_CERT_FILE, ca_pem); + + // Update server certificates using PEM API + ASSERT_TRUE( + svr.update_certs_pem(cert_pem.c_str(), key_pem.c_str(), ca_pem.c_str())); + + ASSERT_TRUE(svr.is_valid()); + svr.stop(); +} + +TEST(SSLClientServerTest, UpdateCertsPemWithClientAuth) { + // Start server with client CA (enables client auth) + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE); + ASSERT_TRUE(svr.is_valid()); + + bool handler_called = false; + svr.Get("/test", [&](const Request &req, Response &res) { + handler_called = true; + // Verify client certificate is present + auto peer_cert = tls::tls_get_peer_cert(req.ssl); + EXPECT_TRUE(peer_cert != nullptr); + if (peer_cert) { tls::tls_free_cert(peer_cert); } + res.set_content("ok", "text/plain"); + }); + + thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + ASSERT_FALSE(svr.is_running()); + }); + + svr.wait_until_ready(); + + // Read PEM files + std::string cert_pem, key_pem, ca_pem; + read_file(SERVER_CERT_FILE, cert_pem); + read_file(SERVER_PRIVATE_KEY_FILE, key_pem); + read_file(CLIENT_CA_CERT_FILE, ca_pem); + + // Update server certificates and client CA using PEM API while server running + ASSERT_TRUE( + svr.update_certs_pem(cert_pem.c_str(), key_pem.c_str(), ca_pem.c_str())); + + // Connect with client certificate + SSLClient cli(HOST, PORT, CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE); + cli.enable_server_certificate_verification(false); + cli.set_connection_timeout(30); + + auto res = cli.Get("/test"); + ASSERT_TRUE(res); + ASSERT_EQ(StatusCode::OK_200, res->status); + ASSERT_TRUE(handler_called); + EXPECT_EQ("ok", res->body); +} #endif TEST(ErrorHandlerTest, ContentLength) { @@ -2822,7 +2972,7 @@ TEST(NoContentTest, ContentLength) { } TEST(RoutingHandlerTest, PreAndPostRoutingHandlers) { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); ASSERT_TRUE(svr.is_valid()); #else @@ -2862,7 +3012,7 @@ TEST(RoutingHandlerTest, PreAndPostRoutingHandlers) { svr.wait_until_ready(); { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED SSLClient cli(HOST, PORT); cli.enable_server_certificate_verification(false); #else @@ -2880,7 +3030,7 @@ TEST(RoutingHandlerTest, PreAndPostRoutingHandlers) { } { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED SSLClient cli(HOST, PORT); cli.enable_server_certificate_verification(false); #else @@ -2896,7 +3046,7 @@ TEST(RoutingHandlerTest, PreAndPostRoutingHandlers) { } { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED SSLClient cli(HOST, PORT); cli.enable_server_certificate_verification(false); #else @@ -3067,12 +3217,12 @@ class ServerTest : public ::testing::Test { protected: ServerTest() : cli_(HOST, PORT) -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED , svr_(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE) #endif { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED cli_.enable_server_certificate_verification(false); #endif } @@ -3781,7 +3931,7 @@ protected: } map persons_; -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED SSLClient cli_; SSLServer svr_; #else @@ -4417,7 +4567,7 @@ TEST_F(ServerTest, AlmostTooLongRequest) { auto res = cli_.Get(request.c_str()); - ASSERT_TRUE(res); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); EXPECT_EQ(StatusCode::NotFound_404, res->status); } @@ -5438,7 +5588,7 @@ TEST_F(ServerTest, PutLargeFileWithGzip) { } TEST_F(ServerTest, PutLargeFileWithGzip2) { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED std::string s = std::string("https://") + HOST + ":" + std::to_string(PORT); Client cli(s.c_str()); cli.enable_server_certificate_verification(false); @@ -5822,7 +5972,7 @@ void TestWithHeadersAndContentReceiver( } TEST_F(ServerTest, PostWithHeadersAndContentReceiver) { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED using ClientT = SSLClient; #else using ClientT = Client; @@ -5836,7 +5986,7 @@ TEST_F(ServerTest, PostWithHeadersAndContentReceiver) { } TEST_F(ServerTest, PutWithHeadersAndContentReceiver) { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED using ClientT = SSLClient; #else using ClientT = Client; @@ -5850,7 +6000,7 @@ TEST_F(ServerTest, PutWithHeadersAndContentReceiver) { } TEST_F(ServerTest, PatchWithHeadersAndContentReceiver) { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED using ClientT = SSLClient; #else using ClientT = Client; @@ -5894,7 +6044,7 @@ void TestWithHeadersAndContentReceiverWithProgress( } TEST_F(ServerTest, PostWithHeadersAndContentReceiverWithProgress) { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED using ClientT = SSLClient; #else using ClientT = Client; @@ -5908,7 +6058,7 @@ TEST_F(ServerTest, PostWithHeadersAndContentReceiverWithProgress) { } TEST_F(ServerTest, PutWithHeadersAndContentReceiverWithProgress) { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED using ClientT = SSLClient; #else using ClientT = Client; @@ -5922,7 +6072,7 @@ TEST_F(ServerTest, PutWithHeadersAndContentReceiverWithProgress) { } TEST_F(ServerTest, PatchWithHeadersAndContentReceiverWithProgress) { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED using ClientT = SSLClient; #else using ClientT = Client; @@ -5960,7 +6110,7 @@ void TestWithHeadersAndContentReceiverError( } TEST_F(ServerTest, PostWithHeadersAndContentReceiverError) { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED using ClientT = SSLClient; #else using ClientT = Client; @@ -5974,7 +6124,7 @@ TEST_F(ServerTest, PostWithHeadersAndContentReceiverError) { } TEST_F(ServerTest, PuttWithHeadersAndContentReceiverError) { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED using ClientT = SSLClient; #else using ClientT = Client; @@ -5988,7 +6138,7 @@ TEST_F(ServerTest, PuttWithHeadersAndContentReceiverError) { } TEST_F(ServerTest, PatchWithHeadersAndContentReceiverError) { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED using ClientT = SSLClient; #else using ClientT = Client; @@ -6634,7 +6784,7 @@ TEST_F(ServerTest, SendLargeBodyAfterRequestLineError) { ASSERT_TRUE(res); EXPECT_EQ(StatusCode::OK_200, res->status); EXPECT_EQ("Hello World!", res->body); - EXPECT_LE(elapsed, 100); + EXPECT_LE(elapsed, 500); } } @@ -7644,7 +7794,7 @@ TEST(KeepAliveTest, Issue1959) { EXPECT_LT(elapsed, 5000); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(KeepAliveTest, SSLClientReconnection) { SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); ASSERT_TRUE(svr.is_valid()); @@ -7743,8 +7893,12 @@ TEST(KeepAliveTest, SSLClientReconnectionPost) { ASSERT_TRUE(result); EXPECT_EQ(200, result->status); } +#endif +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(SNI_AutoDetectionTest, SNI_Logic) { + using namespace httplib::tls; + { SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); ASSERT_TRUE(svr.is_valid()); @@ -7752,10 +7906,8 @@ TEST(SNI_AutoDetectionTest, SNI_Logic) { svr.Get("/sni", [&](const Request &req, Response &res) { std::string expected; if (req.ssl) { - if (const char *sni = - SSL_get_servername(req.ssl, TLSEXT_NAMETYPE_host_name)) { - expected = sni; - } + // Use backend-agnostic TLS API to get SNI + if (const char *sni = tls_get_sni(req.ssl)) { expected = sni; } } EXPECT_EQ(expected, req.get_param_value("expected")); res.set_content("ok", "text/plain"); @@ -8033,7 +8185,7 @@ TEST(ClientDefaultHeadersTest, DefaultHeaders_Online) { auto path = std::string{"/httpbin/range/32"}; #endif -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED SSLClient cli(host); #else Client cli(host); @@ -8084,7 +8236,7 @@ TEST(ServerDefaultHeadersTest, DefaultHeaders) { EXPECT_EQ("World", res->get_header_value("Hello")); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(KeepAliveTest, ReadTimeoutSSL) { SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); ASSERT_TRUE(svr.is_valid()); @@ -8127,12 +8279,12 @@ class ServerTestWithAI_PASSIVE : public ::testing::Test { protected: ServerTestWithAI_PASSIVE() : cli_(HOST, PORT) -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED , svr_(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE) #endif { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED cli_.enable_server_certificate_verification(false); #endif } @@ -8153,7 +8305,7 @@ protected: t_.join(); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED SSLClient cli_; SSLServer svr_; #else @@ -8204,12 +8356,12 @@ class PayloadMaxLengthTest : public ::testing::Test { protected: PayloadMaxLengthTest() : cli_(HOST, PORT) -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED , svr_(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE) #endif { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED cli_.enable_server_certificate_verification(false); #endif } @@ -8231,7 +8383,7 @@ protected: t_.join(); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED SSLClient cli_; SSLServer svr_; #else @@ -8383,12 +8535,12 @@ class LargePayloadMaxLengthTest : public ::testing::Test { protected: LargePayloadMaxLengthTest() : cli_(HOST, PORT) -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED , svr_(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE) #endif { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED cli_.enable_server_certificate_verification(false); #endif } @@ -8411,7 +8563,7 @@ protected: t_.join(); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED SSLClient cli_; SSLServer svr_; #else @@ -8518,7 +8670,7 @@ TEST(HostAndPortPropertiesTest, NoSSLWithSimpleAPI) { ASSERT_EQ(1234, cli.port()); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(HostAndPortPropertiesTest, SSL) { httplib::SSLClient cli("www.google.com"); ASSERT_EQ("www.google.com", cli.host()); @@ -8526,20 +8678,32 @@ TEST(HostAndPortPropertiesTest, SSL) { } #endif -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT -TEST(SSLClientTest, UpdateCAStore) { +#ifdef CPPHTTPLIB_SSL_ENABLED +TEST(SSLClientTest, UpdateCAStoreWithPem) { + // Test updating CA store multiple times using PEM-based load_ca_cert_store + // This is the backend-agnostic equivalent of UpdateCAStore test + std::string cert; + read_file(CA_CERT_FILE, cert); + httplib::SSLClient httplib_client("www.google.com"); - auto ca_store_1 = X509_STORE_new(); - X509_STORE_load_locations(ca_store_1, "/etc/ssl/certs/ca-certificates.crt", - nullptr); - httplib_client.set_ca_cert_store(ca_store_1); - auto ca_store_2 = X509_STORE_new(); - X509_STORE_load_locations(ca_store_2, "/etc/ssl/certs/ca-certificates.crt", - nullptr); - httplib_client.set_ca_cert_store(ca_store_2); + // Load CA store first time + httplib_client.load_ca_cert_store(cert.data(), cert.size()); + + // Load CA store second time (update) + httplib_client.load_ca_cert_store(cert.data(), cert.size()); + + // Verify client is still valid and can make connections + httplib_client.enable_server_certificate_verification(true); + auto res = httplib_client.Get("/"); + ASSERT_TRUE(res); + // Google may return 200 or 301 depending on various factors + EXPECT_TRUE(res->status == StatusCode::OK_200 || + res->status == StatusCode::MovedPermanently_301); } +#endif +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(SSLClientTest, ServerNameIndication_Online) { #ifdef CPPHTTPLIB_DEFAULT_HTTPBIN auto host = "httpcan.org"; @@ -8554,7 +8718,9 @@ TEST(SSLClientTest, ServerNameIndication_Online) { ASSERT_TRUE(res); ASSERT_EQ(StatusCode::OK_200, res->status); } +#endif +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(SSLClientTest, ServerCertificateVerificationError_Online) { // Use a site that will cause SSL verification failure due to self-signed cert SSLClient cli("self-signed.badssl.com", 443); @@ -8564,14 +8730,17 @@ TEST(SSLClientTest, ServerCertificateVerificationError_Online) { ASSERT_TRUE(!res); EXPECT_EQ(Error::SSLServerVerification, res.error()); - // For SSL server verification errors, ssl_error should be 0, only - // ssl_openssl_error should be set + // Verify backend error is captured for SSLServerVerification + // This occurs when certificate verification fails + // OpenSSL: X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT (18) + // Mbed TLS: MBEDTLS_X509_BADCERT_NOT_TRUSTED or similar flags + EXPECT_NE(0UL, res.ssl_backend_error()); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + // For OpenSSL, ssl_error is 0 for verification errors EXPECT_EQ(0, res.ssl_error()); - - // Verify OpenSSL error is captured for SSLServerVerification - // This occurs when SSL_get_verify_result() returns a verification failure EXPECT_EQ(static_cast(X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT), res.ssl_openssl_error()); +#endif } TEST(SSLClientTest, ServerHostnameVerificationError_Online) { @@ -8584,25 +8753,33 @@ TEST(SSLClientTest, ServerHostnameVerificationError_Online) { auto res = cli.Get("/"); ASSERT_TRUE(!res); - EXPECT_EQ(Error::SSLServerHostnameVerification, res.error()); + // The error type depends on when hostname verification occurs: + // - OpenSSL: SSLServerHostnameVerification (post-handshake verification) + // - Mbed TLS: SSLServerVerification (during handshake) + EXPECT_TRUE(res.error() == Error::SSLServerHostnameVerification || + res.error() == Error::SSLServerVerification); - // For SSL hostname verification errors, ssl_error should be 0, only - // ssl_openssl_error should be set + // Verify backend error is captured for hostname verification failure + EXPECT_NE(0UL, res.ssl_backend_error()); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + // For OpenSSL, ssl_error is 0 for verification errors EXPECT_EQ(0, res.ssl_error()); - - // Verify OpenSSL error is captured for SSLServerHostnameVerification - // This occurs when verify_host() fails due to hostname mismatch EXPECT_EQ(static_cast(X509_V_ERR_HOSTNAME_MISMATCH), res.ssl_openssl_error()); +#endif } +#endif +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(SSLClientTest, ServerCertificateVerification1_Online) { Client cli("https://google.com"); auto res = cli.Get("/"); ASSERT_TRUE(res); ASSERT_EQ(StatusCode::MovedPermanently_301, res->status); } +#endif +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(SSLClientTest, ServerCertificateVerification2_Online) { SSLClient cli("google.com"); cli.set_ca_cert_path(CA_CERT_FILE); @@ -8610,7 +8787,9 @@ TEST(SSLClientTest, ServerCertificateVerification2_Online) { ASSERT_TRUE(res); ASSERT_EQ(StatusCode::MovedPermanently_301, res->status); } +#endif +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(SSLClientTest, ServerCertificateVerification3_Online) { SSLClient cli("google.com"); cli.enable_server_certificate_verification(true); @@ -8620,20 +8799,26 @@ TEST(SSLClientTest, ServerCertificateVerification3_Online) { ASSERT_TRUE(!res); EXPECT_EQ(Error::SSLLoadingCerts, res.error()); - // For SSL_CTX operations, ssl_error should be 0, only ssl_openssl_error + // For SSL_CTX operations, ssl_error should be 0, only ssl_backend_error // should be set EXPECT_EQ(0, res.ssl_error()); - // Verify OpenSSL error is captured for SSLLoadingCerts - // This error occurs when SSL_CTX_load_verify_locations() fails + // Verify backend error is captured for SSLLoadingCerts + // This error occurs when loading CA certificates fails + EXPECT_NE(0UL, res.ssl_backend_error()); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + // OpenSSL specific error codes: // > openssl errstr 0x80000002 // error:80000002:system library::No such file or directory // > openssl errstr 0xA000126 // error:0A000126:SSL routines::unexpected eof while reading EXPECT_TRUE(res.ssl_openssl_error() == 0x80000002 || res.ssl_openssl_error() == 0xA000126); +#endif } +#endif +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(SSLClientTest, ServerCertificateVerification4) { SSLServer svr(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE); ASSERT_TRUE(svr.is_valid()); @@ -8705,18 +8890,6 @@ TEST(SSLClientTest, ServerCertificateVerification6_Online) { ASSERT_EQ(StatusCode::MovedPermanently_301, res->status); } -TEST(SSLClientTest, WildcardHostNameMatch_Online) { - SSLClient cli("www.youtube.com"); - - cli.set_ca_cert_path(CA_CERT_FILE); - cli.enable_server_certificate_verification(true); - cli.set_follow_location(true); - - auto res = cli.Get("/"); - ASSERT_TRUE(res); - ASSERT_EQ(StatusCode::OK_200, res->status); -} - TEST(SSLClientTest, Issue2004_Online) { Client client("https://google.com"); client.set_follow_location(true); @@ -8741,7 +8914,23 @@ TEST(SSLClientTest, ErrorReportingWhenInvalid) { ASSERT_FALSE(res); EXPECT_EQ(Error::SSLConnection, res.error()); } +#endif +#ifdef CPPHTTPLIB_SSL_ENABLED +TEST(SSLClientTest, WildcardHostNameMatch_Online) { + SSLClient cli("www.youtube.com"); + + cli.set_ca_cert_path(CA_CERT_FILE); + cli.enable_server_certificate_verification(true); + cli.set_follow_location(true); + + auto res = cli.Get("/"); + ASSERT_TRUE(res); + ASSERT_EQ(StatusCode::OK_200, res->status); +} +#endif + +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(SSLClientTest, Issue2251_SwappedClientCertAndKey) { // Test for Issue #2251: SSL error not properly reported when client cert // and key paths are swapped or mismatched @@ -8761,28 +8950,44 @@ TEST(SSLClientTest, Issue2251_SwappedClientCertAndKey) { // SSL error should be recorded in the Result object (this is the key fix for // Issue #2251) - auto openssl_error = res.ssl_openssl_error(); - EXPECT_NE(0u, openssl_error); + auto backend_error = res.ssl_backend_error(); + EXPECT_NE(0u, backend_error); } -TEST(SSLClientTest, Issue2251_ClientCertFileNotMatchingKey) { - // Another variant: using valid file paths but with mismatched cert/key pair - // This tests the case where files exist but contain incompatible key material +// Backend-agnostic test using abstract TLS API +// Tests cert/key mismatch detection at the TLS context level +TEST(TlsApiTest, ClientCertKeyMismatch) { + using namespace httplib::tls; - // Using client cert with wrong key (cert2 key) - SSLClient cli("localhost", 8080, "client.cert.pem", "key.pem"); + // Create client context + auto ctx = tls_create_client_context(); + ASSERT_NE(nullptr, ctx); - // Should fail validation - ASSERT_FALSE(cli.is_valid()); + // Try to set mismatched cert/key (client.cert.pem with key.pem from cert2) + // The behavior differs by backend: + // - OpenSSL: tls_set_client_cert_file returns false immediately + // - Mbed TLS: succeeds here but fails during handshake + bool result = + tls_set_client_cert_file(ctx, "client.cert.pem", "key.pem", nullptr); - auto res = cli.Get("/"); - ASSERT_FALSE(res); - // Must report error properly, not appear as success - EXPECT_EQ(Error::SSLConnection, res.error()); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + // OpenSSL checks key consistency in SSL_CTX_use_PrivateKey_file + // by calling SSL_CTX_check_private_key internally after loading. + EXPECT_FALSE(result); + auto err = tls_get_error(); + EXPECT_NE(0u, err); +#endif - // OpenSSL error should be captured in Result - EXPECT_NE(0u, res.ssl_openssl_error()); +#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT + // Mbed TLS doesn't perform cert/key consistency check until handshake. + // The mismatch is tested at connection time by + // Issue2251_SwappedClientCertAndKey + EXPECT_TRUE(result); +#endif + + tls_free_context(ctx); } +#endif #if 0 TEST(SSLClientTest, SetInterfaceWithINET6) { @@ -8798,10 +9003,14 @@ TEST(SSLClientTest, SetInterfaceWithINET6) { } #endif +// ClientCertPresent uses tls_get_peer_cert() - works with all TLS backends +#ifdef CPPHTTPLIB_SSL_ENABLED void ClientCertPresent( const std::string &client_cert_file, const std::string &client_private_key_file, const std::string &client_encrypted_private_key_pass = std::string()) { + using namespace httplib::tls; + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE, CLIENT_CA_CERT_DIR); ASSERT_TRUE(svr.is_valid()); @@ -8809,23 +9018,14 @@ void ClientCertPresent( svr.Get("/test", [&](const Request &req, Response &res) { res.set_content("test", "text/plain"); - auto peer_cert = SSL_get_peer_certificate(req.ssl); + // Get peer certificate using backend-agnostic API + auto peer_cert = tls_get_peer_cert(req.ssl); ASSERT_TRUE(peer_cert != nullptr); - auto subject_name = X509_get_subject_name(peer_cert); - ASSERT_TRUE(subject_name != nullptr); - - std::string common_name; - { - char name[BUFSIZ]; - auto name_len = X509_NAME_get_text_by_NID(subject_name, NID_commonName, - name, sizeof(name)); - common_name.assign(name, static_cast(name_len)); - } - + std::string common_name = tls_get_cert_subject_cn(peer_cert); EXPECT_EQ("Common Name", common_name); - X509_free(peer_cert); + tls_free_cert(peer_cert); }); thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); }); @@ -8856,59 +9056,40 @@ TEST(SSLClientServerTest, ClientEncryptedCertPresent) { CLIENT_ENCRYPTED_PRIVATE_KEY_FILE, CLIENT_ENCRYPTED_PRIVATE_KEY_PASS); } +#endif -#if !defined(_WIN32) || defined(OPENSSL_USE_APPLINK) -void MemoryClientCertPresent( +#ifdef CPPHTTPLIB_SSL_ENABLED +// PEM memory-based constructor tests (works with all TLS backends) +void PemMemoryClientCertPresent( const std::string &client_cert_file, const std::string &client_private_key_file, const std::string &client_encrypted_private_key_pass = std::string()) { - auto f = fopen(SERVER_CERT_FILE, "r+"); - auto server_cert = PEM_read_X509(f, nullptr, nullptr, nullptr); - fclose(f); + // Read PEM files into memory + std::string server_cert_pem, server_key_pem; + std::string client_ca_pem; + std::string client_cert_pem, client_key_pem; - f = fopen(SERVER_PRIVATE_KEY_FILE, "r+"); - auto server_private_key = PEM_read_PrivateKey(f, nullptr, nullptr, nullptr); - fclose(f); + read_file(SERVER_CERT_FILE, server_cert_pem); + read_file(SERVER_PRIVATE_KEY_FILE, server_key_pem); + read_file(CLIENT_CA_CERT_FILE, client_ca_pem); + read_file(client_cert_file, client_cert_pem); + read_file(client_private_key_file, client_key_pem); - f = fopen(CLIENT_CA_CERT_FILE, "r+"); - auto client_cert = PEM_read_X509(f, nullptr, nullptr, nullptr); - auto client_ca_cert_store = X509_STORE_new(); - X509_STORE_add_cert(client_ca_cert_store, client_cert); - X509_free(client_cert); - fclose(f); - - f = fopen(client_cert_file.c_str(), "r+"); - client_cert = PEM_read_X509(f, nullptr, nullptr, nullptr); - fclose(f); - - f = fopen(client_private_key_file.c_str(), "r+"); - auto client_private_key = PEM_read_PrivateKey( - f, nullptr, nullptr, (void *)client_encrypted_private_key_pass.c_str()); - fclose(f); - - SSLServer svr(server_cert, server_private_key, client_ca_cert_store); + // Create server with PEM memory + SSLServer::PemMemory server_pem = { + server_cert_pem.c_str(), + server_cert_pem.size(), + server_key_pem.c_str(), + server_key_pem.size(), + client_ca_pem.c_str(), + client_ca_pem.size(), + nullptr // no password for server key + }; + SSLServer svr(server_pem); ASSERT_TRUE(svr.is_valid()); - svr.Get("/test", [&](const Request &req, Response &res) { + svr.Get("/test", [&](const Request &, Response &res) { res.set_content("test", "text/plain"); - - auto peer_cert = SSL_get_peer_certificate(req.ssl); - ASSERT_TRUE(peer_cert != nullptr); - - auto subject_name = X509_get_subject_name(peer_cert); - ASSERT_TRUE(subject_name != nullptr); - - std::string common_name; - { - char name[BUFSIZ]; - auto name_len = X509_NAME_get_text_by_NID(subject_name, NID_commonName, - name, sizeof(name)); - common_name.assign(name, static_cast(name_len)); - } - - EXPECT_EQ("Common Name", common_name); - - X509_free(peer_cert); }); thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); }); @@ -8920,32 +9101,34 @@ void MemoryClientCertPresent( svr.wait_until_ready(); - SSLClient cli(HOST, PORT, client_cert, client_private_key, - client_encrypted_private_key_pass); + // Create client with PEM memory + const char *password = client_encrypted_private_key_pass.empty() + ? nullptr + : client_encrypted_private_key_pass.c_str(); + SSLClient::PemMemory client_pem = { + client_cert_pem.c_str(), client_cert_pem.size(), client_key_pem.c_str(), + client_key_pem.size(), password}; + SSLClient cli(HOST, PORT, client_pem); cli.enable_server_certificate_verification(false); cli.set_connection_timeout(30); auto res = cli.Get("/test"); ASSERT_TRUE(res); ASSERT_EQ(StatusCode::OK_200, res->status); - - X509_free(server_cert); - EVP_PKEY_free(server_private_key); - X509_free(client_cert); - EVP_PKEY_free(client_private_key); } -TEST(SSLClientServerTest, MemoryClientCertPresent) { - MemoryClientCertPresent(CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE); +TEST(SSLClientServerTest, PemMemoryClientCertPresent) { + PemMemoryClientCertPresent(CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE); } -TEST(SSLClientServerTest, MemoryClientEncryptedCertPresent) { - MemoryClientCertPresent(CLIENT_ENCRYPTED_CERT_FILE, - CLIENT_ENCRYPTED_PRIVATE_KEY_FILE, - CLIENT_ENCRYPTED_PRIVATE_KEY_PASS); +TEST(SSLClientServerTest, PemMemoryClientEncryptedCertPresent) { + PemMemoryClientCertPresent(CLIENT_ENCRYPTED_CERT_FILE, + CLIENT_ENCRYPTED_PRIVATE_KEY_FILE, + CLIENT_ENCRYPTED_PRIVATE_KEY_PASS); } #endif +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(SSLClientServerTest, ClientCertMissing) { SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE, CLIENT_CA_CERT_DIR); @@ -8967,16 +9150,15 @@ TEST(SSLClientServerTest, ClientCertMissing) { auto res = cli.Get("/test"); ASSERT_TRUE(!res); - EXPECT_EQ(Error::SSLServerVerification, res.error()); + // When client cert is missing and server requires it, connection fails + // Error type depends on backend implementation + EXPECT_TRUE(res.error() == Error::SSLServerVerification || + res.error() == Error::SSLConnection); - // For SSL server verification errors, ssl_error should be 0, only - // ssl_openssl_error should be set - EXPECT_EQ(0, res.ssl_error()); - - // Verify OpenSSL error is captured for SSLServerVerification + // Verify backend error is captured // Note: This test may have different error codes depending on the exact // verification failure - EXPECT_NE(0UL, res.ssl_openssl_error()); + EXPECT_NE(0UL, res.ssl_backend_error()); } TEST(SSLClientServerTest, TrustDirOptional) { @@ -9004,7 +9186,9 @@ TEST(SSLClientServerTest, TrustDirOptional) { ASSERT_TRUE(res); ASSERT_EQ(StatusCode::OK_200, res->status); } +#endif +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(SSLClientServerTest, SSLConnectTimeout) { class NoListenSSLServer : public SSLServer { public: @@ -9051,61 +9235,47 @@ TEST(SSLClientServerTest, SSLConnectTimeout) { auto res = cli.Get("/test"); ASSERT_TRUE(!res); EXPECT_EQ(Error::SSLConnection, res.error()); - EXPECT_EQ(SSL_ERROR_WANT_READ, res.ssl_error()); + // Timeout results in WantRead error code (maps to backend-specific value) + EXPECT_NE(0, res.ssl_error()); } +#endif -TEST(SSLClientServerTest, CustomizeServerSSLCtx) { - auto setup_ssl_ctx_callback = [](SSL_CTX &ssl_ctx) { - SSL_CTX_set_options(&ssl_ctx, SSL_OP_NO_COMPRESSION); - SSL_CTX_set_options(&ssl_ctx, - SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); - SSL_CTX_set_options(&ssl_ctx, SSL_OP_NO_SSLv2); - SSL_CTX_set_options(&ssl_ctx, SSL_OP_NO_SSLv3); - SSL_CTX_set_options(&ssl_ctx, SSL_OP_NO_TLSv1); - SSL_CTX_set_options(&ssl_ctx, SSL_OP_NO_TLSv1_1); - auto ciphers = "ECDHE-RSA-AES128-SHA256:" - "ECDHE-DSS-AES128-SHA256:" - "ECDHE-RSA-AES256-SHA256:" - "ECDHE-DSS-AES256-SHA256:"; - SSL_CTX_set_cipher_list(&ssl_ctx, ciphers); - if (SSL_CTX_use_certificate_chain_file(&ssl_ctx, SERVER_CERT_FILE) != 1 || - SSL_CTX_use_PrivateKey_file(&ssl_ctx, SERVER_PRIVATE_KEY_FILE, - SSL_FILETYPE_PEM) != 1) { +// Backend-agnostic equivalent of CustomizeServerSSLCtx test +// Uses void* callback constructor and detail::tls API +#ifdef CPPHTTPLIB_SSL_ENABLED +TEST(SSLClientServerTest, CustomizeServerSSLCtxGeneric) { + using namespace httplib::tls; + + auto setup_callback = [](void *ctx) { + // Load server certificate and key using backend-agnostic API + if (!tls_set_server_cert_file(static_cast(ctx), SERVER_CERT_FILE, + SERVER_PRIVATE_KEY_FILE, nullptr)) { return false; } - SSL_CTX_load_verify_locations(&ssl_ctx, CLIENT_CA_CERT_FILE, - CLIENT_CA_CERT_DIR); - SSL_CTX_set_verify( - &ssl_ctx, - SSL_VERIFY_PEER | - SSL_VERIFY_FAIL_IF_NO_PEER_CERT, // SSL_VERIFY_CLIENT_ONCE, - nullptr); + // Set up client certificate verification + if (!tls_set_client_ca_file(static_cast(ctx), + CLIENT_CA_CERT_FILE, CLIENT_CA_CERT_DIR)) { + return false; + } + tls_set_verify_client(static_cast(ctx), true); return true; }; - SSLServer svr(setup_ssl_ctx_callback); + SSLServer svr(setup_callback); ASSERT_TRUE(svr.is_valid()); svr.Get("/test", [&](const Request &req, Response &res) { res.set_content("test", "text/plain"); - auto peer_cert = SSL_get_peer_certificate(req.ssl); + // Get peer certificate using backend-agnostic API + auto peer_cert = tls_get_peer_cert(req.ssl); ASSERT_TRUE(peer_cert != nullptr); - auto subject_name = X509_get_subject_name(peer_cert); - ASSERT_TRUE(subject_name != nullptr); - - std::string common_name; - { - char name[BUFSIZ]; - auto name_len = X509_NAME_get_text_by_NID(subject_name, NID_commonName, - name, sizeof(name)); - common_name.assign(name, static_cast(name_len)); - } - + // Verify common name using backend-agnostic API + auto common_name = tls_get_cert_subject_cn(peer_cert); EXPECT_EQ("Common Name", common_name); - X509_free(peer_cert); + tls_free_cert(peer_cert); }); thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); }); @@ -9126,6 +9296,172 @@ TEST(SSLClientServerTest, CustomizeServerSSLCtx) { ASSERT_EQ(StatusCode::OK_200, res->status); } +// Test tls_verify_hostname for both OpenSSL and MbedTLS backends +// Verifies that wildcard matching and exact matching work consistently +TEST(SSLClientServerTest, TlsVerifyHostname) { + using namespace httplib::tls; + + // We need a running server to test against + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); + ASSERT_TRUE(svr.is_valid()); + + svr.Get("/test", [](const Request &, Response &res) { + res.set_content("ok", "text/plain"); + }); + + thread t([&]() { svr.listen(HOST, PORT); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + }); + svr.wait_until_ready(); + + // Test via backend-agnostic TlsVerifyCallback + bool verify_callback_called = false; + bool verify_result_wrong = false; + + SSLClient cli(HOST, PORT); + cli.enable_server_certificate_verification(true); + cli.set_ca_cert_path(CA_CERT_FILE); + cli.set_connection_timeout(5); + + // Use the backend-agnostic callback with TLS abstraction types + // Note: Test certificate has CN="Common Name", not "localhost" + bool verify_result_cn = false; + cli.set_server_certificate_verifier([&](const TlsVerifyContext &ctx) -> bool { + verify_callback_called = true; + + if (!ctx.cert) return false; + + // Test 1: "Common Name" should match (our test server cert CN) + verify_result_cn = tls_verify_hostname(ctx.cert, "Common Name"); + + // Test 2: wrong hostname should not match + verify_result_wrong = + tls_verify_hostname(ctx.cert, "wronghost.example.com"); + + return true; // Accept for the purpose of this test + }); + + auto res = cli.Get("/test"); + // The request may succeed or fail depending on cert configuration + // but the callback should have been called + + ASSERT_TRUE(verify_callback_called) + << "Verify callback should have been called"; + + // CN="Common Name" should match our test certificate + EXPECT_TRUE(verify_result_cn) + << "tls_verify_hostname should match 'Common Name' (certificate CN)"; + + // Wrong hostname should not match + EXPECT_FALSE(verify_result_wrong) + << "tls_verify_hostname should not match 'wronghost.example.com'"; +} +#endif + +// mbedTLS-specific callback constructor test +// Tests that the void* callback can customize TLS settings via MbedTlsContext +#ifdef CPPHTTPLIB_MBEDTLS_SUPPORT +TEST(SSLClientServerTest, CustomizeServerSSLCtxMbedTLS) { + using namespace httplib::tls; + + // Track if callback was invoked + bool callback_invoked = false; + + // The callback receives void* ctx which is actually MbedTlsContext* + // We can access the mbedtls_ssl_config via the context + auto setup_callback = [&callback_invoked](void *ctx) { + callback_invoked = true; + + // Cast to MbedTlsContext* to access the ssl config + auto *mbedtls_ctx = static_cast(ctx); + mbedtls_ssl_config *conf = &mbedtls_ctx->conf; + + // Use static variables to hold certificate data (simplified for test) + static mbedtls_x509_crt own_cert; + static mbedtls_pk_context own_key; + static mbedtls_x509_crt ca_chain; + static bool initialized = false; + + if (!initialized) { + mbedtls_x509_crt_init(&own_cert); + mbedtls_pk_init(&own_key); + mbedtls_x509_crt_init(&ca_chain); + + // Load server certificate + if (mbedtls_x509_crt_parse_file(&own_cert, SERVER_CERT_FILE) != 0) { + return false; + } + // Load server private key + if (mbedtls_pk_parse_keyfile(&own_key, SERVER_PRIVATE_KEY_FILE, nullptr +#if MBEDTLS_VERSION_MAJOR >= 3 + , + mbedtls_ctr_drbg_random, nullptr +#endif + ) != 0) { + return false; + } + // Load CA chain for client verification + if (mbedtls_x509_crt_parse_file(&ca_chain, CLIENT_CA_CERT_FILE) != 0) { + return false; + } + initialized = true; + } + + // Configure the SSL config + mbedtls_ssl_conf_own_cert(conf, &own_cert, &own_key); + mbedtls_ssl_conf_ca_chain(conf, &ca_chain, nullptr); + mbedtls_ssl_conf_authmode(conf, MBEDTLS_SSL_VERIFY_REQUIRED); + + // Set minimum TLS version using mbedTLS native API +#if MBEDTLS_VERSION_MAJOR >= 3 + mbedtls_ssl_conf_min_tls_version(conf, MBEDTLS_SSL_VERSION_TLS1_2); +#else + mbedtls_ssl_conf_min_version(conf, MBEDTLS_SSL_MAJOR_VERSION_3, + MBEDTLS_SSL_MINOR_VERSION_3); +#endif + + return true; + }; + + SSLServer svr(setup_callback); + ASSERT_TRUE(svr.is_valid()); + ASSERT_TRUE(callback_invoked); + + svr.Get("/test", [&](const Request &req, Response &res) { + res.set_content("test", "text/plain"); + + // Get peer certificate using backend-agnostic API + auto peer_cert = tls_get_peer_cert(req.ssl); + ASSERT_TRUE(peer_cert != nullptr); + + auto common_name = tls_get_cert_subject_cn(peer_cert); + EXPECT_EQ("Common Name", common_name); + + tls_free_cert(peer_cert); + }); + + thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + ASSERT_FALSE(svr.is_running()); + }); + + svr.wait_until_ready(); + + SSLClient cli(HOST, PORT, CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE); + cli.enable_server_certificate_verification(false); + cli.set_connection_timeout(30); + + auto res = cli.Get("/test"); + ASSERT_TRUE(res); + ASSERT_EQ(StatusCode::OK_200, res->status); +} +#endif + +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(SSLClientServerTest, ClientCAListSentToClient) { SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE); ASSERT_TRUE(svr.is_valid()); @@ -9158,39 +9494,38 @@ TEST(SSLClientServerTest, ClientCAListSentToClient) { ASSERT_TRUE(client_cert_verified); EXPECT_EQ("success", res->body); } +#endif +// ClientCAListSetInContext uses tls_get_peer_cert() - works with all TLS +// backends +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(SSLClientServerTest, ClientCAListSetInContext) { + using namespace httplib::tls; + // Test that when client CA cert file is provided, - // SSL_CTX_set_client_CA_list is called and the CA list is properly set + // the server properly requests and validates client certificates // Create a server with client authentication SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE); ASSERT_TRUE(svr.is_valid()); - // We can't directly access the SSL_CTX from SSLServer to verify, - // but we can test that the server properly requests client certificates - // and accepts valid ones from the specified CA - bool handler_called = false; svr.Get("/test", [&](const Request &req, Response &res) { handler_called = true; // Verify that a client certificate was provided - auto peer_cert = SSL_get_peer_certificate(req.ssl); + // Get peer certificate using backend-agnostic API + auto peer_cert = tls_get_peer_cert(req.ssl); ASSERT_TRUE(peer_cert != nullptr); // Get the issuer name - auto issuer_name = X509_get_issuer_name(peer_cert); - ASSERT_TRUE(issuer_name != nullptr); - - char issuer_buf[256]; - X509_NAME_oneline(issuer_name, issuer_buf, sizeof(issuer_buf)); + std::string issuer_str = tls_get_cert_issuer_name(peer_cert); + ASSERT_FALSE(issuer_str.empty()); // The client certificate should be issued by our test CA - std::string issuer_str(issuer_buf); EXPECT_TRUE(issuer_str.find("Root CA Name") != std::string::npos); - X509_free(peer_cert); + tls_free_cert(peer_cert); res.set_content("authenticated", "text/plain"); }); @@ -9214,7 +9549,142 @@ TEST(SSLClientServerTest, ClientCAListSetInContext) { ASSERT_TRUE(handler_called); EXPECT_EQ("authenticated", res->body); } +#endif +#ifdef CPPHTTPLIB_SSL_ENABLED +TEST(TlsCertIntrospectionTest, GetCertSANs) { + using namespace httplib::tls; + + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); + ASSERT_TRUE(svr.is_valid()); + + svr.Get("/test", [](const Request &, Response &res) { + res.set_content("ok", "text/plain"); + }); + + thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + }); + + svr.wait_until_ready(); + + SSLClient cli(HOST, PORT); + cli.enable_server_certificate_verification(false); + cli.set_connection_timeout(30); + + bool cert_checked = false; + cli.set_server_certificate_verifier([&](const TlsVerifyContext &ctx) -> bool { + if (ctx.cert) { + std::vector sans; + bool result = tls_get_cert_sans(ctx.cert, sans); + // tls_get_cert_sans should return true even if no SANs exist + EXPECT_TRUE(result); + // Test certificate may or may not have SANs - just verify the API + // works If SANs exist, verify the types are valid + for (const auto &san : sans) { + EXPECT_TRUE(san.type == SanType::DNS || san.type == SanType::IP || + san.type == SanType::EMAIL || san.type == SanType::URI || + san.type == SanType::OTHER); + EXPECT_FALSE(san.value.empty()); + } + cert_checked = true; + } + return true; + }); + + auto res = cli.Get("/test"); + ASSERT_TRUE(res); + EXPECT_TRUE(cert_checked); +} + +TEST(TlsCertIntrospectionTest, GetCertValidity) { + using namespace httplib::tls; + + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); + ASSERT_TRUE(svr.is_valid()); + + svr.Get("/test", [](const Request &, Response &res) { + res.set_content("ok", "text/plain"); + }); + + thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + }); + + svr.wait_until_ready(); + + SSLClient cli(HOST, PORT); + cli.enable_server_certificate_verification(false); + cli.set_connection_timeout(30); + + bool validity_checked = false; + cli.set_server_certificate_verifier([&](const TlsVerifyContext &ctx) -> bool { + if (ctx.cert) { + time_t not_before = 0, not_after = 0; + bool result = tls_get_cert_validity(ctx.cert, not_before, not_after); + EXPECT_TRUE(result); + // Verify that not_before < now < not_after for a valid cert + time_t now = time(nullptr); + EXPECT_LT(not_before, now); + EXPECT_GT(not_after, now); + validity_checked = true; + } + return true; + }); + + auto res = cli.Get("/test"); + ASSERT_TRUE(res); + EXPECT_TRUE(validity_checked); +} + +TEST(TlsCertIntrospectionTest, GetCertSerial) { + using namespace httplib::tls; + + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); + ASSERT_TRUE(svr.is_valid()); + + svr.Get("/test", [](const Request &, Response &res) { + res.set_content("ok", "text/plain"); + }); + + thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + }); + + svr.wait_until_ready(); + + SSLClient cli(HOST, PORT); + cli.enable_server_certificate_verification(false); + cli.set_connection_timeout(30); + + bool serial_checked = false; + cli.set_server_certificate_verifier([&](const TlsVerifyContext &ctx) -> bool { + if (ctx.cert) { + std::string serial = tls_get_cert_serial(ctx.cert); + EXPECT_FALSE(serial.empty()); + // Serial should be a hex string + for (char c : serial) { + EXPECT_TRUE((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || + (c >= 'a' && c <= 'f')); + } + serial_checked = true; + } + return true; + }); + + auto res = cli.Get("/test"); + ASSERT_TRUE(res); + EXPECT_TRUE(serial_checked); +} +#endif + +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(SSLClientServerTest, ClientCAListLoadErrorRecorded) { // Test 1: Valid CA file - no error should be recorded { @@ -9250,48 +9720,148 @@ TEST(SSLClientServerTest, ClientCAListLoadErrorRecorded) { if (!svr.is_valid()) { EXPECT_NE(0, svr.ssl_last_error()); } } } +#endif -TEST(SSLClientServerTest, ClientCAListFromX509Store) { - // Test SSL server using X509_STORE constructor with client CA certificates - // This test verifies that Phase 2 implementation correctly extracts CA names - // from an X509_STORE and sets them in the SSL context +#ifdef CPPHTTPLIB_SSL_ENABLED +TEST(TlsVerifyCallbackTest, VerifyContextFields) { + using namespace httplib::tls; - // Load the CA certificate into memory - auto bio = BIO_new_file(CLIENT_CA_CERT_FILE, "r"); - ASSERT_NE(nullptr, bio); + SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); + ASSERT_TRUE(svr.is_valid()); - auto ca_cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr); - BIO_free(bio); - ASSERT_NE(nullptr, ca_cert); + svr.Get("/test", [](const Request &, Response &res) { + res.set_content("ok", "text/plain"); + }); - // Create an X509_STORE and add the CA certificate - auto store = X509_STORE_new(); - ASSERT_NE(nullptr, store); - ASSERT_EQ(1, X509_STORE_add_cert(store, ca_cert)); + thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + }); - // Load server certificate and private key - auto cert_bio = BIO_new_file(SERVER_CERT_FILE, "r"); - ASSERT_NE(nullptr, cert_bio); - auto server_cert = PEM_read_bio_X509(cert_bio, nullptr, nullptr, nullptr); - BIO_free(cert_bio); - ASSERT_NE(nullptr, server_cert); + svr.wait_until_ready(); - auto key_bio = BIO_new_file(SERVER_PRIVATE_KEY_FILE, "r"); - ASSERT_NE(nullptr, key_bio); - auto server_key = PEM_read_bio_PrivateKey(key_bio, nullptr, nullptr, nullptr); - BIO_free(key_bio); - ASSERT_NE(nullptr, server_key); + SSLClient cli(HOST, PORT); + cli.enable_server_certificate_verification(false); + cli.set_connection_timeout(30); - // Create SSLServer with X509_STORE constructor - // Note: X509_STORE ownership is transferred to SSL_CTX - SSLServer svr(server_cert, server_key, store); + int callback_count = 0; + bool saw_leaf_cert = false; + + cli.set_server_certificate_verifier([&](const TlsVerifyContext &ctx) -> bool { + if (ctx.cert) { + callback_count++; + // We should see at least one certificate (the leaf) + std::string cn = tls_get_cert_subject_cn(ctx.cert); + if (!cn.empty()) { saw_leaf_cert = true; } + // Verify context fields are populated + EXPECT_NE(ctx.session, nullptr); + EXPECT_GE(ctx.depth, 0); + } + return true; + }); + + auto res = cli.Get("/test"); + ASSERT_TRUE(res); + EXPECT_GT(callback_count, 0); + EXPECT_TRUE(saw_leaf_cert); +} + +TEST(TlsVerifyErrorTest, GetVerifyErrorString) { + using namespace httplib::tls; + + // Test that tls_verify_error_string returns empty for success + std::string success_str = tls_verify_error_string(0); + EXPECT_TRUE(success_str.empty()); + + // Test that tls_verify_error_string returns non-empty for error codes + // Using a common error code (certificate expired) + std::string error_str = + tls_verify_error_string(10); // X509_V_ERR_CERT_HAS_EXPIRED + EXPECT_FALSE(error_str.empty()); +} +#endif + +#ifdef CPPHTTPLIB_SSL_ENABLED +TEST(CryptoHashTest, BasicHashing) { + namespace crypto = httplib::crypto; + + // Test MD5 + std::string md5_result = crypto::hash(crypto::HashAlgorithm::MD5, "hello"); + EXPECT_EQ("5d41402abc4b2a76b9719d911017c592", md5_result); + + // Test SHA256 + std::string sha256_result = + crypto::hash(crypto::HashAlgorithm::SHA256, "hello"); + EXPECT_EQ("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824", + sha256_result); + + // Test SHA512 + std::string sha512_result = + crypto::hash(crypto::HashAlgorithm::SHA512, "hello"); + EXPECT_EQ( + "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d9" + "9ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043", + sha512_result); +} + +TEST(CryptoHashTest, HashSize) { + namespace crypto = httplib::crypto; + + EXPECT_EQ(16u, crypto::hash_size(crypto::HashAlgorithm::MD5)); + EXPECT_EQ(20u, crypto::hash_size(crypto::HashAlgorithm::SHA1)); + EXPECT_EQ(32u, crypto::hash_size(crypto::HashAlgorithm::SHA256)); + EXPECT_EQ(48u, crypto::hash_size(crypto::HashAlgorithm::SHA384)); + EXPECT_EQ(64u, crypto::hash_size(crypto::HashAlgorithm::SHA512)); +} + +TEST(CryptoHashTest, HashRaw) { + namespace crypto = httplib::crypto; + + std::vector digest; + bool result = + crypto::hash_raw(crypto::HashAlgorithm::SHA256, "test", 4, digest); + EXPECT_TRUE(result); + EXPECT_EQ(32u, digest.size()); + + // Verify first few bytes match known SHA256 of "test" + // SHA256("test") = + // 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08 + EXPECT_EQ(0x9f, digest[0]); + EXPECT_EQ(0x86, digest[1]); + EXPECT_EQ(0xd0, digest[2]); +} +#endif + +#ifdef CPPHTTPLIB_SSL_ENABLED +TEST(SSLClientServerTest, ClientCAListFromPem) { + // Test SSL server using PemMemory constructor with client CA certificates + // This is the backend-agnostic equivalent of ClientCAListFromX509Store test + + // Read PEM files + std::string server_cert_pem, server_key_pem, client_ca_pem; + read_file(SERVER_CERT_FILE, server_cert_pem); + read_file(SERVER_PRIVATE_KEY_FILE, server_key_pem); + read_file(CLIENT_CA_CERT_FILE, client_ca_pem); + + // Create SSLServer with PemMemory constructor including client CA + SSLServer::PemMemory server_pem = { + server_cert_pem.c_str(), + server_cert_pem.size(), + server_key_pem.c_str(), + server_key_pem.size(), + client_ca_pem.c_str(), + client_ca_pem.size(), + nullptr // no password for server key + }; + SSLServer svr(server_pem); ASSERT_TRUE(svr.is_valid()); // No SSL error should be recorded for valid setup EXPECT_EQ(0, svr.ssl_last_error()); // Set up server endpoints - svr.Get("/test-x509store", [&](const Request & /*req*/, Response &res) { + svr.Get("/test-pem-ca", [&](const Request & /*req*/, Response &res) { res.set_content("ok", "text/plain"); }); @@ -9303,20 +9873,17 @@ TEST(SSLClientServerTest, ClientCAListFromX509Store) { SSLClient cli(HOST, PORT, CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE); cli.enable_server_certificate_verification(false); - auto res = cli.Get("/test-x509store"); + auto res = cli.Get("/test-pem-ca"); ASSERT_TRUE(res); EXPECT_EQ(200, res->status); EXPECT_EQ("ok", res->body); - // Clean up - X509_free(server_cert); - EVP_PKEY_free(server_key); - X509_free(ca_cert); - svr.stop(); server_thread.join(); } +#endif +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT // Disabled due to the out-of-memory problem on GitHub Actions Workflows TEST(SSLClientServerTest, DISABLED_LargeDataTransfer) { @@ -9368,7 +9935,7 @@ TEST(CleanupTest, WSACleanup) { } #endif -#ifndef CPPHTTPLIB_OPENSSL_SUPPORT +#ifndef CPPHTTPLIB_SSL_ENABLED TEST(NoSSLSupport, SimpleInterface) { ASSERT_ANY_THROW(Client cli("https://yahoo.com")); } @@ -9499,7 +10066,7 @@ TEST(ServerLargeContentTest, DISABLED_SendLargeContent) { } #endif -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(YahooRedirectTest2, SimpleInterface_Online) { Client cli("http://yahoo.com"); @@ -9551,7 +10118,9 @@ TEST(YahooRedirectTest3, NewResultInterface_Online) { EXPECT_EQ(StatusCode::OK_200, res->status); EXPECT_EQ("https://www.yahoo.com/", res->location); } +#endif +#ifdef CPPHTTPLIB_SSL_ENABLED #ifdef CPPHTTPLIB_BROTLI_SUPPORT TEST(DecodeWithChunkedEncoding, BrotliEncoding_Online) { Client cli("https://cdnjs.cloudflare.com"); @@ -9605,6 +10174,7 @@ TEST(HttpsToHttpRedirectTest3, SimpleInterface_Online) { EXPECT_EQ(StatusCode::OK_200, res->status); } +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(HttpToHttpsRedirectTest, CertFile) { Server svr; ASSERT_TRUE(svr.is_valid()); @@ -9681,6 +10251,7 @@ TEST(SSLClientRedirectTest, CertFile) { ASSERT_TRUE(res); ASSERT_EQ(StatusCode::OK_200, res->status); } +#endif TEST(MultipartFormDataTest, LargeData) { SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); @@ -10691,7 +11262,7 @@ TEST(RedirectTest, RedirectToUrlWithPlusInQueryParameters) { } } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(RedirectTest, Issue2185_Online) { SSLClient client("github.com"); client.set_follow_location(true); @@ -11096,7 +11667,7 @@ TEST(MakeHostAndPortStringTest, VariousPatterns) { detail::make_host_and_port_string("example.com", 65536, false)); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(SSLClientHostHeaderTest, Issue2301_Online) { httplib::SSLClient cli("roblox.com", 443); cli.set_follow_location(true); @@ -11470,7 +12041,7 @@ TEST(MaxTimeoutTest, ContentStream) { max_timeout_test(svr, cli, timeout, threshold); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(MaxTimeoutTest, ContentStreamSSL) { time_t timeout = 2000; time_t threshold = 1200; // SSL_shutdown is slow on some operating systems. @@ -12261,7 +12832,7 @@ TEST_F(OpenStreamTest, Zstd) { } #endif -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED class SSLOpenStreamTest : public ::testing::Test { protected: SSLOpenStreamTest() : svr_("cert.pem", "key.pem") {} @@ -12354,7 +12925,7 @@ TEST_F(SSLOpenStreamTest, PostChunked) { auto body = read_all(handle); EXPECT_EQ("Chunked SSL Data", body); } -#endif // CPPHTTPLIB_OPENSSL_SUPPORT +#endif // CPPHTTPLIB_SSL_ENABLED //============================================================================== // Parity Tests: ensure streaming and non-streaming APIs produce identical @@ -12657,7 +13228,7 @@ TEST_F(StreamApiTest, HeadAndOptions) { } // SSL stream::* tests -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED class SSLStreamApiTest : public ::testing::Test { protected: void SetUp() override { @@ -12804,7 +13375,7 @@ TEST(ErrorHandlingTest, StreamConnectionClosed) { t.join(); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(ErrorHandlingTest, SSLStreamReadTimeout) { // Test that read timeout during SSL streaming is detected SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE); diff --git a/test/test_proxy.cc b/test/test_proxy.cc index 80239c5..66c8ef6 100644 --- a/test/test_proxy.cc +++ b/test/test_proxy.cc @@ -26,7 +26,7 @@ TEST(ProxyTest, NoSSLBasic) { ProxyTest(cli, true); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(ProxyTest, SSLBasic) { SSLClient cli("nghttp2.org"); ProxyTest(cli, true); @@ -51,7 +51,7 @@ void RedirectProxyText(T &cli, const char *path, bool basic) { if (basic) { cli.set_proxy_basic_auth("hello", "world"); } else { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED cli.set_proxy_digest_auth("hello", "world"); #endif } @@ -67,7 +67,7 @@ TEST(RedirectTest, HTTPBinNoSSLBasic) { RedirectProxyText(cli, "/httpbin/redirect/2", true); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(RedirectTest, HTTPBinNoSSLDigest) { Client cli("nghttp2.org"); RedirectProxyText(cli, "/httpbin/redirect/2", false); @@ -84,7 +84,7 @@ TEST(RedirectTest, HTTPBinSSLDigest) { } #endif -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(RedirectTest, YouTubeNoSSLBasic) { Client cli("youtube.com"); RedirectProxyText(cli, "/", true); @@ -157,7 +157,7 @@ TEST(BaseAuthTest, NoSSL) { BaseAuthTestFromHTTPWatch(cli); } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(BaseAuthTest, SSL) { SSLClient cli("httpcan.org"); BaseAuthTestFromHTTPWatch(cli); @@ -166,7 +166,7 @@ TEST(BaseAuthTest, SSL) { // ---------------------------------------------------------------------------- -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED template void DigestAuthTestFromHTTPWatch(T &cli) { cli.set_proxy("localhost", 3129); cli.set_proxy_digest_auth("hello", "world"); @@ -230,13 +230,13 @@ template void KeepAliveTest(T &cli, bool basic) { if (basic) { cli.set_proxy_basic_auth("hello", "world"); } else { -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED cli.set_proxy_digest_auth("hello", "world"); #endif } cli.set_follow_location(true); -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED cli.set_digest_auth("hello", "world"); #endif @@ -274,7 +274,7 @@ template void KeepAliveTest(T &cli, bool basic) { } } -#ifdef CPPHTTPLIB_OPENSSL_SUPPORT +#ifdef CPPHTTPLIB_SSL_ENABLED TEST(KeepAliveTest, NoSSLWithBasic) { Client cli("nghttp2.org"); KeepAliveTest(cli, true);