You've already forked cpp-httplib
43
README.md
43
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
|
||||
------
|
||||
|
||||
|
65
httplib.h
65
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<Response> &&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<Response> &&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<Response> 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<Response>();
|
||||
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 <typename U>
|
||||
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<int>(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<unsigned long>(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;
|
||||
}
|
||||
|
86
test/test.cc
86
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<unsigned long>(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<unsigned long>(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) {
|
||||
|
Reference in New Issue
Block a user