aboutsummaryrefslogtreecommitdiff
path: root/src/zenhttp/include
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-02-27 17:13:40 +0100
committerGitHub Enterprise <[email protected]>2026-02-27 17:13:40 +0100
commit0a41fd42aa43080fbc991e7d976dde70aeaec594 (patch)
tree765ce661d98b3659a58091afcaad587f03f4bea9 /src/zenhttp/include
parentadd sentry-sdk logger (#793) (diff)
downloadzen-0a41fd42aa43080fbc991e7d976dde70aeaec594.tar.xz
zen-0a41fd42aa43080fbc991e7d976dde70aeaec594.zip
add full WebSocket (RFC 6455) client/server support for zenhttp (#792)
* This branch adds full WebSocket (RFC 6455) support to the HTTP server layer, covering both transport backends, a client, and tests. - **`websocket.h`** -- Core interfaces: `WebSocketOpcode`, `WebSocketMessage`, `WebSocketConnection` (ref-counted), and `IWebSocketHandler`. Services opt in to WebSocket support by implementing `IWebSocketHandler` alongside their existing `HttpService`. - **`httpwsclient.h`** -- `HttpWsClient`: an ASIO-backed `ws://` client with both standalone (own thread) and shared `io_context` modes. Supports connect timeout and optional auth token injection via `IWsClientHandler` callbacks. - **`wsasio.cpp/h`** -- `WsAsioConnection`: WebSocket over ASIO TCP. Takes over the socket after the HTTP 101 handshake and runs an async read/write loop with a queued write path (guarded by `RwLock`). - **`wshttpsys.cpp/h`** -- `WsHttpSysConnection`: WebSocket over http.sys opaque-mode connections (Windows only). Uses `HttpReceiveRequestEntityBody` / `HttpSendResponseEntityBody` via IOCP, sharing the same threadpool as normal http.sys traffic. Self-ref lifetime management ensures graceful drain of outstanding async ops. - **`httpsys_iocontext.h`** -- Tagged `OVERLAPPED` wrapper (`HttpSysIoContext`) used to distinguish normal HTTP transactions from WebSocket read/write completions in the single IOCP callback. - **`wsframecodec.cpp/h`** -- `WsFrameCodec`: static helpers for parsing (unmasked and masked) and building (unmasked server frames and masked client frames) RFC 6455 frames across all three payload length encodings (7-bit, 16-bit, 64-bit). Also computes `Sec-WebSocket-Accept` keys. - **`clients/httpwsclient.cpp`** -- `HttpWsClient::Impl`: ASIO-based client that performs the HTTP upgrade handshake, then hands off to the frame codec for the read loop. Manages its own `io_context` thread or plugs into an external one. - **`httpasio.cpp`** -- ASIO server now detects `Upgrade: websocket` requests, checks the matching `HttpService` for `IWebSocketHandler` via `dynamic_cast`, performs the RFC 6455 handshake (101 response), and spins up a `WsAsioConnection`. - **`httpsys.cpp`** -- Same upgrade detection and handshake logic for the http.sys backend, using `WsHttpSysConnection` and `HTTP_SEND_RESPONSE_FLAG_OPAQUE`. - **`httpparser.cpp/h`** -- Extended to surface the `Upgrade` / `Connection` / `Sec-WebSocket-Key` headers needed by the handshake. - **`httpcommon.h`** -- Minor additions (probably new header constants or response codes for the WS upgrade). - **`httpserver.h`** -- Small interface changes to support WebSocket registration. - **`zenhttp.cpp` / `xmake.lua`** -- New source files wired in; build config updated. - **Unit tests** (`websocket.framecodec`): round-trip encode/decode for text, binary, close frames; all three payload sizes; masked and unmasked variants; RFC 6455 `Sec-WebSocket-Accept` test vector. - **Integration tests** (`websocket.integration`): full ASIO server tests covering handshake (101), normal HTTP coexistence, echo, server-push broadcast, client close handshake, ping/pong auto-response, sequential messages, and rejection of upgrades on non-WS services. - **Client tests** (`websocket.client`): `HttpWsClient` connect+echo+close, connection failure (bad port -> close code 1006), and server-initiated close. * changed HttpRequestParser::ParseCurrentHeader to use switch instead of if/else chain * remove spurious printf --------- Co-authored-by: Stefan Boberg <[email protected]>
Diffstat (limited to 'src/zenhttp/include')
-rw-r--r--src/zenhttp/include/zenhttp/httpcommon.h7
-rw-r--r--src/zenhttp/include/zenhttp/httpserver.h3
-rw-r--r--src/zenhttp/include/zenhttp/httpwsclient.h79
-rw-r--r--src/zenhttp/include/zenhttp/websocket.h65
4 files changed, 153 insertions, 1 deletions
diff --git a/src/zenhttp/include/zenhttp/httpcommon.h b/src/zenhttp/include/zenhttp/httpcommon.h
index bc18549c9..8fca35ac5 100644
--- a/src/zenhttp/include/zenhttp/httpcommon.h
+++ b/src/zenhttp/include/zenhttp/httpcommon.h
@@ -184,6 +184,13 @@ IsHttpSuccessCode(HttpResponseCode HttpCode) noexcept
return IsHttpSuccessCode(int(HttpCode));
}
+[[nodiscard]] inline bool
+IsHttpOk(HttpResponseCode HttpCode) noexcept
+{
+ return HttpCode == HttpResponseCode::OK || HttpCode == HttpResponseCode::Created || HttpCode == HttpResponseCode::Accepted ||
+ HttpCode == HttpResponseCode::NoContent;
+}
+
std::string_view ToString(HttpResponseCode HttpCode);
} // namespace zen
diff --git a/src/zenhttp/include/zenhttp/httpserver.h b/src/zenhttp/include/zenhttp/httpserver.h
index 00cbc6c14..fee932daa 100644
--- a/src/zenhttp/include/zenhttp/httpserver.h
+++ b/src/zenhttp/include/zenhttp/httpserver.h
@@ -462,6 +462,7 @@ struct IHttpStatsService
virtual void UnregisterHandler(std::string_view Id, IHttpStatsProvider& Provider) = 0;
};
-void http_forcelink(); // internal
+void http_forcelink(); // internal
+void websocket_forcelink(); // internal
} // namespace zen
diff --git a/src/zenhttp/include/zenhttp/httpwsclient.h b/src/zenhttp/include/zenhttp/httpwsclient.h
new file mode 100644
index 000000000..926ec1e3d
--- /dev/null
+++ b/src/zenhttp/include/zenhttp/httpwsclient.h
@@ -0,0 +1,79 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include "zenhttp.h"
+
+#include <zenhttp/httpclient.h>
+#include <zenhttp/websocket.h>
+
+ZEN_THIRD_PARTY_INCLUDES_START
+#include <asio/io_context.hpp>
+ZEN_THIRD_PARTY_INCLUDES_END
+
+#include <chrono>
+#include <cstdint>
+#include <functional>
+#include <memory>
+#include <optional>
+#include <span>
+#include <string>
+#include <string_view>
+
+namespace zen {
+
+/**
+ * Callback interface for WebSocket client events
+ *
+ * Separate from the server-side IWebSocketHandler because the caller
+ * already owns the HttpWsClient — no Ref<WebSocketConnection> needed.
+ */
+class IWsClientHandler
+{
+public:
+ virtual ~IWsClientHandler() = default;
+
+ virtual void OnWsOpen() = 0;
+ virtual void OnWsMessage(const WebSocketMessage& Msg) = 0;
+ virtual void OnWsClose(uint16_t Code, std::string_view Reason) = 0;
+};
+
+struct HttpWsClientSettings
+{
+ std::string LogCategory = "wsclient";
+ std::chrono::milliseconds ConnectTimeout{5000};
+ std::optional<std::function<HttpClientAccessToken()>> AccessTokenProvider;
+};
+
+/**
+ * WebSocket client over TCP (ws:// scheme)
+ *
+ * Uses ASIO for async I/O. Two construction modes:
+ * - Internal io_context + background thread (standalone use)
+ * - External io_context (shared event loop, no internal thread)
+ *
+ * Thread-safe for SendText/SendBinary/Close.
+ */
+class HttpWsClient
+{
+public:
+ HttpWsClient(std::string_view Url, IWsClientHandler& Handler, const HttpWsClientSettings& Settings = {});
+ HttpWsClient(std::string_view Url, IWsClientHandler& Handler, asio::io_context& IoContext, const HttpWsClientSettings& Settings = {});
+
+ ~HttpWsClient();
+
+ HttpWsClient(const HttpWsClient&) = delete;
+ HttpWsClient& operator=(const HttpWsClient&) = delete;
+
+ void Connect();
+ void SendText(std::string_view Text);
+ void SendBinary(std::span<const uint8_t> Data);
+ void Close(uint16_t Code = 1000, std::string_view Reason = {});
+ bool IsOpen() const;
+
+private:
+ struct Impl;
+ std::unique_ptr<Impl> m_Impl;
+};
+
+} // namespace zen
diff --git a/src/zenhttp/include/zenhttp/websocket.h b/src/zenhttp/include/zenhttp/websocket.h
new file mode 100644
index 000000000..7a6fb33dd
--- /dev/null
+++ b/src/zenhttp/include/zenhttp/websocket.h
@@ -0,0 +1,65 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#pragma once
+
+#include <zenbase/refcount.h>
+#include <zencore/iobuffer.h>
+
+#include <cstdint>
+#include <span>
+#include <string_view>
+
+namespace zen {
+
+enum class WebSocketOpcode : uint8_t
+{
+ kText = 0x1,
+ kBinary = 0x2,
+ kClose = 0x8,
+ kPing = 0x9,
+ kPong = 0xA
+};
+
+struct WebSocketMessage
+{
+ WebSocketOpcode Opcode;
+ IoBuffer Payload;
+ uint16_t CloseCode = 0;
+};
+
+/**
+ * Represents an active WebSocket connection
+ *
+ * Derived classes implement the actual transport (e.g. ASIO sockets).
+ * Instances are reference-counted so that both the service layer and
+ * the async read/write loop can share ownership.
+ */
+class WebSocketConnection : public RefCounted
+{
+public:
+ virtual ~WebSocketConnection() = default;
+
+ virtual void SendText(std::string_view Text) = 0;
+ virtual void SendBinary(std::span<const uint8_t> Data) = 0;
+ virtual void Close(uint16_t Code = 1000, std::string_view Reason = {}) = 0;
+ virtual bool IsOpen() const = 0;
+};
+
+/**
+ * Interface for services that accept WebSocket upgrades
+ *
+ * An HttpService may additionally implement this interface to indicate
+ * it supports WebSocket connections. The HTTP server checks for this
+ * via dynamic_cast when it sees an Upgrade: websocket request.
+ */
+class IWebSocketHandler
+{
+public:
+ virtual ~IWebSocketHandler() = default;
+
+ virtual void OnWebSocketOpen(Ref<WebSocketConnection> Connection) = 0;
+ virtual void OnWebSocketMessage(WebSocketConnection& Conn, const WebSocketMessage& Msg) = 0;
+ virtual void OnWebSocketClose(WebSocketConnection& Conn, uint16_t Code, std::string_view Reason) = 0;
+};
+
+} // namespace zen