// Copyright Epic Games, Inc. All Rights Reserved. #include #include #include #include #include #include #include #include #include #include #include #include ZEN_THIRD_PARTY_INCLUDES_START #include ZEN_THIRD_PARTY_INCLUDES_END static std::atomic HttpClientRequestIdCounter{0}; namespace zen { using namespace std::literals; ////////////////////////////////////////////////////////////////////////// // // CPR helpers cpr::Body AsCprBody(const CbObject& Obj) { return cpr::Body((const char*)Obj.GetBuffer().GetData(), Obj.GetBuffer().GetSize()); } cpr::Body AsCprBody(const IoBuffer& Obj) { return cpr::Body((const char*)Obj.GetData(), Obj.GetSize()); } cpr::Body AsCprBody(const CompositeBuffer& Buffers) { SharedBuffer Buffer = Buffers.Flatten(); // This is super inefficient, should be fixed std::string String{(const char*)Buffer.GetData(), Buffer.GetSize()}; return cpr::Body{std::move(String)}; } ////////////////////////////////////////////////////////////////////////// HttpClient::Response ResponseWithPayload(cpr::Response& HttpResponse, const HttpResponseCode WorkResponseCode) { // This ends up doing a memcpy, would be good to get rid of it by streaming results // into buffer directly IoBuffer ResponseBuffer = IoBuffer(IoBuffer::Clone, HttpResponse.text.data(), HttpResponse.text.size()); if (auto It = HttpResponse.header.find("Content-Type"); It != HttpResponse.header.end()) { const HttpContentType ContentType = ParseContentType(It->second); ResponseBuffer.SetContentType(ContentType); } return HttpClient::Response{.StatusCode = WorkResponseCode, .ResponsePayload = std::move(ResponseBuffer)}; } HttpClient::Response CommonResponse(cpr::Response&& HttpResponse) { const HttpResponseCode WorkResponseCode = HttpResponseCode(HttpResponse.status_code); if (HttpResponse.status_code == 0) { // Client side failure code return HttpClient::Response{ .StatusCode = WorkResponseCode, .ResponsePayload = IoBufferBuilder::MakeCloneFromMemory(HttpResponse.error.message.data(), HttpResponse.error.message.size())}; } if (WorkResponseCode == HttpResponseCode::NoContent || HttpResponse.text.empty()) { return HttpClient::Response{.StatusCode = WorkResponseCode}; } else { return ResponseWithPayload(HttpResponse, WorkResponseCode); } } ////////////////////////////////////////////////////////////////////////// struct HttpClient::Impl : public RefCounted { Impl(); ~Impl(); // Session allocation struct Session { Session(Impl* InOuter, cpr::Session* InSession) : Outer(InOuter), CprSession(InSession) {} ~Session() { Outer->ReleaseSession(CprSession); } inline cpr::Session* operator->() const { return CprSession; } private: Impl* Outer; cpr::Session* CprSession; Session(Session&&) = delete; Session& operator=(Session&&) = delete; }; Session AllocSession(const std::string_view BaseUrl, const std::string_view Url); private: RwLock m_SessionLock; std::vector m_Sessions; void ReleaseSession(cpr::Session*); }; HttpClient::Impl::Impl() { } HttpClient::Impl::~Impl() { m_SessionLock.WithExclusiveLock([&] { for (auto CprSession : m_Sessions) { delete CprSession; } m_Sessions.clear(); }); } HttpClient::Impl::Session HttpClient::Impl::AllocSession(const std::string_view BaseUrl, const std::string_view ResourcePath) { RwLock::ExclusiveLockScope _(m_SessionLock); ExtendableStringBuilder<128> UrlBuffer; UrlBuffer << BaseUrl << ResourcePath; if (m_Sessions.empty()) { cpr::Session* NewSession = new cpr::Session(); NewSession->SetUrl(UrlBuffer.c_str()); return Session(this, NewSession); } else { cpr::Session* NewSession = m_Sessions.back(); m_Sessions.pop_back(); NewSession->SetUrl(UrlBuffer.c_str()); return Session(this, NewSession); } } void HttpClient::Impl::ReleaseSession(cpr::Session* CprSession) { m_SessionLock.WithExclusiveLock([&] { m_Sessions.push_back(CprSession); }); } ////////////////////////////////////////////////////////////////////////// HttpClient::HttpClient(std::string_view BaseUri) : m_BaseUri(BaseUri), m_Impl(new Impl) { StringBuilder<32> SessionId; GetSessionId().ToString(SessionId); m_SessionId = SessionId; } HttpClient::~HttpClient() { } HttpClient::Response HttpClient::TransactPackage(std::string_view Url, CbPackage Package) { ZEN_TRACE_CPU("HttpClient::TransactPackage"); Impl::Session Sess = m_Impl->AllocSession(m_BaseUri, Url); // First, list of offered chunks for filtering on the server end std::vector AttachmentsToSend; std::span Attachments = Package.GetAttachments(); const uint32_t RequestId = ++HttpClientRequestIdCounter; auto RequestIdString = fmt::to_string(RequestId); if (Attachments.empty() == false) { CbObjectWriter Writer; Writer.BeginArray("offer"); for (const CbAttachment& Attachment : Attachments) { Writer.AddHash(Attachment.GetHash()); } Writer.EndArray(); BinaryWriter MemWriter; Writer.Save(MemWriter); Sess->SetHeader({{"Content-Type", "application/x-ue-offer"}, {"UE-Session", m_SessionId}, {"UE-Request", RequestIdString}}); Sess->SetBody(cpr::Body{(const char*)MemWriter.Data(), MemWriter.Size()}); cpr::Response FilterResponse = Sess->Post(); if (FilterResponse.status_code == 200) { IoBuffer ResponseBuffer(IoBuffer::Wrap, FilterResponse.text.data(), FilterResponse.text.size()); CbObject ResponseObject = LoadCompactBinaryObject(ResponseBuffer); for (CbFieldView& Entry : ResponseObject["need"]) { ZEN_ASSERT(Entry.IsHash()); AttachmentsToSend.push_back(Entry.AsHash()); } } } // Prepare package for send CbPackage SendPackage; SendPackage.SetObject(Package.GetObject(), Package.GetObjectHash()); for (const IoHash& AttachmentCid : AttachmentsToSend) { const CbAttachment* Attachment = Package.FindAttachment(AttachmentCid); if (Attachment) { SendPackage.AddAttachment(*Attachment); } else { // This should be an error -- server asked to have something we can't find } } // Transmit package payload CompositeBuffer Message = FormatPackageMessageBuffer(SendPackage); SharedBuffer FlatMessage = Message.Flatten(); Sess->SetHeader({{"Content-Type", "application/x-ue-cbpkg"}, {"UE-Session", m_SessionId}, {"UE-Request", RequestIdString}}); Sess->SetBody(cpr::Body{(const char*)FlatMessage.GetData(), FlatMessage.GetSize()}); cpr::Response FilterResponse = Sess->Post(); if (!IsHttpSuccessCode(FilterResponse.status_code)) { return {.StatusCode = HttpResponseCode(FilterResponse.status_code)}; } IoBuffer ResponseBuffer(IoBuffer::Clone, FilterResponse.text.data(), FilterResponse.text.size()); if (auto It = FilterResponse.header.find("Content-Type"); It != FilterResponse.header.end()) { HttpContentType ContentType = ParseContentType(It->second); ResponseBuffer.SetContentType(ContentType); } return {.StatusCode = HttpResponseCode(FilterResponse.status_code), .ResponsePayload = ResponseBuffer}; } ////////////////////////////////////////////////////////////////////////// // // Standard HTTP verbs // HttpClient::Response HttpClient::Put(std::string_view Url, const IoBuffer& Payload) { ZEN_TRACE_CPU("HttpClient::Put"); Impl::Session Sess = m_Impl->AllocSession(m_BaseUri, Url); Sess->SetBody(AsCprBody(Payload)); Sess->SetHeader(cpr::Header{{"Content-Type", std::string(MapContentTypeToString(Payload.GetContentType()))}}); return CommonResponse(Sess->Put()); } HttpClient::Response HttpClient::Get(std::string_view Url) { ZEN_TRACE_CPU("HttpClient::Get"); Impl::Session Sess = m_Impl->AllocSession(m_BaseUri, Url); return CommonResponse(Sess->Get()); } HttpClient::Response HttpClient::Delete(std::string_view Url) { ZEN_TRACE_CPU("HttpClient::Delete"); Impl::Session Sess = m_Impl->AllocSession(m_BaseUri, Url); return CommonResponse(Sess->Delete()); } HttpClient::Response HttpClient::Post(std::string_view Url) { ZEN_TRACE_CPU("HttpClient::PostNoPayload"); Impl::Session Sess = m_Impl->AllocSession(m_BaseUri, Url); return CommonResponse(Sess->Post()); } HttpClient::Response HttpClient::Post(std::string_view Url, const IoBuffer& Payload) { ZEN_TRACE_CPU("HttpClient::PostWithPayload"); Impl::Session Sess = m_Impl->AllocSession(m_BaseUri, Url); Sess->SetBody(AsCprBody(Payload)); Sess->SetHeader(cpr::Header{{"Content-Type", std::string(MapContentTypeToString(Payload.GetContentType()))}}); return CommonResponse(Sess->Post()); } HttpClient::Response HttpClient::Post(std::string_view Url, CbObject Payload) { ZEN_TRACE_CPU("HttpClient::PostObjectPayload"); Impl::Session Sess = m_Impl->AllocSession(m_BaseUri, Url); Sess->SetBody(AsCprBody(Payload)); Sess->SetHeader(cpr::Header{{"Content-Type", std::string(MapContentTypeToString(ZenContentType::kCbObject))}}); return CommonResponse(Sess->Post()); } HttpClient::Response HttpClient::Post(std::string_view Url, CbPackage Pkg) { ZEN_TRACE_CPU("HttpClient::PostPackage"); CompositeBuffer Message = zen::FormatPackageMessageBuffer(Pkg); Impl::Session Sess = m_Impl->AllocSession(m_BaseUri, Url); Sess->SetBody(AsCprBody(Message)); Sess->SetHeader(cpr::Header{{"Content-Type", std::string(MapContentTypeToString(ZenContentType::kCbPackage))}}); return CommonResponse(Sess->Post()); } ////////////////////////////////////////////////////////////////////////// CbObject HttpClient::Response::AsObject() { // TODO: sanity check the payload format etc if (ResponsePayload) { return LoadCompactBinaryObject(ResponsePayload); } return {}; } CbPackage HttpClient::Response::AsPackage() { // TODO: sanity checks and error handling if (ResponsePayload) { return ParsePackageMessage(ResponsePayload); } return {}; } std::string_view HttpClient::Response::AsText() { if (ResponsePayload) { return std::string_view(reinterpret_cast(ResponsePayload.GetData()), ResponsePayload.GetSize()); } return {}; } std::string HttpClient::Response::ToText() { if (!ResponsePayload) return {}; switch (ResponsePayload.GetContentType()) { case ZenContentType::kCbObject: { zen::ExtendableStringBuilder<1024> ObjStr; zen::CbObject Object{SharedBuffer(ResponsePayload)}; zen::CompactBinaryToJson(Object, ObjStr); return ObjStr.ToString(); } break; case ZenContentType::kCSS: case ZenContentType::kHTML: case ZenContentType::kJavaScript: case ZenContentType::kJSON: case ZenContentType::kText: case ZenContentType::kYAML: return std::string{AsText()}; default: return ""; } } bool HttpClient::Response::IsSuccess() const noexcept { return IsHttpSuccessCode(StatusCode); } ////////////////////////////////////////////////////////////////////////// #if ZEN_WITH_TESTS TEST_CASE("httpclient") { using namespace std::literals; SUBCASE("client") {} } void httpclient_forcelink() { } #endif } // namespace zen