Skip to content

Commit ce38b46

Browse files
authored
Merge pull request #14170 from lovesegfault/curl-based-s3-pieces
feat(libstore/filetransfer): add S3 signing support
2 parents 33d9270 + 00c2a57 commit ce38b46

File tree

4 files changed

+124
-24
lines changed

4 files changed

+124
-24
lines changed

src/libstore/aws-creds.cc

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ namespace nix {
2424

2525
namespace {
2626

27+
// Global credential provider cache using boost's concurrent map
28+
// Key: profile name (empty string for default profile)
29+
using CredentialProviderCache =
30+
boost::concurrent_flat_map<std::string, std::shared_ptr<Aws::Crt::Auth::ICredentialsProvider>>;
31+
32+
static CredentialProviderCache credentialProviderCache;
33+
34+
/**
35+
* Clear all cached credential providers.
36+
* Called automatically by CrtWrapper destructor during static destruction.
37+
*/
38+
static void clearAwsCredentialsCache()
39+
{
40+
credentialProviderCache.clear();
41+
}
42+
2743
static void initAwsCrt()
2844
{
2945
struct CrtWrapper
@@ -95,13 +111,6 @@ static AwsCredentials getCredentialsFromProvider(std::shared_ptr<Aws::Crt::Auth:
95111
return fut.get(); // This will throw if set_exception was called
96112
}
97113

98-
// Global credential provider cache using boost's concurrent map
99-
// Key: profile name (empty string for default profile)
100-
using CredentialProviderCache =
101-
boost::concurrent_flat_map<std::string, std::shared_ptr<Aws::Crt::Auth::ICredentialsProvider>>;
102-
103-
static CredentialProviderCache credentialProviderCache;
104-
105114
} // anonymous namespace
106115

107116
AwsCredentials getAwsCredentials(const std::string & profile)
@@ -160,11 +169,6 @@ void invalidateAwsCredentials(const std::string & profile)
160169
credentialProviderCache.erase(profile);
161170
}
162171

163-
void clearAwsCredentialsCache()
164-
{
165-
credentialProviderCache.clear();
166-
}
167-
168172
AwsCredentials preResolveAwsCredentials(const ParsedS3URL & s3Url)
169173
{
170174
std::string profile = s3Url.profile.value_or("");

src/libstore/filetransfer.cc

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@
99
#include "nix/util/signals.hh"
1010

1111
#include "store-config-private.hh"
12+
#include <optional>
1213
#if NIX_WITH_S3_SUPPORT
1314
# include <aws/core/client/ClientConfiguration.h>
1415
#endif
16+
#if NIX_WITH_CURL_S3
17+
# include "nix/store/aws-creds.hh"
18+
# include "nix/store/s3-url.hh"
19+
#endif
1520

1621
#ifdef __linux__
1722
# include "nix/util/linux-namespaces.hh"
@@ -426,6 +431,24 @@ struct curlFileTransfer : public FileTransfer
426431
curl_easy_setopt(req, CURLOPT_ERRORBUFFER, errbuf);
427432
errbuf[0] = 0;
428433

434+
// Set up username/password authentication if provided
435+
if (request.usernameAuth) {
436+
curl_easy_setopt(req, CURLOPT_USERNAME, request.usernameAuth->username.c_str());
437+
if (request.usernameAuth->password) {
438+
curl_easy_setopt(req, CURLOPT_PASSWORD, request.usernameAuth->password->c_str());
439+
}
440+
}
441+
442+
#if NIX_WITH_CURL_S3
443+
// Set up AWS SigV4 signing if this is an S3 request
444+
// Note: AWS SigV4 support guaranteed available (curl >= 7.75.0 checked at build time)
445+
// The username/password (access key ID and secret key) are set via the general
446+
// usernameAuth mechanism above.
447+
if (request.awsSigV4Provider) {
448+
curl_easy_setopt(req, CURLOPT_AWS_SIGV4, request.awsSigV4Provider->c_str());
449+
}
450+
#endif
451+
429452
result.data.clear();
430453
result.bodySize = 0;
431454
}
@@ -800,7 +823,11 @@ struct curlFileTransfer : public FileTransfer
800823

801824
void enqueueItem(std::shared_ptr<TransferItem> item)
802825
{
803-
if (item->request.data && item->request.uri.scheme() != "http" && item->request.uri.scheme() != "https")
826+
if (item->request.data && item->request.uri.scheme() != "http" && item->request.uri.scheme() != "https"
827+
#if NIX_WITH_CURL_S3
828+
&& item->request.uri.scheme() != "s3"
829+
#endif
830+
)
804831
throw nix::Error("uploading to '%s' is not supported", item->request.uri.to_string());
805832

806833
{
@@ -818,9 +845,15 @@ struct curlFileTransfer : public FileTransfer
818845
{
819846
/* Ugly hack to support s3:// URIs. */
820847
if (request.uri.scheme() == "s3") {
848+
#if NIX_WITH_CURL_S3
849+
// New curl-based S3 implementation
850+
auto modifiedRequest = request;
851+
modifiedRequest.setupForS3();
852+
enqueueItem(std::make_shared<TransferItem>(*this, std::move(modifiedRequest), std::move(callback)));
853+
#elif NIX_WITH_S3_SUPPORT
854+
// Old AWS SDK-based implementation
821855
// FIXME: do this on a worker thread
822856
try {
823-
#if NIX_WITH_S3_SUPPORT
824857
auto parsed = ParsedS3URL::parse(request.uri.parsed());
825858

826859
std::string profile = parsed.profile.value_or("");
@@ -838,13 +871,12 @@ struct curlFileTransfer : public FileTransfer
838871
res.data = std::move(*s3Res.data);
839872
res.urls.push_back(request.uri.to_string());
840873
callback(std::move(res));
841-
#else
842-
throw nix::Error(
843-
"cannot download '%s' because Nix is not built with S3 support", request.uri.to_string());
844-
#endif
845874
} catch (...) {
846875
callback.rethrow();
847876
}
877+
#else
878+
throw nix::Error("cannot download '%s' because Nix is not built with S3 support", request.uri.to_string());
879+
#endif
848880
return;
849881
}
850882

@@ -872,6 +904,41 @@ ref<FileTransfer> makeFileTransfer()
872904
return makeCurlFileTransfer();
873905
}
874906

907+
#if NIX_WITH_CURL_S3
908+
void FileTransferRequest::setupForS3()
909+
{
910+
auto parsedS3 = ParsedS3URL::parse(uri.parsed());
911+
// Update the request URI to use HTTPS
912+
uri = parsedS3.toHttpsUrl();
913+
// This gets used later in a curl setopt
914+
awsSigV4Provider = "aws:amz:" + parsedS3.region.value_or("us-east-1") + ":s3";
915+
// check if the request already has pre-resolved credentials
916+
std::optional<std::string> sessionToken;
917+
if (usernameAuth) {
918+
debug("Using pre-resolved AWS credentials from parent process");
919+
sessionToken = preResolvedAwsSessionToken;
920+
} else {
921+
std::string profile = parsedS3.profile.value_or("");
922+
try {
923+
auto creds = getAwsCredentials(profile);
924+
usernameAuth = UsernameAuth{
925+
.username = creds.accessKeyId,
926+
.password = creds.secretAccessKey,
927+
};
928+
sessionToken = creds.sessionToken;
929+
} catch (const AwsAuthError & e) {
930+
warn("AWS authentication failed for S3 request %s: %s", uri, e.what());
931+
// Invalidate the cached credentials so next request will retry
932+
invalidateAwsCredentials(profile);
933+
// Continue without authentication - might be a public bucket
934+
return;
935+
}
936+
}
937+
if (sessionToken)
938+
headers.emplace_back("x-amz-security-token", *sessionToken);
939+
}
940+
#endif
941+
875942
std::future<FileTransferResult> FileTransfer::enqueueFileTransfer(const FileTransferRequest & request)
876943
{
877944
auto promise = std::make_shared<std::promise<FileTransferResult>>();

src/libstore/include/nix/store/aws-creds.hh

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,6 @@ AwsCredentials getAwsCredentials(const std::string & profile = "");
5757
*/
5858
void invalidateAwsCredentials(const std::string & profile);
5959

60-
/**
61-
* Clear all cached credential providers.
62-
* Typically called during application cleanup.
63-
*/
64-
void clearAwsCredentialsCache();
65-
6660
/**
6761
* Pre-resolve AWS credentials for S3 URLs.
6862
* Used to cache credentials in parent process before forking.

src/libstore/include/nix/store/filetransfer.hh

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
#include "nix/util/serialise.hh"
1212
#include "nix/util/url.hh"
1313

14+
#include "nix/store/config.hh"
15+
#if NIX_WITH_CURL_S3
16+
# include "nix/store/aws-creds.hh"
17+
#endif
18+
1419
namespace nix {
1520

1621
struct FileTransferSettings : Config
@@ -77,6 +82,17 @@ extern FileTransferSettings fileTransferSettings;
7782

7883
extern const unsigned int RETRY_TIME_MS_DEFAULT;
7984

85+
/**
86+
* Username and optional password for HTTP basic authentication.
87+
* These are used with curl's CURLOPT_USERNAME and CURLOPT_PASSWORD options
88+
* for various protocols including HTTP, FTP, and others.
89+
*/
90+
struct UsernameAuth
91+
{
92+
std::string username;
93+
std::optional<std::string> password;
94+
};
95+
8096
struct FileTransferRequest
8197
{
8298
ValidURL uri;
@@ -92,6 +108,18 @@ struct FileTransferRequest
92108
std::optional<std::string> data;
93109
std::string mimeType;
94110
std::function<void(std::string_view data)> dataCallback;
111+
/**
112+
* Optional username and password for HTTP basic authentication.
113+
* When provided, these credentials will be used with curl's CURLOPT_USERNAME/PASSWORD option.
114+
*/
115+
std::optional<UsernameAuth> usernameAuth;
116+
#if NIX_WITH_CURL_S3
117+
/**
118+
* Pre-resolved AWS session token for S3 requests.
119+
* When provided along with usernameAuth, this will be used instead of fetching fresh credentials.
120+
*/
121+
std::optional<std::string> preResolvedAwsSessionToken;
122+
#endif
95123

96124
FileTransferRequest(ValidURL uri)
97125
: uri(std::move(uri))
@@ -103,6 +131,13 @@ struct FileTransferRequest
103131
{
104132
return data ? "upload" : "download";
105133
}
134+
135+
#if NIX_WITH_CURL_S3
136+
private:
137+
friend struct curlFileTransfer;
138+
void setupForS3();
139+
std::optional<std::string> awsSigV4Provider;
140+
#endif
106141
};
107142

108143
struct FileTransferResult

0 commit comments

Comments
 (0)