aboutsummaryrefslogtreecommitdiff
path: root/src/zenutil/cloud/s3requestbuilder.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/zenutil/cloud/s3requestbuilder.cpp')
-rw-r--r--src/zenutil/cloud/s3requestbuilder.cpp149
1 files changed, 149 insertions, 0 deletions
diff --git a/src/zenutil/cloud/s3requestbuilder.cpp b/src/zenutil/cloud/s3requestbuilder.cpp
new file mode 100644
index 000000000..7bf200308
--- /dev/null
+++ b/src/zenutil/cloud/s3requestbuilder.cpp
@@ -0,0 +1,149 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zenutil/cloud/s3requestbuilder.h>
+
+#include <zencore/fmtutils.h>
+
+#include <algorithm>
+
+namespace zen {
+
+S3RequestBuilder::S3RequestBuilder(std::string Region, std::string BucketName, std::string Endpoint, bool PathStyle)
+: m_Region(std::move(Region))
+, m_BucketName(std::move(BucketName))
+, m_Endpoint(DeriveEndpoint(Endpoint, m_Region, m_BucketName, PathStyle))
+, m_Host(HostFromEndpoint(m_Endpoint))
+, m_PathStyle(PathStyle)
+{
+}
+
+std::string
+S3RequestBuilder::DeriveEndpoint(std::string_view Endpoint, std::string_view Region, std::string_view BucketName, bool PathStyle)
+{
+ if (!Endpoint.empty())
+ {
+ return std::string(Endpoint);
+ }
+ if (PathStyle)
+ {
+ return fmt::format("https://s3.{}.amazonaws.com", Region);
+ }
+ return fmt::format("https://{}.s3.{}.amazonaws.com", BucketName, Region);
+}
+
+std::string
+S3RequestBuilder::HostFromEndpoint(std::string_view Endpoint)
+{
+ std::string_view Ep = Endpoint;
+ if (size_t Pos = Ep.find("://"); Pos != std::string_view::npos)
+ {
+ Ep = Ep.substr(Pos + 3);
+ }
+ if (!Ep.empty() && Ep.back() == '/')
+ {
+ Ep = Ep.substr(0, Ep.size() - 1);
+ }
+ return std::string(Ep);
+}
+
+std::string
+S3RequestBuilder::KeyToPath(std::string_view Key) const
+{
+ if (m_PathStyle)
+ {
+ return fmt::format("/{}/{}", m_BucketName, Key);
+ }
+ return fmt::format("/{}", Key);
+}
+
+std::string
+S3RequestBuilder::BucketRootPath() const
+{
+ if (m_PathStyle)
+ {
+ return fmt::format("/{}/", m_BucketName);
+ }
+ return "/";
+}
+
+// Cached for the (DateStamp, AccessKeyId) tuple. DateStamp rolls over at UTC
+// midnight; concurrent callers around the rollover may each compute a fresh
+// key once before the cache settles on the new day's value. Harmless extra
+// HMACs, no signature corruption.
+Sha256Digest
+S3RequestBuilder::GetSigningKey(std::string_view DateStamp, const SigV4Credentials& Credentials)
+{
+ {
+ RwLock::SharedLockScope _(m_SigningKeyLock);
+ if (m_CachedDateStamp == DateStamp && m_CachedAccessKeyId == Credentials.AccessKeyId)
+ {
+ return m_CachedSigningKey;
+ }
+ }
+
+ RwLock::ExclusiveLockScope _(m_SigningKeyLock);
+ if (m_CachedDateStamp == DateStamp && m_CachedAccessKeyId == Credentials.AccessKeyId)
+ {
+ return m_CachedSigningKey;
+ }
+
+ std::string SecretPrefix = fmt::format("AWS4{}", Credentials.SecretAccessKey);
+ Sha256Digest DateKey = ComputeHmacSha256(SecretPrefix.data(), SecretPrefix.size(), DateStamp.data(), DateStamp.size());
+ SecureZeroSecret(SecretPrefix.data(), SecretPrefix.size());
+
+ Sha256Digest RegionKey = ComputeHmacSha256(DateKey, m_Region);
+ Sha256Digest ServiceKey = ComputeHmacSha256(RegionKey, "s3");
+ m_CachedSigningKey = ComputeHmacSha256(ServiceKey, "aws4_request");
+ m_CachedDateStamp = std::string(DateStamp);
+ m_CachedAccessKeyId = Credentials.AccessKeyId;
+
+ return m_CachedSigningKey;
+}
+
+HttpClient::KeyValueMap
+S3RequestBuilder::SignRequest(const SigV4Credentials& Credentials,
+ std::string_view Method,
+ std::string_view Path,
+ std::string_view CanonicalQueryString,
+ std::string_view PayloadHash,
+ std::span<const std::pair<std::string, std::string>> ExtraSignedHeaders)
+{
+ std::string AmzDate = GetAmzTimestamp();
+
+ std::vector<std::pair<std::string, std::string>> HeadersToSign;
+ HeadersToSign.reserve(4 + ExtraSignedHeaders.size());
+ HeadersToSign.emplace_back("host", m_Host);
+ HeadersToSign.emplace_back("x-amz-content-sha256", std::string(PayloadHash));
+ HeadersToSign.emplace_back("x-amz-date", AmzDate);
+ if (!Credentials.SessionToken.empty())
+ {
+ HeadersToSign.emplace_back("x-amz-security-token", Credentials.SessionToken);
+ }
+ for (const auto& [K, V] : ExtraSignedHeaders)
+ {
+ HeadersToSign.emplace_back(K, V);
+ }
+ std::sort(HeadersToSign.begin(), HeadersToSign.end());
+
+ std::string_view DateStamp(AmzDate.data(), 8);
+ Sha256Digest SigningKey = GetSigningKey(DateStamp, Credentials);
+
+ SigV4SignedHeaders Signed =
+ SignRequestV4(Credentials, Method, Path, CanonicalQueryString, m_Region, "s3", AmzDate, HeadersToSign, PayloadHash, &SigningKey);
+
+ HttpClient::KeyValueMap Result;
+ Result->emplace("Authorization", std::move(Signed.Authorization));
+ Result->emplace("x-amz-date", std::move(Signed.AmzDate));
+ Result->emplace("x-amz-content-sha256", std::move(Signed.PayloadHash));
+ if (!Credentials.SessionToken.empty())
+ {
+ Result->emplace("x-amz-security-token", Credentials.SessionToken);
+ }
+ for (const auto& [K, V] : ExtraSignedHeaders)
+ {
+ Result->emplace(K, V);
+ }
+ return Result;
+}
+
+} // namespace zen