1
0
mirror of https://github.com/winscp/winscp.git synced 2026-10-24 02:28:55 +03:00

Issue 2351 – Allow S3 connection with IAM roles on instances that require IMDSv2

https://winscp.net/tracker/2351

Source commit: a310c6641ad5533b1492b6c1d5b8ac4fea91954b
This commit is contained in:
Martin Prikryl
2025-02-20 14:35:09 +00:00
parent 18b2430f68
commit 9d3f348f78
5 changed files with 65 additions and 17 deletions

View File

@@ -111,6 +111,7 @@ std::unique_ptr<TCustomIniFile> S3ConfigFile;
UnicodeString S3Profile;
bool S3SecurityProfileChecked = false;
TDateTime S3CredentialsExpiration;
UnicodeString S3SessionToken;
UnicodeString S3SecurityProfile;
typedef std::map<UnicodeString, UnicodeString> TS3Credentials;
TS3Credentials S3Credentials;
@@ -183,12 +184,24 @@ TStrings * GetS3Profiles()
return Result.release();
}
//---------------------------------------------------------------------------
static UnicodeString ReadUrl(const UnicodeString & Url, int ConnectTimeout = 0)
static THttp * CreateHttp(const UnicodeString & Url, int ConnectTimeout)
{
std::unique_ptr<THttp> Http(new THttp());
Http->URL = Url;
Http->ResponseLimit = BasicHttpResponseLimit;
Http->ConnectTimeout = ConnectTimeout;
Http->URL = Url;
return Http.release();
}
//---------------------------------------------------------------------------
static UnicodeString ReadSecurityUrl(const UnicodeString & Url, int ConnectTimeout = 0)
{
std::unique_ptr<THttp> Http(CreateHttp(Url, ConnectTimeout));
std::unique_ptr<TStrings> RequestHeaders(new TStringList());
if (!S3SessionToken.IsEmpty())
{
RequestHeaders->Values[L"X-aws-ec2-metadata-token"] = S3SessionToken;
Http->RequestHeaders = RequestHeaders.get();
}
Http->Get();
return Http->Response.Trim();
}
@@ -300,24 +313,48 @@ static UnicodeString GetS3ConfigValue(
{
if (S3SecurityProfileChecked && (S3CredentialsExpiration != TDateTime()) && (IncHour(S3CredentialsExpiration, -1) < Now()))
{
AppLog(L"AWS security credentials has expired or is close to expiration, will retrieve new");
AppLog(L"AWS session token or security credentials have expired or are close to expiration, will retrieve new");
S3SecurityProfileChecked = false;
}
if (!S3SecurityProfileChecked && !OnlyCached)
{
S3Credentials.clear();
S3SessionToken = EmptyStr;
S3SecurityProfile = EmptyStr;
S3SecurityProfileChecked = true;
S3CredentialsExpiration = TDateTime();
try
{
UnicodeString AWSMetadataService = DefaultStr(Configuration->AWSMetadataService, L"http://169.254.169.254/latest/meta-data/");
UnicodeString SecurityCredentialsUrl = AWSMetadataService + L"iam/security-credentials/";
UnicodeString AWSAPI = DefaultStr(Configuration->AWSAPI, L"http://169.254.169.254/latest/");
AppLogFmt(L"Retrieving AWS security credentials from %s", (SecurityCredentialsUrl));
int ConnectTimeout = StrToIntDef(GetEnvironmentVariable(L"AWS_METADATA_SERVICE_TIMEOUT"), 1);
S3SecurityProfile = ReadUrl(SecurityCredentialsUrl, ConnectTimeout);
UnicodeString TokenUrl = AWSAPI + L"api/token";
AppLogFmt(L"Trying to create IMDSv2 session token via %s", (TokenUrl));
try
{
std::unique_ptr<THttp> Http(CreateHttp(TokenUrl, ConnectTimeout));
int TtlSeconds = 6 * 60 * 60; // max possible
TDateTime TokenExpiration = IncSecond(Now(), TtlSeconds);
std::unique_ptr<TStrings> RequestHeaders(new TStringList());
RequestHeaders->Values[L"X-aws-ec2-metadata-token-ttl-seconds"] = IntToStr(TtlSeconds);
Http->RequestHeaders = RequestHeaders.get();
Http->Put(EmptyStr);
S3SessionToken = Http->Response.Trim();
S3CredentialsExpiration = TokenExpiration;
AppLogFmt(L"Created IMDSv2 session token: %s, with expiration: %s", (S3SessionToken, StandardTimestamp(TokenExpiration)));
}
catch (Exception & E)
{
UnicodeString Message;
ExceptionMessage(&E, Message);
AppLogFmt(L"Error creating IMDSv2 session token: %s", (Message));
}
UnicodeString SecurityCredentialsUrl = AWSAPI + L"meta-data/iam/security-credentials/";
AppLogFmt(L"Retrieving AWS security credentials from %s", (SecurityCredentialsUrl));
S3SecurityProfile = ReadSecurityUrl(SecurityCredentialsUrl, ConnectTimeout);
if (S3SecurityProfile.IsEmpty())
{
@@ -327,7 +364,7 @@ static UnicodeString GetS3ConfigValue(
{
UnicodeString SecurityProfileUrl = SecurityCredentialsUrl + EncodeUrlString(S3SecurityProfile);
AppLogFmt(L"AWS security credentials role detected: %s, retrieving %s", (S3SecurityProfile, SecurityProfileUrl));
UnicodeString ProfileDataStr = ReadUrl(SecurityProfileUrl);
UnicodeString ProfileDataStr = ReadSecurityUrl(SecurityProfileUrl);
std::unique_ptr<TJSONValue> ProfileDataValue(TJSONObject::ParseJSONValue(ProfileDataStr));
TJSONObject * ProfileData = dynamic_cast<TJSONObject *>(ProfileDataValue.get());
@@ -351,8 +388,13 @@ static UnicodeString GetS3ConfigValue(
throw new Exception(L"Missing \"Expiration\" value");
}
UnicodeString ExpirationStr = ExpirationValue->Value();
S3CredentialsExpiration = ParseExpiration(ExpirationStr);
AppLogFmt(L"Credentials expiration: %s", (StandardTimestamp(S3CredentialsExpiration)));
TDateTime CredentialsExpiration = ParseExpiration(ExpirationStr);
AppLogFmt(L"Credentials expiration: %s", (StandardTimestamp(CredentialsExpiration)));
if ((S3CredentialsExpiration == TDateTime()) ||
(CredentialsExpiration < S3CredentialsExpiration))
{
S3CredentialsExpiration = CredentialsExpiration;
}
std::unique_ptr<TJSONObject::TEnumerator> Enumerator(ProfileData->GetEnumerator());
UnicodeString Names;