From 4fcfb143d3911409cece5139f929b117f69b99b5 Mon Sep 17 00:00:00 2001 From: Stefan Boberg Date: Thu, 26 Feb 2026 12:52:21 +0100 Subject: http.sys websocket support v1 (synchronous websocket comms) --- src/zenhttp/servers/httpsys.cpp | 206 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) (limited to 'src/zenhttp/servers/httpsys.cpp') 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 # include +# include # 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(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(Service)) + { + // Collect all request headers (known + unknown) for WebSocketBeginServerHandshake + eastl::fixed_vector 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(KnownHeaderNames[i]); + WsHdr.ulNameLength = (ULONG)strlen(KnownHeaderNames[i]); + WsHdr.pcValue = const_cast(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(Hdr.pName); + WsHdr.ulNameLength = Hdr.NameLength; + WsHdr.pcValue = const_cast(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 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 WsConn(new WsHttpSysConnection(RequestQueueHandle, RequestId, *WsHandler)); + Ref 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); -- cgit v1.2.3