aboutsummaryrefslogtreecommitdiff
path: root/src/zenhttp/clients/httpwsclient.cpp
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-03-10 17:27:26 +0100
committerGitHub Enterprise <[email protected]>2026-03-10 17:27:26 +0100
commitd0a07e555577dcd4a8f55f1b45d9e8e4e6366ab7 (patch)
tree2dfe1e3e0b620043d358e0b7f8bdf8320d985491 /src/zenhttp/clients/httpwsclient.cpp
parentchangelog entry which was inadvertently omitted from PR merge (diff)
downloadzen-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.cpp213
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;