diff options
| author | Stefan Boberg <[email protected]> | 2026-02-26 15:53:36 +0100 |
|---|---|---|
| committer | Stefan Boberg <[email protected]> | 2026-02-26 15:54:12 +0100 |
| commit | af51224258524c550dbfe7daf30a703d1b7dd7bc (patch) | |
| tree | c0f94d989b550f28cba67e8b7055597d0697b05f /src/zenhttp/servers/httpsys.cpp | |
| parent | httpsys websocket using IOCP instead of thread-per-connection (diff) | |
| download | zen-af51224258524c550dbfe7daf30a703d1b7dd7bc.tar.xz zen-af51224258524c550dbfe7daf30a703d1b7dd7bc.zip | |
httpsys websocket now shares handshake logic with asio for consistency
Diffstat (limited to 'src/zenhttp/servers/httpsys.cpp')
| -rw-r--r-- | src/zenhttp/servers/httpsys.cpp | 312 |
1 files changed, 81 insertions, 231 deletions
diff --git a/src/zenhttp/servers/httpsys.cpp b/src/zenhttp/servers/httpsys.cpp index 89d93e258..23d57af57 100644 --- a/src/zenhttp/servers/httpsys.cpp +++ b/src/zenhttp/servers/httpsys.cpp @@ -162,9 +162,7 @@ private: # 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) @@ -2133,258 +2131,110 @@ InitialRequestHandler::HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesT { HTTP_REQUEST* HttpReq = HttpRequest(); -# if 0 - for (int i = 0; i < HttpReq->RequestInfoCount; ++i) - { - auto& ReqInfo = HttpReq->pRequestInfo[i]; - - switch (ReqInfo.InfoType) - { - case HttpRequestInfoTypeRequestTiming: - { - const HTTP_REQUEST_TIMING_INFO* TimingInfo = reinterpret_cast<HTTP_REQUEST_TIMING_INFO*>(ReqInfo.pInfo); - - ZEN_INFO(""); - } - break; - case HttpRequestInfoTypeAuth: - ZEN_INFO(""); - break; - case HttpRequestInfoTypeChannelBind: - ZEN_INFO(""); - break; - case HttpRequestInfoTypeSslProtocol: - ZEN_INFO(""); - break; - case HttpRequestInfoTypeSslTokenBindingDraft: - ZEN_INFO(""); - break; - case HttpRequestInfoTypeSslTokenBinding: - ZEN_INFO(""); - break; - case HttpRequestInfoTypeTcpInfoV0: - { - const TCP_INFO_v0* TcpInfo = reinterpret_cast<const TCP_INFO_v0*>(ReqInfo.pInfo); - - ZEN_INFO(""); - } - break; - case HttpRequestInfoTypeRequestSizing: - { - const HTTP_REQUEST_SIZING_INFO* SizingInfo = reinterpret_cast<const HTTP_REQUEST_SIZING_INFO*>(ReqInfo.pInfo); - ZEN_INFO(""); - } - break; - case HttpRequestInfoTypeQuicStats: - ZEN_INFO(""); - break; - case HttpRequestInfoTypeTcpInfoV1: - { - const TCP_INFO_v1* TcpInfo = reinterpret_cast<const TCP_INFO_v1*>(ReqInfo.pInfo); - - ZEN_INFO(""); - } - break; - } - } -# endif - if (HttpService* Service = reinterpret_cast<HttpService*>(HttpReq->UrlContext)) { // WebSocket upgrade detection if (m_IsInitialRequest) { - auto& UpgradeHeader = HttpReq->Headers.KnownHeaders[HttpHeaderUpgrade]; + const HTTP_KNOWN_HEADER& 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) + // Extract Sec-WebSocket-Key from the unknown headers + // (http.sys has no known-header slot for it) + std::string_view SecWebSocketKey; + for (USHORT i = 0; i < HttpReq->Headers.UnknownHeaderCount; ++i) { - auto& Hdr = HttpReq->Headers.KnownHeaders[i]; - if (Hdr.RawValueLength > 0) + const HTTP_UNKNOWN_HEADER& Hdr = HttpReq->Headers.pUnknownHeaders[i]; + if (Hdr.NameLength == 17 && _strnicmp(Hdr.pName, "Sec-WebSocket-Key", 17) == 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); + SecWebSocketKey = std::string_view(Hdr.pRawValue, Hdr.RawValueLength); + break; } } - // Unknown headers - for (USHORT i = 0; i < HttpReq->Headers.UnknownHeaderCount; ++i) + if (SecWebSocketKey.empty()) { - 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); + ZEN_WARN("WebSocket upgrade missing Sec-WebSocket-Key header"); + return nullptr; } - // 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, Transaction().Iocp())); - 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); - } + const std::string AcceptKey = WsFrameCodec::ComputeAcceptKey(SecWebSocketKey); + + HANDLE RequestQueueHandle = Transaction().RequestQueueHandle(); + HTTP_REQUEST_ID RequestId = HttpReq->RequestId; + + // Build the 101 Switching Protocols response + HTTP_RESPONSE Response = {}; + Response.StatusCode = 101; + Response.pReason = "Switching Protocols"; + Response.ReasonLength = (USHORT)strlen(Response.pReason); + + Response.Headers.KnownHeaders[HttpHeaderUpgrade].pRawValue = "websocket"; + Response.Headers.KnownHeaders[HttpHeaderUpgrade].RawValueLength = 9; + + eastl::fixed_vector<HTTP_UNKNOWN_HEADER, 8> UnknownHeaders; + + // IMPORTANT: Due to some quirk in HttpSendHttpResponse, this cannot use KnownHeaders + // despite there being an entry for it there (HttpHeaderConnection). If you try to do + // that you get an ERROR_INVALID_PARAMETERS error from HttpSendHttpResponse below - WebSocketDeleteHandle(WsHandle); + UnknownHeaders.push_back({.NameLength = 10, .RawValueLength = 7, .pName = "Connection", .pRawValue = "Upgrade"}); + + UnknownHeaders.push_back({.NameLength = 20, + .RawValueLength = (USHORT)AcceptKey.size(), + .pName = "Sec-WebSocket-Accept", + .pRawValue = AcceptKey.c_str()}); + + Response.Headers.UnknownHeaderCount = (USHORT)UnknownHeaders.size(); + Response.Headers.pUnknownHeaders = UnknownHeaders.data(); + + const 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; } - else + + CloseHandle(SendEvent); + + if (SendResult == NO_ERROR) { - ZEN_WARN("WebSocketCreateServerHandle failed: {:#x}", (uint32_t)Hr); + Ref<WsHttpSysConnection> WsConn( + new WsHttpSysConnection(RequestQueueHandle, RequestId, *WsHandler, Transaction().Iocp())); + Ref<WebSocketConnection> WsConnRef(WsConn.Get()); + + WsHandler->OnWebSocketOpen(std::move(WsConnRef)); + WsConn->Start(); + + return nullptr; } + ZEN_WARN("WebSocket 101 send failed: {}", SendResult); + // WebSocket upgrade failed — return nullptr since ServerRequest() // was never populated (no InvokeRequestHandler call) return nullptr; |