diff options
| author | Stefan Boberg <[email protected]> | 2021-09-09 15:14:08 +0200 |
|---|---|---|
| committer | Stefan Boberg <[email protected]> | 2021-09-09 15:14:08 +0200 |
| commit | 6ec26c46d694a1d5291790a9c70bec25dce4b513 (patch) | |
| tree | 4177d45f9703d11f00e495f0fde77f4445453d2d /zenhttp | |
| parent | HttpServer::AddEndpoint -> HttpServer::RegisterService (diff) | |
| download | zen-6ec26c46d694a1d5291790a9c70bec25dce4b513.tar.xz zen-6ec26c46d694a1d5291790a9c70bec25dce4b513.zip | |
Factored out http server related code into zenhttp module since it feels out of place in zencore
Diffstat (limited to 'zenhttp')
| -rw-r--r-- | zenhttp/httpclient.cpp | 23 | ||||
| -rw-r--r-- | zenhttp/httpserver.cpp | 389 | ||||
| -rw-r--r-- | zenhttp/httpsys.cpp | 1250 | ||||
| -rw-r--r-- | zenhttp/httpsys.h | 61 | ||||
| -rw-r--r-- | zenhttp/include/zenhttp/httpclient.h | 22 | ||||
| -rw-r--r-- | zenhttp/include/zenhttp/httpserver.h | 446 | ||||
| -rw-r--r-- | zenhttp/include/zenhttp/zenhttp.h | 5 | ||||
| -rw-r--r-- | zenhttp/iothreadpool.cpp | 36 | ||||
| -rw-r--r-- | zenhttp/iothreadpool.h | 31 | ||||
| -rw-r--r-- | zenhttp/xmake.lua | 6 | ||||
| -rw-r--r-- | zenhttp/zenhttp.vcxproj | 120 | ||||
| -rw-r--r-- | zenhttp/zenhttp.vcxproj.filters | 18 |
12 files changed, 2407 insertions, 0 deletions
diff --git a/zenhttp/httpclient.cpp b/zenhttp/httpclient.cpp new file mode 100644 index 000000000..9d692271d --- /dev/null +++ b/zenhttp/httpclient.cpp @@ -0,0 +1,23 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zenhttp/httpclient.h> + +#include <spdlog/spdlog.h> + +#include <doctest/doctest.h> + +namespace zen { + +TEST_CASE("httpclient") +{ + using namespace std::literals; + + SUBCASE("client") {} +} + +void +httpclient_forcelink() +{ +} + +} // namespace zen diff --git a/zenhttp/httpserver.cpp b/zenhttp/httpserver.cpp new file mode 100644 index 000000000..a0b17fe44 --- /dev/null +++ b/zenhttp/httpserver.cpp @@ -0,0 +1,389 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zenhttp/httpserver.h> + +#include "httpsys.h" + +#include <zencore/compactbinary.h> +#include <zencore/compactbinarypackage.h> +#include <zencore/iobuffer.h> +#include <zencore/logging.h> +#include <zencore/refcount.h> +#include <zencore/stream.h> +#include <zencore/string.h> +#include <zencore/thread.h> + +#include <conio.h> +#include <new.h> +#include <charconv> +#include <span> +#include <string_view> + +#include <doctest/doctest.h> + +namespace zen { + +HttpServerRequest::HttpServerRequest() +{ +} + +HttpServerRequest::~HttpServerRequest() +{ +} + +struct CbPackageHeader +{ + uint32_t HeaderMagic; + uint32_t AttachmentCount; + uint32_t Reserved1; + uint32_t Reserved2; +}; + +static constinit uint32_t kCbPkgMagic = 0xaa77aacc; + +struct CbAttachmentEntry +{ + uint64_t AttachmentSize; + uint32_t Reserved1; + IoHash AttachmentHash; +}; + +void +HttpServerRequest::WriteResponse(HttpResponse HttpResponseCode, CbPackage Data) +{ + const std::span<const CbAttachment>& Attachments = Data.GetAttachments(); + + std::vector<IoBuffer> ResponseBuffers; + ResponseBuffers.reserve(3 + Attachments.size()); // TODO: may want to use an additional fudge factor here to avoid growing since each + // attachment is likely to consist of several buffers + + uint64_t TotalAttachmentsSize = 0; + + // Fixed size header + + CbPackageHeader Hdr{.HeaderMagic = kCbPkgMagic, .AttachmentCount = gsl::narrow<uint32_t>(Attachments.size())}; + + ResponseBuffers.push_back(IoBufferBuilder::MakeCloneFromMemory(&Hdr, sizeof Hdr)); + + // Attachment metadata array + + IoBuffer AttachmentMetadataBuffer = IoBuffer{sizeof(CbAttachmentEntry) * (Attachments.size() + /* root */ 1)}; + + CbAttachmentEntry* AttachmentInfo = reinterpret_cast<CbAttachmentEntry*>(AttachmentMetadataBuffer.MutableData()); + + ResponseBuffers.push_back(AttachmentMetadataBuffer); // Attachment metadata + + // Root object + + IoBuffer RootIoBuffer = Data.GetObject().GetBuffer().AsIoBuffer(); + ResponseBuffers.push_back(RootIoBuffer); // Root object + + *AttachmentInfo++ = {.AttachmentSize = RootIoBuffer.Size(), .AttachmentHash = Data.GetObjectHash()}; + + // Attachment payloads + + for (const CbAttachment& Attachment : Attachments) + { + CompressedBuffer AttachmentBuffer = Attachment.AsCompressedBinary(); + CompositeBuffer Compressed = AttachmentBuffer.GetCompressed(); + + *AttachmentInfo++ = {.AttachmentSize = AttachmentBuffer.GetCompressedSize(), + .AttachmentHash = IoHash::FromBLAKE3(AttachmentBuffer.GetRawHash())}; + + for (const SharedBuffer& Segment : Compressed.GetSegments()) + { + ResponseBuffers.push_back(Segment.AsIoBuffer()); + TotalAttachmentsSize += Segment.GetSize(); + } + } + + return WriteResponse(HttpResponseCode, HttpContentType::kCbPackage, ResponseBuffers); +} + +void +HttpServerRequest::WriteResponse(HttpResponse HttpResponseCode, CbObject Data) +{ + SharedBuffer Buf = Data.GetBuffer(); + std::array<IoBuffer, 1> Buffers{IoBufferBuilder::MakeCloneFromMemory(Buf.GetData(), Buf.GetSize())}; + return WriteResponse(HttpResponseCode, HttpContentType::kCbObject, Buffers); +} + +void +HttpServerRequest::WriteResponse(HttpResponse HttpResponseCode, HttpContentType ContentType, std::string_view ResponseString) +{ + return WriteResponse(HttpResponseCode, ContentType, std::u8string_view{(char8_t*)ResponseString.data(), ResponseString.size()}); +} + +void +HttpServerRequest::WriteResponse(HttpResponse HttpResponseCode, HttpContentType ContentType, IoBuffer Blob) +{ + std::array<IoBuffer, 1> Buffers{Blob}; + return WriteResponse(HttpResponseCode, ContentType, Buffers); +} + +HttpServerRequest::QueryParams +HttpServerRequest::GetQueryParams() +{ + QueryParams Params; + + const std::string_view QStr = QueryString(); + + const char* QueryIt = QStr.data(); + const char* QueryEnd = QueryIt + QStr.size(); + + while (QueryIt != QueryEnd) + { + if (*QueryIt == '&') + { + ++QueryIt; + continue; + } + + const std::string_view Query{QueryIt, QueryEnd}; + + size_t DelimIndex = Query.find('&', 0); + + if (DelimIndex == std::string_view::npos) + { + DelimIndex = Query.size(); + } + + std::string_view ThisQuery{QueryIt, DelimIndex}; + + size_t EqIndex = ThisQuery.find('=', 0); + + if (EqIndex != std::string_view::npos) + { + std::string_view Parm{ThisQuery.data(), EqIndex}; + ThisQuery.remove_prefix(EqIndex + 1); + + Params.KvPairs.emplace_back(Parm, ThisQuery); + } + + QueryIt += DelimIndex; + } + + return Params; +} + +CbObject +HttpServerRequest::ReadPayloadObject() +{ + IoBuffer Payload = ReadPayload(); + + if (Payload) + { + return LoadCompactBinaryObject(std::move(Payload)); + } + else + { + return {}; + } +} + +CbPackage +HttpServerRequest::ReadPayloadPackage() +{ + // TODO: this should not read into a contiguous buffer! + + IoBuffer Payload = ReadPayload(); + MemoryInStream InStream(Payload); + BinaryReader Reader(InStream); + + if (!Payload) + { + return {}; + } + + CbPackage Package; + + CbPackageHeader Hdr; + Reader.Read(&Hdr, sizeof Hdr); + + if (Hdr.HeaderMagic != kCbPkgMagic) + { + // report error + return {}; + } + + uint32_t ChunkCount = Hdr.AttachmentCount + 1; + + std::unique_ptr<CbAttachmentEntry[]> AttachmentEntries{new CbAttachmentEntry[ChunkCount]}; + + Reader.Read(AttachmentEntries.get(), sizeof(CbAttachmentEntry) * ChunkCount); + + for (uint32_t i = 0; i < ChunkCount; ++i) + { + const uint64_t AttachmentSize = AttachmentEntries[i].AttachmentSize; + IoBuffer AttachmentBuffer{AttachmentSize}; + Reader.Read(AttachmentBuffer.MutableData(), AttachmentSize); + CompressedBuffer CompBuf(CompressedBuffer::FromCompressed(SharedBuffer(AttachmentBuffer))); + + if (i == 0) + { + Package.SetObject(LoadCompactBinaryObject(CompBuf)); + } + else + { + CbAttachment Attachment(CompBuf); + Package.AddAttachment(Attachment); + } + } + + return Package; +} + +////////////////////////////////////////////////////////////////////////// + +HttpServerException::HttpServerException(const char* Message, uint32_t Error) : m_ErrorCode(Error) +{ + using namespace fmt::literals; + + m_Message = "{} (HTTP error {})"_format(Message, m_ErrorCode); +} + +const char* +HttpServerException::what() const +{ + return m_Message.c_str(); +} + +////////////////////////////////////////////////////////////////////////// + +void +HttpRequestRouter::AddPattern(const char* Id, const char* Regex) +{ + ZEN_ASSERT(m_PatternMap.find(Id) == m_PatternMap.end()); + + m_PatternMap.insert({Id, Regex}); +} + +void +HttpRequestRouter::RegisterRoute(const char* Regex, HttpRequestRouter::HandlerFunc_t&& HandlerFunc, HttpVerb SupportedVerbs) +{ + ExtendableStringBuilder<128> ExpandedRegex; + ProcessRegexSubstitutions(Regex, ExpandedRegex); + + m_Handlers.emplace_back(ExpandedRegex.c_str(), SupportedVerbs, std::move(HandlerFunc), Regex); +} + +void +HttpRequestRouter::RegisterRoute(const char* Regex, PackageEndpointHandler& Handler) +{ + ExtendableStringBuilder<128> ExpandedRegex; + ProcessRegexSubstitutions(Regex, ExpandedRegex); + + m_Handlers.emplace_back( + ExpandedRegex.c_str(), + HttpVerb::kPost, + [&Handler](HttpRouterRequest& Request) { Handler.HandleRequest(Request); }, + Regex); +} + +void +HttpRequestRouter::ProcessRegexSubstitutions(const char* Regex, StringBuilderBase& OutExpandedRegex) +{ + size_t RegexLen = strlen(Regex); + + for (size_t i = 0; i < RegexLen;) + { + bool matched = false; + + if (Regex[i] == '{' && ((i == 0) || (Regex[i - 1] != '\\'))) + { + // Might have a pattern reference - find closing brace + + for (size_t j = i + 1; j < RegexLen; ++j) + { + if (Regex[j] == '}') + { + std::string Pattern(&Regex[i + 1], j - i - 1); + + if (auto it = m_PatternMap.find(Pattern); it != m_PatternMap.end()) + { + OutExpandedRegex.Append(it->second.c_str()); + } + else + { + // Default to anything goes (or should this just be an error?) + + OutExpandedRegex.Append("(.+?)"); + } + + // skip ahead + i = j + 1; + + matched = true; + + break; + } + } + } + + if (!matched) + { + OutExpandedRegex.Append(Regex[i++]); + } + } +} + +bool +HttpRequestRouter::HandleRequest(zen::HttpServerRequest& Request) +{ + const HttpVerb Verb = Request.RequestVerb(); + + std::string_view Uri = Request.RelativeUri(); + HttpRouterRequest RouterRequest(Request); + + for (const auto& Handler : m_Handlers) + { + if ((Handler.Verbs & Verb) == Verb && regex_match(begin(Uri), end(Uri), RouterRequest.m_Match, Handler.RegEx)) + { + Handler.Handler(RouterRequest); + + return true; // Route matched + } + } + + return false; // No route matched +} + +////////////////////////////////////////////////////////////////////////// + +Ref<HttpServer> +CreateHttpServer() +{ + return new HttpSysServer{32}; +} + +////////////////////////////////////////////////////////////////////////// + +TEST_CASE("http") +{ + using namespace std::literals; + + SUBCASE("router") + { + HttpRequestRouter r; + r.AddPattern("a", "[[:alpha:]]+"); + r.RegisterRoute( + "{a}", + [&](auto) {}, + HttpVerb::kGet); + + // struct TestHttpServerRequest : public HttpServerRequest + //{ + // TestHttpServerRequest(std::string_view Uri) : m_uri{Uri} {} + //}; + + // TestHttpServerRequest req{}; + // r.HandleRequest(req); + } +} + +void +http_forcelink() +{ +} + +} // namespace zen diff --git a/zenhttp/httpsys.cpp b/zenhttp/httpsys.cpp new file mode 100644 index 000000000..da07a13dd --- /dev/null +++ b/zenhttp/httpsys.cpp @@ -0,0 +1,1250 @@ +// 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(int ThreadCount) : m_ThreadPool(ThreadCount) +{ + 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::RegisterService(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()); + } +} + +////////////////////////////////////////////////////////////////////////// +// +// HttpServer interface implementation +// + +void +HttpSysServer::Initialize(int BasePort) +{ + using namespace std::literals; + + WideStringBuilder<64> BaseUri; + BaseUri << u8"http://*:"sv << int64_t(BasePort) << u8"/"sv; + + Initialize(BaseUri.c_str()); + StartServer(); +} + +void +HttpSysServer::RequestExit() +{ + m_ShutdownEvent.Set(); +} +void +HttpSysServer::RegisterService(HttpService& Service) +{ + RegisterService(Service.BaseUri(), Service); +} + +#endif // ZEN_PLATFORM_WINDOWS + +} // namespace zen diff --git a/zenhttp/httpsys.h b/zenhttp/httpsys.h new file mode 100644 index 000000000..ed4a6a182 --- /dev/null +++ b/zenhttp/httpsys.h @@ -0,0 +1,61 @@ +#pragma once + +#include <zenhttp/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 : public HttpServer +{ + friend class HttpSysTransaction; + +public: + explicit HttpSysServer(int ThreadCount); + ~HttpSysServer(); + + // HttpServer interface implementation + + virtual void Initialize(int BasePort) override; + virtual void Run(bool TestMode) override; + virtual void RequestExit() override; + virtual void RegisterService(HttpService& Service) override; + +private: + void Initialize(const wchar_t* UrlPath); + + void StartServer(); + void OnHandlingRequest(); + void IssueNewRequestMaybe(); + + inline bool IsOk() const { return m_IsOk; } + + void RegisterService(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 = 16; + int32_t m_MaxPendingRequests = 128; + Event m_ShutdownEvent; +}; + +} // namespace zen diff --git a/zenhttp/include/zenhttp/httpclient.h b/zenhttp/include/zenhttp/httpclient.h new file mode 100644 index 000000000..4a266e59e --- /dev/null +++ b/zenhttp/include/zenhttp/httpclient.h @@ -0,0 +1,22 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "zenhttp.h" + +#include <zencore/string.h> +#include <gsl/gsl-lite.hpp> + +namespace zen { + +/** Asynchronous HTTP client implementation for Zen use cases + */ +class HttpClient +{ +public: +private: +}; + +} // namespace zen + +void httpclient_forcelink(); // internal diff --git a/zenhttp/include/zenhttp/httpserver.h b/zenhttp/include/zenhttp/httpserver.h new file mode 100644 index 000000000..95824b8f4 --- /dev/null +++ b/zenhttp/include/zenhttp/httpserver.h @@ -0,0 +1,446 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "zenhttp.h" + +#include <zencore/enumflags.h> +#include <zencore/iobuffer.h> +#include <zencore/refcount.h> +#include <zencore/string.h> + +#include <functional> +#include <gsl/gsl-lite.hpp> +#include <list> +#include <regex> +#include <span> +#include <unordered_map> + +namespace zen { + +using HttpContentType = ZenContentType; + +class IoBuffer; +class CbObject; +class CbPackage; +class StringBuilderBase; + +enum class HttpVerb +{ + kGet = 1 << 0, + kPut = 1 << 1, + kPost = 1 << 2, + kDelete = 1 << 3, + kHead = 1 << 4, + kCopy = 1 << 5, + kOptions = 1 << 6 +}; + +gsl_DEFINE_ENUM_BITMASK_OPERATORS(HttpVerb); + +enum class HttpResponse +{ + // 1xx - Informational + + Continue = 100, //!< Indicates that the initial part of a request has been received and has not yet been rejected by the server. + SwitchingProtocols = 101, //!< Indicates that the server understands and is willing to comply with the client's request, via the + //!< Upgrade header field, for a change in the application protocol being used on this connection. + Processing = 102, //!< Is an interim response used to inform the client that the server has accepted the complete request, but has not + //!< yet completed it. + EarlyHints = 103, //!< Indicates to the client that the server is likely to send a final response with the header fields included in + //!< the informational response. + + // 2xx - Successful + + OK = 200, //!< Indicates that the request has succeeded. + Created = 201, //!< Indicates that the request has been fulfilled and has resulted in one or more new resources being created. + Accepted = 202, //!< Indicates that the request has been accepted for processing, but the processing has not been completed. + NonAuthoritativeInformation = 203, //!< Indicates that the request was successful but the enclosed payload has been modified from that + //!< of the origin server's 200 (OK) response by a transforming proxy. + NoContent = 204, //!< Indicates that the server has successfully fulfilled the request and that there is no additional content to send + //!< in the response payload body. + ResetContent = 205, //!< Indicates that the server has fulfilled the request and desires that the user agent reset the \"document + //!< view\", which caused the request to be sent, to its original state as received from the origin server. + PartialContent = 206, //!< Indicates that the server is successfully fulfilling a range request for the target resource by transferring + //!< one or more parts of the selected representation that correspond to the satisfiable ranges found in the + //!< requests's Range header field. + MultiStatus = 207, //!< Provides status for multiple independent operations. + AlreadyReported = 208, //!< Used inside a DAV:propstat response element to avoid enumerating the internal members of multiple bindings + //!< to the same collection repeatedly. [RFC 5842] + IMUsed = 226, //!< The server has fulfilled a GET request for the resource, and the response is a representation of the result of one + //!< or more instance-manipulations applied to the current instance. + + // 3xx - Redirection + + MultipleChoices = 300, //!< Indicates that the target resource has more than one representation, each with its own more specific + //!< identifier, and information about the alternatives is being provided so that the user (or user agent) can + //!< select a preferred representation by redirecting its request to one or more of those identifiers. + MovedPermanently = 301, //!< Indicates that the target resource has been assigned a new permanent URI and any future references to this + //!< resource ought to use one of the enclosed URIs. + Found = 302, //!< Indicates that the target resource resides temporarily under a different URI. + SeeOther = 303, //!< Indicates that the server is redirecting the user agent to a different resource, as indicated by a URI in the + //!< Location header field, that is intended to provide an indirect response to the original request. + NotModified = 304, //!< Indicates that a conditional GET request has been received and would have resulted in a 200 (OK) response if it + //!< were not for the fact that the condition has evaluated to false. + UseProxy = 305, //!< \deprecated \parblock Due to security concerns regarding in-band configuration of a proxy. \endparblock + //!< The requested resource MUST be accessed through the proxy given by the Location field. + TemporaryRedirect = 307, //!< Indicates that the target resource resides temporarily under a different URI and the user agent MUST NOT + //!< change the request method if it performs an automatic redirection to that URI. + PermanentRedirect = 308, //!< The target resource has been assigned a new permanent URI and any future references to this resource + //!< ought to use one of the enclosed URIs. [...] This status code is similar to 301 Moved Permanently + //!< (Section 7.3.2 of rfc7231), except that it does not allow rewriting the request method from POST to GET. + + // 4xx - Client Error + BadRequest = 400, //!< Indicates that the server cannot or will not process the request because the received syntax is invalid, + //!< nonsensical, or exceeds some limitation on what the server is willing to process. + Unauthorized = 401, //!< Indicates that the request has not been applied because it lacks valid authentication credentials for the + //!< target resource. + PaymentRequired = 402, //!< *Reserved* + Forbidden = 403, //!< Indicates that the server understood the request but refuses to authorize it. + NotFound = 404, //!< Indicates that the origin server did not find a current representation for the target resource or is not willing + //!< to disclose that one exists. + MethodNotAllowed = 405, //!< Indicates that the method specified in the request-line is known by the origin server but not supported by + //!< the target resource. + NotAcceptable = 406, //!< Indicates that the target resource does not have a current representation that would be acceptable to the + //!< user agent, according to the proactive negotiation header fields received in the request, and the server is + //!< unwilling to supply a default representation. + ProxyAuthenticationRequired = + 407, //!< Is similar to 401 (Unauthorized), but indicates that the client needs to authenticate itself in order to use a proxy. + RequestTimeout = + 408, //!< Indicates that the server did not receive a complete request message within the time that it was prepared to wait. + Conflict = 409, //!< Indicates that the request could not be completed due to a conflict with the current state of the resource. + Gone = 410, //!< Indicates that access to the target resource is no longer available at the origin server and that this condition is + //!< likely to be permanent. + LengthRequired = 411, //!< Indicates that the server refuses to accept the request without a defined Content-Length. + PreconditionFailed = + 412, //!< Indicates that one or more preconditions given in the request header fields evaluated to false when tested on the server. + PayloadTooLarge = 413, //!< Indicates that the server is refusing to process a request because the request payload is larger than the + //!< server is willing or able to process. + URITooLong = 414, //!< Indicates that the server is refusing to service the request because the request-target is longer than the + //!< server is willing to interpret. + UnsupportedMediaType = 415, //!< Indicates that the origin server is refusing to service the request because the payload is in a format + //!< not supported by the target resource for this method. + RangeNotSatisfiable = 416, //!< Indicates that none of the ranges in the request's Range header field overlap the current extent of the + //!< selected resource or that the set of ranges requested has been rejected due to invalid ranges or an + //!< excessive request of small or overlapping ranges. + ExpectationFailed = 417, //!< Indicates that the expectation given in the request's Expect header field could not be met by at least + //!< one of the inbound servers. + ImATeapot = 418, //!< Any attempt to brew coffee with a teapot should result in the error code 418 I'm a teapot. + UnprocessableEntity = 422, //!< Means the server understands the content type of the request entity (hence a 415(Unsupported Media + //!< Type) status code is inappropriate), and the syntax of the request entity is correct (thus a 400 (Bad + //!< Request) status code is inappropriate) but was unable to process the contained instructions. + Locked = 423, //!< Means the source or destination resource of a method is locked. + FailedDependency = 424, //!< Means that the method could not be performed on the resource because the requested action depended on + //!< another action and that action failed. + UpgradeRequired = 426, //!< Indicates that the server refuses to perform the request using the current protocol but might be willing to + //!< do so after the client upgrades to a different protocol. + PreconditionRequired = 428, //!< Indicates that the origin server requires the request to be conditional. + TooManyRequests = 429, //!< Indicates that the user has sent too many requests in a given amount of time (\"rate limiting\"). + RequestHeaderFieldsTooLarge = + 431, //!< Indicates that the server is unwilling to process the request because its header fields are too large. + UnavailableForLegalReasons = + 451, //!< This status code indicates that the server is denying access to the resource in response to a legal demand. + + // 5xx - Server Error + + InternalServerError = + 500, //!< Indicates that the server encountered an unexpected condition that prevented it from fulfilling the request. + NotImplemented = 501, //!< Indicates that the server does not support the functionality required to fulfill the request. + BadGateway = 502, //!< Indicates that the server, while acting as a gateway or proxy, received an invalid response from an inbound + //!< server it accessed while attempting to fulfill the request. + ServiceUnavailable = 503, //!< Indicates that the server is currently unable to handle the request due to a temporary overload or + //!< scheduled maintenance, which will likely be alleviated after some delay. + GatewayTimeout = 504, //!< Indicates that the server, while acting as a gateway or proxy, did not receive a timely response from an + //!< upstream server it needed to access in order to complete the request. + HTTPVersionNotSupported = 505, //!< Indicates that the server does not support, or refuses to support, the protocol version that was + //!< used in the request message. + VariantAlsoNegotiates = + 506, //!< Indicates that the server has an internal configuration error: the chosen variant resource is configured to engage in + //!< transparent content negotiation itself, and is therefore not a proper end point in the negotiation process. + InsufficientStorage = 507, //!< Means the method could not be performed on the resource because the server is unable to store the + //!< representation needed to successfully complete the request. + LoopDetected = 508, //!< Indicates that the server terminated an operation because it encountered an infinite loop while processing a + //!< request with "Depth: infinity". [RFC 5842] + NotExtended = 510, //!< The policy for accessing the resource has not been met in the request. [RFC 2774] + NetworkAuthenticationRequired = 511, //!< Indicates that the client needs to authenticate to gain network access. +}; + +/** HTTP Server Request + */ +class HttpServerRequest +{ +public: + HttpServerRequest(); + ~HttpServerRequest(); + + // Synchronous operations + + [[nodiscard]] inline std::string_view RelativeUri() const { return m_Uri; } // Returns URI without service prefix + [[nodiscard]] inline std::string_view QueryString() const { return m_QueryString; } + inline bool IsHandled() const { return m_IsHandled; } + + struct QueryParams + { + std::vector<std::pair<std::string_view, std::string_view>> KvPairs; + + std::string_view GetValue(std::string_view ParamName) + { + for (const auto& Kv : KvPairs) + { + const std::string_view& Key = Kv.first; + + if (Key.size() == ParamName.size()) + { + if (0 == _strnicmp(Key.data(), ParamName.data(), Key.size())) + { + return Kv.second; + } + } + } + + return std::string_view(); + } + }; + + QueryParams GetQueryParams(); + + inline HttpVerb RequestVerb() const { return m_Verb; } + inline HttpContentType RequestContentType() { return m_ContentType; } + + const char* HeaderAccept() const; + const char* HeaderAcceptEncoding() const; + const char* HeaderContentType() const; + const char* HeaderContentEncoding() const; + inline uint64_t HeaderContentLength() const { return m_ContentLength; } + + void SetSuppressResponseBody() { m_SuppressBody = true; } + + // Asynchronous operations + + /** Read POST/PUT payload + + This will return a null buffer if the contents are not fully available yet, and the handler should + at that point return - another completion request will be issued once the contents have been received + fully. + + NOTE: in practice, via the http.sys implementation this always operates synchronously. This should + be updated to provide fully asynchronous operation for better scalability on shared instances + */ + virtual IoBuffer ReadPayload() = 0; + + virtual void ReadPayload(std::function<void(HttpServerRequest&, IoBuffer)>&& CompletionHandler) = 0; + + ZENCORE_API CbObject ReadPayloadObject(); + ZENCORE_API CbPackage ReadPayloadPackage(); + + /** Respond with payload + + Note that this is destructive in the sense that the IoBuffer instances referred to by Blobs will be + moved into our response handler array where they are kept alive, in order to reduce ref-counting storms + */ + virtual void WriteResponse(HttpResponse HttpResponseCode, HttpContentType ContentType, std::span<IoBuffer> Blobs) = 0; + virtual void WriteResponse(HttpResponse HttpResponseCode, HttpContentType ContentType, IoBuffer Blob); + virtual void WriteResponse(HttpResponse HttpResponseCode) = 0; + + virtual void WriteResponse(HttpResponse HttpResponseCode, HttpContentType ContentType, std::u8string_view ResponseString) = 0; + + void WriteResponse(HttpResponse HttpResponseCode, CbObject Data); + void WriteResponse(HttpResponse HttpResponseCode, CbPackage Package); + void WriteResponse(HttpResponse HttpResponseCode, HttpContentType ContentType, std::string_view ResponseString); + +protected: + bool m_IsHandled = false; + bool m_SuppressBody = false; + HttpVerb m_Verb = HttpVerb::kGet; + uint64_t m_ContentLength = ~0ull; + HttpContentType m_ContentType = HttpContentType::kBinary; + ExtendableStringBuilder<256> m_Uri; + ExtendableStringBuilder<256> m_QueryString; +}; + +class HttpServerException : public std::exception +{ +public: + HttpServerException(const char* Message, uint32_t Error); + + virtual const char* what() const override; + +private: + uint32_t m_ErrorCode; + std::string m_Message; +}; + +/** + * Base class for implementing an HTTP "service" + * + * A service exposes one or more endpoints with a certain URI prefix + * + */ + +class HttpService +{ +public: + HttpService() = default; + virtual ~HttpService() = default; + + virtual const char* BaseUri() const = 0; + virtual void HandleRequest(HttpServerRequest& HttpServiceRequest) = 0; + + // Internals + + inline void SetUriPrefixLength(size_t PrefixLength) { m_UriPrefixLength = (int)PrefixLength; } + inline int UriPrefixLength() const { return m_UriPrefixLength; } + +private: + int m_UriPrefixLength = 0; +}; + +/** HTTP server + * + * Implements the main event loop to service HTTP requests, and handles routing + * requests to the appropriate endpoint handler as registered via AddEndpoint + */ +class HttpServer : public RefCounted +{ +public: + virtual void RegisterService(HttpService& Service) = 0; + virtual void Initialize(int BasePort) = 0; + virtual void Run(bool TestMode) = 0; + virtual void RequestExit() = 0; +}; + +Ref<HttpServer> CreateHttpServer(); + +////////////////////////////////////////////////////////////////////////// + +class HttpRouterRequest +{ +public: + HttpRouterRequest(HttpServerRequest& Request) : m_HttpRequest(Request) {} + + ZENCORE_API std::string GetCapture(uint32_t Index) const; + inline HttpServerRequest& ServerRequest() { return m_HttpRequest; } + +private: + using MatchResults_t = std::match_results<std::string_view::const_iterator>; + + HttpServerRequest& m_HttpRequest; + MatchResults_t m_Match; + + friend class HttpRequestRouter; +}; + +inline std::string +HttpRouterRequest::GetCapture(uint32_t Index) const +{ + ZEN_ASSERT(Index < m_Match.size()); + + return m_Match[Index]; +} + +////////////////////////////////////////////////////////////////////////// + +class PackageRequestContext +{ +public: + PackageRequestContext(); + ~PackageRequestContext(); + +private: +}; + +class PackageEndpointHandler +{ +public: + virtual void HandleRequest(HttpRouterRequest& Request) = 0; + +private: +}; + +/** HTTP request router helper + * + * This helper class allows a service implementer to register one or more + * endpoints using pattern matching (currently using regex matching) + * + * This is intended to be initialized once only, there is no thread + * safety so you can absolutely not add or remove endpoints once the handler + * goes live + */ + +class HttpRequestRouter +{ +public: + typedef std::function<void(HttpRouterRequest&)> HandlerFunc_t; + + /** + * @brief Add pattern which can be referenced by name, commonly used for URL components + * @param Id String used to identify patterns for replacement + * @param Regex String which will replace the Id string in any registered URL paths + */ + void AddPattern(const char* Id, const char* Regex); + + /** + * @brief Register a an endpoint handler for the given route + * @param Regex Regular expression used to match the handler to a request. This may + * contain pattern aliases registered via AddPattern + * @param HandlerFunc Handler function to call for any matching request + * @param SupportedVerbs Supported HTTP verbs for this handler + */ + void RegisterRoute(const char* Regex, HandlerFunc_t&& HandlerFunc, HttpVerb SupportedVerbs); + /** + * @brief Register CbPackage endpoint handler + * @param Regex Regular expression used to match the handler to a request. This may + * contain pattern aliases registered via AddPattern + * @param Handler Package handler instance + */ + void RegisterRoute(const char* Regex, PackageEndpointHandler& Handler); + + void ProcessRegexSubstitutions(const char* Regex, StringBuilderBase& ExpandedRegex); + + /** + * @brief HTTP request handling function - this should be called to route the + * request to a registered handler + * @param Request Request to route to a handler + * @return Function returns true if the request was routed successfully + */ + bool HandleRequest(zen::HttpServerRequest& Request); + +private: + struct HandlerEntry + { + HandlerEntry(const char* Regex, HttpVerb SupportedVerbs, HandlerFunc_t&& Handler, const char* Pattern) + : RegEx(Regex, std::regex::icase | std::regex::ECMAScript) + , Verbs(SupportedVerbs) + , Handler(std::move(Handler)) + , Pattern(Pattern) + { + } + + ~HandlerEntry() = default; + + std::regex RegEx; + HttpVerb Verbs; + HandlerFunc_t Handler; + const char* Pattern; + + private: + HandlerEntry& operator=(const HandlerEntry&) = delete; + HandlerEntry(const HandlerEntry&) = delete; + }; + + std::list<HandlerEntry> m_Handlers; + std::unordered_map<std::string, std::string> m_PatternMap; +}; + +////////////////////////////////////////////////////////////////////////// +// +// HTTP Client +// + +class HttpClient +{ +}; + +} // namespace zen + +void http_forcelink(); // internal diff --git a/zenhttp/include/zenhttp/zenhttp.h b/zenhttp/include/zenhttp/zenhttp.h new file mode 100644 index 000000000..da4f4cbe8 --- /dev/null +++ b/zenhttp/include/zenhttp/zenhttp.h @@ -0,0 +1,5 @@ +#pragma once + +#include <zencore/zencore.h> + +#define ZENHTTP_API // Placeholder to allow DLL configs in the future diff --git a/zenhttp/iothreadpool.cpp b/zenhttp/iothreadpool.cpp new file mode 100644 index 000000000..4ed81d7a2 --- /dev/null +++ b/zenhttp/iothreadpool.cpp @@ -0,0 +1,36 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "iothreadpool.h" + +namespace zen { + +WinIoThreadPool::WinIoThreadPool(int InThreadCount) +{ + // Thread pool setup + + m_ThreadPool = CreateThreadpool(NULL); + + SetThreadpoolThreadMinimum(m_ThreadPool, InThreadCount); + SetThreadpoolThreadMaximum(m_ThreadPool, InThreadCount * 2); + + InitializeThreadpoolEnvironment(&m_CallbackEnvironment); + + m_CleanupGroup = CreateThreadpoolCleanupGroup(); + + SetThreadpoolCallbackPool(&m_CallbackEnvironment, m_ThreadPool); + + SetThreadpoolCallbackCleanupGroup(&m_CallbackEnvironment, m_CleanupGroup, NULL); +} + +WinIoThreadPool::~WinIoThreadPool() +{ + CloseThreadpool(m_ThreadPool); +} + +void +WinIoThreadPool::CreateIocp(HANDLE IoHandle, PTP_WIN32_IO_CALLBACK Callback, void* Context) +{ + m_ThreadPoolIo = CreateThreadpoolIo(IoHandle, Callback, Context, &m_CallbackEnvironment); +} + +} // namespace zen diff --git a/zenhttp/iothreadpool.h b/zenhttp/iothreadpool.h new file mode 100644 index 000000000..f64868540 --- /dev/null +++ b/zenhttp/iothreadpool.h @@ -0,0 +1,31 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include <zencore/windows.h> + +namespace zen { + +////////////////////////////////////////////////////////////////////////// +// +// Thread pool. Implemented in terms of Windows thread pool right now, will +// need a cross-platform implementation eventually +// + +class WinIoThreadPool +{ +public: + WinIoThreadPool(int InThreadCount); + ~WinIoThreadPool(); + + void CreateIocp(HANDLE IoHandle, PTP_WIN32_IO_CALLBACK Callback, void* Context); + inline PTP_IO Iocp() const { return m_ThreadPoolIo; } + +private: + PTP_POOL m_ThreadPool = nullptr; + PTP_CLEANUP_GROUP m_CleanupGroup = nullptr; + PTP_IO m_ThreadPoolIo = nullptr; + TP_CALLBACK_ENVIRON m_CallbackEnvironment; +}; + +} // namespace zen diff --git a/zenhttp/xmake.lua b/zenhttp/xmake.lua new file mode 100644 index 000000000..a1455f46c --- /dev/null +++ b/zenhttp/xmake.lua @@ -0,0 +1,6 @@ +target('zenhttp') + set_kind("static") + add_files("**.cpp") + add_includedirs("include", {public=true}) + add_deps("zencore") + add_packages("vcpkg::gsl-lite")
\ No newline at end of file diff --git a/zenhttp/zenhttp.vcxproj b/zenhttp/zenhttp.vcxproj new file mode 100644 index 000000000..1e6aba90f --- /dev/null +++ b/zenhttp/zenhttp.vcxproj @@ -0,0 +1,120 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <ItemGroup Label="ProjectConfigurations"> + <ProjectConfiguration Include="Debug|x64"> + <Configuration>Debug</Configuration> + <Platform>x64</Platform> + </ProjectConfiguration> + <ProjectConfiguration Include="Release|x64"> + <Configuration>Release</Configuration> + <Platform>x64</Platform> + </ProjectConfiguration> + </ItemGroup> + <PropertyGroup Label="Globals"> + <VCProjectVersion>16.0</VCProjectVersion> + <Keyword>Win32Proj</Keyword> + <ProjectGuid>{8eeb3be5-7001-46bf-aafd-edb7558ac012}</ProjectGuid> + <RootNamespace>zenhttp</RootNamespace> + <WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion> + </PropertyGroup> + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" /> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration"> + <ConfigurationType>StaticLibrary</ConfigurationType> + <UseDebugLibraries>true</UseDebugLibraries> + <PlatformToolset>v142</PlatformToolset> + <CharacterSet>Unicode</CharacterSet> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration"> + <ConfigurationType>StaticLibrary</ConfigurationType> + <UseDebugLibraries>false</UseDebugLibraries> + <PlatformToolset>v142</PlatformToolset> + <WholeProgramOptimization>true</WholeProgramOptimization> + <CharacterSet>Unicode</CharacterSet> + </PropertyGroup> + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" /> + <ImportGroup Label="ExtensionSettings"> + </ImportGroup> + <ImportGroup Label="Shared"> + </ImportGroup> + <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> + <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> + <Import Project="..\zen_base_debug.props" /> + <Import Project="..\zenfs_common.props" /> + </ImportGroup> + <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> + <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> + <Import Project="..\zen_base_release.props" /> + <Import Project="..\zenfs_common.props" /> + </ImportGroup> + <PropertyGroup Label="UserMacros" /> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> + <LinkIncremental>true</LinkIncremental> + <PublicIncludeDirectories>$(ProjectDir)include;$(PublicIncludeDirectories)</PublicIncludeDirectories> + </PropertyGroup> + <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> + <LinkIncremental>false</LinkIncremental> + <PublicIncludeDirectories>$(ProjectDir)include;$(PublicIncludeDirectories)</PublicIncludeDirectories> + </PropertyGroup> + <PropertyGroup Label="Vcpkg"> + <VcpkgEnableManifest>true</VcpkgEnableManifest> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> + <VcpkgUseStatic>true</VcpkgUseStatic> + </PropertyGroup> + <PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> + <VcpkgUseStatic>true</VcpkgUseStatic> + </PropertyGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> + <ClCompile> + <WarningLevel>Level3</WarningLevel> + <SDLCheck>true</SDLCheck> + <PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <ConformanceMode>true</ConformanceMode> + <AdditionalIncludeDirectories>.\include</AdditionalIncludeDirectories> + </ClCompile> + <Link> + <SubSystem>Console</SubSystem> + <GenerateDebugInformation>true</GenerateDebugInformation> + </Link> + </ItemDefinitionGroup> + <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> + <ClCompile> + <WarningLevel>Level3</WarningLevel> + <FunctionLevelLinking>true</FunctionLevelLinking> + <IntrinsicFunctions>true</IntrinsicFunctions> + <SDLCheck>true</SDLCheck> + <PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions> + <ConformanceMode>true</ConformanceMode> + <AdditionalIncludeDirectories>.\include</AdditionalIncludeDirectories> + </ClCompile> + <Link> + <SubSystem>Console</SubSystem> + <EnableCOMDATFolding>true</EnableCOMDATFolding> + <OptimizeReferences>true</OptimizeReferences> + <GenerateDebugInformation>true</GenerateDebugInformation> + </Link> + </ItemDefinitionGroup> + <ItemGroup> + <ClCompile Include="httpclient.cpp" /> + <ClCompile Include="httpserver.cpp" /> + <ClCompile Include="httpsys.cpp" /> + <ClCompile Include="iothreadpool.cpp" /> + </ItemGroup> + <ItemGroup> + <ClInclude Include="httpsys.h" /> + <ClInclude Include="include\zenhttp\httpclient.h" /> + <ClInclude Include="include\zenhttp\httpserver.h" /> + <ClInclude Include="iothreadpool.h" /> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\zencore\zencore.vcxproj"> + <Project>{d75bf9ab-c61e-4fff-ad59-1563430f05e2}</Project> + </ProjectReference> + </ItemGroup> + <ItemGroup> + <None Include="xmake.lua" /> + </ItemGroup> + <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> + <ImportGroup Label="ExtensionTargets"> + </ImportGroup> +</Project>
\ No newline at end of file diff --git a/zenhttp/zenhttp.vcxproj.filters b/zenhttp/zenhttp.vcxproj.filters new file mode 100644 index 000000000..16e374d0d --- /dev/null +++ b/zenhttp/zenhttp.vcxproj.filters @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <ItemGroup> + <ClCompile Include="httpclient.cpp" /> + <ClCompile Include="httpserver.cpp" /> + <ClCompile Include="httpsys.cpp" /> + <ClCompile Include="iothreadpool.cpp" /> + </ItemGroup> + <ItemGroup> + <ClInclude Include="include\zenhttp\httpclient.h" /> + <ClInclude Include="include\zenhttp\httpserver.h" /> + <ClInclude Include="httpsys.h" /> + <ClInclude Include="iothreadpool.h" /> + </ItemGroup> + <ItemGroup> + <None Include="xmake.lua" /> + </ItemGroup> +</Project>
\ No newline at end of file |