Преглед изворни кода

Bug 2057: AWS S3 profile selection

https://winscp.net/tracker/2057

Source commit: 8c5918d7f523c645570fc54812155ef4335c076e
Martin Prikryl пре 2 година
родитељ
комит
b79aaa06e8

+ 89 - 33
source/core/S3FileSystem.cpp

@@ -57,48 +57,90 @@ bool S3ConfigFileTried = false;
 std::unique_ptr<TCustomIniFile> S3ConfigFile;
 UnicodeString S3Profile;
 //---------------------------------------------------------------------------
-UnicodeString GetS3ConfigValue(const UnicodeString & Name, UnicodeString * Source)
+static void NeedS3Config()
+{
+  if (!S3ConfigFileTried)
+  {
+    S3ConfigFileTried = true;
+
+    S3Profile = GetEnvironmentVariable(AWS_PROFILE);
+    if (S3Profile.IsEmpty())
+    {
+      S3Profile = AWS_PROFILE_DEFAULT;
+    }
+
+    UnicodeString ConfigFileName = GetEnvironmentVariable(AWS_CONFIG_FILE);
+    UnicodeString ProfilePath = GetShellFolderPath(CSIDL_PROFILE);
+    UnicodeString DefaultConfigFileName = IncludeTrailingBackslash(ProfilePath) + L".aws\\credentials";
+    // "aws" cli really prefers the default location over location specificed by AWS_CONFIG_FILE
+    if (FileExists(DefaultConfigFileName))
+    {
+      ConfigFileName = DefaultConfigFileName;
+    }
+
+    S3ConfigFile.reset(new TMemIniFile(ConfigFileName));
+  }
+}
+//---------------------------------------------------------------------------
+UnicodeString GetS3Profile()
+{
+  NeedS3Config();
+  return S3Profile;
+}
+//---------------------------------------------------------------------------
+TStrings * GetS3Profiles()
+{
+  NeedS3Config();
+  std::unique_ptr<TStrings> Result(new TStringList());
+  if (S3ConfigFile.get() != NULL)
+  {
+    S3ConfigFile->ReadSections(Result.get());
+    int Index = 0;
+    while (Index < Result->Count)
+    {
+      UnicodeString Section = Result->Strings[Index];
+      if (S3ConfigFile->ReadString(Section, AWS_ACCESS_KEY_ID, EmptyStr).IsEmpty() &&
+          S3ConfigFile->ReadString(Section, AWS_SECRET_ACCESS_KEY, EmptyStr).IsEmpty() &&
+          S3ConfigFile->ReadString(Section, AWS_SESSION_TOKEN, EmptyStr).IsEmpty())
+      {
+        Result->Delete(Index);
+      }
+      else
+      {
+        Index++;
+      }
+    }
+  }
+
+  return Result.release();
+}
+//---------------------------------------------------------------------------
+UnicodeString GetS3ConfigValue(const UnicodeString & Profile, const UnicodeString & Name, UnicodeString * Source)
 {
   UnicodeString Result;
   UnicodeString ASource;
   TGuard Guard(LibS3Section.get());
   try
   {
-    Result = GetEnvironmentVariable(Name);
+    if (Profile.IsEmpty())
+    {
+      Result = GetEnvironmentVariable(Name);
+    }
     if (!Result.IsEmpty())
     {
       ASource = FORMAT(L"%%%s%%", (Name));
     }
     else
     {
-      if (!S3ConfigFileTried)
-      {
-        S3ConfigFileTried = true;
-
-        S3Profile = GetEnvironmentVariable(AWS_PROFILE);
-        if (S3Profile.IsEmpty())
-        {
-          S3Profile = AWS_PROFILE_DEFAULT;
-        }
-
-        UnicodeString ConfigFileName = GetEnvironmentVariable(AWS_CONFIG_FILE);
-        UnicodeString ProfilePath = GetShellFolderPath(CSIDL_PROFILE);
-        UnicodeString DefaultConfigFileName = IncludeTrailingBackslash(ProfilePath) + L".aws\\credentials";
-        // "aws" cli really prefers the default location over location specificed by AWS_CONFIG_FILE
-        if (FileExists(DefaultConfigFileName))
-        {
-          ConfigFileName = DefaultConfigFileName;
-        }
-
-        S3ConfigFile.reset(new TMemIniFile(ConfigFileName));
-      }
+      NeedS3Config();
 
       if (S3ConfigFile.get() != NULL)
       {
-        Result = S3ConfigFile->ReadString(S3Profile, Name, UnicodeString());
+        UnicodeString AProfile = DefaultStr(Profile, S3Profile);
+        Result = S3ConfigFile->ReadString(AProfile, Name, EmptyStr);
         if (!Result.IsEmpty())
         {
-          ASource = FORMAT(L"%s/%s", (ExtractFileName(S3ConfigFile->FileName), S3Profile));
+          ASource = FORMAT(L"%s/%s", (ExtractFileName(S3ConfigFile->FileName), AProfile));
         }
       }
     }
@@ -114,19 +156,19 @@ UnicodeString GetS3ConfigValue(const UnicodeString & Name, UnicodeString * Sourc
   return Result;
 }
 //---------------------------------------------------------------------------
-UnicodeString S3EnvUserName(UnicodeString * Source)
+UnicodeString S3EnvUserName(const UnicodeString & Profile, UnicodeString * Source)
 {
-  return GetS3ConfigValue(AWS_ACCESS_KEY_ID, Source);
+  return GetS3ConfigValue(Profile, AWS_ACCESS_KEY_ID, Source);
 }
 //---------------------------------------------------------------------------
-UnicodeString S3EnvPassword(UnicodeString * Source)
+UnicodeString S3EnvPassword(const UnicodeString & Profile, UnicodeString * Source)
 {
-  return GetS3ConfigValue(AWS_SECRET_ACCESS_KEY, Source);
+  return GetS3ConfigValue(Profile, AWS_SECRET_ACCESS_KEY, Source);
 }
 //---------------------------------------------------------------------------
-UnicodeString S3EnvSessionToken(UnicodeString * Source)
+UnicodeString S3EnvSessionToken(const UnicodeString & Profile, UnicodeString * Source)
 {
-  return GetS3ConfigValue(AWS_SESSION_TOKEN, Source);
+  return GetS3ConfigValue(Profile, AWS_SESSION_TOKEN, Source);
 }
 //---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
@@ -172,6 +214,20 @@ void __fastcall TS3FileSystem::Open()
 
   FLibS3Protocol = (Data->Ftps != ftpsNone) ? S3ProtocolHTTPS : S3ProtocolHTTP;
 
+  UnicodeString S3Profile;
+  if (Data->S3CredentialsEnv)
+  {
+    S3Profile = FTerminal->SessionData->S3Profile;
+  }
+  if (!S3Profile.IsEmpty() && !FTerminal->SessionData->FingerprintScan)
+  {
+    std::unique_ptr<TStrings> S3Profiles(GetS3Profiles());
+    if (S3Profiles->IndexOf(S3Profile))
+    {
+      throw Exception(MainInstructions(FMTLOAD(S3_PROFILE_NOT_EXIST, (S3Profile))));
+    }
+  }
+
   UnicodeString AccessKeyId = Data->UserNameExpanded;
   if (AccessKeyId.IsEmpty() && !FTerminal->SessionData->FingerprintScan)
   {
@@ -192,7 +248,7 @@ void __fastcall TS3FileSystem::Open()
   if (Password.IsEmpty() && Data->S3CredentialsEnv)
   {
     UnicodeString PasswordSource;
-    Password = S3EnvPassword(&PasswordSource);
+    Password = S3EnvPassword(S3Profile, &PasswordSource);
     if (!Password.IsEmpty())
     {
       FTerminal->LogEvent(FORMAT(L"Password (secret access key) read from %s", (PasswordSource)));
@@ -214,7 +270,7 @@ void __fastcall TS3FileSystem::Open()
   if (SessionToken.IsEmpty() && Data->S3CredentialsEnv)
   {
     UnicodeString SessionTokenSource;
-    SessionToken = S3EnvSessionToken(&SessionTokenSource);
+    SessionToken = S3EnvSessionToken(S3Profile, &SessionTokenSource);
     if (!SessionToken.IsEmpty())
     {
       FTerminal->LogEvent(FORMAT(L"Session token read from %s", (SessionTokenSource)));

+ 4 - 3
source/core/S3FileSystem.h

@@ -192,8 +192,9 @@ protected:
 UnicodeString __fastcall S3LibVersion();
 UnicodeString __fastcall S3LibDefaultHostName();
 UnicodeString __fastcall S3LibDefaultRegion();
-UnicodeString S3EnvUserName(UnicodeString * Source = NULL);
-UnicodeString S3EnvPassword(UnicodeString * Source = NULL);
-UnicodeString S3EnvSessionToken(UnicodeString * Source = NULL);
+TStrings * GetS3Profiles();
+UnicodeString S3EnvUserName(const UnicodeString & Profile, UnicodeString * Source = NULL);
+UnicodeString S3EnvPassword(const UnicodeString & Profile, UnicodeString * Source = NULL);
+UnicodeString S3EnvSessionToken(const UnicodeString & Profile, UnicodeString * Source = NULL);
 //------------------------------------------------------------------------------
 #endif

+ 13 - 4
source/core/SessionData.cpp

@@ -269,8 +269,9 @@ void __fastcall TSessionData::DefaultSettings()
   NotUtf = asAuto;
 
   // S3
-  S3DefaultRegion = L"";
-  S3SessionToken = L"";
+  S3DefaultRegion = EmptyStr;
+  S3SessionToken = EmptyStr;
+  S3Profile = EmptyStr;
   S3UrlStyle = s3usVirtualHost;
   S3MaxKeys = asAuto;
   S3CredentialsEnv = false;
@@ -435,6 +436,7 @@ void __fastcall TSessionData::NonPersistant()
   \
   PROPERTY(S3DefaultRegion); \
   PROPERTY(S3SessionToken); \
+  PROPERTY(S3Profile); \
   PROPERTY(S3UrlStyle); \
   PROPERTY(S3MaxKeys); \
   PROPERTY(S3CredentialsEnv); \
@@ -778,6 +780,7 @@ void __fastcall TSessionData::DoLoad(THierarchicalStorage * Storage, bool PuttyI
 
   S3DefaultRegion = Storage->ReadString(L"S3DefaultRegion", S3DefaultRegion);
   S3SessionToken = Storage->ReadString(L"S3SessionToken", S3SessionToken);
+  S3Profile = Storage->ReadString(L"S3Profile", S3Profile);
   S3UrlStyle = (TS3UrlStyle)Storage->ReadInteger(L"S3UrlStyle", S3UrlStyle);
   S3MaxKeys = Storage->ReadEnum(L"S3MaxKeys", S3MaxKeys, AutoSwitchMapping);
   S3CredentialsEnv = Storage->ReadBool(L"S3CredentialsEnv", S3CredentialsEnv);
@@ -1143,6 +1146,7 @@ void __fastcall TSessionData::DoSave(THierarchicalStorage * Storage,
     WRITE_DATA(Integer, InternalEditorEncoding);
     WRITE_DATA(String, S3DefaultRegion);
     WRITE_DATA(String, S3SessionToken);
+    WRITE_DATA(String, S3Profile);
     WRITE_DATA(Integer, S3UrlStyle);
     WRITE_DATA(Integer, S3MaxKeys);
     WRITE_DATA(Bool, S3CredentialsEnv);
@@ -2844,7 +2848,7 @@ UnicodeString __fastcall TSessionData::GetUserNameExpanded()
   UnicodeString Result = ::ExpandEnvironmentVariables(UserName);
   if (Result.IsEmpty() && HasS3AutoCredentials())
   {
-    Result = S3EnvUserName();
+    Result = S3EnvUserName(S3Profile);
   }
   return Result;
 }
@@ -2854,7 +2858,7 @@ UnicodeString TSessionData::GetUserNameSource()
   UnicodeString Result;
   if (UserName.IsEmpty() && HasS3AutoCredentials())
   {
-    S3EnvUserName(&Result);
+    S3EnvUserName(S3Profile, &Result);
   }
   if (Result.IsEmpty() && (UserName != UserNameExpanded))
   {
@@ -4495,6 +4499,11 @@ void __fastcall TSessionData::SetS3SessionToken(UnicodeString value)
   SET_SESSION_PROPERTY(S3SessionToken);
 }
 //---------------------------------------------------------------------
+void __fastcall TSessionData::SetS3Profile(UnicodeString value)
+{
+  SET_SESSION_PROPERTY(S3Profile);
+}
+//---------------------------------------------------------------------
 void __fastcall TSessionData::SetS3UrlStyle(TS3UrlStyle value)
 {
   SET_SESSION_PROPERTY(S3UrlStyle);

+ 3 - 0
source/core/SessionData.h

@@ -227,6 +227,7 @@ private:
   int FInternalEditorEncoding;
   UnicodeString FS3DefaultRegion;
   UnicodeString FS3SessionToken;
+  UnicodeString FS3Profile;
   TS3UrlStyle FS3UrlStyle;
   TAutoSwitch FS3MaxKeys;
   bool FS3CredentialsEnv;
@@ -416,6 +417,7 @@ private:
   void __fastcall SetInternalEditorEncoding(int value);
   void __fastcall SetS3DefaultRegion(UnicodeString value);
   void __fastcall SetS3SessionToken(UnicodeString value);
+  void __fastcall SetS3Profile(UnicodeString value);
   void __fastcall SetS3UrlStyle(TS3UrlStyle value);
   void __fastcall SetS3MaxKeys(TAutoSwitch value);
   void __fastcall SetS3CredentialsEnv(bool value);
@@ -696,6 +698,7 @@ public:
   __property int InternalEditorEncoding = { read = FInternalEditorEncoding, write = SetInternalEditorEncoding };
   __property UnicodeString S3DefaultRegion = { read = FS3DefaultRegion, write = SetS3DefaultRegion };
   __property UnicodeString S3SessionToken = { read = FS3SessionToken, write = SetS3SessionToken };
+  __property UnicodeString S3Profile = { read = FS3Profile, write = SetS3Profile };
   __property TS3UrlStyle S3UrlStyle = { read = FS3UrlStyle, write = SetS3UrlStyle };
   __property TAutoSwitch S3MaxKeys = { read = FS3MaxKeys, write = SetS3MaxKeys };
   __property bool S3CredentialsEnv = { read = FS3CredentialsEnv, write = SetS3CredentialsEnv };

+ 50 - 12
source/forms/Login.cpp

@@ -554,7 +554,8 @@ void __fastcall TLoginDialog::LoadSession(TSessionData * SessionData)
           UnicodeString::StringOfChar(L'?', 16) : UnicodeString();
     }
 
-    S3CredentialsEnvCheck->Checked = SessionData->S3CredentialsEnv;
+    S3CredentialsEnvCheck2->Checked = SessionData->S3CredentialsEnv;
+    S3ProfileCombo->Text = DefaultStr(SessionData->S3Profile, GetS3GeneralName());
     UpdateS3Credentials();
 
     NoteGroup->Visible = !Trim(SessionData->Note).IsEmpty();
@@ -604,7 +605,8 @@ void __fastcall TLoginDialog::SaveSession(TSessionData * SessionData)
 
   if (SessionData->FSProtocol == fsS3)
   {
-    SessionData->S3CredentialsEnv = S3CredentialsEnvCheck->Checked;
+    SessionData->S3CredentialsEnv = S3CredentialsEnvCheck2->Checked;
+    SessionData->S3Profile = GetS3Profile();
   }
 
   if (SessionData->HasAutoCredentials())
@@ -636,6 +638,28 @@ bool __fastcall TLoginDialog::IsEditable()
   return IsNewSiteNode(SessionTree->Selected) || FEditing;
 }
 //---------------------------------------------------------------------
+UnicodeString TLoginDialog::GetS3GeneralName()
+{
+  return LoadStr(LOGIN_S3_GENERAL_CREDENTIALS);
+}
+//---------------------------------------------------------------------
+UnicodeString TLoginDialog::GetS3Profile()
+{
+  UnicodeString Result;
+  if (S3ProfileCombo->Text != GetS3GeneralName())
+  {
+    Result = S3ProfileCombo->Text;
+  }
+  return Result;
+}
+//---------------------------------------------------------------------
+void TLoginDialog::LoadS3Profiles()
+{
+  std::unique_ptr<TStrings> Profiles(GetS3Profiles());
+  Profiles->Insert(0, GetS3GeneralName());
+  S3ProfileCombo->Items = Profiles.get();
+}
+//---------------------------------------------------------------------
 void __fastcall TLoginDialog::UpdateControls()
 {
   if (Visible && FInitialized)
@@ -658,6 +682,10 @@ void __fastcall TLoginDialog::UpdateControls()
     BasicSshPanel->Visible = SshProtocol;
     BasicFtpPanel->Visible = FtpProtocol && Editable;
     BasicS3Panel->Visible = S3Protocol && Editable;
+    if (BasicS3Panel->Visible && (S3ProfileCombo->Items->Count == 0))
+    {
+      LoadS3Profiles();
+    }
     // we do not support more than one at the same time
     DebugAssert((int(BasicSshPanel->Visible) + int(BasicFtpPanel->Visible) + int(BasicS3Panel->Visible)) <= 1);
     BasicGroup->Height =
@@ -678,16 +706,18 @@ void __fastcall TLoginDialog::UpdateControls()
     ReadOnlyControl(PortNumberEdit, !Editable);
     PortNumberEdit->ButtonsVisible = Editable;
     // FSessionData may be NULL temporary even when Editable while switching nodes
+    bool S3CredentialsEnv = S3Protocol && S3CredentialsEnvCheck2->Checked;
     bool NoAuth =
       Editable && (FSessionData != NULL) &&
       ((SshProtocol && FSessionData->SshNoUserAuth) ||
-       (S3Protocol && S3CredentialsEnvCheck->Checked));
+       S3CredentialsEnv);
     ReadOnlyAndEnabledControl(UserNameEdit, !Editable, !NoAuth);
     EnableControl(UserNameLabel, UserNameEdit->Enabled);
     ReadOnlyAndEnabledControl(PasswordEdit, !Editable, !NoAuth);
     EnableControl(PasswordLabel, PasswordEdit->Enabled);
     UserNameLabel->Caption = S3Protocol ? LoadStr(S3_ACCESS_KEY_ID_PROMPT) : FUserNameLabel;
     PasswordLabel->Caption = S3Protocol ? LoadStr(S3_SECRET_ACCESS_KEY_PROMPT) : FPasswordLabel;
+    EnableControl(S3ProfileCombo, S3CredentialsEnv);
 
     // sites
     if (SitesIncrementalSearchLabel->Visible != !FSitesIncrementalSearch.IsEmpty())
@@ -2168,15 +2198,16 @@ int __fastcall TLoginDialog::DefaultPort()
 //---------------------------------------------------------------------------
 void TLoginDialog::UpdateS3Credentials()
 {
-  if (S3CredentialsEnvCheck->Checked)
+  if (S3CredentialsEnvCheck2->Checked)
   {
-    UserNameEdit->Text = S3EnvUserName();
-    PasswordEdit->Text = S3EnvPassword();
+    UnicodeString S3Profile = GetS3Profile();
+    UserNameEdit->Text = S3EnvUserName(S3Profile);
+    PasswordEdit->Text = S3EnvPassword(S3Profile);
     // Is not set when viewing stored session.
     // We do this, so that when the checkbox is checked and unchecked, the token is preserved, the way username and password are.
     if (FSessionData != NULL)
     {
-      FSessionData->S3SessionToken = S3EnvSessionToken();
+      FSessionData->S3SessionToken = S3EnvSessionToken(S3Profile);
     }
   }
 }
@@ -2196,19 +2227,20 @@ void __fastcall TLoginDialog::TransferProtocolComboChange(TObject * Sender)
     {
       try
       {
+        UnicodeString S3Profile = GetS3Profile();
         if (HostNameEdit->Text == S3HostName)
         {
           HostNameEdit->Clear();
         }
-        if (UserNameEdit->Text == S3EnvUserName())
+        if (UserNameEdit->Text == S3EnvUserName(S3Profile))
         {
           UserNameEdit->Clear();
         }
-        if (PasswordEdit->Text == S3EnvPassword())
+        if (PasswordEdit->Text == S3EnvPassword(S3Profile))
         {
           PasswordEdit->Clear();
         }
-        if ((FSessionData != NULL) && (FSessionData->S3SessionToken == S3EnvSessionToken()))
+        if ((FSessionData != NULL) && (FSessionData->S3SessionToken == S3EnvSessionToken(S3Profile)))
         {
           FSessionData->S3SessionToken = UnicodeString();
         }
@@ -2219,7 +2251,7 @@ void __fastcall TLoginDialog::TransferProtocolComboChange(TObject * Sender)
       }
     }
 
-    S3CredentialsEnvCheck->Checked = false;
+    S3CredentialsEnvCheck2->Checked = false;
   }
 
   UpdatePortWithProtocol();
@@ -3259,7 +3291,13 @@ void __fastcall TLoginDialog::PanelMouseDown(TObject *, TMouseButton, TShiftStat
   CountClicksForWindowPrint(this);
 }
 //---------------------------------------------------------------------------
-void __fastcall TLoginDialog::S3CredentialsEnvCheckClick(TObject *)
+void __fastcall TLoginDialog::S3CredentialsEnvCheck2Click(TObject *)
+{
+  UpdateS3Credentials();
+  UpdateControls();
+}
+//---------------------------------------------------------------------------
+void __fastcall TLoginDialog::S3ProfileComboChange(TObject *)
 {
   UpdateS3Credentials();
   UpdateControls();

+ 26 - 16
source/forms/Login.dfm

@@ -94,13 +94,13 @@ object LoginDialog: TLoginDialog
         Left = 2
         Top = 12
         Width = 347
-        Height = 255
+        Height = 260
         Anchors = [akLeft, akTop, akRight]
         Caption = 'Session'
         TabOrder = 0
         DesignSize = (
           347
-          255)
+          260)
         object Label1: TLabel
           Left = 12
           Top = 72
@@ -162,22 +162,32 @@ object LoginDialog: TLoginDialog
           Left = 12
           Top = 195
           Width = 324
-          Height = 26
+          Height = 31
           Anchors = [akLeft, akTop, akRight]
           BevelOuter = bvNone
           TabOrder = 11
           DesignSize = (
             324
-            26)
-          object S3CredentialsEnvCheck: TCheckBox
+            31)
+          object S3CredentialsEnvCheck2: TCheckBox
             Left = 0
-            Top = 0
+            Top = 2
             Width = 324
             Height = 17
             Anchors = [akLeft, akTop, akRight]
-            Caption = '&Read credentials from AWS CLI configuration'
+            Caption = '&Credentials from AWS CLI:'
             TabOrder = 0
-            OnClick = S3CredentialsEnvCheckClick
+            OnClick = S3CredentialsEnvCheck2Click
+          end
+          object S3ProfileCombo: TComboBox
+            Left = 200
+            Top = 0
+            Width = 124
+            Height = 21
+            DropDownCount = 16
+            TabOrder = 1
+            Text = 'S3ProfileCombo'
+            OnChange = S3ProfileComboChange
           end
         end
         object EncryptionView: TEdit
@@ -310,7 +320,7 @@ object LoginDialog: TLoginDialog
         end
         object AdvancedButton: TButton
           Left = 238
-          Top = 219
+          Top = 224
           Width = 98
           Height = 25
           Action = SessionAdvancedAction
@@ -321,7 +331,7 @@ object LoginDialog: TLoginDialog
         end
         object SaveButton: TButton
           Left = 12
-          Top = 219
+          Top = 224
           Width = 98
           Height = 25
           Action = SaveSessionAction
@@ -332,7 +342,7 @@ object LoginDialog: TLoginDialog
         end
         object EditCancelButton: TButton
           Left = 116
-          Top = 219
+          Top = 224
           Width = 82
           Height = 25
           Action = EditCancelAction
@@ -342,7 +352,7 @@ object LoginDialog: TLoginDialog
         end
         object EditButton: TButton
           Left = 12
-          Top = 219
+          Top = 224
           Width = 98
           Height = 25
           Action = EditSessionAction
@@ -353,20 +363,20 @@ object LoginDialog: TLoginDialog
       end
       object NoteGroup: TGroupBox
         Left = 2
-        Top = 273
+        Top = 278
         Width = 347
-        Height = 70
+        Height = 65
         Anchors = [akLeft, akTop, akRight, akBottom]
         Caption = 'Note'
         TabOrder = 1
         DesignSize = (
           347
-          70)
+          65)
         object NoteMemo: TMemo
           Left = 7
           Top = 15
           Width = 333
-          Height = 46
+          Height = 41
           TabStop = False
           Anchors = [akLeft, akTop, akRight, akBottom]
           BevelInner = bvNone

+ 7 - 2
source/forms/Login.h

@@ -210,8 +210,9 @@ __published:
   TPanel *ShowAgainPanel;
   TCheckBox *ShowAgainCheck;
   TPanel *BasicS3Panel;
-  TCheckBox *S3CredentialsEnvCheck;
+  TCheckBox *S3CredentialsEnvCheck2;
   TMenuItem *OpeninPuTTY4;
+  TComboBox *S3ProfileCombo;
   void __fastcall DataChange(TObject *Sender);
   void __fastcall FormShow(TObject *Sender);
   void __fastcall SessionTreeDblClick(TObject *Sender);
@@ -289,8 +290,9 @@ __published:
   void __fastcall SearchSiteNameActionExecute(TObject *Sender);
   void __fastcall SearchSiteActionExecute(TObject *Sender);
   void __fastcall PanelMouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y);
-  void __fastcall S3CredentialsEnvCheckClick(TObject *Sender);
+  void __fastcall S3CredentialsEnvCheck2Click(TObject *Sender);
   void __fastcall EncryptionComboChange(TObject *Sender);
+  void __fastcall S3ProfileComboChange(TObject *Sender);
 
 private:
   int NoUpdate;
@@ -413,6 +415,9 @@ private:
   void UpdateLoginButton();
   void FloodFill(TBitmap * Bitmap, int X, int Y);
   void UpdatePortWithProtocol();
+  void LoadS3Profiles();
+  UnicodeString GetS3GeneralName();
+  UnicodeString GetS3Profile();
 
 protected:
   void __fastcall Default();

+ 1 - 1
source/forms/SiteAdvanced.cpp

@@ -230,7 +230,7 @@ void __fastcall TSiteAdvancedDialog::LoadSession()
     {
       try
       {
-        S3SessionToken = S3EnvSessionToken();
+        S3SessionToken = S3EnvSessionToken(FSessionData->S3Profile);
       }
       catch (...)
       {

+ 1 - 0
source/resource/TextsCore.h

@@ -279,6 +279,7 @@
 #define OPENSSH_CONFIG_NOT_FOUND 755
 #define OPENSSH_CONFIG_NO_SITES 756
 #define FTP_MALFORMED_RESPONSE  757
+#define S3_PROFILE_NOT_EXIST    758
 
 #define CORE_CONFIRMATION_STRINGS 300
 #define CONFIRM_PROLONG_TIMEOUT3 301

+ 1 - 0
source/resource/TextsCore1.rc

@@ -251,6 +251,7 @@ BEGIN
   OPENSSH_CONFIG_NOT_FOUND, "OpenSSH config file not found."
   OPENSSH_CONFIG_NO_SITES, "No Host directives for specific hosts found in OpenSSH config file."
   FTP_MALFORMED_RESPONSE, "FTP server returned invalid response '%s'."
+  S3_PROFILE_NOT_EXIST, "S3 profile \"%s\" does not exist or does not contain any relevant options."
 
   CORE_CONFIRMATION_STRINGS, "CORE_CONFIRMATION"
   CONFIRM_PROLONG_TIMEOUT3, "Host is not communicating for %d seconds.\n\nWait for another %0:d seconds?"

+ 1 - 0
source/resource/TextsWin.h

@@ -664,6 +664,7 @@
 #define PROPERTIES_S3_EVERYONE  6049
 #define PROPERTIES_S3_R_ACL_HINT 6050
 #define PROPERTIES_S3_W_ACL_HINT 6051
+#define LOGIN_S3_GENERAL_CREDENTIALS 6052
 
 // 2xxx is reserved for TextsFileZilla.h
 

+ 1 - 0
source/resource/TextsWin1.rc

@@ -669,6 +669,7 @@ BEGIN
         PROPERTIES_S3_EVERYONE, "&Everyone"
         PROPERTIES_S3_R_ACL_HINT, "Read ACL"
         PROPERTIES_S3_W_ACL_HINT, "Write ACL"
+        LOGIN_S3_GENERAL_CREDENTIALS, "General"
 
         WIN_VARIABLE_STRINGS, "WIN_VARIABLE"
         WINSCP_COPYRIGHT, "Copyright © 2000–2022 Martin Prikryl"