diff options
| author | Stefan Boberg <[email protected]> | 2026-03-10 17:27:26 +0100 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2026-03-10 17:27:26 +0100 |
| commit | d0a07e555577dcd4a8f55f1b45d9e8e4e6366ab7 (patch) | |
| tree | 2dfe1e3e0b620043d358e0b7f8bdf8320d985491 /src/zenhttp/clients/httpwsclient.cpp | |
| parent | changelog entry which was inadvertently omitted from PR merge (diff) | |
| download | zen-d0a07e555577dcd4a8f55f1b45d9e8e4e6366ab7.tar.xz zen-d0a07e555577dcd4a8f55f1b45d9e8e4e6366ab7.zip | |
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<SocketType>` and `WsAsioConnectionT<SocketType>`
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
Diffstat (limited to 'src/zenhttp/clients/httpwsclient.cpp')
| -rw-r--r-- | src/zenhttp/clients/httpwsclient.cpp | 213 |
1 files changed, 143 insertions, 70 deletions
diff --git a/src/zenhttp/clients/httpwsclient.cpp b/src/zenhttp/clients/httpwsclient.cpp index 9497dadb8..792848a6b 100644 --- a/src/zenhttp/clients/httpwsclient.cpp +++ b/src/zenhttp/clients/httpwsclient.cpp @@ -10,6 +10,9 @@ ZEN_THIRD_PARTY_INCLUDES_START #include <asio.hpp> +#if defined(ASIO_HAS_LOCAL_SOCKETS) +# include <asio/local/stream_protocol.hpp> +#endif ZEN_THIRD_PARTY_INCLUDES_END #include <deque> @@ -47,11 +50,7 @@ struct HttpWsClient::Impl m_WorkGuard.reset(); // Close the socket to cancel pending async ops - if (m_Socket) - { - asio::error_code Ec; - m_Socket->close(Ec); - } + CloseSocket(); if (m_IoThread.joinable()) { @@ -59,6 +58,35 @@ struct HttpWsClient::Impl } } + void CloseSocket() + { + asio::error_code Ec; +#if defined(ASIO_HAS_LOCAL_SOCKETS) + if (m_UnixSocket) + { + m_UnixSocket->close(Ec); + return; + } +#endif + if (m_TcpSocket) + { + m_TcpSocket->close(Ec); + } + } + + template<typename Fn> + void WithSocket(Fn&& Func) + { +#if defined(ASIO_HAS_LOCAL_SOCKETS) + if (m_UnixSocket) + { + Func(*m_UnixSocket); + return; + } +#endif + Func(*m_TcpSocket); + } + void ParseUrl(std::string_view Url) { // Expected format: ws://host:port/path @@ -101,9 +129,47 @@ struct HttpWsClient::Impl m_IoThread = std::thread([this] { m_IoContext.run(); }); } +#if defined(ASIO_HAS_LOCAL_SOCKETS) + if (!m_Settings.UnixSocketPath.empty()) + { + asio::post(m_IoContext, [this] { DoConnectUnix(); }); + return; + } +#endif + asio::post(m_IoContext, [this] { DoResolve(); }); } +#if defined(ASIO_HAS_LOCAL_SOCKETS) + void DoConnectUnix() + { + m_UnixSocket = std::make_unique<asio::local::stream_protocol::socket>(m_IoContext); + + // Start connect timeout timer + m_Timer = std::make_unique<asio::steady_timer>(m_IoContext, m_Settings.ConnectTimeout); + m_Timer->async_wait([this](const asio::error_code& Ec) { + if (!Ec && !m_IsOpen.load(std::memory_order_relaxed)) + { + ZEN_LOG_DEBUG(m_Log, "WebSocket unix connect timeout for {}", m_Settings.UnixSocketPath); + CloseSocket(); + } + }); + + asio::local::stream_protocol::endpoint Endpoint(m_Settings.UnixSocketPath); + m_UnixSocket->async_connect(Endpoint, [this](const asio::error_code& Ec) { + if (Ec) + { + m_Timer->cancel(); + ZEN_LOG_DEBUG(m_Log, "WebSocket unix connect failed for {}: {}", m_Settings.UnixSocketPath, Ec.message()); + m_Handler.OnWsClose(1006, "connect failed"); + return; + } + + DoHandshake(); + }); + } +#endif + void DoResolve() { m_Resolver = std::make_unique<asio::ip::tcp::resolver>(m_IoContext); @@ -122,7 +188,7 @@ struct HttpWsClient::Impl void DoConnect(const asio::ip::tcp::resolver::results_type& Endpoints) { - m_Socket = std::make_unique<asio::ip::tcp::socket>(m_IoContext); + m_TcpSocket = std::make_unique<asio::ip::tcp::socket>(m_IoContext); // Start connect timeout timer m_Timer = std::make_unique<asio::steady_timer>(m_IoContext, m_Settings.ConnectTimeout); @@ -130,15 +196,11 @@ struct HttpWsClient::Impl if (!Ec && !m_IsOpen.load(std::memory_order_relaxed)) { ZEN_LOG_DEBUG(m_Log, "WebSocket connect timeout for {}:{}", m_Host, m_Port); - if (m_Socket) - { - asio::error_code CloseEc; - m_Socket->close(CloseEc); - } + CloseSocket(); } }); - asio::async_connect(*m_Socket, Endpoints, [this](const asio::error_code& Ec, const asio::ip::tcp::endpoint&) { + asio::async_connect(*m_TcpSocket, Endpoints, [this](const asio::error_code& Ec, const asio::ip::tcp::endpoint&) { if (Ec) { m_Timer->cancel(); @@ -194,64 +256,68 @@ struct HttpWsClient::Impl m_HandshakeBuffer = std::make_shared<std::string>(ReqStr); - asio::async_write(*m_Socket, - asio::buffer(m_HandshakeBuffer->data(), m_HandshakeBuffer->size()), - [this](const asio::error_code& Ec, std::size_t) { - if (Ec) - { - m_Timer->cancel(); - ZEN_LOG_DEBUG(m_Log, "WebSocket handshake write failed: {}", Ec.message()); - m_Handler.OnWsClose(1006, "handshake write failed"); - return; - } - - DoReadHandshakeResponse(); - }); + WithSocket([this](auto& Socket) { + asio::async_write(Socket, + asio::buffer(m_HandshakeBuffer->data(), m_HandshakeBuffer->size()), + [this](const asio::error_code& Ec, std::size_t) { + if (Ec) + { + m_Timer->cancel(); + ZEN_LOG_DEBUG(m_Log, "WebSocket handshake write failed: {}", Ec.message()); + m_Handler.OnWsClose(1006, "handshake write failed"); + return; + } + + DoReadHandshakeResponse(); + }); + }); } void DoReadHandshakeResponse() { - asio::async_read_until(*m_Socket, m_ReadBuffer, "\r\n\r\n", [this](const asio::error_code& Ec, std::size_t) { - m_Timer->cancel(); + WithSocket([this](auto& Socket) { + asio::async_read_until(Socket, m_ReadBuffer, "\r\n\r\n", [this](const asio::error_code& Ec, std::size_t) { + m_Timer->cancel(); - if (Ec) - { - ZEN_LOG_DEBUG(m_Log, "WebSocket handshake read failed: {}", Ec.message()); - m_Handler.OnWsClose(1006, "handshake read failed"); - return; - } + if (Ec) + { + ZEN_LOG_DEBUG(m_Log, "WebSocket handshake read failed: {}", Ec.message()); + m_Handler.OnWsClose(1006, "handshake read failed"); + return; + } - // Parse the response - const auto& Data = m_ReadBuffer.data(); - std::string Response(asio::buffers_begin(Data), asio::buffers_end(Data)); + // Parse the response + const auto& Data = m_ReadBuffer.data(); + std::string Response(asio::buffers_begin(Data), asio::buffers_end(Data)); - // Consume the headers from the read buffer (any extra data stays for frame parsing) - auto HeaderEnd = Response.find("\r\n\r\n"); - if (HeaderEnd != std::string::npos) - { - m_ReadBuffer.consume(HeaderEnd + 4); - } + // Consume the headers from the read buffer (any extra data stays for frame parsing) + auto HeaderEnd = Response.find("\r\n\r\n"); + if (HeaderEnd != std::string::npos) + { + m_ReadBuffer.consume(HeaderEnd + 4); + } - // Validate 101 response - if (Response.find("101") == std::string::npos) - { - ZEN_LOG_DEBUG(m_Log, "WebSocket handshake rejected (no 101): {}", Response.substr(0, 80)); - m_Handler.OnWsClose(1006, "handshake rejected"); - return; - } + // Validate 101 response + if (Response.find("101") == std::string::npos) + { + ZEN_LOG_DEBUG(m_Log, "WebSocket handshake rejected (no 101): {}", Response.substr(0, 80)); + m_Handler.OnWsClose(1006, "handshake rejected"); + return; + } - // Validate Sec-WebSocket-Accept - std::string ExpectedAccept = WsFrameCodec::ComputeAcceptKey(m_WebSocketKey); - if (Response.find(ExpectedAccept) == std::string::npos) - { - ZEN_LOG_DEBUG(m_Log, "WebSocket handshake: invalid Sec-WebSocket-Accept"); - m_Handler.OnWsClose(1006, "invalid accept key"); - return; - } + // Validate Sec-WebSocket-Accept + std::string ExpectedAccept = WsFrameCodec::ComputeAcceptKey(m_WebSocketKey); + if (Response.find(ExpectedAccept) == std::string::npos) + { + ZEN_LOG_DEBUG(m_Log, "WebSocket handshake: invalid Sec-WebSocket-Accept"); + m_Handler.OnWsClose(1006, "invalid accept key"); + return; + } - m_IsOpen.store(true); - m_Handler.OnWsOpen(); - EnqueueRead(); + m_IsOpen.store(true); + m_Handler.OnWsOpen(); + EnqueueRead(); + }); }); } @@ -267,8 +333,10 @@ struct HttpWsClient::Impl return; } - asio::async_read(*m_Socket, m_ReadBuffer, asio::transfer_at_least(1), [this](const asio::error_code& Ec, std::size_t) { - OnDataReceived(Ec); + WithSocket([this](auto& Socket) { + asio::async_read(Socket, m_ReadBuffer, asio::transfer_at_least(1), [this](const asio::error_code& Ec, std::size_t) { + OnDataReceived(Ec); + }); }); } @@ -414,9 +482,11 @@ struct HttpWsClient::Impl auto OwnedFrame = std::make_shared<std::vector<uint8_t>>(std::move(Frame)); - asio::async_write(*m_Socket, - asio::buffer(OwnedFrame->data(), OwnedFrame->size()), - [this, OwnedFrame](const asio::error_code& Ec, std::size_t) { OnWriteComplete(Ec); }); + WithSocket([this, OwnedFrame](auto& Socket) { + asio::async_write(Socket, + asio::buffer(OwnedFrame->data(), OwnedFrame->size()), + [this, OwnedFrame](const asio::error_code& Ec, std::size_t) { OnWriteComplete(Ec); }); + }); } void OnWriteComplete(const asio::error_code& Ec) @@ -501,11 +571,14 @@ struct HttpWsClient::Impl // Connection state std::unique_ptr<asio::ip::tcp::resolver> m_Resolver; - std::unique_ptr<asio::ip::tcp::socket> m_Socket; - std::unique_ptr<asio::steady_timer> m_Timer; - asio::streambuf m_ReadBuffer; - std::string m_WebSocketKey; - std::shared_ptr<std::string> m_HandshakeBuffer; + std::unique_ptr<asio::ip::tcp::socket> m_TcpSocket; +#if defined(ASIO_HAS_LOCAL_SOCKETS) + std::unique_ptr<asio::local::stream_protocol::socket> m_UnixSocket; +#endif + std::unique_ptr<asio::steady_timer> m_Timer; + asio::streambuf m_ReadBuffer; + std::string m_WebSocketKey; + std::shared_ptr<std::string> m_HandshakeBuffer; // Write queue RwLock m_WriteLock; |