aboutsummaryrefslogtreecommitdiff
path: root/src/zenhttp/httpserver.cpp
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2023-05-02 10:01:47 +0200
committerGitHub <[email protected]>2023-05-02 10:01:47 +0200
commit075d17f8ada47e990fe94606c3d21df409223465 (patch)
treee50549b766a2f3c354798a54ff73404217b4c9af /src/zenhttp/httpserver.cpp
parentfix: bundle shouldn't append content zip to zen (diff)
downloadzen-075d17f8ada47e990fe94606c3d21df409223465.tar.xz
zen-075d17f8ada47e990fe94606c3d21df409223465.zip
moved source directories into `/src` (#264)
* moved source directories into `/src` * updated bundle.lua for new `src` path * moved some docs, icon * removed old test trees
Diffstat (limited to 'src/zenhttp/httpserver.cpp')
-rw-r--r--src/zenhttp/httpserver.cpp885
1 files changed, 885 insertions, 0 deletions
diff --git a/src/zenhttp/httpserver.cpp b/src/zenhttp/httpserver.cpp
new file mode 100644
index 000000000..671cbd319
--- /dev/null
+++ b/src/zenhttp/httpserver.cpp
@@ -0,0 +1,885 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zenhttp/httpserver.h>
+
+#include "httpasio.h"
+#include "httpnull.h"
+#include "httpsys.h"
+
+#include <zencore/compactbinary.h>
+#include <zencore/compactbinarybuilder.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/testing.h>
+#include <zencore/thread.h>
+#include <zenhttp/httpshared.h>
+
+#include <charconv>
+#include <mutex>
+#include <span>
+#include <string_view>
+
+namespace zen {
+
+using namespace std::literals;
+
+std::string_view
+MapContentTypeToString(HttpContentType ContentType)
+{
+ switch (ContentType)
+ {
+ default:
+ case HttpContentType::kUnknownContentType:
+ case HttpContentType::kBinary:
+ return "application/octet-stream"sv;
+
+ case HttpContentType::kText:
+ return "text/plain"sv;
+
+ case HttpContentType::kJSON:
+ return "application/json"sv;
+
+ case HttpContentType::kCbObject:
+ return "application/x-ue-cb"sv;
+
+ case HttpContentType::kCbPackage:
+ return "application/x-ue-cbpkg"sv;
+
+ case HttpContentType::kCbPackageOffer:
+ return "application/x-ue-offer"sv;
+
+ case HttpContentType::kCompressedBinary:
+ return "application/x-ue-comp"sv;
+
+ case HttpContentType::kYAML:
+ return "text/yaml"sv;
+
+ case HttpContentType::kHTML:
+ return "text/html"sv;
+
+ case HttpContentType::kJavaScript:
+ return "application/javascript"sv;
+
+ case HttpContentType::kCSS:
+ return "text/css"sv;
+
+ case HttpContentType::kPNG:
+ return "image/png"sv;
+
+ case HttpContentType::kIcon:
+ return "image/x-icon"sv;
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////
+//
+// Note that in addition to MIME types we accept abbreviated versions, for
+// use in suffix parsing as well as for convenience when using curl
+
+static constinit uint32_t HashBinary = HashStringDjb2("application/octet-stream"sv);
+static constinit uint32_t HashJson = HashStringDjb2("json"sv);
+static constinit uint32_t HashApplicationJson = HashStringDjb2("application/json"sv);
+static constinit uint32_t HashYaml = HashStringDjb2("yaml"sv);
+static constinit uint32_t HashTextYaml = HashStringDjb2("text/yaml"sv);
+static constinit uint32_t HashText = HashStringDjb2("text/plain"sv);
+static constinit uint32_t HashApplicationCompactBinary = HashStringDjb2("application/x-ue-cb"sv);
+static constinit uint32_t HashCompactBinary = HashStringDjb2("ucb"sv);
+static constinit uint32_t HashCompactBinaryPackage = HashStringDjb2("application/x-ue-cbpkg"sv);
+static constinit uint32_t HashCompactBinaryPackageShort = HashStringDjb2("cbpkg"sv);
+static constinit uint32_t HashCompactBinaryPackageOffer = HashStringDjb2("application/x-ue-offer"sv);
+static constinit uint32_t HashCompressedBinary = HashStringDjb2("application/x-ue-comp"sv);
+static constinit uint32_t HashHtml = HashStringDjb2("html"sv);
+static constinit uint32_t HashTextHtml = HashStringDjb2("text/html"sv);
+static constinit uint32_t HashJavaScript = HashStringDjb2("js"sv);
+static constinit uint32_t HashApplicationJavaScript = HashStringDjb2("application/javascript"sv);
+static constinit uint32_t HashCss = HashStringDjb2("css"sv);
+static constinit uint32_t HashTextCss = HashStringDjb2("text/css"sv);
+static constinit uint32_t HashPng = HashStringDjb2("png"sv);
+static constinit uint32_t HashImagePng = HashStringDjb2("image/png"sv);
+static constinit uint32_t HashIcon = HashStringDjb2("ico"sv);
+static constinit uint32_t HashImageIcon = HashStringDjb2("image/x-icon"sv);
+
+std::once_flag InitContentTypeLookup;
+
+struct HashedTypeEntry
+{
+ uint32_t Hash;
+ HttpContentType Type;
+} TypeHashTable[] = {
+ // clang-format off
+ {HashBinary, HttpContentType::kBinary},
+ {HashApplicationCompactBinary, HttpContentType::kCbObject},
+ {HashCompactBinary, HttpContentType::kCbObject},
+ {HashCompactBinaryPackage, HttpContentType::kCbPackage},
+ {HashCompactBinaryPackageShort, HttpContentType::kCbPackage},
+ {HashCompactBinaryPackageOffer, HttpContentType::kCbPackageOffer},
+ {HashJson, HttpContentType::kJSON},
+ {HashApplicationJson, HttpContentType::kJSON},
+ {HashYaml, HttpContentType::kYAML},
+ {HashTextYaml, HttpContentType::kYAML},
+ {HashText, HttpContentType::kText},
+ {HashCompressedBinary, HttpContentType::kCompressedBinary},
+ {HashHtml, HttpContentType::kHTML},
+ {HashTextHtml, HttpContentType::kHTML},
+ {HashJavaScript, HttpContentType::kJavaScript},
+ {HashApplicationJavaScript, HttpContentType::kJavaScript},
+ {HashCss, HttpContentType::kCSS},
+ {HashTextCss, HttpContentType::kCSS},
+ {HashPng, HttpContentType::kPNG},
+ {HashImagePng, HttpContentType::kPNG},
+ {HashIcon, HttpContentType::kIcon},
+ {HashImageIcon, HttpContentType::kIcon},
+ // clang-format on
+};
+
+HttpContentType
+ParseContentTypeImpl(const std::string_view& ContentTypeString)
+{
+ if (!ContentTypeString.empty())
+ {
+ const uint32_t CtHash = HashStringDjb2(ContentTypeString);
+
+ if (auto It = std::lower_bound(std::begin(TypeHashTable),
+ std::end(TypeHashTable),
+ CtHash,
+ [](const HashedTypeEntry& Lhs, const uint32_t Rhs) { return Lhs.Hash < Rhs; });
+ It != std::end(TypeHashTable))
+ {
+ if (It->Hash == CtHash)
+ {
+ return It->Type;
+ }
+ }
+ }
+
+ return HttpContentType::kUnknownContentType;
+}
+
+HttpContentType
+ParseContentTypeInit(const std::string_view& ContentTypeString)
+{
+ std::call_once(InitContentTypeLookup, [] {
+ std::sort(std::begin(TypeHashTable), std::end(TypeHashTable), [](const HashedTypeEntry& Lhs, const HashedTypeEntry& Rhs) {
+ return Lhs.Hash < Rhs.Hash;
+ });
+
+ // validate that there are no hash collisions
+
+ uint32_t LastHash = 0;
+
+ for (const auto& Item : TypeHashTable)
+ {
+ ZEN_ASSERT(LastHash != Item.Hash);
+ LastHash = Item.Hash;
+ }
+ });
+
+ ParseContentType = ParseContentTypeImpl;
+
+ return ParseContentTypeImpl(ContentTypeString);
+}
+
+HttpContentType (*ParseContentType)(const std::string_view& ContentTypeString) = &ParseContentTypeInit;
+
+bool
+TryParseHttpRangeHeader(std::string_view RangeHeader, HttpRanges& Ranges)
+{
+ if (RangeHeader.empty())
+ {
+ return false;
+ }
+
+ const size_t Count = Ranges.size();
+
+ std::size_t UnitDelim = RangeHeader.find_first_of('=');
+ if (UnitDelim == std::string_view::npos)
+ {
+ return false;
+ }
+
+ // only bytes for now
+ std::string_view Unit = RangeHeader.substr(0, UnitDelim);
+ if (Unit != "bytes"sv)
+ {
+ return false;
+ }
+
+ std::string_view Tokens = RangeHeader.substr(UnitDelim);
+ while (!Tokens.empty())
+ {
+ // Skip =,
+ Tokens = Tokens.substr(1);
+
+ size_t Delim = Tokens.find_first_of(',');
+ if (Delim == std::string_view::npos)
+ {
+ Delim = Tokens.length();
+ }
+
+ std::string_view Token = Tokens.substr(0, Delim);
+ Tokens = Tokens.substr(Delim);
+
+ Delim = Token.find_first_of('-');
+ if (Delim == std::string_view::npos)
+ {
+ return false;
+ }
+
+ const auto Start = ParseInt<uint32_t>(Token.substr(0, Delim));
+ const auto End = ParseInt<uint32_t>(Token.substr(Delim + 1));
+
+ if (Start.has_value() && End.has_value() && End.value() > Start.value())
+ {
+ Ranges.push_back({.Start = Start.value(), .End = End.value()});
+ }
+ else if (Start)
+ {
+ Ranges.push_back({.Start = Start.value()});
+ }
+ else if (End)
+ {
+ Ranges.push_back({.End = End.value()});
+ }
+ }
+
+ return Count != Ranges.size();
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+const std::string_view
+ToString(HttpVerb Verb)
+{
+ switch (Verb)
+ {
+ case HttpVerb::kGet:
+ return "GET"sv;
+ case HttpVerb::kPut:
+ return "PUT"sv;
+ case HttpVerb::kPost:
+ return "POST"sv;
+ case HttpVerb::kDelete:
+ return "DELETE"sv;
+ case HttpVerb::kHead:
+ return "HEAD"sv;
+ case HttpVerb::kCopy:
+ return "COPY"sv;
+ case HttpVerb::kOptions:
+ return "OPTIONS"sv;
+ default:
+ return "???"sv;
+ }
+}
+
+std::string_view
+ReasonStringForHttpResultCode(int HttpCode)
+{
+ switch (HttpCode)
+ {
+ // 1xx Informational
+
+ case 100:
+ return "Continue"sv;
+ case 101:
+ return "Switching Protocols"sv;
+
+ // 2xx Success
+
+ case 200:
+ return "OK"sv;
+ case 201:
+ return "Created"sv;
+ case 202:
+ return "Accepted"sv;
+ case 204:
+ return "No Content"sv;
+ case 205:
+ return "Reset Content"sv;
+ case 206:
+ return "Partial Content"sv;
+
+ // 3xx Redirection
+
+ case 300:
+ return "Multiple Choices"sv;
+ case 301:
+ return "Moved Permanently"sv;
+ case 302:
+ return "Found"sv;
+ case 303:
+ return "See Other"sv;
+ case 304:
+ return "Not Modified"sv;
+ case 305:
+ return "Use Proxy"sv;
+ case 306:
+ return "Switch Proxy"sv;
+ case 307:
+ return "Temporary Redirect"sv;
+ case 308:
+ return "Permanent Redirect"sv;
+
+ // 4xx Client errors
+
+ case 400:
+ return "Bad Request"sv;
+ case 401:
+ return "Unauthorized"sv;
+ case 402:
+ return "Payment Required"sv;
+ case 403:
+ return "Forbidden"sv;
+ case 404:
+ return "Not Found"sv;
+ case 405:
+ return "Method Not Allowed"sv;
+ case 406:
+ return "Not Acceptable"sv;
+ case 407:
+ return "Proxy Authentication Required"sv;
+ case 408:
+ return "Request Timeout"sv;
+ case 409:
+ return "Conflict"sv;
+ case 410:
+ return "Gone"sv;
+ case 411:
+ return "Length Required"sv;
+ case 412:
+ return "Precondition Failed"sv;
+ case 413:
+ return "Payload Too Large"sv;
+ case 414:
+ return "URI Too Long"sv;
+ case 415:
+ return "Unsupported Media Type"sv;
+ case 416:
+ return "Range Not Satisifiable"sv;
+ case 417:
+ return "Expectation Failed"sv;
+ case 418:
+ return "I'm a teapot"sv;
+ case 421:
+ return "Misdirected Request"sv;
+ case 422:
+ return "Unprocessable Entity"sv;
+ case 423:
+ return "Locked"sv;
+ case 424:
+ return "Failed Dependency"sv;
+ case 425:
+ return "Too Early"sv;
+ case 426:
+ return "Upgrade Required"sv;
+ case 428:
+ return "Precondition Required"sv;
+ case 429:
+ return "Too Many Requests"sv;
+ case 431:
+ return "Request Header Fields Too Large"sv;
+
+ // 5xx Server errors
+
+ case 500:
+ return "Internal Server Error"sv;
+ case 501:
+ return "Not Implemented"sv;
+ case 502:
+ return "Bad Gateway"sv;
+ case 503:
+ return "Service Unavailable"sv;
+ case 504:
+ return "Gateway Timeout"sv;
+ case 505:
+ return "HTTP Version Not Supported"sv;
+ case 506:
+ return "Variant Also Negotiates"sv;
+ case 507:
+ return "Insufficient Storage"sv;
+ case 508:
+ return "Loop Detected"sv;
+ case 510:
+ return "Not Extended"sv;
+ case 511:
+ return "Network Authentication Required"sv;
+
+ default:
+ return "Unknown Result"sv;
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+Ref<IHttpPackageHandler>
+HttpService::HandlePackageRequest(HttpServerRequest& HttpServiceRequest)
+{
+ ZEN_UNUSED(HttpServiceRequest);
+
+ return Ref<IHttpPackageHandler>();
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+HttpServerRequest::HttpServerRequest()
+{
+}
+
+HttpServerRequest::~HttpServerRequest()
+{
+}
+
+void
+HttpServerRequest::WriteResponse(HttpResponseCode ResponseCode, CbPackage Data)
+{
+ std::vector<IoBuffer> ResponseBuffers = FormatPackageMessage(Data);
+ return WriteResponse(ResponseCode, HttpContentType::kCbPackage, ResponseBuffers);
+}
+
+void
+HttpServerRequest::WriteResponse(HttpResponseCode ResponseCode, CbObject Data)
+{
+ if (m_AcceptType == HttpContentType::kJSON)
+ {
+ ExtendableStringBuilder<1024> Sb;
+ WriteResponse(ResponseCode, HttpContentType::kJSON, Data.ToJson(Sb).ToView());
+ }
+ else
+ {
+ SharedBuffer Buf = Data.GetBuffer();
+ std::array<IoBuffer, 1> Buffers{IoBufferBuilder::MakeCloneFromMemory(Buf.GetData(), Buf.GetSize())};
+ return WriteResponse(ResponseCode, HttpContentType::kCbObject, Buffers);
+ }
+}
+
+void
+HttpServerRequest::WriteResponse(HttpResponseCode ResponseCode, CbArray Array)
+{
+ if (m_AcceptType == HttpContentType::kJSON)
+ {
+ ExtendableStringBuilder<1024> Sb;
+ WriteResponse(ResponseCode, HttpContentType::kJSON, Array.ToJson(Sb).ToView());
+ }
+ else
+ {
+ SharedBuffer Buf = Array.GetBuffer();
+ std::array<IoBuffer, 1> Buffers{IoBufferBuilder::MakeCloneFromMemory(Buf.GetData(), Buf.GetSize())};
+ return WriteResponse(ResponseCode, HttpContentType::kCbObject, Buffers);
+ }
+}
+
+void
+HttpServerRequest::WriteResponse(HttpResponseCode ResponseCode, HttpContentType ContentType, std::string_view ResponseString)
+{
+ return WriteResponse(ResponseCode, ContentType, std::u8string_view{(char8_t*)ResponseString.data(), ResponseString.size()});
+}
+
+void
+HttpServerRequest::WriteResponse(HttpResponseCode ResponseCode, HttpContentType ContentType, IoBuffer Blob)
+{
+ std::array<IoBuffer, 1> Buffers{Blob};
+ return WriteResponse(ResponseCode, ContentType, Buffers);
+}
+
+void
+HttpServerRequest::WriteResponse(HttpResponseCode ResponseCode, HttpContentType ContentType, CompositeBuffer& Payload)
+{
+ std::span<const SharedBuffer> Segments = Payload.GetSegments();
+
+ std::vector<IoBuffer> Buffers;
+
+ for (auto& Segment : Segments)
+ {
+ Buffers.push_back(Segment.AsIoBuffer());
+ }
+
+ WriteResponse(ResponseCode, 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;
+ }
+
+ size_t QueryLen = ptrdiff_t(QueryEnd - QueryIt);
+ const std::string_view Query{QueryIt, QueryLen};
+
+ 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 Param{ThisQuery.data(), EqIndex};
+ ThisQuery.remove_prefix(EqIndex + 1);
+
+ Params.KvPairs.emplace_back(Param, ThisQuery);
+ }
+
+ QueryIt += DelimIndex;
+ }
+
+ return Params;
+}
+
+Oid
+HttpServerRequest::SessionId() const
+{
+ if (m_Flags & kHaveSessionId)
+ {
+ return m_SessionId;
+ }
+
+ m_SessionId = ParseSessionId();
+ m_Flags |= kHaveSessionId;
+ return m_SessionId;
+}
+
+uint32_t
+HttpServerRequest::RequestId() const
+{
+ if (m_Flags & kHaveRequestId)
+ {
+ return m_RequestId;
+ }
+
+ m_RequestId = ParseRequestId();
+ m_Flags |= kHaveRequestId;
+ return m_RequestId;
+}
+
+CbObject
+HttpServerRequest::ReadPayloadObject()
+{
+ if (IoBuffer Payload = ReadPayload())
+ {
+ return LoadCompactBinaryObject(std::move(Payload));
+ }
+
+ return {};
+}
+
+CbPackage
+HttpServerRequest::ReadPayloadPackage()
+{
+ if (IoBuffer Payload = ReadPayload())
+ {
+ return ParsePackageMessage(std::move(Payload));
+ }
+
+ return {};
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+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::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
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+HttpRpcHandler::HttpRpcHandler()
+{
+}
+
+HttpRpcHandler::~HttpRpcHandler()
+{
+}
+
+void
+HttpRpcHandler::AddRpc(std::string_view RpcId, std::function<void(CbObject& RpcArgs)> HandlerFunction)
+{
+ ZEN_UNUSED(RpcId, HandlerFunction);
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+enum class HttpServerClass
+{
+ kHttpAsio,
+ kHttpSys,
+ kHttpNull
+};
+
+// Implemented in httpsys.cpp
+Ref<HttpServer> CreateHttpSysServer(int Concurrency, int BackgroundWorkerThreads);
+
+Ref<HttpServer>
+CreateHttpServer(std::string_view ServerClass)
+{
+ using namespace std::literals;
+
+ HttpServerClass Class = HttpServerClass::kHttpNull;
+
+#if ZEN_WITH_HTTPSYS
+ Class = HttpServerClass::kHttpSys;
+#elif 1
+ Class = HttpServerClass::kHttpAsio;
+#endif
+
+ if (ServerClass == "asio"sv)
+ {
+ Class = HttpServerClass::kHttpAsio;
+ }
+ else if (ServerClass == "httpsys"sv)
+ {
+ Class = HttpServerClass::kHttpSys;
+ }
+ else if (ServerClass == "null"sv)
+ {
+ Class = HttpServerClass::kHttpNull;
+ }
+
+ switch (Class)
+ {
+ default:
+ case HttpServerClass::kHttpAsio:
+ ZEN_INFO("using asio HTTP server implementation");
+ return Ref<HttpServer>(new HttpAsioServer());
+
+#if ZEN_WITH_HTTPSYS
+ case HttpServerClass::kHttpSys:
+ ZEN_INFO("using http.sys server implementation");
+ return Ref<HttpServer>(new HttpSysServer(std::thread::hardware_concurrency(), /* background worker threads */ 16));
+#endif
+
+ case HttpServerClass::kHttpNull:
+ ZEN_INFO("using null HTTP server implementation");
+ return Ref<HttpServer>(new HttpNullServer);
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+bool
+HandlePackageOffers(HttpService& Service, HttpServerRequest& Request, Ref<IHttpPackageHandler>& PackageHandlerRef)
+{
+ if (Request.RequestVerb() == HttpVerb::kPost)
+ {
+ if (Request.RequestContentType() == HttpContentType::kCbPackageOffer)
+ {
+ // The client is presenting us with a package attachments offer, we need
+ // to filter it down to the list of attachments we need them to send in
+ // the follow-up request
+
+ PackageHandlerRef = Service.HandlePackageRequest(Request);
+
+ if (PackageHandlerRef)
+ {
+ CbObject OfferMessage = LoadCompactBinaryObject(Request.ReadPayload());
+
+ std::vector<IoHash> OfferCids;
+
+ for (auto& CidEntry : OfferMessage["offer"])
+ {
+ if (!CidEntry.IsHash())
+ {
+ // Should yield bad request response?
+
+ ZEN_WARN("found invalid entry in offer");
+
+ continue;
+ }
+
+ OfferCids.push_back(CidEntry.AsHash());
+ }
+
+ ZEN_TRACE("request #{} -> filtering offer of {} entries", Request.RequestId(), OfferCids.size());
+
+ PackageHandlerRef->FilterOffer(OfferCids);
+
+ ZEN_TRACE("request #{} -> filtered to {} entries", Request.RequestId(), OfferCids.size());
+
+ CbObjectWriter ResponseWriter;
+ ResponseWriter.BeginArray("need");
+
+ for (const IoHash& Cid : OfferCids)
+ {
+ ResponseWriter.AddHash(Cid);
+ }
+
+ ResponseWriter.EndArray();
+
+ // Emit filter response
+ Request.WriteResponse(HttpResponseCode::OK, ResponseWriter.Save());
+ return true;
+ }
+ }
+ else if (Request.RequestContentType() == HttpContentType::kCbPackage)
+ {
+ // Process chunks in package request
+
+ PackageHandlerRef = Service.HandlePackageRequest(Request);
+
+ // TODO: this should really be done in a streaming fashion, currently this emulates
+ // the intended flow from an API perspective
+
+ if (PackageHandlerRef)
+ {
+ PackageHandlerRef->OnRequestBegin();
+
+ auto CreateBuffer = [&](const IoHash& Cid, uint64_t Size) -> IoBuffer {
+ return PackageHandlerRef->CreateTarget(Cid, Size);
+ };
+
+ CbPackage Package = ParsePackageMessage(Request.ReadPayload(), CreateBuffer);
+
+ PackageHandlerRef->OnRequestComplete();
+ }
+ }
+ }
+ return false;
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+#if ZEN_WITH_TESTS
+
+TEST_CASE("http.common")
+{
+ 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);
+ }
+
+ SUBCASE("content-type")
+ {
+ for (uint8_t i = 0; i < uint8_t(HttpContentType::kCOUNT); ++i)
+ {
+ HttpContentType Ct{i};
+
+ if (Ct != HttpContentType::kUnknownContentType)
+ {
+ CHECK_EQ(Ct, ParseContentType(MapContentTypeToString(Ct)));
+ }
+ }
+ }
+}
+
+void
+http_forcelink()
+{
+}
+
+#endif
+
+} // namespace zen