From d0a07e555577dcd4a8f55f1b45d9e8e4e6366ab7 Mon Sep 17 00:00:00 2001 From: Stefan Boberg Date: Tue, 10 Mar 2026 17:27:26 +0100 Subject: HttpClient using libcurl, Unix Sockets for HTTP. HTTPS support (#770) The main goal of this change is to eliminate the cpr back-end altogether and replace it with the curl implementation. I would expect to drop cpr as soon as we feel happy with the libcurl back-end. That would leave us with a direct dependency on libcurl only, and cpr can be eliminated as a dependency. ### HttpClient Backend Overhaul - Implemented a new **libcurl-based HttpClient** backend (`httpclientcurl.cpp`, ~2000 lines) as an alternative to the cpr-based one - Made HttpClient backend **configurable at runtime** via constructor arguments and `-httpclient=...` CLI option (for zen, zenserver, and tests) - Extended HttpClient test suite to cover multipart/content-range scenarios ### Unix Domain Socket Support - Added Unix domain socket support to **httpasio** (server side) - Added Unix domain socket support to **HttpClient** - Added Unix domain socket support to **HttpWsClient** (WebSocket client) - Templatized `HttpServerConnectionT` and `WsAsioConnectionT` to handle TCP, Unix, and SSL sockets uniformly via `if constexpr` dispatch ### HTTPS Support - Added **preliminary HTTPS support to httpasio** (for Mac/Linux via OpenSSL) - Added **basic HTTPS support for http.sys** (Windows) - Implemented HTTPS test for httpasio - Split `InitializeServer` into smaller sub-functions for http.sys ### Other Notable Changes - Improved **zenhttp-test stability** with dynamic port allocation - Enhanced port retry logic in http.sys (handles ERROR_ACCESS_DENIED) - Fatal signal/exception handlers for backtrace generation in tests - Added `zen bench http` subcommand to exercise network + HTTP client/server communication stack --- src/zenhttp/clients/httpclientcpr.cpp | 135 +++++++++++++++------------------- 1 file changed, 61 insertions(+), 74 deletions(-) (limited to 'src/zenhttp/clients/httpclientcpr.cpp') diff --git a/src/zenhttp/clients/httpclientcpr.cpp b/src/zenhttp/clients/httpclientcpr.cpp index 14e40b02a..f3082e0a2 100644 --- a/src/zenhttp/clients/httpclientcpr.cpp +++ b/src/zenhttp/clients/httpclientcpr.cpp @@ -14,6 +14,11 @@ #include #include +ZEN_THIRD_PARTY_INCLUDES_START +#include +#include +ZEN_THIRD_PARTY_INCLUDES_END + namespace zen { HttpClientBase* @@ -24,84 +29,42 @@ CreateCprHttpClient(std::string_view BaseUri, const HttpClientSettings& Connecti static std::atomic HttpClientRequestIdCounter{0}; -bool -HttpClient::ErrorContext::IsConnectionError() const +////////////////////////////////////////////////////////////////////////// + +static HttpClientErrorCode +MapCprError(cpr::ErrorCode Code) { - switch (static_cast(ErrorCode)) + switch (Code) { + case cpr::ErrorCode::OK: + return HttpClientErrorCode::kOK; case cpr::ErrorCode::CONNECTION_FAILURE: - case cpr::ErrorCode::OPERATION_TIMEDOUT: + return HttpClientErrorCode::kConnectionFailure; case cpr::ErrorCode::HOST_RESOLUTION_FAILURE: + return HttpClientErrorCode::kHostResolutionFailure; case cpr::ErrorCode::PROXY_RESOLUTION_FAILURE: - return true; + return HttpClientErrorCode::kProxyResolutionFailure; + case cpr::ErrorCode::INTERNAL_ERROR: + return HttpClientErrorCode::kInternalError; + case cpr::ErrorCode::NETWORK_RECEIVE_ERROR: + return HttpClientErrorCode::kNetworkReceiveError; + case cpr::ErrorCode::NETWORK_SEND_FAILURE: + return HttpClientErrorCode::kNetworkSendFailure; + case cpr::ErrorCode::OPERATION_TIMEDOUT: + return HttpClientErrorCode::kOperationTimedOut; + case cpr::ErrorCode::SSL_CONNECT_ERROR: + return HttpClientErrorCode::kSSLConnectError; + case cpr::ErrorCode::SSL_LOCAL_CERTIFICATE_ERROR: + case cpr::ErrorCode::SSL_REMOTE_CERTIFICATE_ERROR: + return HttpClientErrorCode::kSSLCertificateError; + case cpr::ErrorCode::SSL_CACERT_ERROR: + return HttpClientErrorCode::kSSLCACertError; + case cpr::ErrorCode::GENERIC_SSL_ERROR: + return HttpClientErrorCode::kGenericSSLError; + case cpr::ErrorCode::REQUEST_CANCELLED: + return HttpClientErrorCode::kRequestCancelled; default: - return false; - } -} - -// If we want to support different HTTP client implementations then we'll need to make this more abstract - -HttpClientError::ResponseClass -HttpClientError::GetResponseClass() const -{ - if ((cpr::ErrorCode)m_Error != cpr::ErrorCode::OK) - { - switch ((cpr::ErrorCode)m_Error) - { - case cpr::ErrorCode::CONNECTION_FAILURE: - return ResponseClass::kHttpCantConnectError; - case cpr::ErrorCode::HOST_RESOLUTION_FAILURE: - case cpr::ErrorCode::PROXY_RESOLUTION_FAILURE: - return ResponseClass::kHttpNoHost; - case cpr::ErrorCode::INTERNAL_ERROR: - case cpr::ErrorCode::NETWORK_RECEIVE_ERROR: - case cpr::ErrorCode::NETWORK_SEND_FAILURE: - case cpr::ErrorCode::OPERATION_TIMEDOUT: - return ResponseClass::kHttpTimeout; - case cpr::ErrorCode::SSL_CONNECT_ERROR: - case cpr::ErrorCode::SSL_LOCAL_CERTIFICATE_ERROR: - case cpr::ErrorCode::SSL_REMOTE_CERTIFICATE_ERROR: - case cpr::ErrorCode::SSL_CACERT_ERROR: - case cpr::ErrorCode::GENERIC_SSL_ERROR: - return ResponseClass::kHttpSLLError; - default: - return ResponseClass::kHttpOtherClientError; - } - } - else if (IsHttpSuccessCode(m_ResponseCode)) - { - return ResponseClass::kSuccess; - } - else - { - switch (m_ResponseCode) - { - case HttpResponseCode::Unauthorized: - return ResponseClass::kHttpUnauthorized; - case HttpResponseCode::NotFound: - return ResponseClass::kHttpNotFound; - case HttpResponseCode::Forbidden: - return ResponseClass::kHttpForbidden; - case HttpResponseCode::Conflict: - return ResponseClass::kHttpConflict; - case HttpResponseCode::InternalServerError: - return ResponseClass::kHttpInternalServerError; - case HttpResponseCode::ServiceUnavailable: - return ResponseClass::kHttpServiceUnavailable; - case HttpResponseCode::BadGateway: - return ResponseClass::kHttpBadGateway; - case HttpResponseCode::GatewayTimeout: - return ResponseClass::kHttpGatewayTimeout; - default: - if (m_ResponseCode >= HttpResponseCode::InternalServerError) - { - return ResponseClass::kHttpOtherServerError; - } - else - { - return ResponseClass::kHttpOtherClientError; - } - } + return HttpClientErrorCode::kOtherError; } } @@ -257,8 +220,8 @@ CprHttpClient::CommonResponse(std::string_view SessionId, .UploadedBytes = gsl::narrow(HttpResponse.uploaded_bytes), .DownloadedBytes = gsl::narrow(HttpResponse.downloaded_bytes), .ElapsedSeconds = HttpResponse.elapsed, - .Error = HttpClient::ErrorContext{.ErrorCode = gsl::narrow(HttpResponse.error.code), - .ErrorMessage = HttpResponse.error.message}}; + .Error = + HttpClient::ErrorContext{.ErrorCode = MapCprError(HttpResponse.error.code), .ErrorMessage = HttpResponse.error.message}}; } if (WorkResponseCode == HttpResponseCode::NoContent || (HttpResponse.text.empty() && !Payload)) @@ -526,6 +489,10 @@ CprHttpClient::AllocSession(const std::string_view BaseUrl, { CprSession->UpdateHeader({{"UE-Session", std::string(SessionId)}}); } + if (ConnectionSettings.ForbidReuseConnection) + { + CprSession->UpdateHeader({{"Connection", "close"}}); + } if (AccessToken) { CprSession->UpdateHeader({{"Authorization", AccessToken->Value}}); @@ -544,6 +511,26 @@ CprHttpClient::AllocSession(const std::string_view BaseUrl, CprSession->SetParameters({}); } + if (!ConnectionSettings.UnixSocketPath.empty()) + { + CprSession->SetUnixSocket(cpr::UnixSocket(ConnectionSettings.UnixSocketPath)); + } + + if (ConnectionSettings.InsecureSsl || !ConnectionSettings.CaBundlePath.empty()) + { + cpr::SslOptions SslOpts; + if (ConnectionSettings.InsecureSsl) + { + SslOpts.SetOption(cpr::ssl::VerifyHost{false}); + SslOpts.SetOption(cpr::ssl::VerifyPeer{false}); + } + if (!ConnectionSettings.CaBundlePath.empty()) + { + SslOpts.SetOption(cpr::ssl::CaInfo{ConnectionSettings.CaBundlePath}); + } + CprSession->SetSslOptions(SslOpts); + } + ExtendableStringBuilder<128> UrlBuffer; UrlBuffer << BaseUrl << ResourcePath; CprSession->SetUrl(UrlBuffer.c_str()); -- cgit v1.2.3