diff options
| author | Stefan Boberg <[email protected]> | 2021-09-09 13:37:15 +0200 |
|---|---|---|
| committer | Stefan Boberg <[email protected]> | 2021-09-09 13:37:15 +0200 |
| commit | f8ca66bd0de36de9a29a409b0e527cede19ef91c (patch) | |
| tree | 3c72f457c57d97eef13650be865faa2ac7e8f202 | |
| parent | Merge branch 'main' into cbpackage-update (diff) | |
| download | zen-f8ca66bd0de36de9a29a409b0e527cede19ef91c.tar.xz zen-f8ca66bd0de36de9a29a409b0e527cede19ef91c.zip | |
Moved http.sys server implementation into dedicated source files
| -rw-r--r-- | zencore/httpserver.cpp | 1251 | ||||
| -rw-r--r-- | zencore/httpsys.cpp | 1226 | ||||
| -rw-r--r-- | zencore/httpsys.h | 58 | ||||
| -rw-r--r-- | zencore/zencore.vcxproj | 2 | ||||
| -rw-r--r-- | zencore/zencore.vcxproj.filters | 2 |
5 files changed, 1289 insertions, 1250 deletions
diff --git a/zencore/httpserver.cpp b/zencore/httpserver.cpp index a9096b99b..b40b74208 100644 --- a/zencore/httpserver.cpp +++ b/zencore/httpserver.cpp @@ -2,13 +2,9 @@ #include <zencore/httpserver.h> -#define _WINSOCKAPI_ -#include <zencore/windows.h> -#include "iothreadpool.h" +#include "httpsys.h" -#include <atlbase.h> #include <conio.h> -#include <http.h> #include <new.h> #include <zencore/compactbinary.h> #include <zencore/compactbinarypackage.h> @@ -26,242 +22,12 @@ #include <doctest/doctest.h> -#if ZEN_PLATFORM_WINDOWS -# pragma comment(lib, "httpapi.lib") -#endif - -////////////////////////////////////////////////////////////////////////// - -std::wstring -UTF8_to_wstring(const char* in) -{ - std::wstring out; - unsigned int codepoint; - - while (*in != 0) - { - unsigned char ch = static_cast<unsigned char>(*in); - - if (ch <= 0x7f) - codepoint = ch; - else if (ch <= 0xbf) - codepoint = (codepoint << 6) | (ch & 0x3f); - else if (ch <= 0xdf) - codepoint = ch & 0x1f; - else if (ch <= 0xef) - codepoint = ch & 0x0f; - else - codepoint = ch & 0x07; - - ++in; - - if (((*in & 0xc0) != 0x80) && (codepoint <= 0x10ffff)) - { - if (sizeof(wchar_t) > 2) - { - out.append(1, static_cast<wchar_t>(codepoint)); - } - else if (codepoint > 0xffff) - { - out.append(1, static_cast<wchar_t>(0xd800 + (codepoint >> 10))); - out.append(1, static_cast<wchar_t>(0xdc00 + (codepoint & 0x03ff))); - } - else if (codepoint < 0xd800 || codepoint >= 0xe000) - { - out.append(1, static_cast<wchar_t>(codepoint)); - } - } - } - - return out; -} - ////////////////////////////////////////////////////////////////////////// -const char* -ReasonStringForHttpResultCode(int HttpCode) -{ - switch (HttpCode) - { - // 1xx Informational - - case 100: - return "Continue"; - case 101: - return "Switching Protocols"; - - // 2xx Success - - case 200: - return "OK"; - case 201: - return "Created"; - case 202: - return "Accepted"; - case 204: - return "No Content"; - case 205: - return "Reset Content"; - case 206: - return "Partial Content"; - - // 3xx Redirection - - case 300: - return "Multiple Choices"; - case 301: - return "Moved Permanently"; - case 302: - return "Found"; - case 303: - return "See Other"; - case 304: - return "Not Modified"; - case 305: - return "Use Proxy"; - case 306: - return "Switch Proxy"; - case 307: - return "Temporary Redirect"; - case 308: - return "Permanent Redirect"; - - // 4xx Client errors - - case 400: - return "Bad Request"; - case 401: - return "Unauthorized"; - case 402: - return "Payment Required"; - case 403: - return "Forbidden"; - case 404: - return "Not Found"; - case 405: - return "Method Not Allowed"; - case 406: - return "Not Acceptable"; - case 407: - return "Proxy Authentication Required"; - case 408: - return "Request Timeout"; - case 409: - return "Conflict"; - case 410: - return "Gone"; - case 411: - return "Length Required"; - case 412: - return "Precondition Failed"; - case 413: - return "Payload Too Large"; - case 414: - return "URI Too Long"; - case 415: - return "Unsupported Media Type"; - case 416: - return "Range Not Satisifiable"; - case 417: - return "Expectation Failed"; - case 418: - return "I'm a teapot"; - case 421: - return "Misdirected Request"; - case 422: - return "Unprocessable Entity"; - case 423: - return "Locked"; - case 424: - return "Failed Dependency"; - case 425: - return "Too Early"; - case 426: - return "Upgrade Required"; - case 428: - return "Precondition Required"; - case 429: - return "Too Many Requests"; - case 431: - return "Request Header Fields Too Large"; - - // 5xx Server errors - - case 500: - return "Internal Server Error"; - case 501: - return "Not Implemented"; - case 502: - return "Bad Gateway"; - case 503: - return "Service Unavailable"; - case 504: - return "Gateway Timeout"; - case 505: - return "HTTP Version Not Supported"; - case 506: - return "Variant Also Negotiates"; - case 507: - return "Insufficient Storage"; - case 508: - return "Loop Detected"; - case 510: - return "Not Extended"; - case 511: - return "Network Authentication Required"; - - default: - return "Unknown Result"; - } -} - namespace zen { using namespace std::literals; -static const uint32_t HashBinary = HashStringDjb2("application/octet-stream"sv); -static const uint32_t HashJson = HashStringDjb2("application/json"sv); -static const uint32_t HashYaml = HashStringDjb2("text/yaml"sv); -static const uint32_t HashText = HashStringDjb2("text/plain"sv); -static const uint32_t HashCompactBinary = HashStringDjb2("application/x-ue-cb"sv); -static const uint32_t HashCompactBinaryPackage = HashStringDjb2("application/x-ue-cbpkg"sv); - -HttpContentType -MapContentType(const std::string_view& ContentTypeString) -{ - if (!ContentTypeString.empty()) - { - const uint32_t CtHash = HashStringDjb2(ContentTypeString); - - if (CtHash == HashBinary) - { - return HttpContentType::kBinary; - } - else if (CtHash == HashCompactBinary) - { - return HttpContentType::kCbObject; - } - else if (CtHash == HashCompactBinaryPackage) - { - return HttpContentType::kCbPackage; - } - else if (CtHash == HashJson) - { - return HttpContentType::kJSON; - } - else if (CtHash == HashYaml) - { - return HttpContentType::kYAML; - } - else if (CtHash == HashText) - { - return HttpContentType::kText; - } - } - - return HttpContentType::kUnknownContentType; -} - ////////////////////////////////////////////////////////////////////////// HttpServerRequest::HttpServerRequest() @@ -475,1021 +241,6 @@ HttpServerRequest::ReadPayloadPackage() } ////////////////////////////////////////////////////////////////////////// -// -// http.sys implementation -// - -#if ZEN_PLATFORM_WINDOWS -class HttpSysServer; -class HttpSysTransaction; -class HttpMessageResponseRequest; - -class HttpSysRequestHandler -{ -public: - HttpSysRequestHandler(HttpSysTransaction& InRequest) : m_Request(InRequest) {} - virtual ~HttpSysRequestHandler() = default; - - virtual void IssueRequest() = 0; - virtual HttpSysRequestHandler* HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesTransferred) = 0; - - HttpSysTransaction& Transaction() { return m_Request; } - -private: - HttpSysTransaction& m_Request; // Outermost HTTP transaction object -}; - -struct InitialRequestHandler : public HttpSysRequestHandler -{ - inline PHTTP_REQUEST HttpRequest() { return (PHTTP_REQUEST)m_RequestBuffer; } - inline uint32_t RequestBufferSize() const { return sizeof m_RequestBuffer; } - - InitialRequestHandler(HttpSysTransaction& InRequest) : HttpSysRequestHandler(InRequest) {} - ~InitialRequestHandler() {} - - virtual void IssueRequest() override; - virtual HttpSysRequestHandler* HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesTransferred) override; - - PHTTP_REQUEST m_HttpRequestPtr = (HTTP_REQUEST*)(m_RequestBuffer); - UCHAR m_RequestBuffer[16384 + sizeof(HTTP_REQUEST)]; -}; - -class HttpSysServerRequest : public HttpServerRequest -{ -public: - HttpSysServerRequest() = default; - HttpSysServerRequest(HttpSysTransaction& Tx, HttpService& Service); - ~HttpSysServerRequest() = default; - - virtual void ReadPayload(std::function<void(HttpServerRequest&, IoBuffer)>&& CompletionHandler) override; - virtual IoBuffer ReadPayload() override; - virtual void WriteResponse(HttpResponse HttpResponseCode) override; - virtual void WriteResponse(HttpResponse HttpResponseCode, HttpContentType ContentType, std::span<IoBuffer> Blobs) override; - virtual void WriteResponse(HttpResponse HttpResponseCode, HttpContentType ContentType, std::u8string_view ResponseString) override; - - bool m_IsInitialized = false; - HttpSysTransaction& m_HttpTx; - HttpMessageResponseRequest* m_Response = nullptr; // TODO: make this more general -}; - -/** HTTP transaction - - There will be an instance of this per pending and in-flight HTTP transaction - - */ -class HttpSysTransaction -{ -public: - HttpSysTransaction(HttpSysServer& Server) : m_HttpServer(Server), m_HttpHandler(&m_InitialHttpHandler) {} - - virtual ~HttpSysTransaction() {} - - enum class Status - { - kDone, - kRequestPending - }; - - Status HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesTransferred); - - static void __stdcall IoCompletionCallback(PTP_CALLBACK_INSTANCE Instance, - PVOID pContext /* HttpSysServer */, - PVOID pOverlapped, - ULONG IoResult, - ULONG_PTR NumberOfBytesTransferred, - PTP_IO Io) - { - UNREFERENCED_PARAMETER(Io); - UNREFERENCED_PARAMETER(Instance); - UNREFERENCED_PARAMETER(pContext); - - // Note that for a given transaction we may be in this completion function on more - // than one thread at any given moment. This means we need to be careful about what - // happens in here - - HttpSysTransaction* Transaction = CONTAINING_RECORD(pOverlapped, HttpSysTransaction, m_HttpOverlapped); - - if (Transaction->HandleCompletion(IoResult, NumberOfBytesTransferred) == HttpSysTransaction::Status::kDone) - { - delete Transaction; - } - } - - void IssueInitialRequest(); - PTP_IO Iocp(); - HANDLE RequestQueueHandle(); - inline OVERLAPPED* Overlapped() { return &m_HttpOverlapped; } - inline HttpSysServer& Server() { return m_HttpServer; } - - inline PHTTP_REQUEST HttpRequest() { return m_InitialHttpHandler.HttpRequest(); } - -protected: - OVERLAPPED m_HttpOverlapped{}; - HttpSysServer& m_HttpServer; - HttpSysRequestHandler* m_HttpHandler{nullptr}; - RwLock m_Lock; - -private: - InitialRequestHandler m_InitialHttpHandler{*this}; -}; - -////////////////////////////////////////////////////////////////////////// - -class HttpPayloadReadRequest : public HttpSysRequestHandler -{ -public: - HttpPayloadReadRequest(HttpSysTransaction& InRequest) : HttpSysRequestHandler(InRequest) {} - - virtual void IssueRequest() override; - virtual HttpSysRequestHandler* HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesTransferred) override; -}; - -void -HttpPayloadReadRequest::IssueRequest() -{ -} - -HttpSysRequestHandler* -HttpPayloadReadRequest::HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesTransferred) -{ - ZEN_UNUSED(IoResult, NumberOfBytesTransferred); - return nullptr; -} - -////////////////////////////////////////////////////////////////////////// - -class HttpMessageResponseRequest : public HttpSysRequestHandler -{ -public: - HttpMessageResponseRequest(HttpSysTransaction& InRequest, uint16_t ResponseCode); - HttpMessageResponseRequest(HttpSysTransaction& InRequest, uint16_t ResponseCode, const char* Message); - HttpMessageResponseRequest(HttpSysTransaction& InRequest, uint16_t ResponseCode, const void* Payload, size_t PayloadSize); - HttpMessageResponseRequest(HttpSysTransaction& InRequest, uint16_t ResponseCode, std::span<IoBuffer> Blobs); - ~HttpMessageResponseRequest(); - - virtual void IssueRequest() override; - virtual HttpSysRequestHandler* HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesTransferred) override; - - void SuppressResponseBody(); - -private: - std::vector<HTTP_DATA_CHUNK> m_HttpDataChunks; - uint64_t m_TotalDataSize = 0; // Sum of all chunk sizes - - uint16_t m_HttpResponseCode = 0; - uint32_t m_NextDataChunkOffset = 0; // This is used for responses where the number of chunks exceed the maximum number for one API call - uint32_t m_RemainingChunkCount = 0; - bool m_IsInitialResponse = true; - - void Initialize(uint16_t ResponseCode, std::span<IoBuffer> Blobs); - - std::vector<IoBuffer> m_DataBuffers; -}; - -HttpMessageResponseRequest::HttpMessageResponseRequest(HttpSysTransaction& InRequest, uint16_t ResponseCode) -: HttpSysRequestHandler(InRequest) -{ - std::array<IoBuffer, 0> buffers; - - Initialize(ResponseCode, buffers); -} - -HttpMessageResponseRequest::HttpMessageResponseRequest(HttpSysTransaction& InRequest, uint16_t ResponseCode, const char* Message) -: HttpSysRequestHandler(InRequest) -{ - IoBuffer MessageBuffer(IoBuffer::Wrap, Message, strlen(Message)); - std::array<IoBuffer, 1> buffers({MessageBuffer}); - - Initialize(ResponseCode, buffers); -} - -HttpMessageResponseRequest::HttpMessageResponseRequest(HttpSysTransaction& InRequest, - uint16_t ResponseCode, - const void* Payload, - size_t PayloadSize) -: HttpSysRequestHandler(InRequest) -{ - IoBuffer MessageBuffer(IoBuffer::Wrap, Payload, PayloadSize); - std::array<IoBuffer, 1> buffers({MessageBuffer}); - - Initialize(ResponseCode, buffers); -} - -HttpMessageResponseRequest::HttpMessageResponseRequest(HttpSysTransaction& InRequest, uint16_t ResponseCode, std::span<IoBuffer> Blobs) -: HttpSysRequestHandler(InRequest) -{ - Initialize(ResponseCode, Blobs); -} - -HttpMessageResponseRequest::~HttpMessageResponseRequest() -{ -} - -void -HttpMessageResponseRequest::Initialize(uint16_t ResponseCode, std::span<IoBuffer> Blobs) -{ - m_HttpResponseCode = ResponseCode; - - const uint32_t ChunkCount = (uint32_t)Blobs.size(); - - m_HttpDataChunks.resize(ChunkCount); - m_DataBuffers.reserve(ChunkCount); - - for (IoBuffer& Buffer : Blobs) - { - m_DataBuffers.emplace_back(std::move(Buffer)).MakeOwned(); - } - - // Initialize the full array up front - - uint64_t LocalDataSize = 0; - - { - PHTTP_DATA_CHUNK ChunkPtr = m_HttpDataChunks.data(); - - for (IoBuffer& Buffer : m_DataBuffers) - { - const ULONG BufferDataSize = (ULONG)Buffer.Size(); - - ZEN_ASSERT(BufferDataSize); - - IoBufferFileReference FileRef; - if (Buffer.GetFileReference(/* out */ FileRef)) - { - ChunkPtr->DataChunkType = HttpDataChunkFromFileHandle; - ChunkPtr->FromFileHandle.FileHandle = FileRef.FileHandle; - ChunkPtr->FromFileHandle.ByteRange.StartingOffset.QuadPart = FileRef.FileChunkOffset; - ChunkPtr->FromFileHandle.ByteRange.Length.QuadPart = BufferDataSize; - } - else - { - ChunkPtr->DataChunkType = HttpDataChunkFromMemory; - ChunkPtr->FromMemory.pBuffer = (void*)Buffer.Data(); - ChunkPtr->FromMemory.BufferLength = BufferDataSize; - } - ++ChunkPtr; - - LocalDataSize += BufferDataSize; - } - } - - m_RemainingChunkCount = ChunkCount; - m_TotalDataSize = LocalDataSize; -} - -void -HttpMessageResponseRequest::SuppressResponseBody() -{ - m_RemainingChunkCount = 0; - m_HttpDataChunks.clear(); - m_DataBuffers.clear(); -} - -HttpSysRequestHandler* -HttpMessageResponseRequest::HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesTransferred) -{ - ZEN_UNUSED(NumberOfBytesTransferred); - ZEN_UNUSED(IoResult); - - if (m_RemainingChunkCount == 0) - { - return nullptr; // All done - } - - return this; -} - -void -HttpMessageResponseRequest::IssueRequest() -{ - HttpSysTransaction& Tx = Transaction(); - HTTP_REQUEST* const HttpReq = Tx.HttpRequest(); - PTP_IO const Iocp = Tx.Iocp(); - - StartThreadpoolIo(Iocp); - - // Split payload into batches to play well with the underlying API - - const int MaxChunksPerCall = 9999; - - const int ThisRequestChunkCount = std::min<int>(m_RemainingChunkCount, MaxChunksPerCall); - const int ThisRequestChunkOffset = m_NextDataChunkOffset; - - m_RemainingChunkCount -= ThisRequestChunkCount; - m_NextDataChunkOffset += ThisRequestChunkCount; - - ULONG SendFlags = 0; - - if (m_RemainingChunkCount) - { - // We need to make more calls to send the full amount of data - SendFlags |= HTTP_SEND_RESPONSE_FLAG_MORE_DATA; - } - - ULONG SendResult = 0; - - if (m_IsInitialResponse) - { - // Populate response structure - - HTTP_RESPONSE HttpResponse = {}; - - HttpResponse.EntityChunkCount = USHORT(ThisRequestChunkCount); - HttpResponse.pEntityChunks = m_HttpDataChunks.data() + ThisRequestChunkOffset; - - // Content-length header - - char ContentLengthString[32]; - _ui64toa_s(m_TotalDataSize, ContentLengthString, sizeof ContentLengthString, 10); - - PHTTP_KNOWN_HEADER ContentLengthHeader = &HttpResponse.Headers.KnownHeaders[HttpHeaderContentLength]; - ContentLengthHeader->pRawValue = ContentLengthString; - ContentLengthHeader->RawValueLength = (USHORT)strlen(ContentLengthString); - - // Content-type header - - PHTTP_KNOWN_HEADER ContentTypeHeader = &HttpResponse.Headers.KnownHeaders[HttpHeaderContentType]; - - ContentTypeHeader->pRawValue = "application/octet-stream"; /* TODO! We must respect the content type specified */ - ContentTypeHeader->RawValueLength = (USHORT)strlen(ContentTypeHeader->pRawValue); - - HttpResponse.StatusCode = m_HttpResponseCode; - HttpResponse.pReason = ReasonStringForHttpResultCode(m_HttpResponseCode); - HttpResponse.ReasonLength = (USHORT)strlen(HttpResponse.pReason); - - // Cache policy - - HTTP_CACHE_POLICY CachePolicy; - - CachePolicy.Policy = HttpCachePolicyNocache; // HttpCachePolicyUserInvalidates; - CachePolicy.SecondsToLive = 0; - - // Initial response API call - - SendResult = HttpSendHttpResponse(Tx.RequestQueueHandle(), - HttpReq->RequestId, - SendFlags, - &HttpResponse, - &CachePolicy, - NULL, - NULL, - 0, - Tx.Overlapped(), - NULL); - - m_IsInitialResponse = false; - } - else - { - // Subsequent response API calls - - SendResult = HttpSendResponseEntityBody(Tx.RequestQueueHandle(), - HttpReq->RequestId, - SendFlags, - (USHORT)ThisRequestChunkCount, // EntityChunkCount - &m_HttpDataChunks[ThisRequestChunkOffset], // EntityChunks - NULL, // BytesSent - NULL, // Reserved1 - 0, // Reserved2 - Tx.Overlapped(), // Overlapped - NULL // LogData - ); - } - - if ((SendResult != NO_ERROR) // Synchronous completion, but the completion event will still be posted to IOCP - && (SendResult != ERROR_IO_PENDING) // Asynchronous completion - ) - { - // Some error occurred, no completion will be posted - - CancelThreadpoolIo(Iocp); - - spdlog::error("failed to send HTTP response (error: {}) URL: {}", SendResult, HttpReq->pRawUrl); - - throw HttpServerException("Failed to send HTTP response", SendResult); - } -} - -////////////////////////////////////////////////////////////////////////// - -class HttpSysServer -{ - friend class HttpSysTransaction; - -public: - HttpSysServer(WinIoThreadPool& InThreadPool); - ~HttpSysServer(); - - void Initialize(const wchar_t* UrlPath); - void Run(bool TestMode); - - void RequestExit() { m_ShutdownEvent.Set(); } - - void StartServer(); - void StopServer(); - - void OnHandlingRequest(); - void IssueNewRequestMaybe(); - - inline bool IsOk() const { return m_IsOk; } - - void AddEndpoint(const char* Endpoint, HttpService& Service); - void RemoveEndpoint(const char* Endpoint, HttpService& Service); - -private: - bool m_IsOk = false; - bool m_IsHttpInitialized = false; - WinIoThreadPool& m_ThreadPool; - - std::wstring m_BaseUri; // http://*:nnnn/ - HTTP_SERVER_SESSION_ID m_HttpSessionId = 0; - HTTP_URL_GROUP_ID m_HttpUrlGroupId = 0; - HANDLE m_RequestQueueHandle = 0; - std::atomic_int32_t m_PendingRequests{0}; - int32_t m_MinPendingRequests = 4; - int32_t m_MaxPendingRequests = 32; - Event m_ShutdownEvent; -}; - -HttpSysServer::HttpSysServer(WinIoThreadPool& InThreadPool) : m_ThreadPool(InThreadPool) -{ - ULONG Result = HttpInitialize(HTTPAPI_VERSION_2, HTTP_INITIALIZE_SERVER, nullptr); - - if (Result != NO_ERROR) - { - return; - } - - m_IsHttpInitialized = true; - m_IsOk = true; -} - -HttpSysServer::~HttpSysServer() -{ - if (m_IsHttpInitialized) - { - HttpTerminate(HTTP_INITIALIZE_SERVER, nullptr); - } -} - -void -HttpSysServer::Initialize(const wchar_t* UrlPath) -{ - // check(bIsOk); - - ULONG Result = HttpCreateServerSession(HTTPAPI_VERSION_2, &m_HttpSessionId, 0); - - if (Result != NO_ERROR) - { - // Flag error - - return; - } - - Result = HttpCreateUrlGroup(m_HttpSessionId, &m_HttpUrlGroupId, 0); - - if (Result != NO_ERROR) - { - // Flag error - - return; - } - - m_BaseUri = UrlPath; - - Result = HttpAddUrlToUrlGroup(m_HttpUrlGroupId, UrlPath, /* #TODO UrlContext */ HTTP_URL_CONTEXT(0), 0); - - if (Result != NO_ERROR) - { - // Flag error - - return; - } - - HTTP_BINDING_INFO HttpBindingInfo = {{0}, 0}; - - Result = HttpCreateRequestQueue(HTTPAPI_VERSION_2, NULL, NULL, 0, &m_RequestQueueHandle); - - if (Result != NO_ERROR) - { - // Flag error! - - return; - } - - HttpBindingInfo.Flags.Present = 1; - HttpBindingInfo.RequestQueueHandle = m_RequestQueueHandle; - - Result = HttpSetUrlGroupProperty(m_HttpUrlGroupId, HttpServerBindingProperty, &HttpBindingInfo, sizeof(HttpBindingInfo)); - - if (Result != NO_ERROR) - { - // Flag error! - - return; - } - - // Create I/O completion port - - m_ThreadPool.CreateIocp(m_RequestQueueHandle, HttpSysTransaction::IoCompletionCallback, this); - - // Check result! -} - -void -HttpSysServer::StartServer() -{ - int RequestCount = 32; - - for (int i = 0; i < RequestCount; ++i) - { - IssueNewRequestMaybe(); - } -} - -void -HttpSysServer::Run(bool TestMode) -{ - if (TestMode == false) - { - zen::logging::ConsoleLog().info("Zen Server running. Press ESC or Q to quit"); - } - - do - { - int WaitTimeout = -1; - - if (!TestMode) - { - WaitTimeout = 1000; - } - - if (!TestMode && _kbhit() != 0) - { - char c = (char)_getch(); - - if (c == 27 || c == 'Q' || c == 'q') - { - RequestApplicationExit(0); - } - } - - m_ShutdownEvent.Wait(WaitTimeout); - } while (!IsApplicationExitRequested()); -} - -void -HttpSysServer::OnHandlingRequest() -{ - --m_PendingRequests; - - if (m_PendingRequests > m_MinPendingRequests) - { - // We have more than the minimum number of requests pending, just let someone else - // enqueue new requests - return; - } - - IssueNewRequestMaybe(); -} - -void -HttpSysServer::IssueNewRequestMaybe() -{ - if (m_PendingRequests.load(std::memory_order::relaxed) >= m_MaxPendingRequests) - { - return; - } - - std::unique_ptr<HttpSysTransaction> Request = std::make_unique<HttpSysTransaction>(*this); - - Request->IssueInitialRequest(); - - // This may end up exceeding the MaxPendingRequests limit, but it's not - // really a problem. I'm doing it this way mostly to avoid dealing with - // exceptions here - ++m_PendingRequests; - - Request.release(); -} - -void -HttpSysServer::StopServer() -{ -} - -void -HttpSysServer::AddEndpoint(const char* UrlPath, HttpService& Service) -{ - if (UrlPath[0] == '/') - { - ++UrlPath; - } - - const std::wstring Path16 = UTF8_to_wstring(UrlPath); - Service.SetUriPrefixLength(Path16.size() + 1 /* leading slash */); - - // Convert to wide string - - std::wstring Url16 = m_BaseUri + Path16; - - ULONG Result = HttpAddUrlToUrlGroup(m_HttpUrlGroupId, Url16.c_str(), HTTP_URL_CONTEXT(&Service), 0 /* Reserved */); - - if (Result != NO_ERROR) - { - spdlog::error("HttpAddUrlToUrlGroup failed with result {}", Result); - - return; - } -} - -void -HttpSysServer::RemoveEndpoint(const char* UrlPath, HttpService& Service) -{ - ZEN_UNUSED(Service); - - if (UrlPath[0] == '/') - { - ++UrlPath; - } - - const std::wstring Path16 = UTF8_to_wstring(UrlPath); - - // Convert to wide string - - std::wstring Url16 = m_BaseUri + Path16; - - ULONG Result = HttpRemoveUrlFromUrlGroup(m_HttpUrlGroupId, Url16.c_str(), 0); - - if (Result != NO_ERROR) - { - spdlog::error("HttpRemoveUrlFromUrlGroup failed with result {}", Result); - } -} - -////////////////////////////////////////////////////////////////////////// - -HttpSysServerRequest::HttpSysServerRequest(HttpSysTransaction& Tx, HttpService& Service) : m_IsInitialized(true), m_HttpTx(Tx) -{ - PHTTP_REQUEST HttpRequestPtr = Tx.HttpRequest(); - - const int PrefixLength = Service.UriPrefixLength(); - const int AbsPathLength = HttpRequestPtr->CookedUrl.AbsPathLength / sizeof(char16_t); - - if (AbsPathLength >= PrefixLength) - { - // We convert the URI immediately because most of the code involved prefers to deal - // with utf8. This has some performance impact which I'd prefer to avoid but for now - // we just have to live with it - - WideToUtf8({(char16_t*)HttpRequestPtr->CookedUrl.pAbsPath + PrefixLength, gsl::narrow<size_t>(AbsPathLength - PrefixLength)}, - m_Uri); - } - else - { - m_Uri.Reset(); - } - - if (auto QueryStringLength = HttpRequestPtr->CookedUrl.QueryStringLength) - { - --QueryStringLength; - - WideToUtf8({(char16_t*)(HttpRequestPtr->CookedUrl.pQueryString) + 1, QueryStringLength / sizeof(char16_t)}, m_QueryString); - } - else - { - m_QueryString.Reset(); - } - - switch (HttpRequestPtr->Verb) - { - case HttpVerbOPTIONS: - m_Verb = HttpVerb::kOptions; - break; - - case HttpVerbGET: - m_Verb = HttpVerb::kGet; - break; - - case HttpVerbHEAD: - m_Verb = HttpVerb::kHead; - break; - - case HttpVerbPOST: - m_Verb = HttpVerb::kPost; - break; - - case HttpVerbPUT: - m_Verb = HttpVerb::kPut; - break; - - case HttpVerbDELETE: - m_Verb = HttpVerb::kDelete; - break; - - case HttpVerbCOPY: - m_Verb = HttpVerb::kCopy; - break; - - default: - // TODO: invalid request? - m_Verb = (HttpVerb)0; - break; - } - - const HTTP_KNOWN_HEADER& clh = HttpRequestPtr->Headers.KnownHeaders[HttpHeaderContentLength]; - std::string_view cl(clh.pRawValue, clh.RawValueLength); - std::from_chars(cl.data(), cl.data() + cl.size(), m_ContentLength); - - const HTTP_KNOWN_HEADER& CtHdr = HttpRequestPtr->Headers.KnownHeaders[HttpHeaderContentType]; - m_ContentType = MapContentType({CtHdr.pRawValue, CtHdr.RawValueLength}); -} - -void -HttpSysServerRequest::ReadPayload(std::function<void(HttpServerRequest&, IoBuffer)>&& CompletionHandler) -{ - ZEN_UNUSED(CompletionHandler); -} - -IoBuffer -HttpSysServerRequest::ReadPayload() -{ - // This is presently synchronous for simplicity, but we - // need to implement an asynchronous version also - - HTTP_REQUEST* const HttpReq = m_HttpTx.HttpRequest(); - - IoBuffer PayloadBuffer(m_ContentLength); - - HttpContentType ContentType = RequestContentType(); - PayloadBuffer.SetContentType(ContentType); - - uint64_t BytesToRead = m_ContentLength; - - uint8_t* ReadPointer = reinterpret_cast<uint8_t*>(PayloadBuffer.MutableData()); - - // First deal with any payload which has already been copied - // into our request buffer - - const int EntityChunkCount = HttpReq->EntityChunkCount; - - for (int i = 0; i < EntityChunkCount; ++i) - { - HTTP_DATA_CHUNK& EntityChunk = HttpReq->pEntityChunks[i]; - - ZEN_ASSERT(EntityChunk.DataChunkType == HttpDataChunkFromMemory); - - const uint64_t BufferLength = EntityChunk.FromMemory.BufferLength; - - ZEN_ASSERT(BufferLength <= BytesToRead); - - memcpy(ReadPointer, EntityChunk.FromMemory.pBuffer, BufferLength); - - ReadPointer += BufferLength; - BytesToRead -= BufferLength; - } - - if (BytesToRead == 0) - { - PayloadBuffer.MakeImmutable(); - - return PayloadBuffer; - } - - // Call http.sys API to receive the remaining data SYNCHRONOUSLY - - static const uint64_t kMaxBytesPerApiCall = 1 * 1024 * 1024; - - while (BytesToRead) - { - ULONG BytesRead = 0; - - const uint64_t BytesToReadThisCall = zen::Min(BytesToRead, kMaxBytesPerApiCall); - - ULONG ApiResult = HttpReceiveRequestEntityBody(m_HttpTx.RequestQueueHandle(), - HttpReq->RequestId, - 0, /* Flags */ - ReadPointer, - gsl::narrow<ULONG>(BytesToReadThisCall), - &BytesRead, - NULL /* Overlapped */ - ); - - if (ApiResult != NO_ERROR && ApiResult != ERROR_HANDLE_EOF) - { - throw HttpServerException("payload read failed", ApiResult); - } - - BytesToRead -= BytesRead; - ReadPointer += BytesRead; - } - - PayloadBuffer.MakeImmutable(); - - return PayloadBuffer; -} - -void -HttpSysServerRequest::WriteResponse(HttpResponse HttpResponseCode) -{ - ZEN_ASSERT(m_IsHandled == false); - - m_Response = new HttpMessageResponseRequest(m_HttpTx, (uint16_t)HttpResponseCode); - - if (m_SuppressBody) - { - m_Response->SuppressResponseBody(); - } - - m_IsHandled = true; -} - -void -HttpSysServerRequest::WriteResponse(HttpResponse HttpResponseCode, HttpContentType ContentType, std::span<IoBuffer> Blobs) -{ - ZEN_ASSERT(m_IsHandled == false); - ZEN_UNUSED(ContentType); - - m_Response = new HttpMessageResponseRequest(m_HttpTx, (uint16_t)HttpResponseCode, Blobs); - - if (m_SuppressBody) - { - m_Response->SuppressResponseBody(); - } - - m_IsHandled = true; -} - -void -HttpSysServerRequest::WriteResponse(HttpResponse HttpResponseCode, HttpContentType ContentType, std::u8string_view ResponseString) -{ - ZEN_ASSERT(m_IsHandled == false); - ZEN_UNUSED(ContentType); - - m_Response = new HttpMessageResponseRequest(m_HttpTx, (uint16_t)HttpResponseCode, ResponseString.data(), ResponseString.size()); - - if (m_SuppressBody) - { - m_Response->SuppressResponseBody(); - } - - m_IsHandled = true; -} - -////////////////////////////////////////////////////////////////////////// - -PTP_IO -HttpSysTransaction::Iocp() -{ - return m_HttpServer.m_ThreadPool.Iocp(); -} - -HANDLE -HttpSysTransaction::RequestQueueHandle() -{ - return m_HttpServer.m_RequestQueueHandle; -} - -void -HttpSysTransaction::IssueInitialRequest() -{ - m_InitialHttpHandler.IssueRequest(); -} - -HttpSysTransaction::Status -HttpSysTransaction::HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesTransferred) -{ - // We use this to ensure sequential execution of completion handlers - // for any given transaction. - RwLock::ExclusiveLockScope _(m_Lock); - - bool RequestPending = false; - - if (HttpSysRequestHandler* CurrentHandler = m_HttpHandler) - { - const bool IsInitialRequest = (CurrentHandler == &m_InitialHttpHandler); - - if (IsInitialRequest) - { - // Ensure we have a sufficient number of pending requests outstanding - m_HttpServer.OnHandlingRequest(); - } - - m_HttpHandler = CurrentHandler->HandleCompletion(IoResult, NumberOfBytesTransferred); - - if (m_HttpHandler) - { - try - { - m_HttpHandler->IssueRequest(); - - RequestPending = true; - } - catch (std::exception& Ex) - { - spdlog::error("exception caught from IssueRequest(): {}", Ex.what()); - - // something went wrong, no request is pending - } - } - else - { - if (IsInitialRequest == false) - { - delete CurrentHandler; - } - } - } - - // Ensure new requests are enqueued - m_HttpServer.IssueNewRequestMaybe(); - - if (RequestPending) - { - return Status::kRequestPending; - } - - return Status::kDone; -} - -////////////////////////////////////////////////////////////////////////// - -void -InitialRequestHandler::IssueRequest() -{ - PTP_IO Iocp = Transaction().Iocp(); - - StartThreadpoolIo(Iocp); - - HttpSysTransaction& Tx = Transaction(); - - HTTP_REQUEST* HttpReq = Tx.HttpRequest(); - - ULONG Result = HttpReceiveHttpRequest(Tx.RequestQueueHandle(), - HTTP_NULL_ID, - HTTP_RECEIVE_REQUEST_FLAG_COPY_BODY, - HttpReq, - RequestBufferSize(), - NULL, - Tx.Overlapped()); - - if (Result != ERROR_IO_PENDING && Result != NO_ERROR) - { - CancelThreadpoolIo(Iocp); - - if (Result == ERROR_MORE_DATA) - { - // ProcessReceiveAndPostResponse(pIoRequest, pServerContext->Io, ERROR_MORE_DATA); - } - - // CleanupHttpIoRequest(pIoRequest); - - spdlog::error("HttpReceiveHttpRequest failed, error {:x}", Result); - - return; - } -} - -HttpSysRequestHandler* -InitialRequestHandler::HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesTransferred) -{ - ZEN_UNUSED(IoResult); - ZEN_UNUSED(NumberOfBytesTransferred); - - // Route requests - - try - { - if (HttpService* Service = reinterpret_cast<HttpService*>(m_HttpRequestPtr->UrlContext)) - { - HttpSysServerRequest ThisRequest(Transaction(), *Service); - - Service->HandleRequest(ThisRequest); - - if (!ThisRequest.IsHandled()) - { - return new HttpMessageResponseRequest(Transaction(), 404, "Not found"); - } - - if (ThisRequest.m_Response) - { - return ThisRequest.m_Response; - } - } - - // Unable to route - return new HttpMessageResponseRequest(Transaction(), 404, "Item unknown"); - } - catch (std::exception& ex) - { - // TODO provide more meaningful error output - - return new HttpMessageResponseRequest(Transaction(), 500, ex.what()); - } -} -#endif // ZEN_PLATFORM_WINDOWS - -////////////////////////////////////////////////////////////////////////// struct HttpServer::Impl : public RefCounted { diff --git a/zencore/httpsys.cpp b/zencore/httpsys.cpp new file mode 100644 index 000000000..7a17d6b9c --- /dev/null +++ b/zencore/httpsys.cpp @@ -0,0 +1,1226 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "httpsys.h" + +#include <zencore/logging.h> +#include <zencore/string.h> + +#include <conio.h> + +#if ZEN_PLATFORM_WINDOWS +# pragma comment(lib, "httpapi.lib") +#endif + +std::wstring +UTF8_to_wstring(const char* in) +{ + std::wstring out; + unsigned int codepoint; + + while (*in != 0) + { + unsigned char ch = static_cast<unsigned char>(*in); + + if (ch <= 0x7f) + codepoint = ch; + else if (ch <= 0xbf) + codepoint = (codepoint << 6) | (ch & 0x3f); + else if (ch <= 0xdf) + codepoint = ch & 0x1f; + else if (ch <= 0xef) + codepoint = ch & 0x0f; + else + codepoint = ch & 0x07; + + ++in; + + if (((*in & 0xc0) != 0x80) && (codepoint <= 0x10ffff)) + { + if (sizeof(wchar_t) > 2) + { + out.append(1, static_cast<wchar_t>(codepoint)); + } + else if (codepoint > 0xffff) + { + out.append(1, static_cast<wchar_t>(0xd800 + (codepoint >> 10))); + out.append(1, static_cast<wchar_t>(0xdc00 + (codepoint & 0x03ff))); + } + else if (codepoint < 0xd800 || codepoint >= 0xe000) + { + out.append(1, static_cast<wchar_t>(codepoint)); + } + } + } + + return out; +} + +////////////////////////////////////////////////////////////////////////// +// +// http.sys implementation +// + +namespace zen { + +using namespace std::literals; + +static const uint32_t HashBinary = HashStringDjb2("application/octet-stream"sv); +static const uint32_t HashJson = HashStringDjb2("application/json"sv); +static const uint32_t HashYaml = HashStringDjb2("text/yaml"sv); +static const uint32_t HashText = HashStringDjb2("text/plain"sv); +static const uint32_t HashCompactBinary = HashStringDjb2("application/x-ue-cb"sv); +static const uint32_t HashCompactBinaryPackage = HashStringDjb2("application/x-ue-cbpkg"sv); + +HttpContentType +MapContentType(const std::string_view& ContentTypeString) +{ + if (!ContentTypeString.empty()) + { + const uint32_t CtHash = HashStringDjb2(ContentTypeString); + + if (CtHash == HashBinary) + { + return HttpContentType::kBinary; + } + else if (CtHash == HashCompactBinary) + { + return HttpContentType::kCbObject; + } + else if (CtHash == HashCompactBinaryPackage) + { + return HttpContentType::kCbPackage; + } + else if (CtHash == HashJson) + { + return HttpContentType::kJSON; + } + else if (CtHash == HashYaml) + { + return HttpContentType::kYAML; + } + else if (CtHash == HashText) + { + return HttpContentType::kText; + } + } + + return HttpContentType::kUnknownContentType; +} + +////////////////////////////////////////////////////////////////////////// + +const char* +ReasonStringForHttpResultCode(int HttpCode) +{ + switch (HttpCode) + { + // 1xx Informational + + case 100: + return "Continue"; + case 101: + return "Switching Protocols"; + + // 2xx Success + + case 200: + return "OK"; + case 201: + return "Created"; + case 202: + return "Accepted"; + case 204: + return "No Content"; + case 205: + return "Reset Content"; + case 206: + return "Partial Content"; + + // 3xx Redirection + + case 300: + return "Multiple Choices"; + case 301: + return "Moved Permanently"; + case 302: + return "Found"; + case 303: + return "See Other"; + case 304: + return "Not Modified"; + case 305: + return "Use Proxy"; + case 306: + return "Switch Proxy"; + case 307: + return "Temporary Redirect"; + case 308: + return "Permanent Redirect"; + + // 4xx Client errors + + case 400: + return "Bad Request"; + case 401: + return "Unauthorized"; + case 402: + return "Payment Required"; + case 403: + return "Forbidden"; + case 404: + return "Not Found"; + case 405: + return "Method Not Allowed"; + case 406: + return "Not Acceptable"; + case 407: + return "Proxy Authentication Required"; + case 408: + return "Request Timeout"; + case 409: + return "Conflict"; + case 410: + return "Gone"; + case 411: + return "Length Required"; + case 412: + return "Precondition Failed"; + case 413: + return "Payload Too Large"; + case 414: + return "URI Too Long"; + case 415: + return "Unsupported Media Type"; + case 416: + return "Range Not Satisifiable"; + case 417: + return "Expectation Failed"; + case 418: + return "I'm a teapot"; + case 421: + return "Misdirected Request"; + case 422: + return "Unprocessable Entity"; + case 423: + return "Locked"; + case 424: + return "Failed Dependency"; + case 425: + return "Too Early"; + case 426: + return "Upgrade Required"; + case 428: + return "Precondition Required"; + case 429: + return "Too Many Requests"; + case 431: + return "Request Header Fields Too Large"; + + // 5xx Server errors + + case 500: + return "Internal Server Error"; + case 501: + return "Not Implemented"; + case 502: + return "Bad Gateway"; + case 503: + return "Service Unavailable"; + case 504: + return "Gateway Timeout"; + case 505: + return "HTTP Version Not Supported"; + case 506: + return "Variant Also Negotiates"; + case 507: + return "Insufficient Storage"; + case 508: + return "Loop Detected"; + case 510: + return "Not Extended"; + case 511: + return "Network Authentication Required"; + + default: + return "Unknown Result"; + } +} + +#if ZEN_PLATFORM_WINDOWS +class HttpSysServer; +class HttpSysTransaction; +class HttpMessageResponseRequest; + +class HttpSysRequestHandler +{ +public: + HttpSysRequestHandler(HttpSysTransaction& InRequest) : m_Request(InRequest) {} + virtual ~HttpSysRequestHandler() = default; + + virtual void IssueRequest() = 0; + virtual HttpSysRequestHandler* HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesTransferred) = 0; + + HttpSysTransaction& Transaction() { return m_Request; } + +private: + HttpSysTransaction& m_Request; // Outermost HTTP transaction object +}; + +struct InitialRequestHandler : public HttpSysRequestHandler +{ + inline PHTTP_REQUEST HttpRequest() { return (PHTTP_REQUEST)m_RequestBuffer; } + inline uint32_t RequestBufferSize() const { return sizeof m_RequestBuffer; } + + InitialRequestHandler(HttpSysTransaction& InRequest) : HttpSysRequestHandler(InRequest) {} + ~InitialRequestHandler() {} + + virtual void IssueRequest() override; + virtual HttpSysRequestHandler* HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesTransferred) override; + + PHTTP_REQUEST m_HttpRequestPtr = (HTTP_REQUEST*)(m_RequestBuffer); + UCHAR m_RequestBuffer[16384 + sizeof(HTTP_REQUEST)]; +}; + +class HttpSysServerRequest : public HttpServerRequest +{ +public: + HttpSysServerRequest() = default; + HttpSysServerRequest(HttpSysTransaction& Tx, HttpService& Service); + ~HttpSysServerRequest() = default; + + virtual void ReadPayload(std::function<void(HttpServerRequest&, IoBuffer)>&& CompletionHandler) override; + virtual IoBuffer ReadPayload() override; + virtual void WriteResponse(HttpResponse HttpResponseCode) override; + virtual void WriteResponse(HttpResponse HttpResponseCode, HttpContentType ContentType, std::span<IoBuffer> Blobs) override; + virtual void WriteResponse(HttpResponse HttpResponseCode, HttpContentType ContentType, std::u8string_view ResponseString) override; + + bool m_IsInitialized = false; + HttpSysTransaction& m_HttpTx; + HttpMessageResponseRequest* m_Response = nullptr; // TODO: make this more general +}; + +/** HTTP transaction + + There will be an instance of this per pending and in-flight HTTP transaction + + */ +class HttpSysTransaction final +{ +public: + HttpSysTransaction(HttpSysServer& Server) : m_HttpServer(Server), m_HttpHandler(&m_InitialHttpHandler) {} + + virtual ~HttpSysTransaction() {} + + enum class Status + { + kDone, + kRequestPending + }; + + Status HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesTransferred); + + static void __stdcall IoCompletionCallback(PTP_CALLBACK_INSTANCE Instance, + PVOID pContext /* HttpSysServer */, + PVOID pOverlapped, + ULONG IoResult, + ULONG_PTR NumberOfBytesTransferred, + PTP_IO Io); + + void IssueInitialRequest(); + PTP_IO Iocp(); + HANDLE RequestQueueHandle(); + inline OVERLAPPED* Overlapped() { return &m_HttpOverlapped; } + inline HttpSysServer& Server() { return m_HttpServer; } + + inline PHTTP_REQUEST HttpRequest() { return m_InitialHttpHandler.HttpRequest(); } + +private: + OVERLAPPED m_HttpOverlapped{}; + HttpSysServer& m_HttpServer; + HttpSysRequestHandler* m_HttpHandler{nullptr}; // Tracks which handler is due to handle the next I/O completion event + RwLock m_CompletionMutex; + InitialRequestHandler m_InitialHttpHandler{*this}; +}; + +////////////////////////////////////////////////////////////////////////// + +class HttpPayloadReadRequest : public HttpSysRequestHandler +{ +public: + HttpPayloadReadRequest(HttpSysTransaction& InRequest) : HttpSysRequestHandler(InRequest) {} + + virtual void IssueRequest() override; + virtual HttpSysRequestHandler* HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesTransferred) override; +}; + +void +HttpPayloadReadRequest::IssueRequest() +{ +} + +HttpSysRequestHandler* +HttpPayloadReadRequest::HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesTransferred) +{ + ZEN_UNUSED(IoResult, NumberOfBytesTransferred); + return nullptr; +} + +////////////////////////////////////////////////////////////////////////// + +class HttpMessageResponseRequest : public HttpSysRequestHandler +{ +public: + HttpMessageResponseRequest(HttpSysTransaction& InRequest, uint16_t ResponseCode); + HttpMessageResponseRequest(HttpSysTransaction& InRequest, uint16_t ResponseCode, const char* Message); + HttpMessageResponseRequest(HttpSysTransaction& InRequest, uint16_t ResponseCode, const void* Payload, size_t PayloadSize); + HttpMessageResponseRequest(HttpSysTransaction& InRequest, uint16_t ResponseCode, std::span<IoBuffer> Blobs); + ~HttpMessageResponseRequest(); + + virtual void IssueRequest() override; + virtual HttpSysRequestHandler* HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesTransferred) override; + + void SuppressResponseBody(); + +private: + std::vector<HTTP_DATA_CHUNK> m_HttpDataChunks; + uint64_t m_TotalDataSize = 0; // Sum of all chunk sizes + + uint16_t m_HttpResponseCode = 0; + uint32_t m_NextDataChunkOffset = 0; // This is used for responses where the number of chunks exceed the maximum number for one API call + uint32_t m_RemainingChunkCount = 0; + bool m_IsInitialResponse = true; + + void Initialize(uint16_t ResponseCode, std::span<IoBuffer> Blobs); + + std::vector<IoBuffer> m_DataBuffers; +}; + +HttpMessageResponseRequest::HttpMessageResponseRequest(HttpSysTransaction& InRequest, uint16_t ResponseCode) +: HttpSysRequestHandler(InRequest) +{ + std::array<IoBuffer, 0> buffers; + + Initialize(ResponseCode, buffers); +} + +HttpMessageResponseRequest::HttpMessageResponseRequest(HttpSysTransaction& InRequest, uint16_t ResponseCode, const char* Message) +: HttpSysRequestHandler(InRequest) +{ + IoBuffer MessageBuffer(IoBuffer::Wrap, Message, strlen(Message)); + std::array<IoBuffer, 1> buffers({MessageBuffer}); + + Initialize(ResponseCode, buffers); +} + +HttpMessageResponseRequest::HttpMessageResponseRequest(HttpSysTransaction& InRequest, + uint16_t ResponseCode, + const void* Payload, + size_t PayloadSize) +: HttpSysRequestHandler(InRequest) +{ + IoBuffer MessageBuffer(IoBuffer::Wrap, Payload, PayloadSize); + std::array<IoBuffer, 1> buffers({MessageBuffer}); + + Initialize(ResponseCode, buffers); +} + +HttpMessageResponseRequest::HttpMessageResponseRequest(HttpSysTransaction& InRequest, uint16_t ResponseCode, std::span<IoBuffer> Blobs) +: HttpSysRequestHandler(InRequest) +{ + Initialize(ResponseCode, Blobs); +} + +HttpMessageResponseRequest::~HttpMessageResponseRequest() +{ +} + +void +HttpMessageResponseRequest::Initialize(uint16_t ResponseCode, std::span<IoBuffer> Blobs) +{ + m_HttpResponseCode = ResponseCode; + + const uint32_t ChunkCount = (uint32_t)Blobs.size(); + + m_HttpDataChunks.resize(ChunkCount); + m_DataBuffers.reserve(ChunkCount); + + for (IoBuffer& Buffer : Blobs) + { + m_DataBuffers.emplace_back(std::move(Buffer)).MakeOwned(); + } + + // Initialize the full array up front + + uint64_t LocalDataSize = 0; + + { + PHTTP_DATA_CHUNK ChunkPtr = m_HttpDataChunks.data(); + + for (IoBuffer& Buffer : m_DataBuffers) + { + const ULONG BufferDataSize = (ULONG)Buffer.Size(); + + ZEN_ASSERT(BufferDataSize); + + IoBufferFileReference FileRef; + if (Buffer.GetFileReference(/* out */ FileRef)) + { + ChunkPtr->DataChunkType = HttpDataChunkFromFileHandle; + ChunkPtr->FromFileHandle.FileHandle = FileRef.FileHandle; + ChunkPtr->FromFileHandle.ByteRange.StartingOffset.QuadPart = FileRef.FileChunkOffset; + ChunkPtr->FromFileHandle.ByteRange.Length.QuadPart = BufferDataSize; + } + else + { + ChunkPtr->DataChunkType = HttpDataChunkFromMemory; + ChunkPtr->FromMemory.pBuffer = (void*)Buffer.Data(); + ChunkPtr->FromMemory.BufferLength = BufferDataSize; + } + ++ChunkPtr; + + LocalDataSize += BufferDataSize; + } + } + + m_RemainingChunkCount = ChunkCount; + m_TotalDataSize = LocalDataSize; +} + +void +HttpMessageResponseRequest::SuppressResponseBody() +{ + m_RemainingChunkCount = 0; + m_HttpDataChunks.clear(); + m_DataBuffers.clear(); +} + +HttpSysRequestHandler* +HttpMessageResponseRequest::HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesTransferred) +{ + ZEN_UNUSED(NumberOfBytesTransferred); + ZEN_UNUSED(IoResult); + + if (m_RemainingChunkCount == 0) + { + return nullptr; // All done + } + + return this; +} + +void +HttpMessageResponseRequest::IssueRequest() +{ + HttpSysTransaction& Tx = Transaction(); + HTTP_REQUEST* const HttpReq = Tx.HttpRequest(); + PTP_IO const Iocp = Tx.Iocp(); + + StartThreadpoolIo(Iocp); + + // Split payload into batches to play well with the underlying API + + const int MaxChunksPerCall = 9999; + + const int ThisRequestChunkCount = std::min<int>(m_RemainingChunkCount, MaxChunksPerCall); + const int ThisRequestChunkOffset = m_NextDataChunkOffset; + + m_RemainingChunkCount -= ThisRequestChunkCount; + m_NextDataChunkOffset += ThisRequestChunkCount; + + ULONG SendFlags = 0; + + if (m_RemainingChunkCount) + { + // We need to make more calls to send the full amount of data + SendFlags |= HTTP_SEND_RESPONSE_FLAG_MORE_DATA; + } + + ULONG SendResult = 0; + + if (m_IsInitialResponse) + { + // Populate response structure + + HTTP_RESPONSE HttpResponse = {}; + + HttpResponse.EntityChunkCount = USHORT(ThisRequestChunkCount); + HttpResponse.pEntityChunks = m_HttpDataChunks.data() + ThisRequestChunkOffset; + + // Content-length header + + char ContentLengthString[32]; + _ui64toa_s(m_TotalDataSize, ContentLengthString, sizeof ContentLengthString, 10); + + PHTTP_KNOWN_HEADER ContentLengthHeader = &HttpResponse.Headers.KnownHeaders[HttpHeaderContentLength]; + ContentLengthHeader->pRawValue = ContentLengthString; + ContentLengthHeader->RawValueLength = (USHORT)strlen(ContentLengthString); + + // Content-type header + + PHTTP_KNOWN_HEADER ContentTypeHeader = &HttpResponse.Headers.KnownHeaders[HttpHeaderContentType]; + + ContentTypeHeader->pRawValue = "application/octet-stream"; /* TODO! We must respect the content type specified */ + ContentTypeHeader->RawValueLength = (USHORT)strlen(ContentTypeHeader->pRawValue); + + HttpResponse.StatusCode = m_HttpResponseCode; + HttpResponse.pReason = ReasonStringForHttpResultCode(m_HttpResponseCode); + HttpResponse.ReasonLength = (USHORT)strlen(HttpResponse.pReason); + + // Cache policy + + HTTP_CACHE_POLICY CachePolicy; + + CachePolicy.Policy = HttpCachePolicyNocache; // HttpCachePolicyUserInvalidates; + CachePolicy.SecondsToLive = 0; + + // Initial response API call + + SendResult = HttpSendHttpResponse(Tx.RequestQueueHandle(), + HttpReq->RequestId, + SendFlags, + &HttpResponse, + &CachePolicy, + NULL, + NULL, + 0, + Tx.Overlapped(), + NULL); + + m_IsInitialResponse = false; + } + else + { + // Subsequent response API calls + + SendResult = HttpSendResponseEntityBody(Tx.RequestQueueHandle(), + HttpReq->RequestId, + SendFlags, + (USHORT)ThisRequestChunkCount, // EntityChunkCount + &m_HttpDataChunks[ThisRequestChunkOffset], // EntityChunks + NULL, // BytesSent + NULL, // Reserved1 + 0, // Reserved2 + Tx.Overlapped(), // Overlapped + NULL // LogData + ); + } + + if ((SendResult != NO_ERROR) // Synchronous completion, but the completion event will still be posted to IOCP + && (SendResult != ERROR_IO_PENDING) // Asynchronous completion + ) + { + // Some error occurred, no completion will be posted + + CancelThreadpoolIo(Iocp); + + spdlog::error("failed to send HTTP response (error: {}) URL: {}", SendResult, HttpReq->pRawUrl); + + throw HttpServerException("Failed to send HTTP response", SendResult); + } +} + +////////////////////////////////////////////////////////////////////////// + +HttpSysServer::HttpSysServer(WinIoThreadPool& InThreadPool) : m_ThreadPool(InThreadPool) +{ + ULONG Result = HttpInitialize(HTTPAPI_VERSION_2, HTTP_INITIALIZE_SERVER, nullptr); + + if (Result != NO_ERROR) + { + return; + } + + m_IsHttpInitialized = true; + m_IsOk = true; +} + +HttpSysServer::~HttpSysServer() +{ + if (m_IsHttpInitialized) + { + HttpTerminate(HTTP_INITIALIZE_SERVER, nullptr); + } +} + +void +HttpSysServer::Initialize(const wchar_t* UrlPath) +{ + // check(bIsOk); + + ULONG Result = HttpCreateServerSession(HTTPAPI_VERSION_2, &m_HttpSessionId, 0); + + if (Result != NO_ERROR) + { + // Flag error + + return; + } + + Result = HttpCreateUrlGroup(m_HttpSessionId, &m_HttpUrlGroupId, 0); + + if (Result != NO_ERROR) + { + // Flag error + + return; + } + + m_BaseUri = UrlPath; + + Result = HttpAddUrlToUrlGroup(m_HttpUrlGroupId, UrlPath, /* #TODO UrlContext */ HTTP_URL_CONTEXT(0), 0); + + if (Result != NO_ERROR) + { + // Flag error + + return; + } + + HTTP_BINDING_INFO HttpBindingInfo = {{0}, 0}; + + Result = HttpCreateRequestQueue(HTTPAPI_VERSION_2, NULL, NULL, 0, &m_RequestQueueHandle); + + if (Result != NO_ERROR) + { + // Flag error! + + return; + } + + HttpBindingInfo.Flags.Present = 1; + HttpBindingInfo.RequestQueueHandle = m_RequestQueueHandle; + + Result = HttpSetUrlGroupProperty(m_HttpUrlGroupId, HttpServerBindingProperty, &HttpBindingInfo, sizeof(HttpBindingInfo)); + + if (Result != NO_ERROR) + { + // Flag error! + + return; + } + + // Create I/O completion port + + m_ThreadPool.CreateIocp(m_RequestQueueHandle, HttpSysTransaction::IoCompletionCallback, this); + + // Check result! +} + +void +HttpSysServer::StartServer() +{ + int RequestCount = 32; + + for (int i = 0; i < RequestCount; ++i) + { + IssueNewRequestMaybe(); + } +} + +void +HttpSysServer::Run(bool TestMode) +{ + if (TestMode == false) + { + zen::logging::ConsoleLog().info("Zen Server running. Press ESC or Q to quit"); + } + + do + { + int WaitTimeout = -1; + + if (!TestMode) + { + WaitTimeout = 1000; + } + + if (!TestMode && _kbhit() != 0) + { + char c = (char)_getch(); + + if (c == 27 || c == 'Q' || c == 'q') + { + RequestApplicationExit(0); + } + } + + m_ShutdownEvent.Wait(WaitTimeout); + } while (!IsApplicationExitRequested()); +} + +void +HttpSysServer::OnHandlingRequest() +{ + --m_PendingRequests; + + if (m_PendingRequests > m_MinPendingRequests) + { + // We have more than the minimum number of requests pending, just let someone else + // enqueue new requests + return; + } + + IssueNewRequestMaybe(); +} + +void +HttpSysServer::IssueNewRequestMaybe() +{ + if (m_PendingRequests.load(std::memory_order::relaxed) >= m_MaxPendingRequests) + { + return; + } + + std::unique_ptr<HttpSysTransaction> Request = std::make_unique<HttpSysTransaction>(*this); + + Request->IssueInitialRequest(); + + // This may end up exceeding the MaxPendingRequests limit, but it's not + // really a problem. I'm doing it this way mostly to avoid dealing with + // exceptions here + ++m_PendingRequests; + + Request.release(); +} + +void +HttpSysServer::StopServer() +{ +} + +void +HttpSysServer::AddEndpoint(const char* UrlPath, HttpService& Service) +{ + if (UrlPath[0] == '/') + { + ++UrlPath; + } + + const std::wstring Path16 = UTF8_to_wstring(UrlPath); + Service.SetUriPrefixLength(Path16.size() + 1 /* leading slash */); + + // Convert to wide string + + std::wstring Url16 = m_BaseUri + Path16; + + ULONG Result = HttpAddUrlToUrlGroup(m_HttpUrlGroupId, Url16.c_str(), HTTP_URL_CONTEXT(&Service), 0 /* Reserved */); + + if (Result != NO_ERROR) + { + spdlog::error("HttpAddUrlToUrlGroup failed with result {}", Result); + + return; + } +} + +void +HttpSysServer::RemoveEndpoint(const char* UrlPath, HttpService& Service) +{ + ZEN_UNUSED(Service); + + if (UrlPath[0] == '/') + { + ++UrlPath; + } + + const std::wstring Path16 = UTF8_to_wstring(UrlPath); + + // Convert to wide string + + std::wstring Url16 = m_BaseUri + Path16; + + ULONG Result = HttpRemoveUrlFromUrlGroup(m_HttpUrlGroupId, Url16.c_str(), 0); + + if (Result != NO_ERROR) + { + spdlog::error("HttpRemoveUrlFromUrlGroup failed with result {}", Result); + } +} + +////////////////////////////////////////////////////////////////////////// + +HttpSysServerRequest::HttpSysServerRequest(HttpSysTransaction& Tx, HttpService& Service) : m_IsInitialized(true), m_HttpTx(Tx) +{ + PHTTP_REQUEST HttpRequestPtr = Tx.HttpRequest(); + + const int PrefixLength = Service.UriPrefixLength(); + const int AbsPathLength = HttpRequestPtr->CookedUrl.AbsPathLength / sizeof(char16_t); + + if (AbsPathLength >= PrefixLength) + { + // We convert the URI immediately because most of the code involved prefers to deal + // with utf8. This has some performance impact which I'd prefer to avoid but for now + // we just have to live with it + + WideToUtf8({(char16_t*)HttpRequestPtr->CookedUrl.pAbsPath + PrefixLength, gsl::narrow<size_t>(AbsPathLength - PrefixLength)}, + m_Uri); + } + else + { + m_Uri.Reset(); + } + + if (auto QueryStringLength = HttpRequestPtr->CookedUrl.QueryStringLength) + { + --QueryStringLength; + + WideToUtf8({(char16_t*)(HttpRequestPtr->CookedUrl.pQueryString) + 1, QueryStringLength / sizeof(char16_t)}, m_QueryString); + } + else + { + m_QueryString.Reset(); + } + + switch (HttpRequestPtr->Verb) + { + case HttpVerbOPTIONS: + m_Verb = HttpVerb::kOptions; + break; + + case HttpVerbGET: + m_Verb = HttpVerb::kGet; + break; + + case HttpVerbHEAD: + m_Verb = HttpVerb::kHead; + break; + + case HttpVerbPOST: + m_Verb = HttpVerb::kPost; + break; + + case HttpVerbPUT: + m_Verb = HttpVerb::kPut; + break; + + case HttpVerbDELETE: + m_Verb = HttpVerb::kDelete; + break; + + case HttpVerbCOPY: + m_Verb = HttpVerb::kCopy; + break; + + default: + // TODO: invalid request? + m_Verb = (HttpVerb)0; + break; + } + + const HTTP_KNOWN_HEADER& clh = HttpRequestPtr->Headers.KnownHeaders[HttpHeaderContentLength]; + std::string_view cl(clh.pRawValue, clh.RawValueLength); + std::from_chars(cl.data(), cl.data() + cl.size(), m_ContentLength); + + const HTTP_KNOWN_HEADER& CtHdr = HttpRequestPtr->Headers.KnownHeaders[HttpHeaderContentType]; + m_ContentType = MapContentType({CtHdr.pRawValue, CtHdr.RawValueLength}); +} + +void +HttpSysServerRequest::ReadPayload(std::function<void(HttpServerRequest&, IoBuffer)>&& CompletionHandler) +{ + ZEN_UNUSED(CompletionHandler); +} + +IoBuffer +HttpSysServerRequest::ReadPayload() +{ + // This is presently synchronous for simplicity, but we + // need to implement an asynchronous version also + + HTTP_REQUEST* const HttpReq = m_HttpTx.HttpRequest(); + + IoBuffer PayloadBuffer(m_ContentLength); + + HttpContentType ContentType = RequestContentType(); + PayloadBuffer.SetContentType(ContentType); + + uint64_t BytesToRead = m_ContentLength; + + uint8_t* ReadPointer = reinterpret_cast<uint8_t*>(PayloadBuffer.MutableData()); + + // First deal with any payload which has already been copied + // into our request buffer + + const int EntityChunkCount = HttpReq->EntityChunkCount; + + for (int i = 0; i < EntityChunkCount; ++i) + { + HTTP_DATA_CHUNK& EntityChunk = HttpReq->pEntityChunks[i]; + + ZEN_ASSERT(EntityChunk.DataChunkType == HttpDataChunkFromMemory); + + const uint64_t BufferLength = EntityChunk.FromMemory.BufferLength; + + ZEN_ASSERT(BufferLength <= BytesToRead); + + memcpy(ReadPointer, EntityChunk.FromMemory.pBuffer, BufferLength); + + ReadPointer += BufferLength; + BytesToRead -= BufferLength; + } + + if (BytesToRead == 0) + { + PayloadBuffer.MakeImmutable(); + + return PayloadBuffer; + } + + // Call http.sys API to receive the remaining data SYNCHRONOUSLY + + static const uint64_t kMaxBytesPerApiCall = 1 * 1024 * 1024; + + while (BytesToRead) + { + ULONG BytesRead = 0; + + const uint64_t BytesToReadThisCall = zen::Min(BytesToRead, kMaxBytesPerApiCall); + + ULONG ApiResult = HttpReceiveRequestEntityBody(m_HttpTx.RequestQueueHandle(), + HttpReq->RequestId, + 0, /* Flags */ + ReadPointer, + gsl::narrow<ULONG>(BytesToReadThisCall), + &BytesRead, + NULL /* Overlapped */ + ); + + if (ApiResult != NO_ERROR && ApiResult != ERROR_HANDLE_EOF) + { + throw HttpServerException("payload read failed", ApiResult); + } + + BytesToRead -= BytesRead; + ReadPointer += BytesRead; + } + + PayloadBuffer.MakeImmutable(); + + return PayloadBuffer; +} + +void +HttpSysServerRequest::WriteResponse(HttpResponse HttpResponseCode) +{ + ZEN_ASSERT(m_IsHandled == false); + + m_Response = new HttpMessageResponseRequest(m_HttpTx, (uint16_t)HttpResponseCode); + + if (m_SuppressBody) + { + m_Response->SuppressResponseBody(); + } + + m_IsHandled = true; +} + +void +HttpSysServerRequest::WriteResponse(HttpResponse HttpResponseCode, HttpContentType ContentType, std::span<IoBuffer> Blobs) +{ + ZEN_ASSERT(m_IsHandled == false); + ZEN_UNUSED(ContentType); + + m_Response = new HttpMessageResponseRequest(m_HttpTx, (uint16_t)HttpResponseCode, Blobs); + + if (m_SuppressBody) + { + m_Response->SuppressResponseBody(); + } + + m_IsHandled = true; +} + +void +HttpSysServerRequest::WriteResponse(HttpResponse HttpResponseCode, HttpContentType ContentType, std::u8string_view ResponseString) +{ + ZEN_ASSERT(m_IsHandled == false); + ZEN_UNUSED(ContentType); + + m_Response = new HttpMessageResponseRequest(m_HttpTx, (uint16_t)HttpResponseCode, ResponseString.data(), ResponseString.size()); + + if (m_SuppressBody) + { + m_Response->SuppressResponseBody(); + } + + m_IsHandled = true; +} + +////////////////////////////////////////////////////////////////////////// + +PTP_IO +HttpSysTransaction::Iocp() +{ + return m_HttpServer.m_ThreadPool.Iocp(); +} + +HANDLE +HttpSysTransaction::RequestQueueHandle() +{ + return m_HttpServer.m_RequestQueueHandle; +} + +void +HttpSysTransaction::IssueInitialRequest() +{ + m_InitialHttpHandler.IssueRequest(); +} + +void +HttpSysTransaction::IoCompletionCallback(PTP_CALLBACK_INSTANCE Instance, + PVOID pContext /* HttpSysServer */, + PVOID pOverlapped, + ULONG IoResult, + ULONG_PTR NumberOfBytesTransferred, + PTP_IO Io) +{ + UNREFERENCED_PARAMETER(Io); + UNREFERENCED_PARAMETER(Instance); + UNREFERENCED_PARAMETER(pContext); + + // Note that for a given transaction we may be in this completion function on more + // than one thread at any given moment. This means we need to be careful about what + // happens in here + + HttpSysTransaction* Transaction = CONTAINING_RECORD(pOverlapped, HttpSysTransaction, m_HttpOverlapped); + + if (Transaction->HandleCompletion(IoResult, NumberOfBytesTransferred) == HttpSysTransaction::Status::kDone) + { + delete Transaction; + } +} + +HttpSysTransaction::Status +HttpSysTransaction::HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesTransferred) +{ + // We use this to ensure sequential execution of completion handlers + // for any given transaction. + RwLock::ExclusiveLockScope _(m_CompletionMutex); + + bool RequestPending = false; + + if (HttpSysRequestHandler* CurrentHandler = m_HttpHandler) + { + const bool IsInitialRequest = (CurrentHandler == &m_InitialHttpHandler); + + if (IsInitialRequest) + { + // Ensure we have a sufficient number of pending requests outstanding + m_HttpServer.OnHandlingRequest(); + } + + m_HttpHandler = CurrentHandler->HandleCompletion(IoResult, NumberOfBytesTransferred); + + if (m_HttpHandler) + { + try + { + m_HttpHandler->IssueRequest(); + + RequestPending = true; + } + catch (std::exception& Ex) + { + spdlog::error("exception caught from IssueRequest(): {}", Ex.what()); + + // something went wrong, no request is pending + } + } + else + { + if (IsInitialRequest == false) + { + delete CurrentHandler; + } + } + } + + // Ensure new requests are enqueued + m_HttpServer.IssueNewRequestMaybe(); + + if (RequestPending) + { + return Status::kRequestPending; + } + + return Status::kDone; +} + +////////////////////////////////////////////////////////////////////////// + +void +InitialRequestHandler::IssueRequest() +{ + PTP_IO Iocp = Transaction().Iocp(); + + StartThreadpoolIo(Iocp); + + HttpSysTransaction& Tx = Transaction(); + + HTTP_REQUEST* HttpReq = Tx.HttpRequest(); + + ULONG Result = HttpReceiveHttpRequest(Tx.RequestQueueHandle(), + HTTP_NULL_ID, + HTTP_RECEIVE_REQUEST_FLAG_COPY_BODY, + HttpReq, + RequestBufferSize(), + NULL, + Tx.Overlapped()); + + if (Result != ERROR_IO_PENDING && Result != NO_ERROR) + { + CancelThreadpoolIo(Iocp); + + if (Result == ERROR_MORE_DATA) + { + // ProcessReceiveAndPostResponse(pIoRequest, pServerContext->Io, ERROR_MORE_DATA); + } + + // CleanupHttpIoRequest(pIoRequest); + + spdlog::error("HttpReceiveHttpRequest failed, error {:x}", Result); + + return; + } +} + +HttpSysRequestHandler* +InitialRequestHandler::HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesTransferred) +{ + ZEN_UNUSED(IoResult); + ZEN_UNUSED(NumberOfBytesTransferred); + + // Route requests + + try + { + if (HttpService* Service = reinterpret_cast<HttpService*>(m_HttpRequestPtr->UrlContext)) + { + HttpSysServerRequest ThisRequest(Transaction(), *Service); + + Service->HandleRequest(ThisRequest); + + if (!ThisRequest.IsHandled()) + { + return new HttpMessageResponseRequest(Transaction(), 404, "Not found"); + } + + if (ThisRequest.m_Response) + { + return ThisRequest.m_Response; + } + } + + // Unable to route + return new HttpMessageResponseRequest(Transaction(), 404, "Item unknown"); + } + catch (std::exception& ex) + { + // TODO provide more meaningful error output + + return new HttpMessageResponseRequest(Transaction(), 500, ex.what()); + } +} +#endif // ZEN_PLATFORM_WINDOWS + +} // namespace zen diff --git a/zencore/httpsys.h b/zencore/httpsys.h new file mode 100644 index 000000000..8a7b372dd --- /dev/null +++ b/zencore/httpsys.h @@ -0,0 +1,58 @@ +#pragma once + +#include <zencore/httpserver.h> + +#define _WINSOCKAPI_ +#include <zencore/windows.h> +#include "iothreadpool.h" + +#include <atlbase.h> +#include <http.h> + +namespace zen { + +/** + * @brief Windows implementation of HTTP server based on http.sys + * + * This requires elevation to function + */ +class HttpSysServer +{ + friend class HttpSysTransaction; + +public: + HttpSysServer(WinIoThreadPool& InThreadPool); + ~HttpSysServer(); + + void Initialize(const wchar_t* UrlPath); + void Run(bool TestMode); + + void RequestExit() { m_ShutdownEvent.Set(); } + + void StartServer(); + void StopServer(); + + void OnHandlingRequest(); + void IssueNewRequestMaybe(); + + inline bool IsOk() const { return m_IsOk; } + + void AddEndpoint(const char* Endpoint, HttpService& Service); + void RemoveEndpoint(const char* Endpoint, HttpService& Service); + +private: + bool m_IsOk = false; + bool m_IsHttpInitialized = false; + WinIoThreadPool& m_ThreadPool; + + std::wstring m_BaseUri; // http://*:nnnn/ + HTTP_SERVER_SESSION_ID m_HttpSessionId = 0; + HTTP_URL_GROUP_ID m_HttpUrlGroupId = 0; + HANDLE m_RequestQueueHandle = 0; + std::atomic_int32_t m_PendingRequests{0}; + int32_t m_MinPendingRequests = 4; + int32_t m_MaxPendingRequests = 32; + Event m_ShutdownEvent; +}; + +} diff --git a/zencore/zencore.vcxproj b/zencore/zencore.vcxproj index bf35f8f5a..ef83e95f1 100644 --- a/zencore/zencore.vcxproj +++ b/zencore/zencore.vcxproj @@ -112,6 +112,7 @@ </Lib> </ItemDefinitionGroup> <ItemGroup> + <ClInclude Include="httpsys.h" /> <ClInclude Include="include\zencore\atomic.h" /> <ClInclude Include="include\zencore\blake3.h" /> <ClInclude Include="include\zencore\compositebuffer.h" /> @@ -165,6 +166,7 @@ <ClCompile Include="filesystem.cpp" /> <ClCompile Include="httpclient.cpp" /> <ClCompile Include="httpserver.cpp" /> + <ClCompile Include="httpsys.cpp" /> <ClCompile Include="iohash.cpp" /> <ClCompile Include="iothreadpool.cpp" /> <ClCompile Include="logging.cpp" /> diff --git a/zencore/zencore.vcxproj.filters b/zencore/zencore.vcxproj.filters index b1b1b9359..519ee0ce1 100644 --- a/zencore/zencore.vcxproj.filters +++ b/zencore/zencore.vcxproj.filters @@ -44,6 +44,7 @@ <ClInclude Include="include\zencore\prewindows.h" /> <ClInclude Include="include\zencore\postwindows.h" /> <ClInclude Include="include\zencore\logging.h" /> + <ClInclude Include="httpsys.h" /> </ItemGroup> <ItemGroup> <ClCompile Include="snapshot_manifest.cpp" /> @@ -77,6 +78,7 @@ <ClCompile Include="compositebuffer.cpp" /> <ClCompile Include="crc32.cpp" /> <ClCompile Include="logging.cpp" /> + <ClCompile Include="httpsys.cpp" /> </ItemGroup> <ItemGroup> <Filter Include="CAS"> |