// Copyright Epic Games, Inc. All Rights Reserved. #include "Http/HttpClient.h" #include "Async/ManualResetEvent.h" #include "Containers/LockFreeList.h" #include "Memory/MemoryView.h" #include "Misc/AsciiSet.h" #include "ProfilingDebugging/CpuProfilerTrace.h" #include "String/Find.h" #include "String/LexFromString.h" namespace UE { FAnsiStringView LexToString(const EHttpMethod Method) { switch (Method) { case EHttpMethod::Get: return ANSITEXTVIEW("GET"); case EHttpMethod::Put: return ANSITEXTVIEW("PUT"); case EHttpMethod::Post: return ANSITEXTVIEW("POST"); case EHttpMethod::Head: return ANSITEXTVIEW("HEAD"); case EHttpMethod::Delete: return ANSITEXTVIEW("DELETE"); default: return ANSITEXTVIEW("UNKNOWN"); } } bool TryLexFromString(EHttpMethod& OutMethod, const FAnsiStringView View) { if (View == ANSITEXTVIEW("GET")) { OutMethod = EHttpMethod::Get; } else if (View == ANSITEXTVIEW("PUT")) { OutMethod = EHttpMethod::Put; } else if (View == ANSITEXTVIEW("POST")) { OutMethod = EHttpMethod::Post; } else if (View == ANSITEXTVIEW("HEAD")) { OutMethod = EHttpMethod::Head; } else if (View == ANSITEXTVIEW("DELETE")) { OutMethod = EHttpMethod::Delete; } else { return false; } return true; } FAnsiStringView LexToString(const EHttpMediaType MediaType) { switch (MediaType) { case EHttpMediaType::Any: return ANSITEXTVIEW("*/*"); case EHttpMediaType::Binary: return ANSITEXTVIEW("application/octet-stream"); case EHttpMediaType::Text: return ANSITEXTVIEW("text/plain"); case EHttpMediaType::Json: return ANSITEXTVIEW("application/json"); case EHttpMediaType::Yaml: return ANSITEXTVIEW("text/yaml"); case EHttpMediaType::CbObject: return ANSITEXTVIEW("application/x-ue-cb"); case EHttpMediaType::CbPackage: return ANSITEXTVIEW("application/x-ue-cbpkg"); case EHttpMediaType::CbPackageOffer: return ANSITEXTVIEW("application/x-ue-offer"); case EHttpMediaType::CompressedBinary: return ANSITEXTVIEW("application/x-ue-comp"); case EHttpMediaType::FormUrlEncoded: return ANSITEXTVIEW("application/x-www-form-urlencoded"); default: return ANSITEXTVIEW("unknown"); } } bool TryLexFromString(EHttpMediaType& OutMediaType, const FAnsiStringView View) { const int32 SlashIndex = String::FindFirstChar(View, '/'); if (SlashIndex == INDEX_NONE) { return false; } const FAnsiStringView Type = View.Left(SlashIndex); const FAnsiStringView SubType = View.RightChop(SlashIndex + 1); if (Type == ANSITEXTVIEW("application")) { if (SubType == ANSITEXTVIEW("octet-stream")) { OutMediaType = EHttpMediaType::Binary; } else if (SubType == ANSITEXTVIEW("json")) { OutMediaType = EHttpMediaType::Json; } else if (SubType == ANSITEXTVIEW("x-ue-cb")) { OutMediaType = EHttpMediaType::CbObject; } else if (SubType == ANSITEXTVIEW("x-ue-cbpkg")) { OutMediaType = EHttpMediaType::CbPackage; } else if (SubType == ANSITEXTVIEW("x-ue-offer")) { OutMediaType = EHttpMediaType::CbPackageOffer; } else if (SubType == ANSITEXTVIEW("x-ue-comp")) { OutMediaType = EHttpMediaType::CompressedBinary; } else if (SubType == ANSITEXTVIEW("x-www-form-urlencoded")) { OutMediaType = EHttpMediaType::FormUrlEncoded; } else { return false; } } else if (Type == ANSITEXTVIEW("text")) { if (SubType == ANSITEXTVIEW("plain")) { OutMediaType = EHttpMediaType::Text; } else if (SubType == ANSITEXTVIEW("yaml")) { OutMediaType = EHttpMediaType::Yaml; } else { return false; } } else if (Type == ANSITEXTVIEW("*") && SubType == ANSITEXTVIEW("*")) { OutMediaType = EHttpMediaType::Any; } else { return false; } return true; } static FAnsiStringView LexStatusCodeToString(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 Satisfiable"; 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"; } } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void IHttpRequest::SetContentType(const EHttpMediaType Type, const FAnsiStringView Param) { TAnsiStringBuilder<64> Value; Value << LexToString(Type); if (!Param.IsEmpty()) { Value << ANSITEXTVIEW("; ") << Param; } AddHeader(ANSITEXTVIEW("Content-Type"), Value); } void IHttpRequest::AddAcceptType(const EHttpMediaType Type, const float Weight) { TAnsiStringBuilder<64> Value; Value << LexToString(Type); if (Weight != 1.0f) { Value.Appendf(";q=%.3f", Weight); } AddHeader(ANSITEXTVIEW("Accept"), Value); } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// FAnsiStringView IHttpResponse::GetHeader(const FAnsiStringView Name) const { const int32 NameLen = Name.Len(); for (const FAnsiStringView& Header : GetAllHeaders()) { if (Header.StartsWith(Name, ESearchCase::IgnoreCase) && Header.Len() > NameLen && Header.GetData()[NameLen] == ':') { return Header.RightChop(NameLen + 1).TrimStartAndEnd(); } } return {}; } int32 IHttpResponse::GetHeaders(FAnsiStringView Name, TArrayView OutValues) const { int32 MatchIndex = 0; const int32 NameLen = Name.Len(); const int32 ValuesCount = OutValues.Num(); FAnsiStringView* Values = OutValues.GetData(); for (const FAnsiStringView& Header : GetAllHeaders()) { if (Header.StartsWith(Name, ESearchCase::IgnoreCase) && Header.Len() > NameLen && Header.GetData()[NameLen] == ':') { if (ValuesCount > MatchIndex) { Values[MatchIndex] = Header.RightChop(NameLen + 1).TrimStartAndEnd(); } ++MatchIndex; } } return MatchIndex; } EHttpMediaType IHttpResponse::GetContentType() const { const FAnsiStringView ContentType = GetHeader(ANSITEXTVIEW("Content-Type")); const FAnsiStringView ContentTypeNoParams = FAsciiSet::FindPrefixWithout(ContentType, "; \t"); EHttpMediaType MediaType = EHttpMediaType::Any; TryLexFromString(MediaType, ContentTypeNoParams); return MediaType; } uint64 IHttpResponse::GetContentLength() const { const FAnsiStringView ContentLength = GetHeader(ANSITEXTVIEW("Content-Length")); const FAnsiStringView ContentLengthNoParams = FAsciiSet::FindPrefixWithout(ContentLength, "; \t"); if (ContentLengthNoParams.IsEmpty()) { return -1; } uint64 ContentLengthValue = static_cast(-1); LexFromString(ContentLengthValue, ContentLengthNoParams); return ContentLengthValue; } FStringBuilderBase& operator<<(FStringBuilderBase& Builder, const IHttpResponse& Response) { Builder << LexToString(Response.GetMethod()) << TEXT(' ') << Response.GetUri(); if (const int32 StatusCode = Response.GetStatusCode(); StatusCode > 0) { Builder << TEXTVIEW(" -> ") << LexStatusCodeToString(StatusCode) << " (" << StatusCode << ")"; } if (const FAnsiStringView Error = Response.GetError(); !Error.IsEmpty()) { Builder << TEXTVIEW(": ") << Error; } return Builder; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// FHttpByteArrayReceiver::FHttpByteArrayReceiver(TArray64& OutArray, IHttpReceiver* InNext) : Array(OutArray) , Next(InNext) { Array.Reset(); } IHttpReceiver* FHttpByteArrayReceiver::OnBody(IHttpResponse& Response, FMemoryView& Data) { if (Array.IsEmpty()) { constexpr int64 MaxReserveSize = 96 * 1024 * 1024; if (const FAnsiStringView View = Response.GetHeader(ANSITEXTVIEW("Content-Length")); !View.IsEmpty()) { constexpr int32 MaxStringLen = 16; ANSICHAR String[MaxStringLen]; if (const int32 CopyLen = View.CopyString(String, MaxStringLen); CopyLen < MaxStringLen) { String[CopyLen] = '\0'; const int64 ContentLength = FCStringAnsi::Atoi64(String); if (ContentLength > 0 && ContentLength <= MaxReserveSize) { Array.Reserve(ContentLength); } } } } Array.Append(static_cast(Data.GetData()), int64(Data.GetSize())); return this; } /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// struct Http::Private::FHttpRequestQueueData { struct FWaiter { THttpUniquePtr Request; FManualResetEvent Event; }; FHttpRequestQueueData(IHttpConnectionPool& ConnectionPool, const FHttpClientParams& ClientParams) { FHttpClientParams QueueParams = ClientParams; QueueParams.OnDestroyRequest = [this, OnDestroyRequest = MoveTemp(QueueParams.OnDestroyRequest)] { if (OnDestroyRequest) { OnDestroyRequest(); } if (!Waiters.IsEmpty()) { if (THttpUniquePtr Request = Client->TryCreateRequest({})) { if (FWaiter* Waiter = Waiters.Pop()) { Waiter->Request = MoveTemp(Request); Waiter->Event.Notify(); } } } }; Client = ConnectionPool.CreateClient(QueueParams); } THttpUniquePtr CreateRequest(const FHttpRequestParams& Params) { if (Params.bIgnoreMaxRequests) { THttpUniquePtr Request = Client->TryCreateRequest(Params); checkf(Request, TEXT("IHttpClient::TryCreateRequest returned null in spite of bIgnoreMaxRequests.")); return Request; } while (THttpUniquePtr Request = Client->TryCreateRequest(Params)) { if (FWaiter* Waiter = Waiters.Pop()) { Waiter->Request = MoveTemp(Request); Waiter->Event.Notify(); } else { return Request; } } FWaiter LocalWaiter; Waiters.Push(&LocalWaiter); while (THttpUniquePtr Request = Client->TryCreateRequest(Params)) { if (FWaiter* Waiter = Waiters.Pop()) { Waiter->Request = MoveTemp(Request); Waiter->Event.Notify(); } if (LocalWaiter.Event.IsNotified()) { checkf(LocalWaiter.Request, TEXT("CreateRequest returning null after IsNotified().")); return MoveTemp(LocalWaiter.Request); } } TRACE_CPUPROFILER_EVENT_SCOPE(HttpRequestQueue_Wait); LocalWaiter.Event.Wait(); checkf(LocalWaiter.Request, TEXT("CreateRequest returning null after Wait().")); return MoveTemp(LocalWaiter.Request); } THttpUniquePtr Client; TLockFreePointerListFIFO Waiters; }; FHttpRequestQueue::FHttpRequestQueue(IHttpConnectionPool& ConnectionPool, const FHttpClientParams& ClientParams) : Data(MakePimpl(ConnectionPool, ClientParams)) { } THttpUniquePtr FHttpRequestQueue::CreateRequest(const FHttpRequestParams& Params) { static_assert(sizeof(Params) == sizeof(bool), "CreateRequest only handles bIgnoreMaxRequests"); check(Data); return Data->CreateRequest(Params); } } // UE