diff options
| author | Per Larsson <[email protected]> | 2021-12-09 17:01:57 +0100 |
|---|---|---|
| committer | Per Larsson <[email protected]> | 2021-12-09 17:01:57 +0100 |
| commit | 20f3c16b0012cfb8ce7bf9b6dd06a2720b6885c6 (patch) | |
| tree | fbb0f274840a12c32d93c7e342c0427f2617a651 /zenhttp | |
| parent | Disabled cache tracker. (diff) | |
| parent | Return status_code as ErrorCode from jupiter api if not successful (diff) | |
| download | zen-20f3c16b0012cfb8ce7bf9b6dd06a2720b6885c6.tar.xz zen-20f3c16b0012cfb8ce7bf9b6dd06a2720b6885c6.zip | |
Merged main.
Diffstat (limited to 'zenhttp')
| -rw-r--r-- | zenhttp/httpasio.cpp | 99 | ||||
| -rw-r--r-- | zenhttp/httpserver.cpp | 81 | ||||
| -rw-r--r-- | zenhttp/httpsys.cpp | 167 | ||||
| -rw-r--r-- | zenhttp/httpsys.h | 20 | ||||
| -rw-r--r-- | zenhttp/include/zenhttp/httpserver.h | 2 |
5 files changed, 241 insertions, 128 deletions
diff --git a/zenhttp/httpasio.cpp b/zenhttp/httpasio.cpp index d5fe9adbb..7ee7193d1 100644 --- a/zenhttp/httpasio.cpp +++ b/zenhttp/httpasio.cpp @@ -15,6 +15,14 @@ ZEN_THIRD_PARTY_INCLUDES_START #include <asio.hpp> ZEN_THIRD_PARTY_INCLUDES_END +#define ASIO_VERBOSE_TRACE 0 + +#if ASIO_VERBOSE_TRACE +#define ZEN_TRACE_VERBOSE ZEN_TRACE +#else +#define ZEN_TRACE_VERBOSE(fmtstr, ...) +#endif + namespace zen::asio_http { using namespace std::literals; @@ -114,7 +122,7 @@ struct HttpRequest HttpVerb RequestVerb() const { return m_RequestVerb; } bool IsKeepAlive() const { return m_KeepAlive; } - std::string_view Url() const { return std::string_view(m_Url, m_UrlLength); } + std::string_view Url() const { return m_NormalizedUrl.empty() ? std::string_view(m_Url, m_UrlLength) : m_NormalizedUrl; } std::string_view QueryString() const { return std::string_view(m_QueryString, m_QueryLength); } IoBuffer Body() { return m_BodyBuffer; } @@ -171,6 +179,7 @@ private: uint64_t m_BodyPosition = 0; http_parser m_Parser; char m_HeaderBuffer[1024]; + std::string m_NormalizedUrl; void AppendInputBytes(const char* Data, size_t Bytes); void AppendCurrentHeader(); @@ -318,6 +327,7 @@ private: std::unique_ptr<asio::ip::tcp::socket> m_Socket; std::atomic<uint32_t> m_RequestCounter{0}; uint32_t m_ConnectionId = 0; + Ref<IHttpPackageHandler> m_PackageHandler; RwLock m_ResponsesLock; std::deque<std::unique_ptr<HttpResponse>> m_Responses; @@ -330,12 +340,12 @@ HttpServerConnection::HttpServerConnection(HttpAsioServerImpl& Server, std::uniq , m_Socket(std::move(Socket)) , m_ConnectionId(g_ConnectionIdCounter.fetch_add(1)) { - ZEN_TRACE("new connection #{}", m_ConnectionId); + ZEN_TRACE_VERBOSE("new connection #{}", m_ConnectionId); } HttpServerConnection::~HttpServerConnection() { - ZEN_TRACE("destroying connection #{}", m_ConnectionId); + ZEN_TRACE_VERBOSE("destroying connection #{}", m_ConnectionId); } void @@ -371,18 +381,18 @@ HttpServerConnection::EnqueueRead() asio::async_read(*m_Socket.get(), m_RequestBuffer, - asio::transfer_at_least(16), + asio::transfer_at_least(1), [Conn = AsSharedPtr()](const asio::error_code& Ec, std::size_t ByteCount) { Conn->OnDataReceived(Ec, ByteCount); }); } void -HttpServerConnection::OnDataReceived(const asio::error_code& Ec, std::size_t ByteCount) +HttpServerConnection::OnDataReceived(const asio::error_code& Ec, [[maybe_unused]] std::size_t ByteCount) { if (Ec) { if (m_RequestState == RequestState::kDone || m_RequestState == RequestState::kInitialRead) { - ZEN_TRACE("on data received ERROR (EXPECTED), connection '{}' reason '{}'", m_ConnectionId, Ec.message()); + ZEN_TRACE_VERBOSE("on data received ERROR (EXPECTED), connection '{}' reason '{}'", m_ConnectionId, Ec.message()); return; } else @@ -392,7 +402,7 @@ HttpServerConnection::OnDataReceived(const asio::error_code& Ec, std::size_t Byt } } - ZEN_TRACE("on data received, connection '{}', request '{}', thread '{}', bytes '{}'", + ZEN_TRACE_VERBOSE("on data received, connection '{}', request '{}', thread '{}', bytes '{}'", m_ConnectionId, m_RequestCounter.load(std::memory_order_relaxed), GetCurrentThreadId(), @@ -421,7 +431,7 @@ HttpServerConnection::OnDataReceived(const asio::error_code& Ec, std::size_t Byt } void -HttpServerConnection::OnResponseDataSent(const asio::error_code& Ec, std::size_t ByteCount, bool Pop) +HttpServerConnection::OnResponseDataSent(const asio::error_code& Ec, [[maybe_unused]] std::size_t ByteCount, bool Pop) { if (Ec) { @@ -430,7 +440,7 @@ HttpServerConnection::OnResponseDataSent(const asio::error_code& Ec, std::size_t } else { - ZEN_TRACE("on data sent, connection '{}', request '{}', thread '{}', bytes '{}'", + ZEN_TRACE_VERBOSE("on data sent, connection '{}', request '{}', thread '{}', bytes '{}'", m_ConnectionId, m_RequestCounter.load(std::memory_order_relaxed), GetCurrentThreadId(), @@ -485,9 +495,23 @@ HttpServerConnection::HandleRequest() { HttpAsioServerRequest Request(m_RequestData, *Service, m_RequestData.Body()); - ZEN_TRACE("handle request, connection '{}' request '{}'", m_ConnectionId, m_RequestCounter.load(std::memory_order_relaxed)); - Service->HandleRequest(Request); + ZEN_TRACE_VERBOSE("handle request, connection '{}' request '{}'", m_ConnectionId, m_RequestCounter.load(std::memory_order_relaxed)); + + if (!HandlePackageOffers(*Service, Request, m_PackageHandler)) + { + try + { + Service->HandleRequest(Request); + } + catch (std::exception& ex) + { + ZEN_ERROR("Caught exception while handling request: '{}'", ex.what()); + + Request.WriteResponse(HttpResponseCode::InternalServerError, HttpContentType::kText, ex.what()); + } + + } if (std::unique_ptr<HttpResponse> Response = std::move(Request.m_Response)) { @@ -737,6 +761,37 @@ HttpRequest::TerminateConnection() m_Connection.TerminateConnection(); } +static void +NormalizeUrlPath(const char* Url, size_t UrlLength, std::string& NormalizedUrl) +{ + bool LastCharWasSeparator = false; + for (std::string_view::size_type UrlIndex = 0; UrlIndex < UrlLength; ++UrlIndex) + { + const char UrlChar = Url[UrlIndex]; + const bool IsSeparator = (UrlChar == '/'); + + if (IsSeparator && LastCharWasSeparator) + { + if (NormalizedUrl.empty()) + { + NormalizedUrl.reserve(UrlLength); + NormalizedUrl.append(Url, UrlIndex); + } + + if (!LastCharWasSeparator) + { + NormalizedUrl.push_back('/'); + } + } + else if (!NormalizedUrl.empty()) + { + NormalizedUrl.push_back(UrlChar); + } + + LastCharWasSeparator = IsSeparator; + } +} + int HttpRequest::OnHeadersComplete() { @@ -803,6 +858,9 @@ HttpRequest::OnHeadersComplete() m_QueryLength = Url.size() - QuerySplit - 1; } + NormalizeUrlPath(m_Url, m_UrlLength, m_NormalizedUrl); + + return 0; } @@ -843,6 +901,7 @@ HttpRequest::ResetState() m_BodyBuffer = {}; m_BodyPosition = 0; m_Headers.clear(); + m_NormalizedUrl.clear(); } int @@ -935,7 +994,7 @@ HttpAsioServerRequest::HttpAsioServerRequest(asio_http::HttpRequest& Request, Ht const int PrefixLength = Service.UriPrefixLength(); std::string_view Uri = Request.Url(); - Uri.remove_prefix(PrefixLength); + Uri.remove_prefix(std::min(PrefixLength, static_cast<int>(Uri.size()))); m_Uri = Uri; m_QueryString = Request.QueryString(); @@ -1099,6 +1158,10 @@ HttpAsioServerImpl::RegisterService(const char* InUrlPath, HttpService& Service) { std::string_view UrlPath(InUrlPath); Service.SetUriPrefixLength(UrlPath.size()); + if (!UrlPath.empty() && UrlPath.back() == '/') + { + UrlPath.remove_suffix(1); + } RwLock::ExclusiveLockScope _(m_Lock); m_UriHandlers.push_back({std::string(UrlPath), &Service}); @@ -1109,16 +1172,22 @@ HttpAsioServerImpl::RouteRequest(std::string_view Url) { RwLock::SharedLockScope _(m_Lock); + HttpService* CandidateService = nullptr; + std::string::size_type CandidateMatchSize = 0; for (const ServiceEntry& SvcEntry : m_UriHandlers) { const std::string& SvcUrl = SvcEntry.ServiceUrlPath; - if (Url.compare(0, SvcUrl.size(), SvcUrl) == 0) + const std::string::size_type SvcUrlSize = SvcUrl.size(); + if ((SvcUrlSize >= CandidateMatchSize) && + Url.compare(0, SvcUrlSize, SvcUrl) == 0 && + ((SvcUrlSize == Url.size()) || (Url[SvcUrlSize] == '/'))) { - return SvcEntry.Service; + CandidateMatchSize = SvcUrl.size(); + CandidateService = SvcEntry.Service; } } - return nullptr; + return CandidateService; } } // namespace zen::asio_http diff --git a/zenhttp/httpserver.cpp b/zenhttp/httpserver.cpp index 011468715..3326f3d4a 100644 --- a/zenhttp/httpserver.cpp +++ b/zenhttp/httpserver.cpp @@ -7,6 +7,7 @@ #include "httpsys.h" #include <zencore/compactbinary.h> +#include <zencore/compactbinarybuilder.h> #include <zencore/compactbinarypackage.h> #include <zencore/iobuffer.h> #include <zencore/logging.h> @@ -637,6 +638,86 @@ CreateHttpServer(std::string_view ServerClass) ////////////////////////////////////////////////////////////////////////// +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") diff --git a/zenhttp/httpsys.cpp b/zenhttp/httpsys.cpp index cdf9e0a39..e9472e3b8 100644 --- a/zenhttp/httpsys.cpp +++ b/zenhttp/httpsys.cpp @@ -748,15 +748,20 @@ HttpSysServer::~HttpSysServer() } void -HttpSysServer::Initialize(const wchar_t* UrlPath) +HttpSysServer::InitializeServer(int BasePort) { + using namespace std::literals; + + WideStringBuilder<64> WildcardUrlPath; + WildcardUrlPath << u8"http://*:"sv << int64_t(BasePort) << u8"/"sv; + m_IsOk = false; ULONG Result = HttpCreateServerSession(HTTPAPI_VERSION_2, &m_HttpSessionId, 0); if (Result != NO_ERROR) { - ZEN_ERROR("Failed to create server session for '{}': {:#x}", WideToUtf8(UrlPath), Result); + ZEN_ERROR("Failed to create server session for '{}': {:#x}", WideToUtf8(WildcardUrlPath), Result); return; } @@ -765,18 +770,44 @@ HttpSysServer::Initialize(const wchar_t* UrlPath) if (Result != NO_ERROR) { - ZEN_ERROR("Failed to create URL group for '{}': {:#x}", WideToUtf8(UrlPath), Result); + ZEN_ERROR("Failed to create URL group for '{}': {:#x}", WideToUtf8(WildcardUrlPath), Result); return; } - m_BaseUri = UrlPath; + Result = HttpAddUrlToUrlGroup(m_HttpUrlGroupId, WildcardUrlPath.c_str(), HTTP_URL_CONTEXT(0), 0); - Result = HttpAddUrlToUrlGroup(m_HttpUrlGroupId, UrlPath, HTTP_URL_CONTEXT(0), 0); + m_BaseUris.clear(); + if (Result == NO_ERROR) + { + m_BaseUris.push_back(WildcardUrlPath.c_str()); + } + else + { + // If we can't register the wildcard path, we fall back to local paths + // This local paths allow requests originating locally to function, but will not allow + // remote origin requests to function. This can be remedied by using netsh + // during an install process to grant permissions to route public access to the appropriate + // port for the current user. eg: + // netsh http add urlacl url=http://*:1337/ user=<some_user> - if (Result != NO_ERROR) + const std::u8string_view Hosts[] = { u8"[::1]"sv, u8"localhost"sv, u8"127.0.0.1"sv }; + + for (const std::u8string_view Host : Hosts) + { + WideStringBuilder<64> LocalUrlPath; + LocalUrlPath << u8"http://"sv << Host << u8":"sv << int64_t(BasePort) << u8"/"sv; + + if (HttpAddUrlToUrlGroup(m_HttpUrlGroupId, LocalUrlPath.c_str(), HTTP_URL_CONTEXT(0), 0) == NO_ERROR) + { + m_BaseUris.push_back(LocalUrlPath.c_str()); + } + } + } + + if (m_BaseUris.empty()) { - ZEN_ERROR("Failed to add base URL to URL group for '{}': {:#x}", WideToUtf8(UrlPath), Result); + ZEN_ERROR("Failed to add base URL to URL group for '{}': {:#x}", WideToUtf8(WildcardUrlPath), Result); return; } @@ -791,7 +822,7 @@ HttpSysServer::Initialize(const wchar_t* UrlPath) if (Result != NO_ERROR) { - ZEN_ERROR("Failed to create request queue for '{}': {:#x}", WideToUtf8(UrlPath), Result); + ZEN_ERROR("Failed to create request queue for '{}': {:#x}", WideToUtf8(m_BaseUris.front()), Result); return; } @@ -803,7 +834,7 @@ HttpSysServer::Initialize(const wchar_t* UrlPath) if (Result != NO_ERROR) { - ZEN_ERROR("Failed to set server binding property for '{}': {:#x}", WideToUtf8(UrlPath), Result); + ZEN_ERROR("Failed to set server binding property for '{}': {:#x}", WideToUtf8(m_BaseUris.front()), Result); return; } @@ -815,13 +846,13 @@ HttpSysServer::Initialize(const wchar_t* UrlPath) if (ErrorCode) { - ZEN_ERROR("Failed to create IOCP for '{}': {}", WideToUtf8(UrlPath), ErrorCode.message()); + ZEN_ERROR("Failed to create IOCP for '{}': {}", WideToUtf8(m_BaseUris.front()), ErrorCode.message()); } else { m_IsOk = true; - ZEN_INFO("Started http.sys server at '{}'", WideToUtf8(UrlPath)); + ZEN_INFO("Started http.sys server at '{}'", WideToUtf8(m_BaseUris.front())); } } @@ -952,15 +983,18 @@ HttpSysServer::RegisterService(const char* UrlPath, HttpService& Service) // Convert to wide string - std::wstring Url16 = m_BaseUri + PathUtf16; + for (const std::wstring& BaseUri : m_BaseUris) + { + std::wstring Url16 = BaseUri + PathUtf16; - ULONG Result = HttpAddUrlToUrlGroup(m_HttpUrlGroupId, Url16.c_str(), HTTP_URL_CONTEXT(&Service), 0 /* Reserved */); + ULONG Result = HttpAddUrlToUrlGroup(m_HttpUrlGroupId, Url16.c_str(), HTTP_URL_CONTEXT(&Service), 0 /* Reserved */); - if (Result != NO_ERROR) - { - ZEN_ERROR("HttpAddUrlToUrlGroup failed with result: '{}'", GetSystemErrorAsString(Result)); + if (Result != NO_ERROR) + { + ZEN_ERROR("HttpAddUrlToUrlGroup failed with result: '{}'", GetSystemErrorAsString(Result)); - return; + return; + } } } @@ -978,13 +1012,16 @@ HttpSysServer::UnregisterService(const char* UrlPath, HttpService& Service) // Convert to wide string - std::wstring Url16 = m_BaseUri + PathUtf16; + for (const std::wstring& BaseUri : m_BaseUris) + { + std::wstring Url16 = BaseUri + PathUtf16; - ULONG Result = HttpRemoveUrlFromUrlGroup(m_HttpUrlGroupId, Url16.c_str(), 0); + ULONG Result = HttpRemoveUrlFromUrlGroup(m_HttpUrlGroupId, Url16.c_str(), 0); - if (Result != NO_ERROR) - { - ZEN_ERROR("HttpRemoveUrlFromUrlGroup failed with result: '{}'", GetSystemErrorAsString(Result)); + if (Result != NO_ERROR) + { + ZEN_ERROR("HttpRemoveUrlFromUrlGroup failed with result: '{}'", GetSystemErrorAsString(Result)); + } } } @@ -1131,83 +1168,12 @@ HttpSysTransaction::InvokeRequestHandler(HttpService& Service, IoBuffer Payload) { HttpSysServerRequest& ThisRequest = m_HandlerRequest.emplace(*this, Service, Payload); - if (ThisRequest.RequestVerb() == HttpVerb::kPost) - { - if (ThisRequest.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 - - m_PackageHandler = Service.HandlePackageRequest(ThisRequest); - - if (m_PackageHandler) - { - CbObject OfferMessage = LoadCompactBinaryObject(Payload); - - 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", ThisRequest.RequestId(), OfferCids.size()); - - m_PackageHandler->FilterOffer(OfferCids); - - ZEN_TRACE("request #{} -> filtered to {} entries", ThisRequest.RequestId(), OfferCids.size()); - - CbObjectWriter ResponseWriter; - ResponseWriter.BeginArray("need"); - - for (const IoHash& Cid : OfferCids) - { - ResponseWriter.AddHash(Cid); - } - - ResponseWriter.EndArray(); - - // Emit filter response - ThisRequest.WriteResponse(HttpResponseCode::OK, ResponseWriter.Save()); - - return ThisRequest; - } - } - else if (ThisRequest.RequestContentType() == HttpContentType::kCbPackage) - { - // Process chunks in package request - - m_PackageHandler = Service.HandlePackageRequest(ThisRequest); - - // TODO: this should really be done in a streaming fashion, currently this emulates - // the intended flow from an API perspective - - if (m_PackageHandler) - { - m_PackageHandler->OnRequestBegin(); - - auto CreateBuffer = [&](const IoHash& Cid, uint64_t Size) -> IoBuffer { return m_PackageHandler->CreateTarget(Cid, Size); }; - - CbPackage Package = ParsePackageMessage(ThisRequest.ReadPayload(), CreateBuffer); - - m_PackageHandler->OnRequestComplete(); - } - } - } - // Default request handling - Service.HandleRequest(ThisRequest); + if (!HandlePackageOffers(Service, ThisRequest, m_PackageHandler)) + { + Service.HandleRequest(ThisRequest); + } return ThisRequest; } @@ -1641,12 +1607,7 @@ InitialRequestHandler::HandleCompletion(ULONG IoResult, ULONG_PTR NumberOfBytesT 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()); + InitializeServer(BasePort); StartServer(); } diff --git a/zenhttp/httpsys.h b/zenhttp/httpsys.h index 46ba122cc..7df8fba8f 100644 --- a/zenhttp/httpsys.h +++ b/zenhttp/httpsys.h @@ -52,7 +52,7 @@ public: inline bool IsAsyncResponseEnabled() const { return m_IsAsyncResponseEnabled; } private: - void Initialize(const wchar_t* UrlPath); + void InitializeServer(int BasePort); void Cleanup(); void StartServer(); @@ -75,15 +75,15 @@ private: WinIoThreadPool m_ThreadPool; WorkerThreadPool m_AsyncWorkPool; - 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}; - std::atomic<int32_t> m_IsShuttingDown{0}; - int32_t m_MinPendingRequests = 16; - int32_t m_MaxPendingRequests = 128; - Event m_ShutdownEvent; + std::vector<std::wstring> m_BaseUris; // eg: 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}; + std::atomic_int32_t m_IsShuttingDown{0}; + int32_t m_MinPendingRequests = 16; + int32_t m_MaxPendingRequests = 128; + Event m_ShutdownEvent; }; } // namespace zen diff --git a/zenhttp/include/zenhttp/httpserver.h b/zenhttp/include/zenhttp/httpserver.h index 19b4c63d6..b32359d67 100644 --- a/zenhttp/include/zenhttp/httpserver.h +++ b/zenhttp/include/zenhttp/httpserver.h @@ -303,6 +303,8 @@ private: std::map<std::string, RpcFunction> m_Functions; }; +bool HandlePackageOffers(HttpService& Service, HttpServerRequest& Request, Ref<IHttpPackageHandler>& PackageHandlerRef); + void http_forcelink(); // internal } // namespace zen |