// Copyright Epic Games, Inc. All Rights Reserved. #pragma once // Shared helpers for curl-based HTTP client implementations (sync and async). // This is an internal header, not part of the public API. #include #include ZEN_THIRD_PARTY_INCLUDES_START #include ZEN_THIRD_PARTY_INCLUDES_END #include #include #include #include namespace zen { ////////////////////////////////////////////////////////////////////////// // // Error mapping inline HttpClientErrorCode MapCurlError(CURLcode Code) { switch (Code) { case CURLE_OK: return HttpClientErrorCode::kOK; case CURLE_COULDNT_CONNECT: return HttpClientErrorCode::kConnectionFailure; case CURLE_COULDNT_RESOLVE_HOST: return HttpClientErrorCode::kHostResolutionFailure; case CURLE_COULDNT_RESOLVE_PROXY: return HttpClientErrorCode::kProxyResolutionFailure; case CURLE_RECV_ERROR: return HttpClientErrorCode::kNetworkReceiveError; case CURLE_SEND_ERROR: return HttpClientErrorCode::kNetworkSendFailure; case CURLE_OPERATION_TIMEDOUT: return HttpClientErrorCode::kOperationTimedOut; case CURLE_SSL_CONNECT_ERROR: return HttpClientErrorCode::kSSLConnectError; case CURLE_SSL_CERTPROBLEM: return HttpClientErrorCode::kSSLCertificateError; case CURLE_PEER_FAILED_VERIFICATION: return HttpClientErrorCode::kSSLCACertError; case CURLE_SSL_CIPHER: case CURLE_SSL_ENGINE_NOTFOUND: case CURLE_SSL_ENGINE_SETFAILED: return HttpClientErrorCode::kGenericSSLError; case CURLE_ABORTED_BY_CALLBACK: return HttpClientErrorCode::kRequestCancelled; default: return HttpClientErrorCode::kOtherError; } } ////////////////////////////////////////////////////////////////////////// // // Curl callback data structures and callbacks struct CurlWriteCallbackData { std::string* Body = nullptr; std::function* CheckIfAbortFunction = nullptr; }; inline size_t CurlWriteCallback(char* Ptr, size_t Size, size_t Nmemb, void* UserData) { auto* Data = static_cast(UserData); size_t TotalBytes = Size * Nmemb; if (Data->CheckIfAbortFunction && *Data->CheckIfAbortFunction && (*Data->CheckIfAbortFunction)()) { return 0; // Signal abort to curl } Data->Body->append(Ptr, TotalBytes); return TotalBytes; } struct CurlHeaderCallbackData { std::vector>* Headers = nullptr; }; // Trims trailing CRLF, splits on the first colon, and trims whitespace from key and value. // Returns nullopt for blank lines or lines without a colon (e.g. HTTP status lines). inline std::optional> ParseHeaderLine(std::string_view Line) { while (!Line.empty() && (Line.back() == '\r' || Line.back() == '\n')) { Line.remove_suffix(1); } if (Line.empty()) { return std::nullopt; } size_t ColonPos = Line.find(':'); if (ColonPos == std::string_view::npos) { return std::nullopt; } std::string_view Key = Line.substr(0, ColonPos); std::string_view Value = Line.substr(ColonPos + 1); while (!Key.empty() && Key.back() == ' ') { Key.remove_suffix(1); } while (!Value.empty() && Value.front() == ' ') { Value.remove_prefix(1); } return std::pair{Key, Value}; } inline size_t CurlHeaderCallback(char* Buffer, size_t Size, size_t Nmemb, void* UserData) { auto* Data = static_cast(UserData); size_t TotalBytes = Size * Nmemb; if (auto Header = ParseHeaderLine(std::string_view(Buffer, TotalBytes))) { auto& [Key, Value] = *Header; Data->Headers->emplace_back(std::string(Key), std::string(Value)); } return TotalBytes; } struct CurlReadCallbackData { const uint8_t* DataPtr = nullptr; size_t DataSize = 0; size_t Offset = 0; std::function* CheckIfAbortFunction = nullptr; }; inline size_t CurlReadCallback(char* Buffer, size_t Size, size_t Nmemb, void* UserData) { auto* Data = static_cast(UserData); size_t MaxRead = Size * Nmemb; if (Data->CheckIfAbortFunction && *Data->CheckIfAbortFunction && (*Data->CheckIfAbortFunction)()) { return CURL_READFUNC_ABORT; } size_t Remaining = Data->DataSize - Data->Offset; size_t ToRead = std::min(MaxRead, Remaining); if (ToRead > 0) { memcpy(Buffer, Data->DataPtr + Data->Offset, ToRead); Data->Offset += ToRead; } return ToRead; } ////////////////////////////////////////////////////////////////////////// // // URL and header construction inline void AppendUrlEncoded(StringBuilderBase& Out, std::string_view Input) { static constexpr char HexDigits[] = "0123456789ABCDEF"; static constexpr AsciiSet Unreserved("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~"); for (char C : Input) { if (Unreserved.Contains(C)) { Out.Append(C); } else { uint8_t Byte = static_cast(C); char Encoded[3] = {'%', HexDigits[Byte >> 4], HexDigits[Byte & 0x0F]}; Out.Append(std::string_view(Encoded, 3)); } } } inline void BuildUrlWithParameters(StringBuilderBase& Url, std::string_view BaseUrl, std::string_view ResourcePath, const HttpClient::KeyValueMap& Parameters) { Url.Append(BaseUrl); Url.Append(ResourcePath); if (!Parameters->empty()) { char Separator = '?'; for (const auto& [Key, Value] : *Parameters) { Url.Append(Separator); AppendUrlEncoded(Url, Key); Url.Append('='); AppendUrlEncoded(Url, Value); Separator = '&'; } } } inline std::pair HeaderContentType(ZenContentType ContentType) { return std::make_pair("Content-Type", std::string(MapContentTypeToString(ContentType))); } inline curl_slist* BuildHeaderList(const HttpClient::KeyValueMap& AdditionalHeader, std::string_view SessionId, const std::optional& AccessToken, const std::vector>& ExtraHeaders = {}) { curl_slist* Headers = nullptr; for (const auto& [Key, Value] : *AdditionalHeader) { ExtendableStringBuilder<64> HeaderLine; HeaderLine << Key << ": " << Value; Headers = curl_slist_append(Headers, HeaderLine.c_str()); } if (!SessionId.empty()) { ExtendableStringBuilder<64> SessionHeader; SessionHeader << "UE-Session: " << SessionId; Headers = curl_slist_append(Headers, SessionHeader.c_str()); } if (AccessToken.has_value()) { ExtendableStringBuilder<128> AuthHeader; AuthHeader << "Authorization: " << AccessToken.value(); Headers = curl_slist_append(Headers, AuthHeader.c_str()); } for (const auto& [Key, Value] : ExtraHeaders) { ExtendableStringBuilder<128> HeaderLine; HeaderLine << Key << ": " << Value; Headers = curl_slist_append(Headers, HeaderLine.c_str()); } return Headers; } inline HttpClient::KeyValueMap BuildHeaderMap(const std::vector>& Headers) { HttpClient::KeyValueMap HeaderMap; for (const auto& [Key, Value] : Headers) { HeaderMap->insert_or_assign(Key, Value); } return HeaderMap; } // Scans response headers for Content-Type and applies it to the buffer. inline void ApplyContentTypeFromHeaders(IoBuffer& Buffer, const std::vector>& Headers) { for (const auto& [Key, Value] : Headers) { if (StrCaseCompare(Key, "Content-Type") == 0) { Buffer.SetContentType(ParseContentType(Value)); break; } } } } // namespace zen