diff options
| author | Stefan Boberg <[email protected]> | 2026-03-16 10:56:11 +0100 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2026-03-16 10:56:11 +0100 |
| commit | 8c3ba4e8c522d119df3cb48966e36c0eaa80aeb9 (patch) | |
| tree | cf51b07e097904044b4bf65bc3fe0ad14134074f /src/zenhttp/clients | |
| parent | Merge branch 'sb/no-network' of https://github.ol.epicgames.net/ue-foundation... (diff) | |
| parent | Enable cross compilation of Windows targets on Linux (#839) (diff) | |
| download | zen-sb/no-network.tar.xz zen-sb/no-network.zip | |
Merge branch 'main' into sb/no-networksb/no-network
Diffstat (limited to 'src/zenhttp/clients')
| -rw-r--r-- | src/zenhttp/clients/httpclientcpr.cpp | 3 | ||||
| -rw-r--r-- | src/zenhttp/clients/httpclientcurl.cpp | 1100 | ||||
| -rw-r--r-- | src/zenhttp/clients/httpclientcurl.h | 25 | ||||
| -rw-r--r-- | src/zenhttp/clients/httpwsclient.cpp | 4 |
4 files changed, 430 insertions, 702 deletions
diff --git a/src/zenhttp/clients/httpclientcpr.cpp b/src/zenhttp/clients/httpclientcpr.cpp index a0f5cc38f..a52b8f74b 100644 --- a/src/zenhttp/clients/httpclientcpr.cpp +++ b/src/zenhttp/clients/httpclientcpr.cpp @@ -7,6 +7,7 @@ #include <zencore/compactbinarypackage.h> #include <zencore/compactbinaryutil.h> #include <zencore/compress.h> +#include <zencore/filesystem.h> #include <zencore/iobuffer.h> #include <zencore/iohash.h> #include <zencore/session.h> @@ -513,7 +514,7 @@ CprHttpClient::AllocSession(const std::string_view BaseUrl, if (!ConnectionSettings.UnixSocketPath.empty()) { - CprSession->SetUnixSocket(cpr::UnixSocket(ConnectionSettings.UnixSocketPath)); + CprSession->SetUnixSocket(cpr::UnixSocket(PathToUtf8(ConnectionSettings.UnixSocketPath))); } if (ConnectionSettings.InsecureSsl || !ConnectionSettings.CaBundlePath.empty()) diff --git a/src/zenhttp/clients/httpclientcurl.cpp b/src/zenhttp/clients/httpclientcurl.cpp index 341adc5f7..ec9b7bac6 100644 --- a/src/zenhttp/clients/httpclientcurl.cpp +++ b/src/zenhttp/clients/httpclientcurl.cpp @@ -7,6 +7,8 @@ #include <zencore/compactbinarypackage.h> #include <zencore/compactbinaryutil.h> #include <zencore/compress.h> +#include <zencore/except.h> +#include <zencore/filesystem.h> #include <zencore/iobuffer.h> #include <zencore/iohash.h> #include <zencore/session.h> @@ -93,15 +95,11 @@ struct HeaderCallbackData std::vector<std::pair<std::string, std::string>>* Headers = nullptr; }; -static size_t -CurlHeaderCallback(char* Buffer, size_t Size, size_t Nmemb, void* UserData) +// 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). +static std::optional<std::pair<std::string_view, std::string_view>> +ParseHeaderLine(std::string_view Line) { - auto* Data = static_cast<HeaderCallbackData*>(UserData); - size_t TotalBytes = Size * Nmemb; - - std::string_view Line(Buffer, TotalBytes); - - // Trim trailing \r\n while (!Line.empty() && (Line.back() == '\r' || Line.back() == '\n')) { Line.remove_suffix(1); @@ -109,25 +107,39 @@ CurlHeaderCallback(char* Buffer, size_t Size, size_t Nmemb, void* UserData) if (Line.empty()) { - return TotalBytes; + return std::nullopt; } size_t ColonPos = Line.find(':'); - if (ColonPos != std::string_view::npos) + if (ColonPos == std::string_view::npos) { - std::string_view Key = Line.substr(0, ColonPos); - std::string_view Value = Line.substr(ColonPos + 1); + return std::nullopt; + } - // Trim whitespace - while (!Key.empty() && Key.back() == ' ') - { - Key.remove_suffix(1); - } - while (!Value.empty() && Value.front() == ' ') - { - Value.remove_prefix(1); - } + 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}; +} + +static size_t +CurlHeaderCallback(char* Buffer, size_t Size, size_t Nmemb, void* UserData) +{ + auto* Data = static_cast<HeaderCallbackData*>(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)); } @@ -285,57 +297,102 @@ BuildHeaderList(const HttpClient::KeyValueMap& AdditionalHeader, for (const auto& [Key, Value] : *AdditionalHeader) { - std::string HeaderLine = fmt::format("{}: {}", Key, Value); - Headers = curl_slist_append(Headers, HeaderLine.c_str()); + ExtendableStringBuilder<64> HeaderLine; + HeaderLine << Key << ": " << Value; + Headers = curl_slist_append(Headers, HeaderLine.c_str()); } if (!SessionId.empty()) { - std::string SessionHeader = fmt::format("UE-Session: {}", SessionId); - Headers = curl_slist_append(Headers, SessionHeader.c_str()); + ExtendableStringBuilder<64> SessionHeader; + SessionHeader << "UE-Session: " << SessionId; + Headers = curl_slist_append(Headers, SessionHeader.c_str()); } if (AccessToken) { - std::string AuthHeader = fmt::format("Authorization: {}", AccessToken->Value); - Headers = curl_slist_append(Headers, AuthHeader.c_str()); + ExtendableStringBuilder<128> AuthHeader; + AuthHeader << "Authorization: " << AccessToken->Value; + Headers = curl_slist_append(Headers, AuthHeader.c_str()); } for (const auto& [Key, Value] : ExtraHeaders) { - std::string HeaderLine = fmt::format("{}: {}", Key, Value); - Headers = curl_slist_append(Headers, HeaderLine.c_str()); + ExtendableStringBuilder<128> HeaderLine; + HeaderLine << Key << ": " << Value; + Headers = curl_slist_append(Headers, HeaderLine.c_str()); } return Headers; } -static std::string -BuildUrlWithParameters(std::string_view BaseUrl, std::string_view ResourcePath, const HttpClient::KeyValueMap& Parameters) +static HttpClient::KeyValueMap +BuildHeaderMap(const std::vector<std::pair<std::string, std::string>>& 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. +static void +ApplyContentTypeFromHeaders(IoBuffer& Buffer, const std::vector<std::pair<std::string, std::string>>& Headers) +{ + for (const auto& [Key, Value] : Headers) + { + if (StrCaseCompare(Key, "Content-Type") == 0) + { + Buffer.SetContentType(ParseContentType(Value)); + break; + } + } +} + +static 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<uint8_t>(C); + char Encoded[3] = {'%', HexDigits[Byte >> 4], HexDigits[Byte & 0x0F]}; + Out.Append(std::string_view(Encoded, 3)); + } + } +} + +static void +BuildUrlWithParameters(StringBuilderBase& Url, + std::string_view BaseUrl, + std::string_view ResourcePath, + const HttpClient::KeyValueMap& Parameters) { - std::string Url; - Url.reserve(BaseUrl.size() + ResourcePath.size() + 64); - Url.append(BaseUrl); - Url.append(ResourcePath); + Url.Append(BaseUrl); + Url.Append(ResourcePath); if (!Parameters->empty()) { char Separator = '?'; for (const auto& [Key, Value] : *Parameters) { - char* EncodedKey = curl_easy_escape(nullptr, Key.c_str(), static_cast<int>(Key.size())); - char* EncodedValue = curl_easy_escape(nullptr, Value.c_str(), static_cast<int>(Value.size())); - Url += Separator; - Url += EncodedKey; - Url += '='; - Url += EncodedValue; - curl_free(EncodedKey); - curl_free(EncodedValue); + Url.Append(Separator); + AppendUrlEncoded(Url, Key); + Url.Append('='); + AppendUrlEncoded(Url, Value); Separator = '&'; } } - - return Url; } ////////////////////////////////////////////////////////////////////////// @@ -359,6 +416,48 @@ CurlHttpClient::~CurlHttpClient() }); } +CurlHttpClient::Session::~Session() +{ + if (HeaderList) + { + curl_slist_free_all(HeaderList); + } + Outer->ReleaseSession(Handle); +} + +void +CurlHttpClient::Session::SetHeaders(curl_slist* Headers) +{ + if (HeaderList) + { + curl_slist_free_all(HeaderList); + } + HeaderList = Headers; + curl_easy_setopt(Handle, CURLOPT_HTTPHEADER, HeaderList); +} + +CurlHttpClient::CurlResult +CurlHttpClient::Session::PerformWithResponseCallbacks() +{ + std::string Body; + WriteCallbackData WriteData{.Body = &Body, + .CheckIfAbortFunction = Outer->m_CheckIfAbortFunction ? &Outer->m_CheckIfAbortFunction : nullptr}; + HeaderCallbackData HdrData{}; + std::vector<std::pair<std::string, std::string>> ResponseHeaders; + HdrData.Headers = &ResponseHeaders; + + curl_easy_setopt(Handle, CURLOPT_WRITEFUNCTION, CurlWriteCallback); + curl_easy_setopt(Handle, CURLOPT_WRITEDATA, &WriteData); + curl_easy_setopt(Handle, CURLOPT_HEADERFUNCTION, CurlHeaderCallback); + curl_easy_setopt(Handle, CURLOPT_HEADERDATA, &HdrData); + + CurlResult Result = Perform(); + Result.Body = std::move(Body); + Result.Headers = std::move(ResponseHeaders); + + return Result; +} + CurlHttpClient::CurlResult CurlHttpClient::Session::Perform() { @@ -411,15 +510,7 @@ CurlHttpClient::ResponseWithPayload(std::string_view SessionId, { IoBuffer ResponseBuffer = Payload ? std::move(Payload) : IoBuffer(IoBuffer::Clone, Result.Body.data(), Result.Body.size()); - for (const auto& [Key, Value] : Result.Headers) - { - if (StrCaseCompare(Key.c_str(), "Content-Type") == 0) - { - const HttpContentType ContentType = ParseContentType(Value); - ResponseBuffer.SetContentType(ContentType); - break; - } - } + ApplyContentTypeFromHeaders(ResponseBuffer, Result.Headers); if (!IsHttpSuccessCode(WorkResponseCode) && WorkResponseCode != HttpResponseCode::NotFound) { @@ -438,15 +529,9 @@ CurlHttpClient::ResponseWithPayload(std::string_view SessionId, return Lhs.RangeOffset < Rhs.RangeOffset; }); - HttpClient::KeyValueMap HeaderMap; - for (const auto& [Key, Value] : Result.Headers) - { - HeaderMap->insert_or_assign(Key, Value); - } - return HttpClient::Response{.StatusCode = WorkResponseCode, .ResponsePayload = std::move(ResponseBuffer), - .Header = std::move(HeaderMap), + .Header = BuildHeaderMap(Result.Headers), .UploadedBytes = Result.UploadedBytes, .DownloadedBytes = Result.DownloadedBytes, .ElapsedSeconds = Result.ElapsedSeconds, @@ -475,16 +560,10 @@ CurlHttpClient::CommonResponse(std::string_view SessionId, } } - HttpClient::KeyValueMap HeaderMap; - for (const auto& [Key, Value] : Result.Headers) - { - HeaderMap->insert_or_assign(Key, Value); - } - return HttpClient::Response{ .StatusCode = WorkResponseCode, .ResponsePayload = IoBufferBuilder::MakeCloneFromMemory(Result.Body.data(), Result.Body.size()), - .Header = std::move(HeaderMap), + .Header = BuildHeaderMap(Result.Headers), .UploadedBytes = Result.UploadedBytes, .DownloadedBytes = Result.DownloadedBytes, .ElapsedSeconds = Result.ElapsedSeconds, @@ -493,14 +572,8 @@ CurlHttpClient::CommonResponse(std::string_view SessionId, if (WorkResponseCode == HttpResponseCode::NoContent || (Result.Body.empty() && !Payload)) { - HttpClient::KeyValueMap HeaderMap; - for (const auto& [Key, Value] : Result.Headers) - { - HeaderMap->insert_or_assign(Key, Value); - } - return HttpClient::Response{.StatusCode = WorkResponseCode, - .Header = std::move(HeaderMap), + .Header = BuildHeaderMap(Result.Headers), .UploadedBytes = Result.UploadedBytes, .DownloadedBytes = Result.DownloadedBytes, .ElapsedSeconds = Result.ElapsedSeconds}; @@ -519,25 +592,43 @@ CurlHttpClient::ValidatePayload(CurlResult& Result, std::unique_ptr<detail::Temp IoBuffer ResponseBuffer = (Result.Body.empty() && PayloadFile) ? PayloadFile->BorrowIoBuffer() : IoBuffer(IoBuffer::Wrap, Result.Body.data(), Result.Body.size()); - // Find Content-Length in headers + // Collect relevant headers in a single pass + std::string_view ContentLengthValue; + std::string_view IoHashValue; + std::string_view ContentTypeValue; + for (const auto& [Key, Value] : Result.Headers) { - if (StrCaseCompare(Key.c_str(), "Content-Length") == 0) + if (ContentLengthValue.empty() && StrCaseCompare(Key, "Content-Length") == 0) { - std::optional<uint64_t> ExpectedContentSize = ParseInt<uint64_t>(Value); - if (!ExpectedContentSize.has_value()) - { - Result.ErrorCode = CURLE_RECV_ERROR; - Result.ErrorMessage = fmt::format("Can not parse Content-Length header. Value: '{}'", Value); - return false; - } - if (ExpectedContentSize.value() != ResponseBuffer.GetSize()) - { - Result.ErrorCode = CURLE_RECV_ERROR; - Result.ErrorMessage = fmt::format("Payload size {} does not match Content-Length {}", ResponseBuffer.GetSize(), Value); - return false; - } - break; + ContentLengthValue = Value; + } + else if (IoHashValue.empty() && StrCaseCompare(Key, "X-Jupiter-IoHash") == 0) + { + IoHashValue = Value; + } + else if (ContentTypeValue.empty() && StrCaseCompare(Key, "Content-Type") == 0) + { + ContentTypeValue = Value; + } + } + + // Validate Content-Length + if (!ContentLengthValue.empty()) + { + std::optional<uint64_t> ExpectedContentSize = ParseInt<uint64_t>(ContentLengthValue); + if (!ExpectedContentSize.has_value()) + { + Result.ErrorCode = CURLE_RECV_ERROR; + Result.ErrorMessage = fmt::format("Can not parse Content-Length header. Value: '{}'", ContentLengthValue); + return false; + } + if (ExpectedContentSize.value() != ResponseBuffer.GetSize()) + { + Result.ErrorCode = CURLE_RECV_ERROR; + Result.ErrorMessage = + fmt::format("Payload size {} does not match Content-Length {}", ResponseBuffer.GetSize(), ContentLengthValue); + return false; } } @@ -546,66 +637,55 @@ CurlHttpClient::ValidatePayload(CurlResult& Result, std::unique_ptr<detail::Temp return true; } - // Check X-Jupiter-IoHash - for (const auto& [Key, Value] : Result.Headers) + // Validate X-Jupiter-IoHash + if (!IoHashValue.empty()) { - if (StrCaseCompare(Key.c_str(), "X-Jupiter-IoHash") == 0) + IoHash ExpectedPayloadHash; + if (IoHash::TryParse(IoHashValue, ExpectedPayloadHash)) { - IoHash ExpectedPayloadHash; - if (IoHash::TryParse(Value, ExpectedPayloadHash)) + IoHash PayloadHash = IoHash::HashBuffer(ResponseBuffer); + if (PayloadHash != ExpectedPayloadHash) { - IoHash PayloadHash = IoHash::HashBuffer(ResponseBuffer); - if (PayloadHash != ExpectedPayloadHash) - { - Result.ErrorCode = CURLE_RECV_ERROR; - Result.ErrorMessage = fmt::format("Payload hash {} does not match X-Jupiter-IoHash {}", - PayloadHash.ToHexString(), - ExpectedPayloadHash.ToHexString()); - return false; - } + Result.ErrorCode = CURLE_RECV_ERROR; + Result.ErrorMessage = fmt::format("Payload hash {} does not match X-Jupiter-IoHash {}", + PayloadHash.ToHexString(), + ExpectedPayloadHash.ToHexString()); + return false; } - break; } } // Validate content-type specific payload - for (const auto& [Key, Value] : Result.Headers) + if (ContentTypeValue == "application/x-ue-comp") { - if (StrCaseCompare(Key.c_str(), "Content-Type") == 0) + IoHash RawHash; + uint64_t RawSize; + if (CompressedBuffer::ValidateCompressedHeader(ResponseBuffer, + RawHash, + RawSize, + /*OutOptionalTotalCompressedSize*/ nullptr)) { - if (Value == "application/x-ue-comp") - { - IoHash RawHash; - uint64_t RawSize; - if (CompressedBuffer::ValidateCompressedHeader(ResponseBuffer, - RawHash, - RawSize, - /*OutOptionalTotalCompressedSize*/ nullptr)) - { - return true; - } - else - { - Result.ErrorCode = CURLE_RECV_ERROR; - Result.ErrorMessage = "Compressed binary failed validation"; - return false; - } - } - if (Value == "application/x-ue-cb") - { - if (CbValidateError Error = ValidateCompactBinary(ResponseBuffer.GetView(), CbValidateMode::Default); - Error == CbValidateError::None) - { - return true; - } - else - { - Result.ErrorCode = CURLE_RECV_ERROR; - Result.ErrorMessage = fmt::format("Compact binary failed validation: {}", ToString(Error)); - return false; - } - } - break; + return true; + } + else + { + Result.ErrorCode = CURLE_RECV_ERROR; + Result.ErrorMessage = "Compressed binary failed validation"; + return false; + } + } + if (ContentTypeValue == "application/x-ue-cb") + { + if (CbValidateError Error = ValidateCompactBinary(ResponseBuffer.GetView(), CbValidateMode::Default); + Error == CbValidateError::None) + { + return true; + } + else + { + Result.ErrorCode = CURLE_RECV_ERROR; + Result.ErrorMessage = fmt::format("Compact binary failed validation: {}", ToString(Error)); + return false; } } @@ -666,10 +746,24 @@ CurlHttpClient::DoWithRetry(std::string_view SessionId, std::function<CurlResult Attempt++; if (ShouldLogErrorCode(HttpResponseCode(Result.StatusCode))) { - ZEN_INFO("{} Attempt {}/{}", - CommonResponse(SessionId, std::move(Result), {}).ErrorMessage("Retry"), - Attempt, - m_ConnectionSettings.RetryCount + 1); + if (Result.ErrorCode != CURLE_OK) + { + ZEN_INFO("Retry (session: {}): HTTP error ({}) '{}' Attempt {}/{}", + SessionId, + static_cast<int>(MapCurlError(Result.ErrorCode)), + Result.ErrorMessage, + Attempt, + m_ConnectionSettings.RetryCount + 1); + } + else + { + ZEN_INFO("Retry (session: {}): HTTP status ({}) '{}' Attempt {}/{}", + SessionId, + Result.StatusCode, + zen::ToString(HttpResponseCode(Result.StatusCode)), + Attempt, + m_ConnectionSettings.RetryCount + 1); + } } Result = Func(); } @@ -681,51 +775,14 @@ CurlHttpClient::DoWithRetry(std::string_view SessionId, std::function<CurlResult()>&& Func, std::unique_ptr<detail::TempPayloadFile>& PayloadFile) { - uint8_t Attempt = 0; - CurlResult Result = Func(); - while (Attempt < m_ConnectionSettings.RetryCount) - { - if (m_CheckIfAbortFunction && m_CheckIfAbortFunction()) - { - return Result; - } - if (!ShouldRetry(Result)) - { - if (Result.ErrorCode != CURLE_OK || !IsHttpSuccessCode(Result.StatusCode)) - { - break; - } - if (ValidatePayload(Result, PayloadFile)) - { - break; - } - } - Sleep(100 * (Attempt + 1)); - Attempt++; - if (ShouldLogErrorCode(HttpResponseCode(Result.StatusCode))) - { - ZEN_INFO("{} Attempt {}/{}", - CommonResponse(SessionId, std::move(Result), {}).ErrorMessage("Retry"), - Attempt, - m_ConnectionSettings.RetryCount + 1); - } - Result = Func(); - } - return Result; + return DoWithRetry(SessionId, std::move(Func), [&](CurlResult& Result) { return ValidatePayload(Result, PayloadFile); }); } ////////////////////////////////////////////////////////////////////////// CurlHttpClient::Session -CurlHttpClient::AllocSession(std::string_view BaseUrl, - std::string_view ResourcePath, - const HttpClientSettings& ConnectionSettings, - const KeyValueMap& AdditionalHeader, - const KeyValueMap& Parameters, - std::string_view SessionId, - std::optional<HttpClientAccessToken> AccessToken) +CurlHttpClient::AllocSession(std::string_view ResourcePath, const KeyValueMap& Parameters) { - ZEN_UNUSED(AccessToken, SessionId, AdditionalHeader); ZEN_TRACE_CPU("CurlHttpClient::AllocSession"); CURL* Handle = nullptr; m_SessionLock.WithExclusiveLock([&] { @@ -739,6 +796,10 @@ CurlHttpClient::AllocSession(std::string_view BaseUrl, if (Handle == nullptr) { Handle = curl_easy_init(); + if (Handle == nullptr) + { + ThrowOutOfMemory("curl_easy_init"); + } } else { @@ -746,33 +807,35 @@ CurlHttpClient::AllocSession(std::string_view BaseUrl, } // Unix domain socket - if (!ConnectionSettings.UnixSocketPath.empty()) + if (!m_ConnectionSettings.UnixSocketPath.empty()) { - curl_easy_setopt(Handle, CURLOPT_UNIX_SOCKET_PATH, ConnectionSettings.UnixSocketPath.c_str()); + std::string SocketPathUtf8 = PathToUtf8(m_ConnectionSettings.UnixSocketPath); + curl_easy_setopt(Handle, CURLOPT_UNIX_SOCKET_PATH, SocketPathUtf8.c_str()); } // Build URL with parameters - std::string Url = BuildUrlWithParameters(BaseUrl, ResourcePath, Parameters); + ExtendableStringBuilder<256> Url; + BuildUrlWithParameters(Url, m_BaseUri, ResourcePath, Parameters); curl_easy_setopt(Handle, CURLOPT_URL, Url.c_str()); // Timeouts - if (ConnectionSettings.ConnectTimeout.count() > 0) + if (m_ConnectionSettings.ConnectTimeout.count() > 0) { - curl_easy_setopt(Handle, CURLOPT_CONNECTTIMEOUT_MS, static_cast<long>(ConnectionSettings.ConnectTimeout.count())); + curl_easy_setopt(Handle, CURLOPT_CONNECTTIMEOUT_MS, static_cast<long>(m_ConnectionSettings.ConnectTimeout.count())); } - if (ConnectionSettings.Timeout.count() > 0) + if (m_ConnectionSettings.Timeout.count() > 0) { - curl_easy_setopt(Handle, CURLOPT_TIMEOUT_MS, static_cast<long>(ConnectionSettings.Timeout.count())); + curl_easy_setopt(Handle, CURLOPT_TIMEOUT_MS, static_cast<long>(m_ConnectionSettings.Timeout.count())); } // HTTP/2 - if (ConnectionSettings.AssumeHttp2) + if (m_ConnectionSettings.AssumeHttp2) { curl_easy_setopt(Handle, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE); } // Verbose/debug - if (ConnectionSettings.Verbose) + if (m_ConnectionSettings.Verbose) { curl_easy_setopt(Handle, CURLOPT_VERBOSE, 1L); curl_easy_setopt(Handle, CURLOPT_DEBUGFUNCTION, CurlDebugCallback); @@ -780,27 +843,27 @@ CurlHttpClient::AllocSession(std::string_view BaseUrl, } // SSL options - if (ConnectionSettings.InsecureSsl) + if (m_ConnectionSettings.InsecureSsl) { curl_easy_setopt(Handle, CURLOPT_SSL_VERIFYPEER, 0L); curl_easy_setopt(Handle, CURLOPT_SSL_VERIFYHOST, 0L); } - if (!ConnectionSettings.CaBundlePath.empty()) + if (!m_ConnectionSettings.CaBundlePath.empty()) { - curl_easy_setopt(Handle, CURLOPT_CAINFO, ConnectionSettings.CaBundlePath.c_str()); + curl_easy_setopt(Handle, CURLOPT_CAINFO, m_ConnectionSettings.CaBundlePath.c_str()); } // Disable signal handling for thread safety curl_easy_setopt(Handle, CURLOPT_NOSIGNAL, 1L); - if (ConnectionSettings.ForbidReuseConnection) + if (m_ConnectionSettings.ForbidReuseConnection) { curl_easy_setopt(Handle, CURLOPT_FORBID_REUSE, 1L); } // Note: Headers are NOT set here. Each method builds its own header list - // (potentially adding method-specific headers like Content-Type) and is - // responsible for freeing it with curl_slist_free_all. + // (potentially adding method-specific headers like Content-Type) and passes + // ownership to the Session via SetHeaders(). return Session(this, Handle); } @@ -809,15 +872,13 @@ void CurlHttpClient::ReleaseSession(CURL* Handle) { ZEN_TRACE_CPU("CurlHttpClient::ReleaseSession"); - - // Free any header list that was set - // curl_easy_reset will be called on next AllocSession, which cleans up the handle state. - // We just push the handle back to the pool. m_SessionLock.WithExclusiveLock([&] { m_Sessions.push_back(Handle); }); } ////////////////////////////////////////////////////////////////////////// +// TransactPackage is a two-phase protocol (offer + send) with server-side state +// between phases, so retrying individual phases would be incorrect. CurlHttpClient::Response CurlHttpClient::TransactPackage(std::string_view Url, CbPackage Package, const KeyValueMap& AdditionalHeader) { @@ -831,7 +892,7 @@ CurlHttpClient::TransactPackage(std::string_view Url, CbPackage Package, const K const uint32_t RequestId = ++CurlHttpClientRequestIdCounter; auto RequestIdString = fmt::to_string(RequestId); - if (Attachments.empty() == false) + if (!Attachments.empty()) { CbObjectWriter Writer; Writer.BeginArray("offer"); @@ -850,27 +911,19 @@ CurlHttpClient::TransactPackage(std::string_view Url, CbPackage Package, const K OfferExtraHeaders.emplace_back(HeaderContentType(HttpContentType::kCbPackageOffer)); OfferExtraHeaders.emplace_back("UE-Request", RequestIdString); - Session Sess = AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); + Session Sess = AllocSession(Url, {}); CURL* H = Sess.Get(); - curl_slist* HeaderList = BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken(), OfferExtraHeaders); - curl_easy_setopt(H, CURLOPT_HTTPHEADER, HeaderList); + Sess.SetHeaders(BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken(), OfferExtraHeaders)); curl_easy_setopt(H, CURLOPT_POST, 1L); curl_easy_setopt(H, CURLOPT_POSTFIELDS, reinterpret_cast<const char*>(MemWriter.Data())); curl_easy_setopt(H, CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(MemWriter.Size())); - std::string FilterBody; - WriteCallbackData WriteData{.Body = &FilterBody}; - curl_easy_setopt(H, CURLOPT_WRITEFUNCTION, CurlWriteCallback); - curl_easy_setopt(H, CURLOPT_WRITEDATA, &WriteData); - - CurlResult Result = Sess.Perform(); - - curl_slist_free_all(HeaderList); + CurlResult Result = Sess.PerformWithResponseCallbacks(); - if (Result.ErrorCode == CURLE_OK && Result.StatusCode == 200) + if (Result.ErrorCode == CURLE_OK && IsHttpSuccessCode(Result.StatusCode)) { - IoBuffer ResponseBuffer(IoBuffer::Wrap, FilterBody.data(), FilterBody.size()); + IoBuffer ResponseBuffer(IoBuffer::Wrap, Result.Body.data(), Result.Body.size()); CbValidateError ValidationError = CbValidateError::None; if (CbObject ResponseObject = ValidateAndReadCompactBinaryObject(std::move(ResponseBuffer), ValidationError); ValidationError == CbValidateError::None) @@ -908,41 +961,17 @@ CurlHttpClient::TransactPackage(std::string_view Url, CbPackage Package, const K PkgExtraHeaders.emplace_back(HeaderContentType(HttpContentType::kCbPackage)); PkgExtraHeaders.emplace_back("UE-Request", RequestIdString); - Session Sess = AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); + Session Sess = AllocSession(Url, {}); CURL* H = Sess.Get(); - curl_slist* HeaderList = BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken(), PkgExtraHeaders); - curl_easy_setopt(H, CURLOPT_HTTPHEADER, HeaderList); + Sess.SetHeaders(BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken(), PkgExtraHeaders)); curl_easy_setopt(H, CURLOPT_POST, 1L); curl_easy_setopt(H, CURLOPT_POSTFIELDS, reinterpret_cast<const char*>(FlatMessage.GetData())); curl_easy_setopt(H, CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(FlatMessage.GetSize())); - std::string PkgBody; - WriteCallbackData WriteData{.Body = &PkgBody}; - curl_easy_setopt(H, CURLOPT_WRITEFUNCTION, CurlWriteCallback); - curl_easy_setopt(H, CURLOPT_WRITEDATA, &WriteData); - - CurlResult Result = Sess.Perform(); - - curl_slist_free_all(HeaderList); + CurlResult Result = Sess.PerformWithResponseCallbacks(); - if (Result.ErrorCode != CURLE_OK || !IsHttpSuccessCode(Result.StatusCode)) - { - return {.StatusCode = HttpResponseCode(Result.StatusCode)}; - } - - IoBuffer ResponseBuffer(IoBuffer::Clone, PkgBody.data(), PkgBody.size()); - - for (const auto& [Key, Value] : Result.Headers) - { - if (StrCaseCompare(Key.c_str(), "Content-Type") == 0) - { - ResponseBuffer.SetContentType(ParseContentType(Value)); - break; - } - } - - return {.StatusCode = HttpResponseCode(Result.StatusCode), .ResponsePayload = std::move(ResponseBuffer)}; + return CommonResponse(m_SessionId, std::move(Result), {}, {}); } ////////////////////////////////////////////////////////////////////////// @@ -957,44 +986,26 @@ CurlHttpClient::Put(std::string_view Url, const IoBuffer& Payload, const KeyValu return CommonResponse( m_SessionId, - DoWithRetry(m_SessionId, - [&]() -> CurlResult { - Session Sess = - AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); - CURL* H = Sess.Get(); - - curl_slist* Headers = - BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken(), {HeaderContentType(Payload.GetContentType())}); - curl_easy_setopt(H, CURLOPT_HTTPHEADER, Headers); - - curl_easy_setopt(H, CURLOPT_UPLOAD, 1L); - curl_easy_setopt(H, CURLOPT_INFILESIZE_LARGE, static_cast<curl_off_t>(Payload.GetSize())); - - ReadCallbackData ReadData{.DataPtr = static_cast<const uint8_t*>(Payload.GetData()), - .DataSize = Payload.GetSize(), - .CheckIfAbortFunction = m_CheckIfAbortFunction ? &m_CheckIfAbortFunction : nullptr}; - curl_easy_setopt(H, CURLOPT_READFUNCTION, CurlReadCallback); - curl_easy_setopt(H, CURLOPT_READDATA, &ReadData); - - std::string Body; - WriteCallbackData WriteData{.Body = &Body}; - HeaderCallbackData HdrData{}; - std::vector<std::pair<std::string, std::string>> ResponseHeaders; - HdrData.Headers = &ResponseHeaders; + DoWithRetry( + m_SessionId, + [&]() -> CurlResult { + Session Sess = AllocSession(Url, {}); + CURL* H = Sess.Get(); - curl_easy_setopt(H, CURLOPT_WRITEFUNCTION, CurlWriteCallback); - curl_easy_setopt(H, CURLOPT_WRITEDATA, &WriteData); - curl_easy_setopt(H, CURLOPT_HEADERFUNCTION, CurlHeaderCallback); - curl_easy_setopt(H, CURLOPT_HEADERDATA, &HdrData); + Sess.SetHeaders( + BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken(), {HeaderContentType(Payload.GetContentType())})); - CurlResult Result = Sess.Perform(); - Result.Body = std::move(Body); - Result.Headers = std::move(ResponseHeaders); + curl_easy_setopt(H, CURLOPT_UPLOAD, 1L); + curl_easy_setopt(H, CURLOPT_INFILESIZE_LARGE, static_cast<curl_off_t>(Payload.GetSize())); - curl_slist_free_all(Headers); + ReadCallbackData ReadData{.DataPtr = static_cast<const uint8_t*>(Payload.GetData()), + .DataSize = Payload.GetSize(), + .CheckIfAbortFunction = m_CheckIfAbortFunction ? &m_CheckIfAbortFunction : nullptr}; + curl_easy_setopt(H, CURLOPT_READFUNCTION, CurlReadCallback); + curl_easy_setopt(H, CURLOPT_READDATA, &ReadData); - return Result; - }), + return Sess.PerformWithResponseCallbacks(); + }), {}); } @@ -1005,39 +1016,19 @@ CurlHttpClient::Put(std::string_view Url, const KeyValueMap& Parameters) return CommonResponse( m_SessionId, - DoWithRetry( - m_SessionId, - [&]() -> CurlResult { - KeyValueMap HeaderWithContentLength{std::pair<std::string_view, std::string_view>{"Content-Length", "0"}}; - Session Sess = - AllocSession(m_BaseUri, Url, m_ConnectionSettings, HeaderWithContentLength, Parameters, m_SessionId, GetAccessToken()); - CURL* H = Sess.Get(); - - curl_slist* Headers = BuildHeaderList(HeaderWithContentLength, m_SessionId, GetAccessToken()); - curl_easy_setopt(H, CURLOPT_HTTPHEADER, Headers); - - curl_easy_setopt(H, CURLOPT_UPLOAD, 1L); - curl_easy_setopt(H, CURLOPT_INFILESIZE_LARGE, 0LL); - - std::string Body; - WriteCallbackData WriteData{.Body = &Body}; - HeaderCallbackData HdrData{}; - std::vector<std::pair<std::string, std::string>> ResponseHeaders; - HdrData.Headers = &ResponseHeaders; - - curl_easy_setopt(H, CURLOPT_WRITEFUNCTION, CurlWriteCallback); - curl_easy_setopt(H, CURLOPT_WRITEDATA, &WriteData); - curl_easy_setopt(H, CURLOPT_HEADERFUNCTION, CurlHeaderCallback); - curl_easy_setopt(H, CURLOPT_HEADERDATA, &HdrData); + DoWithRetry(m_SessionId, + [&]() -> CurlResult { + KeyValueMap HeaderWithContentLength{std::pair<std::string_view, std::string_view>{"Content-Length", "0"}}; + Session Sess = AllocSession(Url, Parameters); + CURL* H = Sess.Get(); - CurlResult Result = Sess.Perform(); - Result.Body = std::move(Body); - Result.Headers = std::move(ResponseHeaders); + Sess.SetHeaders(BuildHeaderList(HeaderWithContentLength, m_SessionId, GetAccessToken())); - curl_slist_free_all(Headers); + curl_easy_setopt(H, CURLOPT_UPLOAD, 1L); + curl_easy_setopt(H, CURLOPT_INFILESIZE_LARGE, 0LL); - return Result; - }), + return Sess.PerformWithResponseCallbacks(); + }), {}); } @@ -1045,43 +1036,20 @@ CurlHttpClient::Response CurlHttpClient::Get(std::string_view Url, const KeyValueMap& AdditionalHeader, const KeyValueMap& Parameters) { ZEN_TRACE_CPU("CurlHttpClient::Get"); - return CommonResponse( - m_SessionId, - DoWithRetry( - m_SessionId, - [&]() -> CurlResult { - Session Sess = - AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, Parameters, m_SessionId, GetAccessToken()); - CURL* H = Sess.Get(); - - curl_slist* Headers = BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken()); - curl_easy_setopt(H, CURLOPT_HTTPHEADER, Headers); - curl_easy_setopt(H, CURLOPT_HTTPGET, 1L); - - std::string Body; - WriteCallbackData WriteData{.Body = &Body}; - HeaderCallbackData HdrData{}; - std::vector<std::pair<std::string, std::string>> ResponseHeaders; - HdrData.Headers = &ResponseHeaders; - - curl_easy_setopt(H, CURLOPT_WRITEFUNCTION, CurlWriteCallback); - curl_easy_setopt(H, CURLOPT_WRITEDATA, &WriteData); - curl_easy_setopt(H, CURLOPT_HEADERFUNCTION, CurlHeaderCallback); - curl_easy_setopt(H, CURLOPT_HEADERDATA, &HdrData); - - CurlResult Result = Sess.Perform(); - Result.Body = std::move(Body); - Result.Headers = std::move(ResponseHeaders); - - curl_slist_free_all(Headers); - - return Result; - }, - [this](CurlResult& Result) { - std::unique_ptr<detail::TempPayloadFile> NoTempFile; - return ValidatePayload(Result, NoTempFile); - }), - {}); + return CommonResponse(m_SessionId, + DoWithRetry( + m_SessionId, + [&]() -> CurlResult { + Session Sess = AllocSession(Url, Parameters); + Sess.SetHeaders(BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken())); + curl_easy_setopt(Sess.Get(), CURLOPT_HTTPGET, 1L); + return Sess.PerformWithResponseCallbacks(); + }, + [this](CurlResult& Result) { + std::unique_ptr<detail::TempPayloadFile> NoTempFile; + return ValidatePayload(Result, NoTempFile); + }), + {}); } CurlHttpClient::Response @@ -1089,33 +1057,15 @@ CurlHttpClient::Head(std::string_view Url, const KeyValueMap& AdditionalHeader) { ZEN_TRACE_CPU("CurlHttpClient::Head"); - return CommonResponse( - m_SessionId, - DoWithRetry(m_SessionId, - [&]() -> CurlResult { - Session Sess = - AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); - CURL* H = Sess.Get(); - - curl_slist* Headers = BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken()); - curl_easy_setopt(H, CURLOPT_HTTPHEADER, Headers); - curl_easy_setopt(H, CURLOPT_NOBODY, 1L); - - HeaderCallbackData HdrData{}; - std::vector<std::pair<std::string, std::string>> ResponseHeaders; - HdrData.Headers = &ResponseHeaders; - - curl_easy_setopt(H, CURLOPT_HEADERFUNCTION, CurlHeaderCallback); - curl_easy_setopt(H, CURLOPT_HEADERDATA, &HdrData); - - CurlResult Result = Sess.Perform(); - Result.Headers = std::move(ResponseHeaders); - - curl_slist_free_all(Headers); - - return Result; - }), - {}); + return CommonResponse(m_SessionId, + DoWithRetry(m_SessionId, + [&]() -> CurlResult { + Session Sess = AllocSession(Url, {}); + Sess.SetHeaders(BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken())); + curl_easy_setopt(Sess.Get(), CURLOPT_NOBODY, 1L); + return Sess.PerformWithResponseCallbacks(); + }), + {}); } CurlHttpClient::Response @@ -1123,38 +1073,15 @@ CurlHttpClient::Delete(std::string_view Url, const KeyValueMap& AdditionalHeader { ZEN_TRACE_CPU("CurlHttpClient::Delete"); - return CommonResponse( - m_SessionId, - DoWithRetry(m_SessionId, - [&]() -> CurlResult { - Session Sess = - AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); - CURL* H = Sess.Get(); - - curl_slist* Headers = BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken()); - curl_easy_setopt(H, CURLOPT_HTTPHEADER, Headers); - curl_easy_setopt(H, CURLOPT_CUSTOMREQUEST, "DELETE"); - - std::string Body; - WriteCallbackData WriteData{.Body = &Body}; - HeaderCallbackData HdrData{}; - std::vector<std::pair<std::string, std::string>> ResponseHeaders; - HdrData.Headers = &ResponseHeaders; - - curl_easy_setopt(H, CURLOPT_WRITEFUNCTION, CurlWriteCallback); - curl_easy_setopt(H, CURLOPT_WRITEDATA, &WriteData); - curl_easy_setopt(H, CURLOPT_HEADERFUNCTION, CurlHeaderCallback); - curl_easy_setopt(H, CURLOPT_HEADERDATA, &HdrData); - - CurlResult Result = Sess.Perform(); - Result.Body = std::move(Body); - Result.Headers = std::move(ResponseHeaders); - - curl_slist_free_all(Headers); - - return Result; - }), - {}); + return CommonResponse(m_SessionId, + DoWithRetry(m_SessionId, + [&]() -> CurlResult { + Session Sess = AllocSession(Url, {}); + Sess.SetHeaders(BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken())); + curl_easy_setopt(Sess.Get(), CURLOPT_CUSTOMREQUEST, "DELETE"); + return Sess.PerformWithResponseCallbacks(); + }), + {}); } CurlHttpClient::Response @@ -1162,39 +1089,16 @@ CurlHttpClient::Post(std::string_view Url, const KeyValueMap& AdditionalHeader, { ZEN_TRACE_CPU("CurlHttpClient::PostNoPayload"); - return CommonResponse( - m_SessionId, - DoWithRetry(m_SessionId, - [&]() -> CurlResult { - Session Sess = - AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, Parameters, m_SessionId, GetAccessToken()); - CURL* H = Sess.Get(); - - curl_slist* Headers = BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken()); - curl_easy_setopt(H, CURLOPT_HTTPHEADER, Headers); - curl_easy_setopt(H, CURLOPT_POST, 1L); - curl_easy_setopt(H, CURLOPT_POSTFIELDSIZE, 0L); - - std::string Body; - WriteCallbackData WriteData{.Body = &Body}; - HeaderCallbackData HdrData{}; - std::vector<std::pair<std::string, std::string>> ResponseHeaders; - HdrData.Headers = &ResponseHeaders; - - curl_easy_setopt(H, CURLOPT_WRITEFUNCTION, CurlWriteCallback); - curl_easy_setopt(H, CURLOPT_WRITEDATA, &WriteData); - curl_easy_setopt(H, CURLOPT_HEADERFUNCTION, CurlHeaderCallback); - curl_easy_setopt(H, CURLOPT_HEADERDATA, &HdrData); - - CurlResult Result = Sess.Perform(); - Result.Body = std::move(Body); - Result.Headers = std::move(ResponseHeaders); - - curl_slist_free_all(Headers); - - return Result; - }), - {}); + return CommonResponse(m_SessionId, + DoWithRetry(m_SessionId, + [&]() -> CurlResult { + Session Sess = AllocSession(Url, Parameters); + Sess.SetHeaders(BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken())); + curl_easy_setopt(Sess.Get(), CURLOPT_POST, 1L); + curl_easy_setopt(Sess.Get(), CURLOPT_POSTFIELDSIZE, 0L); + return Sess.PerformWithResponseCallbacks(); + }), + {}); } CurlHttpClient::Response @@ -1213,12 +1117,10 @@ CurlHttpClient::Post(std::string_view Url, const IoBuffer& Payload, ZenContentTy DoWithRetry( m_SessionId, [&]() -> CurlResult { - Session Sess = AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); + Session Sess = AllocSession(Url, {}); CURL* H = Sess.Get(); - // Rebuild headers with content type - curl_slist* Headers = BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken(), {HeaderContentType(ContentType)}); - curl_easy_setopt(H, CURLOPT_HTTPHEADER, Headers); + Sess.SetHeaders(BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken(), {HeaderContentType(ContentType)})); IoBufferFileReference FileRef = {nullptr, 0, 0}; if (Payload.GetFileReference(FileRef)) @@ -1234,46 +1136,14 @@ CurlHttpClient::Post(std::string_view Url, const IoBuffer& Payload, ZenContentTy curl_easy_setopt(H, CURLOPT_READFUNCTION, CurlFileReadCallback); curl_easy_setopt(H, CURLOPT_READDATA, &ReadData); - std::string Body; - WriteCallbackData WriteData{.Body = &Body}; - HeaderCallbackData HdrData{}; - std::vector<std::pair<std::string, std::string>> ResponseHeaders; - HdrData.Headers = &ResponseHeaders; - - curl_easy_setopt(H, CURLOPT_WRITEFUNCTION, CurlWriteCallback); - curl_easy_setopt(H, CURLOPT_WRITEDATA, &WriteData); - curl_easy_setopt(H, CURLOPT_HEADERFUNCTION, CurlHeaderCallback); - curl_easy_setopt(H, CURLOPT_HEADERDATA, &HdrData); - - CurlResult Result = Sess.Perform(); - Result.Body = std::move(Body); - Result.Headers = std::move(ResponseHeaders); - - curl_slist_free_all(Headers); - return Result; + return Sess.PerformWithResponseCallbacks(); } curl_easy_setopt(H, CURLOPT_POST, 1L); curl_easy_setopt(H, CURLOPT_POSTFIELDS, reinterpret_cast<const char*>(Payload.GetData())); curl_easy_setopt(H, CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(Payload.GetSize())); - std::string Body; - WriteCallbackData WriteData{.Body = &Body}; - HeaderCallbackData HdrData{}; - std::vector<std::pair<std::string, std::string>> ResponseHeaders; - HdrData.Headers = &ResponseHeaders; - - curl_easy_setopt(H, CURLOPT_WRITEFUNCTION, CurlWriteCallback); - curl_easy_setopt(H, CURLOPT_WRITEDATA, &WriteData); - curl_easy_setopt(H, CURLOPT_HEADERFUNCTION, CurlHeaderCallback); - curl_easy_setopt(H, CURLOPT_HEADERDATA, &HdrData); - - CurlResult Result = Sess.Perform(); - Result.Body = std::move(Body); - Result.Headers = std::move(ResponseHeaders); - - curl_slist_free_all(Headers); - return Result; + return Sess.PerformWithResponseCallbacks(); }), {}); } @@ -1295,12 +1165,11 @@ CurlHttpClient::Post(std::string_view Url, PayloadString.clear(); PayloadFile.reset(); - Session Sess = AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); + Session Sess = AllocSession(Url, {}); CURL* H = Sess.Get(); - curl_slist* Headers = - BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken(), {HeaderContentType(ZenContentType::kCbObject)}); - curl_easy_setopt(H, CURLOPT_HTTPHEADER, Headers); + Sess.SetHeaders( + BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken(), {HeaderContentType(ZenContentType::kCbObject)})); curl_easy_setopt(H, CURLOPT_POST, 1L); curl_easy_setopt(H, CURLOPT_POSTFIELDS, reinterpret_cast<const char*>(Payload.GetBuffer().GetData())); @@ -1329,33 +1198,11 @@ CurlHttpClient::Post(std::string_view Url, auto* Data = static_cast<PostHeaderCallbackData*>(UserData); size_t TotalBytes = Size * Nmemb; - std::string_view Line(Buffer, TotalBytes); - while (!Line.empty() && (Line.back() == '\r' || Line.back() == '\n')) + if (auto Header = ParseHeaderLine(std::string_view(Buffer, TotalBytes))) { - Line.remove_suffix(1); - } - - if (Line.empty()) - { - return TotalBytes; - } - - size_t ColonPos = Line.find(':'); - if (ColonPos != std::string_view::npos) - { - 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); - } + auto& [Key, Value] = *Header; - if (StrCaseCompare(std::string(Key).c_str(), "Content-Length") == 0) + if (StrCaseCompare(Key, "Content-Length") == 0) { std::optional<size_t> ContentLength = ParseInt<size_t>(Value); if (ContentLength.has_value()) @@ -1444,7 +1291,6 @@ CurlHttpClient::Post(std::string_view Url, Res.Body = std::move(PayloadString); } - curl_slist_free_all(Headers); return Res; }, PayloadFile); @@ -1467,13 +1313,10 @@ CurlHttpClient::Post(std::string_view Url, const CompositeBuffer& Payload, ZenCo m_SessionId, DoWithRetry(m_SessionId, [&]() -> CurlResult { - Session Sess = - AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); - CURL* H = Sess.Get(); + Session Sess = AllocSession(Url, {}); + CURL* H = Sess.Get(); - curl_slist* Headers = - BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken(), {HeaderContentType(ContentType)}); - curl_easy_setopt(H, CURLOPT_HTTPHEADER, Headers); + Sess.SetHeaders(BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken(), {HeaderContentType(ContentType)})); detail::CompositeBufferReadStream Reader(Payload, 512u * 1024u); @@ -1485,23 +1328,7 @@ CurlHttpClient::Post(std::string_view Url, const CompositeBuffer& Payload, ZenCo curl_easy_setopt(H, CURLOPT_READFUNCTION, CurlStreamReadCallback); curl_easy_setopt(H, CURLOPT_READDATA, &ReadData); - std::string Body; - WriteCallbackData WriteData{.Body = &Body}; - HeaderCallbackData HdrData{}; - std::vector<std::pair<std::string, std::string>> ResponseHeaders; - HdrData.Headers = &ResponseHeaders; - - curl_easy_setopt(H, CURLOPT_WRITEFUNCTION, CurlWriteCallback); - curl_easy_setopt(H, CURLOPT_WRITEDATA, &WriteData); - curl_easy_setopt(H, CURLOPT_HEADERFUNCTION, CurlHeaderCallback); - curl_easy_setopt(H, CURLOPT_HEADERDATA, &HdrData); - - CurlResult Result = Sess.Perform(); - Result.Body = std::move(Body); - Result.Headers = std::move(ResponseHeaders); - - curl_slist_free_all(Headers); - return Result; + return Sess.PerformWithResponseCallbacks(); }), {}); } @@ -1516,12 +1343,11 @@ CurlHttpClient::Upload(std::string_view Url, const IoBuffer& Payload, const KeyV DoWithRetry( m_SessionId, [&]() -> CurlResult { - Session Sess = AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); + Session Sess = AllocSession(Url, {}); CURL* H = Sess.Get(); - curl_slist* Headers = - BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken(), {HeaderContentType(Payload.GetContentType())}); - curl_easy_setopt(H, CURLOPT_HTTPHEADER, Headers); + Sess.SetHeaders( + BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken(), {HeaderContentType(Payload.GetContentType())})); curl_easy_setopt(H, CURLOPT_UPLOAD, 1L); curl_easy_setopt(H, CURLOPT_INFILESIZE_LARGE, static_cast<curl_off_t>(Payload.GetSize())); @@ -1538,23 +1364,7 @@ CurlHttpClient::Upload(std::string_view Url, const IoBuffer& Payload, const KeyV curl_easy_setopt(H, CURLOPT_READFUNCTION, CurlFileReadCallback); curl_easy_setopt(H, CURLOPT_READDATA, &ReadData); - std::string Body; - WriteCallbackData WriteData{.Body = &Body}; - HeaderCallbackData HdrData{}; - std::vector<std::pair<std::string, std::string>> ResponseHeaders; - HdrData.Headers = &ResponseHeaders; - - curl_easy_setopt(H, CURLOPT_WRITEFUNCTION, CurlWriteCallback); - curl_easy_setopt(H, CURLOPT_WRITEDATA, &WriteData); - curl_easy_setopt(H, CURLOPT_HEADERFUNCTION, CurlHeaderCallback); - curl_easy_setopt(H, CURLOPT_HEADERDATA, &HdrData); - - CurlResult Result = Sess.Perform(); - Result.Body = std::move(Body); - Result.Headers = std::move(ResponseHeaders); - - curl_slist_free_all(Headers); - return Result; + return Sess.PerformWithResponseCallbacks(); } ReadCallbackData ReadData{.DataPtr = static_cast<const uint8_t*>(Payload.GetData()), @@ -1563,23 +1373,7 @@ CurlHttpClient::Upload(std::string_view Url, const IoBuffer& Payload, const KeyV curl_easy_setopt(H, CURLOPT_READFUNCTION, CurlReadCallback); curl_easy_setopt(H, CURLOPT_READDATA, &ReadData); - std::string Body; - WriteCallbackData WriteData{.Body = &Body}; - HeaderCallbackData HdrData{}; - std::vector<std::pair<std::string, std::string>> ResponseHeaders; - HdrData.Headers = &ResponseHeaders; - - curl_easy_setopt(H, CURLOPT_WRITEFUNCTION, CurlWriteCallback); - curl_easy_setopt(H, CURLOPT_WRITEDATA, &WriteData); - curl_easy_setopt(H, CURLOPT_HEADERFUNCTION, CurlHeaderCallback); - curl_easy_setopt(H, CURLOPT_HEADERDATA, &HdrData); - - CurlResult Result = Sess.Perform(); - Result.Body = std::move(Body); - Result.Headers = std::move(ResponseHeaders); - - curl_slist_free_all(Headers); - return Result; + return Sess.PerformWithResponseCallbacks(); }), {}); } @@ -1596,13 +1390,10 @@ CurlHttpClient::Upload(std::string_view Url, m_SessionId, DoWithRetry(m_SessionId, [&]() -> CurlResult { - Session Sess = - AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); - CURL* H = Sess.Get(); + Session Sess = AllocSession(Url, {}); + CURL* H = Sess.Get(); - curl_slist* Headers = - BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken(), {HeaderContentType(ContentType)}); - curl_easy_setopt(H, CURLOPT_HTTPHEADER, Headers); + Sess.SetHeaders(BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken(), {HeaderContentType(ContentType)})); curl_easy_setopt(H, CURLOPT_UPLOAD, 1L); curl_easy_setopt(H, CURLOPT_INFILESIZE_LARGE, static_cast<curl_off_t>(Payload.GetSize())); @@ -1615,23 +1406,7 @@ CurlHttpClient::Upload(std::string_view Url, curl_easy_setopt(H, CURLOPT_READFUNCTION, CurlStreamReadCallback); curl_easy_setopt(H, CURLOPT_READDATA, &ReadData); - std::string Body; - WriteCallbackData WriteData{.Body = &Body}; - HeaderCallbackData HdrData{}; - std::vector<std::pair<std::string, std::string>> ResponseHeaders; - HdrData.Headers = &ResponseHeaders; - - curl_easy_setopt(H, CURLOPT_WRITEFUNCTION, CurlWriteCallback); - curl_easy_setopt(H, CURLOPT_WRITEDATA, &WriteData); - curl_easy_setopt(H, CURLOPT_HEADERFUNCTION, CurlHeaderCallback); - curl_easy_setopt(H, CURLOPT_HEADERDATA, &HdrData); - - CurlResult Result = Sess.Perform(); - Result.Body = std::move(Body); - Result.Headers = std::move(ResponseHeaders); - - curl_slist_free_all(Headers); - return Result; + return Sess.PerformWithResponseCallbacks(); }), {}); } @@ -1651,11 +1426,10 @@ CurlHttpClient::Download(std::string_view Url, const std::filesystem::path& Temp CurlResult Result = DoWithRetry( m_SessionId, [&]() -> CurlResult { - Session Sess = AllocSession(m_BaseUri, Url, m_ConnectionSettings, AdditionalHeader, {}, m_SessionId, GetAccessToken()); + Session Sess = AllocSession(Url, {}); CURL* H = Sess.Get(); - curl_slist* DlHeaders = BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken()); - curl_easy_setopt(H, CURLOPT_HTTPHEADER, DlHeaders); + Sess.SetHeaders(BuildHeaderList(AdditionalHeader, m_SessionId, GetAccessToken())); curl_easy_setopt(H, CURLOPT_HTTPGET, 1L); // Reset state from any previous attempt @@ -1673,7 +1447,7 @@ CurlHttpClient::Download(std::string_view Url, const std::filesystem::path& Temp { std::string_view RangeValue(RangeIt->second); size_t RangeStartPos = RangeValue.find('=', 5); - if (RangeStartPos != std::string::npos) + if (RangeStartPos != std::string_view::npos) { RangeStartPos++; while (RangeStartPos < RangeValue.length() && RangeValue[RangeStartPos] == ' ') @@ -1685,14 +1459,14 @@ CurlHttpClient::Download(std::string_view Url, const std::filesystem::path& Temp while (RangeStartPos < RangeValue.length()) { size_t RangeEnd = RangeValue.find_first_of(", \r\n", RangeStartPos); - if (RangeEnd == std::string::npos) + if (RangeEnd == std::string_view::npos) { RangeEnd = RangeValue.length(); } std::string_view RangeString = RangeValue.substr(RangeStartPos, RangeEnd - RangeStartPos); size_t RangeSplitPos = RangeString.find('-'); - if (RangeSplitPos != std::string::npos) + if (RangeSplitPos != std::string_view::npos) { std::optional<size_t> RequestedRangeStart = ParseInt<size_t>(RangeString.substr(0, RangeSplitPos)); std::optional<size_t> RequestedRangeEnd = ParseInt<size_t>(RangeString.substr(RangeSplitPos + 1)); @@ -1742,36 +1516,12 @@ CurlHttpClient::Download(std::string_view Url, const std::filesystem::path& Temp auto* Data = static_cast<DownloadHeaderCallbackData*>(UserData); size_t TotalBytes = Size * Nmemb; - std::string_view Line(Buffer, TotalBytes); - - while (!Line.empty() && (Line.back() == '\r' || Line.back() == '\n')) - { - Line.remove_suffix(1); - } - - if (Line.empty()) + if (auto Header = ParseHeaderLine(std::string_view(Buffer, TotalBytes))) { - return TotalBytes; - } - - size_t ColonPos = Line.find(':'); - if (ColonPos != std::string_view::npos) - { - std::string_view KeyView = Line.substr(0, ColonPos); - std::string_view Value = Line.substr(ColonPos + 1); - - while (!KeyView.empty() && KeyView.back() == ' ') - { - KeyView.remove_suffix(1); - } - while (!Value.empty() && Value.front() == ' ') - { - Value.remove_prefix(1); - } - + auto& [KeyView, Value] = *Header; const std::string Key(KeyView); - if (StrCaseCompare(Key.c_str(), "Content-Length") == 0) + if (StrCaseCompare(Key, "Content-Length") == 0) { std::optional<size_t> ContentLength = ParseInt<size_t>(Value); if (ContentLength.has_value()) @@ -1795,7 +1545,7 @@ CurlHttpClient::Download(std::string_view Url, const std::filesystem::path& Temp } } } - else if (StrCaseCompare(Key.c_str(), "Content-Type") == 0) + else if (StrCaseCompare(Key, "Content-Type") == 0) { *Data->IsMultiRange = Data->BoundaryParser->Init(Value); if (!*Data->IsMultiRange) @@ -1803,7 +1553,7 @@ CurlHttpClient::Download(std::string_view Url, const std::filesystem::path& Temp *Data->ContentTypeOut = ParseContentType(Value); } } - else if (StrCaseCompare(Key.c_str(), "Content-Range") == 0) + else if (StrCaseCompare(Key, "Content-Range") == 0) { if (!*Data->IsMultiRange) { @@ -1819,7 +1569,7 @@ CurlHttpClient::Download(std::string_view Url, const std::filesystem::path& Temp } } - Data->Headers->emplace_back(std::string(Key), std::string(Value)); + Data->Headers->emplace_back(Key, std::string(Value)); } return TotalBytes; @@ -1894,11 +1644,11 @@ CurlHttpClient::Download(std::string_view Url, const std::filesystem::path& Temp auto SupportsRanges = [](const CurlResult& R) -> bool { for (const auto& [K, V] : R.Headers) { - if (StrCaseCompare(K.c_str(), "Content-Range") == 0) + if (StrCaseCompare(K, "Content-Range") == 0) { return true; } - if (StrCaseCompare(K.c_str(), "Accept-Ranges") == 0) + if (StrCaseCompare(K, "Accept-Ranges") == 0) { return V == "bytes"sv; } @@ -1924,7 +1674,7 @@ CurlHttpClient::Download(std::string_view Url, const std::filesystem::path& Temp std::string ContentLengthValue; for (const auto& [K, V] : Res.Headers) { - if (StrCaseCompare(K.c_str(), "Content-Length") == 0) + if (StrCaseCompare(K, "Content-Length") == 0) { ContentLengthValue = V; break; @@ -1943,6 +1693,7 @@ CurlHttpClient::Download(std::string_view Url, const std::filesystem::path& Temp } KeyValueMap HeadersWithRange(AdditionalHeader); + uint8_t ResumeAttempt = 0; do { uint64_t DownloadedSize = PayloadFile ? PayloadFile->GetSize() : PayloadString.length(); @@ -1957,12 +1708,10 @@ CurlHttpClient::Download(std::string_view Url, const std::filesystem::path& Temp } HeadersWithRange.Entries.insert_or_assign("Range", Range); - Session ResumeSess = - AllocSession(m_BaseUri, Url, m_ConnectionSettings, HeadersWithRange, {}, m_SessionId, GetAccessToken()); - CURL* ResumeH = ResumeSess.Get(); + Session ResumeSess = AllocSession(Url, {}); + CURL* ResumeH = ResumeSess.Get(); - curl_slist* ResumeHdrList = BuildHeaderList(HeadersWithRange, m_SessionId, GetAccessToken()); - curl_easy_setopt(ResumeH, CURLOPT_HTTPHEADER, ResumeHdrList); + ResumeSess.SetHeaders(BuildHeaderList(HeadersWithRange, m_SessionId, GetAccessToken())); curl_easy_setopt(ResumeH, CURLOPT_HTTPGET, 1L); std::vector<std::pair<std::string, std::string>> ResumeHeaders; @@ -1983,72 +1732,51 @@ CurlHttpClient::Download(std::string_view Url, const std::filesystem::path& Temp auto* Data = static_cast<ResumeHeaderCbData*>(UserData); size_t TotalBytes = Size * Nmemb; - std::string_view Line(Buffer, TotalBytes); - while (!Line.empty() && (Line.back() == '\r' || Line.back() == '\n')) - { - Line.remove_suffix(1); - } - - if (Line.empty()) + auto Header = ParseHeaderLine(std::string_view(Buffer, TotalBytes)); + if (!Header) { return TotalBytes; } + auto& [Key, Value] = *Header; - size_t ColonPos = Line.find(':'); - if (ColonPos != std::string_view::npos) + if (StrCaseCompare(Key, "Content-Range") == 0) { - std::string_view Key = Line.substr(0, ColonPos); - std::string_view Value = Line.substr(ColonPos + 1); - while (!Key.empty() && Key.back() == ' ') + if (Value.starts_with("bytes "sv)) { - Key.remove_suffix(1); - } - while (!Value.empty() && Value.front() == ' ') - { - Value.remove_prefix(1); - } - - if (StrCaseCompare(std::string(Key).c_str(), "Content-Range") == 0) - { - if (Value.starts_with("bytes "sv)) + size_t RangeStartEnd = Value.find('-', 6); + if (RangeStartEnd != std::string_view::npos) { - size_t RangeStartEnd = Value.find('-', 6); - if (RangeStartEnd != std::string_view::npos) + const std::optional<uint64_t> Start = ParseInt<uint64_t>(Value.substr(6, RangeStartEnd - 6)); + if (Start) { - const std::optional<uint64_t> Start = - ParseInt<uint64_t>(Value.substr(6, RangeStartEnd - 6)); - if (Start) + uint64_t DownloadedSize = + *Data->PayloadFile ? (*Data->PayloadFile)->GetSize() : Data->PayloadString->length(); + if (Start.value() == DownloadedSize) { - uint64_t DownloadedSize = *Data->PayloadFile ? (*Data->PayloadFile)->GetSize() - : Data->PayloadString->length(); - if (Start.value() == DownloadedSize) - { - Data->Headers->emplace_back(std::string(Key), std::string(Value)); - return TotalBytes; - } - else if (Start.value() > DownloadedSize) - { - return 0; - } - if (*Data->PayloadFile) - { - (*Data->PayloadFile)->ResetWritePos(Start.value()); - } - else - { - *Data->PayloadString = Data->PayloadString->substr(0, Start.value()); - } Data->Headers->emplace_back(std::string(Key), std::string(Value)); return TotalBytes; } + else if (Start.value() > DownloadedSize) + { + return 0; + } + if (*Data->PayloadFile) + { + (*Data->PayloadFile)->ResetWritePos(Start.value()); + } + else + { + *Data->PayloadString = Data->PayloadString->substr(0, Start.value()); + } + Data->Headers->emplace_back(std::string(Key), std::string(Value)); + return TotalBytes; } } - return 0; } - - Data->Headers->emplace_back(std::string(Key), std::string(Value)); + return 0; } + Data->Headers->emplace_back(std::string(Key), std::string(Value)); return TotalBytes; }; @@ -2064,8 +1792,8 @@ CurlHttpClient::Download(std::string_view Url, const std::filesystem::path& Temp Res = ResumeSess.Perform(); Res.Headers = std::move(ResumeHeaders); - curl_slist_free_all(ResumeHdrList); - } while (ShouldResumeCheck(Res)); + ResumeAttempt++; + } while (ResumeAttempt < m_ConnectionSettings.RetryCount && ShouldResumeCheck(Res)); } } } @@ -2075,8 +1803,6 @@ CurlHttpClient::Download(std::string_view Url, const std::filesystem::path& Temp Res.Body = std::move(PayloadString); } - curl_slist_free_all(DlHeaders); - return Res; }, PayloadFile); diff --git a/src/zenhttp/clients/httpclientcurl.h b/src/zenhttp/clients/httpclientcurl.h index 871877863..b7fa52e6c 100644 --- a/src/zenhttp/clients/httpclientcurl.h +++ b/src/zenhttp/clients/httpclientcurl.h @@ -75,40 +75,39 @@ private: struct Session { Session(CurlHttpClient* InOuter, CURL* InHandle) : Outer(InOuter), Handle(InHandle) {} - ~Session() { Outer->ReleaseSession(Handle); } + ~Session(); CURL* Get() const { return Handle; } + // Takes ownership of the curl_slist and sets it on the handle. + // The list is freed automatically when the Session is destroyed. + void SetHeaders(curl_slist* Headers); + + // Low-level perform: executes the request and collects status/timing. CurlResult Perform(); + // Sets up standard write+header callbacks, performs the request, and + // moves the collected body and headers into the returned CurlResult. + CurlResult PerformWithResponseCallbacks(); + LoggerRef Log() { return Outer->Log(); } private: CurlHttpClient* Outer; CURL* Handle; + curl_slist* HeaderList = nullptr; Session(Session&&) = delete; Session& operator=(Session&&) = delete; }; - Session AllocSession(std::string_view BaseUrl, - std::string_view Url, - const HttpClientSettings& ConnectionSettings, - const KeyValueMap& AdditionalHeader, - const KeyValueMap& Parameters, - std::string_view SessionId, - std::optional<HttpClientAccessToken> AccessToken); + Session AllocSession(std::string_view ResourcePath, const KeyValueMap& Parameters); RwLock m_SessionLock; std::vector<CURL*> m_Sessions; void ReleaseSession(CURL* Handle); - struct RetryResult - { - CurlResult Result; - }; - CurlResult DoWithRetry(std::string_view SessionId, std::function<CurlResult()>&& Func, std::unique_ptr<detail::TempPayloadFile>& PayloadFile); diff --git a/src/zenhttp/clients/httpwsclient.cpp b/src/zenhttp/clients/httpwsclient.cpp index 2d566ae86..fbae9f5fe 100644 --- a/src/zenhttp/clients/httpwsclient.cpp +++ b/src/zenhttp/clients/httpwsclient.cpp @@ -5,6 +5,8 @@ #include "../servers/wsframecodec.h" #include <zencore/base64.h> +#include <zencore/filesystem.h> +#include <zencore/fmtutils.h> #include <zencore/logging.h> #include <zencore/string.h> @@ -155,7 +157,7 @@ struct HttpWsClient::Impl } }); - asio::local::stream_protocol::endpoint Endpoint(m_Settings.UnixSocketPath); + asio::local::stream_protocol::endpoint Endpoint(PathToUtf8(m_Settings.UnixSocketPath)); m_UnixSocket->async_connect(Endpoint, [this](const asio::error_code& Ec) { if (Ec) { |