diff --git a/README.md b/README.md index 71bc170..cd933be 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,49 @@ cli.enable_server_hostname_verification(false); > [!NOTE] > When using SSL, it seems impossible to avoid SIGPIPE in all cases, since on some operating systems, SIGPIPE can only be suppressed on a per-message basis, but there is no way to make the OpenSSL library do so for its internal communications. If your program needs to avoid being terminated on SIGPIPE, the only fully general way might be to set up a signal handler for SIGPIPE to handle or ignore it yourself. +### SSL Error Handling + +When SSL operations fail, cpp-httplib provides detailed error information through two separate error fields: + +```c++ +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "path/to/httplib.h" + +httplib::Client cli("https://example.com"); + +auto res = cli.Get("/"); +if (!res) { + // Check the error type + auto err = res.error(); + + switch (err) { + case httplib::Error::SSLConnection: + std::cout << "SSL connection failed, SSL error: " + << res->ssl_error() << std::endl; + break; + + case httplib::Error::SSLLoadingCerts: + std::cout << "SSL cert loading failed, OpenSSL error: " + << std::hex << res->ssl_openssl_error() << std::endl; + break; + + case httplib::Error::SSLServerVerification: + std::cout << "SSL verification failed, X509 error: " + << res->ssl_openssl_error() << std::endl; + break; + + case httplib::Error::SSLServerHostnameVerification: + std::cout << "SSL hostname verification failed, X509 error: " + << res->ssl_openssl_error() << std::endl; + break; + + default: + std::cout << "HTTP error: " << httplib::to_string(err) << std::endl; + } + } +} +``` + Server ------ diff --git a/httplib.h b/httplib.h index bf02a80..c2d9860 100644 --- a/httplib.h +++ b/httplib.h @@ -1246,6 +1246,17 @@ 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; } @@ -1260,6 +1271,13 @@ 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, @@ -1273,6 +1291,10 @@ private: std::unique_ptr res_; Error err_ = Error::Unknown; Headers request_headers_; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + int ssl_error_ = 0; + unsigned long ssl_openssl_error_ = 0; +#endif }; class ClientImpl { @@ -1570,6 +1592,11 @@ protected: Logger 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); @@ -1840,6 +1867,9 @@ private: SSL_CTX *ctx_; std::mutex ctx_mutex_; +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + int last_ssl_error_ = 0; +#endif }; class SSLClient final : public ClientImpl { @@ -8173,7 +8203,12 @@ 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 + return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers), + last_ssl_error_, last_openssl_error_}; +#else return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers)}; +#endif } inline bool ClientImpl::handle_request(Stream &strm, Request &req, @@ -8723,7 +8758,12 @@ inline Result ClientImpl::send_with_content_provider( req, body, content_length, std::move(content_provider), std::move(content_provider_without_length), content_type, error); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + return Result{std::move(res), error, std::move(req.headers), last_ssl_error_, + last_openssl_error_}; +#else return Result{std::move(res), error, std::move(req.headers)}; +#endif } inline std::string @@ -9790,8 +9830,8 @@ inline void ssl_delete(std::mutex &ctx_mutex, SSL *ssl, socket_t sock, 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) { + 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); @@ -9804,6 +9844,7 @@ bool ssl_connect_or_accept_nonblocking(socket_t sock, SSL *ssl, break; default: break; } + if (ssl_error) { *ssl_error = err; } return false; } return true; @@ -9897,9 +9938,10 @@ inline ssize_t SSLSocketStream::read(char *ptr, size_t size) { if (ret >= 0) { return ret; } err = SSL_get_error(ssl_, ret); } else { - return -1; + break; } } + assert(ret < 0); } return ret; } else { @@ -9929,9 +9971,10 @@ inline ssize_t SSLSocketStream::write(const char *ptr, size_t size) { if (ret >= 0) { return ret; } err = SSL_get_error(ssl_, ret); } else { - return -1; + break; } } + assert(ret < 0); } return ret; } @@ -9982,6 +10025,7 @@ inline SSLServer::SSLServer(const char *cert_path, const char *private_key_path, 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) { @@ -10055,7 +10099,8 @@ inline bool SSLServer::process_and_close_socket(socket_t sock) { sock, ctx_, ctx_mutex_, [&](SSL *ssl2) { return detail::ssl_connect_or_accept_nonblocking( - sock, ssl2, SSL_accept, read_timeout_sec_, read_timeout_usec_); + sock, ssl2, SSL_accept, read_timeout_sec_, read_timeout_usec_, + &last_ssl_error_); }, [](SSL * /*ssl2*/) { return true; }); @@ -10123,6 +10168,7 @@ inline SSLClient::SSLClient(const std::string &host, int port, 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_); ctx_ = nullptr; } @@ -10149,6 +10195,7 @@ inline SSLClient::SSLClient(const std::string &host, int port, 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_); ctx_ = nullptr; } @@ -10292,11 +10339,13 @@ inline bool SSLClient::load_certs() { 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(); 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(); ret = false; } } else { @@ -10329,7 +10378,7 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { if (!detail::ssl_connect_or_accept_nonblocking( socket.sock, ssl2, SSL_connect, connection_timeout_sec_, - connection_timeout_usec_)) { + connection_timeout_usec_, &last_ssl_error_)) { error = Error::SSLConnection; return false; } @@ -10342,6 +10391,7 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { } if (verification_status == SSLVerifierResponse::CertificateRejected) { + last_openssl_error_ = ERR_get_error(); error = Error::SSLServerVerification; return false; } @@ -10350,6 +10400,7 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { verify_result_ = SSL_get_verify_result(ssl2); if (verify_result_ != X509_V_OK) { + last_openssl_error_ = static_cast(verify_result_); error = Error::SSLServerVerification; return false; } @@ -10358,12 +10409,14 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { auto se = detail::scope_exit([&] { X509_free(server_cert); }); if (server_cert == nullptr) { + last_openssl_error_ = ERR_get_error(); error = Error::SSLServerVerification; return false; } if (server_hostname_verification_) { if (!verify_host(server_cert)) { + last_openssl_error_ = X509_V_ERR_HOSTNAME_MISMATCH; error = Error::SSLServerHostnameVerification; return false; } diff --git a/test/test.cc b/test/test.cc index 878624e..34a875d 100644 --- a/test/test.cc +++ b/test/test.cc @@ -7492,6 +7492,47 @@ TEST(SSLClientTest, ServerNameIndication_Online) { ASSERT_EQ(StatusCode::OK_200, res->status); } +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); + cli.enable_server_certificate_verification(true); + auto res = cli.Get("/"); + + 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 + 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()); +} + +TEST(SSLClientTest, ServerHostnameVerificationError_Online) { + // Use a site where hostname doesn't match the certificate + // badssl.com provides wrong.host.badssl.com which has cert for *.badssl.com + SSLClient cli("wrong.host.badssl.com", 443); + cli.enable_server_certificate_verification(true); + cli.enable_server_hostname_verification(true); + + auto res = cli.Get("/"); + ASSERT_TRUE(!res); + + EXPECT_EQ(Error::SSLServerHostnameVerification, res.error()); + + // For SSL hostname 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 SSLServerHostnameVerification + // This occurs when verify_host() fails due to hostname mismatch + EXPECT_EQ(static_cast(X509_V_ERR_HOSTNAME_MISMATCH), + res.ssl_openssl_error()); +} + TEST(SSLClientTest, ServerCertificateVerification1_Online) { Client cli("https://google.com"); auto res = cli.Get("/"); @@ -7500,15 +7541,6 @@ TEST(SSLClientTest, ServerCertificateVerification1_Online) { } TEST(SSLClientTest, ServerCertificateVerification2_Online) { - SSLClient cli("google.com"); - cli.enable_server_certificate_verification(true); - cli.set_ca_cert_path("hello"); - auto res = cli.Get("/"); - ASSERT_TRUE(!res); - EXPECT_EQ(Error::SSLLoadingCerts, res.error()); -} - -TEST(SSLClientTest, ServerCertificateVerification3_Online) { SSLClient cli("google.com"); cli.set_ca_cert_path(CA_CERT_FILE); auto res = cli.Get("/"); @@ -7516,6 +7548,29 @@ TEST(SSLClientTest, ServerCertificateVerification3_Online) { ASSERT_EQ(StatusCode::MovedPermanently_301, res->status); } +TEST(SSLClientTest, ServerCertificateVerification3_Online) { + SSLClient cli("google.com"); + cli.enable_server_certificate_verification(true); + cli.set_ca_cert_path("hello"); + + auto res = cli.Get("/"); + ASSERT_TRUE(!res); + EXPECT_EQ(Error::SSLLoadingCerts, res.error()); + + // For SSL_CTX operations, ssl_error should be 0, only ssl_openssl_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 + // > 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); +} + TEST(SSLClientTest, ServerCertificateVerification4) { SSLServer svr(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE); ASSERT_TRUE(svr.is_valid()); @@ -7790,10 +7845,20 @@ TEST(SSLClientServerTest, ClientCertMissing) { svr.wait_until_ready(); SSLClient cli(HOST, PORT); - auto res = cli.Get("/test"); cli.set_connection_timeout(30); + + auto res = cli.Get("/test"); 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 + EXPECT_EQ(0, res.ssl_error()); + + // Verify OpenSSL error is captured for SSLServerVerification + // Note: This test may have different error codes depending on the exact + // verification failure + EXPECT_NE(0UL, res.ssl_openssl_error()); } TEST(SSLClientServerTest, TrustDirOptional) { @@ -7868,6 +7933,7 @@ 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()); } TEST(SSLClientServerTest, CustomizeServerSSLCtx) {