aboutsummaryrefslogtreecommitdiff
path: root/src/zenhttp/servers/httpsys.cpp
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-02-26 12:52:21 +0100
committerStefan Boberg <[email protected]>2026-02-26 12:52:21 +0100
commit4fcfb143d3911409cece5139f929b117f69b99b5 (patch)
tree45bae2d559b5144cd9dd6d36c302793a5233afbb /src/zenhttp/servers/httpsys.cpp
parentmore orderly shutdown for compute server (diff)
downloadzen-4fcfb143d3911409cece5139f929b117f69b99b5.tar.xz
zen-4fcfb143d3911409cece5139f929b117f69b99b5.zip
http.sys websocket support v1 (synchronous websocket comms)
Diffstat (limited to 'src/zenhttp/servers/httpsys.cpp')
-rw-r--r--src/zenhttp/servers/httpsys.cpp206
1 files changed, 206 insertions, 0 deletions
diff --git a/src/zenhttp/servers/httpsys.cpp b/src/zenhttp/servers/httpsys.cpp
index e93ae4853..0b33bc39c 100644
--- a/src/zenhttp/servers/httpsys.cpp
+++ b/src/zenhttp/servers/httpsys.cpp
@@ -156,9 +156,14 @@ private:
#if ZEN_WITH_HTTPSYS
+# include "wshttpsys.h"
+# include "wsframecodec.h"
+
# include <conio.h>
# include <mstcpip.h>
+# include <websocket.h>
# pragma comment(lib, "httpapi.lib")
+# pragma comment(lib, "websocket.lib")
std::wstring
UTF8_to_UTF16(const char* InPtr)
@@ -2169,6 +2174,207 @@ InitialRequestHandler::HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesT
if (HttpService* Service = reinterpret_cast<HttpService*>(HttpReq->UrlContext))
{
+ // WebSocket upgrade detection
+ if (m_IsInitialRequest)
+ {
+ auto& UpgradeHeader = HttpReq->Headers.KnownHeaders[HttpHeaderUpgrade];
+ if (UpgradeHeader.RawValueLength > 0 &&
+ StrCaseCompare(UpgradeHeader.pRawValue, "websocket", UpgradeHeader.RawValueLength) == 0)
+ {
+ if (IWebSocketHandler* WsHandler = dynamic_cast<IWebSocketHandler*>(Service))
+ {
+ // Collect all request headers (known + unknown) for WebSocketBeginServerHandshake
+ eastl::fixed_vector<WEB_SOCKET_HTTP_HEADER, 32> RequestHeaders;
+
+ // Known headers
+ static const char* KnownHeaderNames[] = {"Cache-Control",
+ "Connection",
+ "Date",
+ "Keep-Alive",
+ "Pragma",
+ "Trailer",
+ "Transfer-Encoding",
+ "Upgrade",
+ "Via",
+ "Warning",
+ "Allow",
+ "Content-Length",
+ "Content-Type",
+ "Content-Encoding",
+ "Content-Language",
+ "Content-Location",
+ "Content-MD5",
+ "Content-Range",
+ "Expires",
+ "Last-Modified",
+ "Accept",
+ "Accept-Charset",
+ "Accept-Encoding",
+ "Accept-Language",
+ "Authorization",
+ "Cookie",
+ "Expect",
+ "From",
+ "Host",
+ "If-Match",
+ "If-Modified-Since",
+ "If-None-Match",
+ "If-Range",
+ "If-Unmodified-Since",
+ "Max-Forwards",
+ "Proxy-Authorization",
+ "Referer",
+ "Range",
+ "TE",
+ "Translate",
+ "User-Agent"};
+
+ for (int i = 0; i < HttpHeaderRequestMaximum; ++i)
+ {
+ auto& Hdr = HttpReq->Headers.KnownHeaders[i];
+ if (Hdr.RawValueLength > 0)
+ {
+ WEB_SOCKET_HTTP_HEADER WsHdr;
+ WsHdr.pcName = const_cast<PCHAR>(KnownHeaderNames[i]);
+ WsHdr.ulNameLength = (ULONG)strlen(KnownHeaderNames[i]);
+ WsHdr.pcValue = const_cast<PCHAR>(Hdr.pRawValue);
+ WsHdr.ulValueLength = Hdr.RawValueLength;
+ RequestHeaders.push_back(WsHdr);
+ }
+ }
+
+ // Unknown headers
+ for (USHORT i = 0; i < HttpReq->Headers.UnknownHeaderCount; ++i)
+ {
+ auto& Hdr = HttpReq->Headers.pUnknownHeaders[i];
+ WEB_SOCKET_HTTP_HEADER WsHdr;
+ WsHdr.pcName = const_cast<PCHAR>(Hdr.pName);
+ WsHdr.ulNameLength = Hdr.NameLength;
+ WsHdr.pcValue = const_cast<PCHAR>(Hdr.pRawValue);
+ WsHdr.ulValueLength = Hdr.RawValueLength;
+ RequestHeaders.push_back(WsHdr);
+ }
+
+ // Use Windows WebSocket Protocol Component API for the handshake.
+ // This produces the correct response headers that http.sys expects
+ // when the OPAQUE flag is used.
+ WEB_SOCKET_HANDLE WsHandle = nullptr;
+ HRESULT Hr = WebSocketCreateServerHandle(nullptr, 0, &WsHandle);
+ if (SUCCEEDED(Hr))
+ {
+ PWEB_SOCKET_HTTP_HEADER ResponseHeaders = nullptr;
+ ULONG ResponseHeaderCount = 0;
+
+ Hr = WebSocketBeginServerHandshake(WsHandle,
+ nullptr, // no subprotocol
+ nullptr,
+ 0, // no extensions
+ RequestHeaders.data(),
+ (ULONG)RequestHeaders.size(),
+ &ResponseHeaders,
+ &ResponseHeaderCount);
+
+ if (SUCCEEDED(Hr))
+ {
+ HANDLE RequestQueueHandle = Transaction().RequestQueueHandle();
+ HTTP_REQUEST_ID RequestId = HttpReq->RequestId;
+
+ // Build the 101 response with headers from the WS handshake API
+ HTTP_RESPONSE Response = {};
+ Response.StatusCode = 101;
+ Response.pReason = "Switching Protocols";
+ Response.ReasonLength = (USHORT)strlen(Response.pReason);
+
+ // Convert WEB_SOCKET_HTTP_HEADER[] to HTTP_UNKNOWN_HEADER[]
+ eastl::fixed_vector<HTTP_UNKNOWN_HEADER, 8> UnknownHeaders;
+ for (ULONG i = 0; i < ResponseHeaderCount; ++i)
+ {
+ if (_strnicmp(ResponseHeaders[i].pcName, "Upgrade", ResponseHeaders[i].ulNameLength) == 0)
+ {
+ Response.Headers.KnownHeaders[HttpHeaderUpgrade].pRawValue = ResponseHeaders[i].pcValue;
+ Response.Headers.KnownHeaders[HttpHeaderUpgrade].RawValueLength =
+ (USHORT)ResponseHeaders[i].ulValueLength;
+ }
+ else
+ {
+ HTTP_UNKNOWN_HEADER UH = {};
+ UH.pName = ResponseHeaders[i].pcName;
+ UH.NameLength = (USHORT)ResponseHeaders[i].ulNameLength;
+ UH.pRawValue = ResponseHeaders[i].pcValue;
+ UH.RawValueLength = (USHORT)ResponseHeaders[i].ulValueLength;
+ UnknownHeaders.push_back(UH);
+ }
+ }
+
+ Response.Headers.UnknownHeaderCount = (USHORT)UnknownHeaders.size();
+ Response.Headers.pUnknownHeaders = UnknownHeaders.data();
+
+ ULONG Flags = HTTP_SEND_RESPONSE_FLAG_OPAQUE | HTTP_SEND_RESPONSE_FLAG_MORE_DATA;
+
+ // Use an OVERLAPPED with an event so we can wait synchronously.
+ // The request queue is IOCP-associated, so passing NULL for pOverlapped
+ // may return ERROR_IO_PENDING. Setting the low-order bit of hEvent
+ // prevents IOCP delivery and lets us wait on the event directly.
+ OVERLAPPED SendOverlapped = {};
+ HANDLE SendEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
+ SendOverlapped.hEvent = (HANDLE)((uintptr_t)SendEvent | 1);
+
+ ULONG SendResult = HttpSendHttpResponse(RequestQueueHandle,
+ RequestId,
+ Flags,
+ &Response,
+ nullptr, // CachePolicy
+ nullptr, // BytesSent
+ nullptr, // Reserved1
+ 0, // Reserved2
+ &SendOverlapped,
+ nullptr // LogData
+ );
+
+ if (SendResult == ERROR_IO_PENDING)
+ {
+ WaitForSingleObject(SendEvent, INFINITE);
+ SendResult = (SendOverlapped.Internal == 0) ? NO_ERROR : ERROR_IO_INCOMPLETE;
+ }
+
+ CloseHandle(SendEvent);
+
+ if (SendResult == NO_ERROR)
+ {
+ WebSocketEndServerHandshake(WsHandle);
+ WebSocketDeleteHandle(WsHandle);
+
+ Ref<WsHttpSysConnection> WsConn(new WsHttpSysConnection(RequestQueueHandle, RequestId, *WsHandler));
+ Ref<WebSocketConnection> WsConnRef(WsConn.Get());
+
+ WsHandler->OnWebSocketOpen(std::move(WsConnRef));
+ WsConn->Start();
+
+ return nullptr;
+ }
+
+ ZEN_WARN("WebSocket 101 send failed: {}", SendResult);
+ }
+ else
+ {
+ ZEN_WARN("WebSocketBeginServerHandshake failed: {:#x}", (uint32_t)Hr);
+ }
+
+ WebSocketDeleteHandle(WsHandle);
+ }
+ else
+ {
+ ZEN_WARN("WebSocketCreateServerHandle failed: {:#x}", (uint32_t)Hr);
+ }
+
+ // WebSocket upgrade failed — return nullptr since ServerRequest()
+ // was never populated (no InvokeRequestHandler call)
+ return nullptr;
+ }
+ // Service doesn't support WebSocket or missing key — fall through to normal handling
+ }
+ }
+
if (m_IsInitialRequest)
{
m_ContentLength = GetContentLength(HttpReq);