浏览代码

Bug 1941: Support reading S3 credentials from AWS CLI configuration

https://winscp.net/tracker/1941
(cherry picked from commit 1bcbb70a45f035be33f0a9fab193848fdee84043)

# Conflicts:
#	source/core/SessionData.cpp

Source commit: 1eb78423984500d739fd67928595a89e562ca9e0
Martin Prikryl 4 年之前
父节点
当前提交
0e894c33e4

+ 107 - 2
source/core/S3FileSystem.cpp

@@ -27,6 +27,13 @@
 //---------------------------------------------------------------------------
 #define FILE_OPERATION_LOOP_TERMINAL FTerminal
 //---------------------------------------------------------------------------
+#define AWS_ACCESS_KEY_ID L"AWS_ACCESS_KEY_ID"
+#define AWS_SECRET_ACCESS_KEY L"AWS_SECRET_ACCESS_KEY"
+#define AWS_SESSION_TOKEN L"AWS_SESSION_TOKEN"
+#define AWS_CONFIG_FILE L"AWS_CONFIG_FILE"
+#define AWS_PROFILE L"AWS_PROFILE"
+#define AWS_PROFILE_DEFAULT L"default"
+//---------------------------------------------------------------------------
 static std::unique_ptr<TCriticalSection> LibS3Section(TraceInitPtr(new TCriticalSection()));
 //---------------------------------------------------------------------------
 UTF8String LibS3Delimiter(L"/");
@@ -46,6 +53,84 @@ UnicodeString __fastcall S3LibDefaultRegion()
   return StrFromS3(S3_DEFAULT_REGION);
 }
 //---------------------------------------------------------------------------
+bool S3ConfigFileTried = false;
+std::unique_ptr<TCustomIniFile> S3ConfigFile;
+UnicodeString S3Profile;
+//---------------------------------------------------------------------------
+UnicodeString GetS3ConfigValue(const UnicodeString & Name, UnicodeString * Source)
+{
+  UnicodeString Result;
+  UnicodeString ASource;
+  TGuard Guard(LibS3Section.get());
+  try
+  {
+    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);
+        if (Result.IsEmpty())
+        {
+          UnicodeString ProfilePath = GetShellFolderPath(CSIDL_PROFILE);
+          UnicodeString DefaultConfigFileName = IncludeTrailingBackslash(ProfilePath) + L".aws\\credentials";
+          if (FileExists(DefaultConfigFileName))
+          {
+            ConfigFileName = DefaultConfigFileName;
+          }
+        }
+
+        S3ConfigFile.reset(new TMemIniFile(ConfigFileName));
+      }
+
+      if (S3ConfigFile.get() != NULL)
+      {
+        Result = S3ConfigFile->ReadString(S3Profile, Name, UnicodeString());
+        if (!Result.IsEmpty())
+        {
+          ASource = FORMAT(L"%s/%s", (ExtractFileName(S3ConfigFile->FileName), S3Profile));
+        }
+      }
+    }
+  }
+  catch (Exception & E)
+  {
+    throw ExtException(&E, MainInstructions(LoadStr(S3_CONFIG_ERROR)));
+  }
+  if (Source != NULL)
+  {
+    *Source = ASource;
+  }
+  return Result;
+}
+//---------------------------------------------------------------------------
+UnicodeString S3EnvUserName(UnicodeString * Source)
+{
+  return GetS3ConfigValue(AWS_ACCESS_KEY_ID, Source);
+}
+//---------------------------------------------------------------------------
+UnicodeString S3EnvPassword(UnicodeString * Source)
+{
+  return GetS3ConfigValue(AWS_SECRET_ACCESS_KEY, Source);
+}
+//---------------------------------------------------------------------------
+UnicodeString S3EnvSessionToken(UnicodeString * Source)
+{
+  return GetS3ConfigValue(AWS_SESSION_TOKEN, Source);
+}
+//---------------------------------------------------------------------------
 //---------------------------------------------------------------------------
 const int TS3FileSystem::S3MinMultiPartChunkSize = 5 * 1024 * 1024;
 const int TS3FileSystem::S3MaxMultiPartChunks = 10000;
@@ -105,7 +190,17 @@ void __fastcall TS3FileSystem::Open()
     FAccessKeyId.SetLength(S3_MAX_ACCESS_KEY_ID_LENGTH);
   }
 
-  UnicodeString SecretAccessKey = UTF8String(NormalizeString(Data->Password));
+  UnicodeString Password = Data->Password;
+  if (Password.IsEmpty() && Data->S3CredentialsEnv)
+  {
+    UnicodeString PasswordSource;
+    Password = S3EnvPassword(&PasswordSource);
+    if (!Password.IsEmpty())
+    {
+      FTerminal->LogEvent(FORMAT(L"Password (secret access key) read from %s", (PasswordSource)));
+    }
+  }
+  UnicodeString SecretAccessKey = UTF8String(NormalizeString(Password));
   if (SecretAccessKey.IsEmpty() && !FTerminal->SessionData->FingerprintScan)
   {
     if (!FTerminal->PromptUser(Data, pkPassword, LoadStr(S3_SECRET_ACCESS_KEY_TITLE), L"",
@@ -117,7 +212,17 @@ void __fastcall TS3FileSystem::Open()
   }
   FSecretAccessKey = UTF8String(SecretAccessKey);
 
-  FSecurityTokenBuf = UTF8String(Data->S3SessionToken);
+  UnicodeString SessionToken = Data->S3SessionToken;
+  if (SessionToken.IsEmpty() && Data->S3CredentialsEnv)
+  {
+    UnicodeString SessionTokenSource;
+    SessionToken = S3EnvSessionToken(&SessionTokenSource);
+    if (!SessionToken.IsEmpty())
+    {
+      FTerminal->LogEvent(FORMAT(L"Session token read from %s", (SessionTokenSource)));
+    }
+  }
+  FSecurityTokenBuf = UTF8String(SessionToken);
   FSecurityToken = static_cast<const char *>(FSecurityTokenBuf.data());
 
   FHostName = UTF8String(Data->HostNameExpanded);

+ 3 - 0
source/core/S3FileSystem.h

@@ -184,5 +184,8 @@ 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);
 //------------------------------------------------------------------------------
 #endif

+ 35 - 3
source/core/SessionData.cpp

@@ -233,10 +233,12 @@ void __fastcall TSessionData::DefaultSettings()
   SCPLsFullTime = asAuto;
   NotUtf = asAuto;
 
+  // S3
   S3DefaultRegion = L"";
   S3SessionToken = L"";
   S3UrlStyle = s3usVirtualHost;
   S3MaxKeys = asAuto;
+  S3CredentialsEnv = false;
 
   // SFTP
   SftpServer = L"";
@@ -399,6 +401,7 @@ void __fastcall TSessionData::NonPersistant()
   PROPERTY(S3SessionToken); \
   PROPERTY(S3UrlStyle); \
   PROPERTY(S3MaxKeys); \
+  PROPERTY(S3CredentialsEnv); \
   \
   PROPERTY(ProxyMethod); \
   PROPERTY(ProxyHost); \
@@ -746,6 +749,7 @@ void __fastcall TSessionData::DoLoad(THierarchicalStorage * Storage, bool PuttyI
   S3SessionToken = Storage->ReadString(L"S3SessionToken", S3SessionToken);
   S3UrlStyle = (TS3UrlStyle)Storage->ReadInteger(L"S3UrlStyle", S3UrlStyle);
   S3MaxKeys = Storage->ReadEnum(L"S3MaxKeys", S3MaxKeys, AutoSwitchMapping);
+  S3CredentialsEnv = Storage->ReadBool(L"S3CredentialsEnv", S3CredentialsEnv);
 
   // PuTTY defaults to TcpNoDelay, but the psftp/pscp ignores this preference, and always set this to off (what is our default too)
   if (!PuttyImport)
@@ -1136,6 +1140,7 @@ void __fastcall TSessionData::DoSave(THierarchicalStorage * Storage,
     WRITE_DATA(String, S3SessionToken);
     WRITE_DATA(Integer, S3UrlStyle);
     WRITE_DATA(Integer, S3MaxKeys);
+    WRITE_DATA(Bool, S3CredentialsEnv);
     WRITE_DATA(Integer, SendBuf);
     WRITE_DATA(String, SourceAddress);
     WRITE_DATA(String, ProtocolFeatures);
@@ -2568,13 +2573,22 @@ void __fastcall TSessionData::SetUserName(UnicodeString value)
 //---------------------------------------------------------------------
 UnicodeString __fastcall TSessionData::GetUserNameExpanded()
 {
-  return ::ExpandEnvironmentVariables(UserName);
+  UnicodeString Result = ::ExpandEnvironmentVariables(UserName);
+  if (Result.IsEmpty() && HasS3AutoCredentials())
+  {
+    Result = S3EnvUserName();
+  }
+  return Result;
 }
 //---------------------------------------------------------------------
 UnicodeString TSessionData::GetUserNameSource()
 {
   UnicodeString Result;
-  if (UserName != UserNameExpanded)
+  if (UserName.IsEmpty() && HasS3AutoCredentials())
+  {
+    S3EnvUserName(&Result);
+  }
+  if (Result.IsEmpty() && (UserName != UserNameExpanded))
   {
     Result = UserName;
   }
@@ -3276,7 +3290,10 @@ UnicodeString __fastcall TSessionData::GenerateSessionUrl(unsigned int Flags)
 
   Url += GetProtocolUrl(FLAGSET(Flags, sufHttpForWebDAV));
 
-  if (FLAGSET(Flags, sufUserName) && !UserNameExpanded.IsEmpty())
+  // Add username only if it was somehow explicitly specified (so not with S3CredentialsEnv), but if it was, add it in the expanded form.
+  // For scripting, we might use unexpanded form (keeping the environment variables),
+  // but for consistency with code generation (where explicit expansion code would need to be added), we do not.
+  if (FLAGSET(Flags, sufUserName) && !UserName.IsEmpty())
   {
     Url += EncodeUrlString(UserNameExpanded);
 
@@ -4179,6 +4196,11 @@ void __fastcall TSessionData::SetS3MaxKeys(TAutoSwitch value)
   SET_SESSION_PROPERTY(S3MaxKeys);
 }
 //---------------------------------------------------------------------
+void __fastcall TSessionData::SetS3CredentialsEnv(bool value)
+{
+  SET_SESSION_PROPERTY(S3CredentialsEnv);
+}
+//---------------------------------------------------------------------
 void __fastcall TSessionData::SetIsWorkspace(bool value)
 {
   SET_SESSION_PROPERTY(IsWorkspace);
@@ -4312,6 +4334,16 @@ TStrings * TSessionData::GetAllOptionNames(bool PuttyExport)
   std::unique_ptr<TSessionData> FactoryDefaults(new TSessionData(L""));
   return FactoryDefaults->SaveToOptions(NULL, false, PuttyExport);
 }
+//---------------------------------------------------------------------
+bool TSessionData::HasS3AutoCredentials()
+{
+  return (FSProtocol == fsS3) && S3CredentialsEnv;
+}
+//---------------------------------------------------------------------
+bool TSessionData::HasAutoCredentials()
+{
+  return HasS3AutoCredentials();
+}
 //=== TStoredSessionList ----------------------------------------------
 __fastcall TStoredSessionList::TStoredSessionList(bool aReadOnly):
   TNamedObjectList(), FReadOnly(aReadOnly)

+ 5 - 0
source/core/SessionData.h

@@ -228,6 +228,7 @@ private:
   UnicodeString FS3SessionToken;
   TS3UrlStyle FS3UrlStyle;
   TAutoSwitch FS3MaxKeys;
+  bool FS3CredentialsEnv;
   bool FIsWorkspace;
   UnicodeString FLink;
   UnicodeString FNameOverride;
@@ -414,6 +415,7 @@ private:
   void __fastcall SetS3SessionToken(UnicodeString value);
   void __fastcall SetS3UrlStyle(TS3UrlStyle value);
   void __fastcall SetS3MaxKeys(TAutoSwitch value);
+  void __fastcall SetS3CredentialsEnv(bool value);
   void __fastcall SetLogicalHostName(UnicodeString value);
   void __fastcall SetIsWorkspace(bool value);
   void __fastcall SetLink(UnicodeString value);
@@ -469,6 +471,7 @@ private:
     const UnicodeString & Name, bool Value);
   TStrings * __fastcall GetRawSettingsForUrl();
   void __fastcall DoCopyData(TSessionData * SourceData, bool NoRecrypt);
+  bool HasS3AutoCredentials();
   template<class AlgoT>
   void __fastcall SetAlgoList(AlgoT * List, const AlgoT * DefaultList, const UnicodeString * Names,
     int Count, AlgoT WarnAlgo, UnicodeString value);
@@ -518,6 +521,7 @@ public:
   UnicodeString __fastcall GenerateSessionUrl(unsigned int Flags);
   bool __fastcall HasRawSettingsForUrl();
   bool __fastcall HasSessionName();
+  bool HasAutoCredentials();
 
   UnicodeString __fastcall GenerateOpenCommandArgs(bool Rtf);
   void __fastcall GenerateAssemblyCode(TAssemblyLanguage Language, UnicodeString & Head, UnicodeString & Tail, int & Indent);
@@ -684,6 +688,7 @@ public:
   __property UnicodeString S3SessionToken = { read = FS3SessionToken, write = SetS3SessionToken };
   __property TS3UrlStyle S3UrlStyle = { read = FS3UrlStyle, write = SetS3UrlStyle };
   __property TAutoSwitch S3MaxKeys = { read = FS3MaxKeys, write = SetS3MaxKeys };
+  __property bool S3CredentialsEnv = { read = FS3CredentialsEnv, write = SetS3CredentialsEnv };
   __property bool IsWorkspace = { read = FIsWorkspace, write = SetIsWorkspace };
   __property UnicodeString Link = { read = FLink, write = SetLink };
   __property UnicodeString NameOverride = { read = FNameOverride, write = SetNameOverride };

+ 102 - 26
source/forms/Login.cpp

@@ -20,6 +20,7 @@
 #include "WinConfiguration.h"
 #include "ProgParams.h"
 #include "WinApi.h"
+#include "S3FileSystem.h"
 //---------------------------------------------------------------------
 #pragma link "ComboEdit"
 #pragma link "PasswordEdit"
@@ -81,7 +82,7 @@ __fastcall TLoginDialog::TLoginDialog(TComponent* AOwner)
   FFixedSessionImages = SessionImageList->Count;
   DebugAssert(SiteColorMaskImageIndex == FFixedSessionImages - 1);
 
-  FBasicGroupBaseHeight = BasicGroup->Height - BasicSshPanel->Height - BasicFtpPanel->Height;
+  FBasicGroupBaseHeight = BasicGroup->Height - BasicSshPanel->Height - BasicFtpPanel->Height - BasicS3Panel->Height;
   FNoteGroupOffset = NoteGroup->Top - (BasicGroup->Top + BasicGroup->Height);
   FUserNameLabel = UserNameLabel->Caption;
   FPasswordLabel = PasswordLabel->Caption;
@@ -136,6 +137,7 @@ void __fastcall TLoginDialog::InitControls()
   WebDavsCombo->ItemIndex = Index;
 
   BasicSshPanel->Top = BasicFtpPanel->Top;
+  BasicS3Panel->Top = BasicFtpPanel->Top;
 
   SitesIncrementalSearchLabel->AutoSize = false;
   SitesIncrementalSearchLabel->Left = SessionTree->Left;
@@ -498,9 +500,22 @@ void __fastcall TLoginDialog::LoadSession(TSessionData * SessionData)
   WinConfiguration->BeginMasterPasswordSession();
   try
   {
-    UserNameEdit->Text = SessionData->UserName;
     PortNumberEdit->AsInteger = SessionData->PortNumber;
 
+    int FtpsIndex = FtpsToIndex(SessionData->Ftps);
+    FtpsCombo->ItemIndex = FtpsIndex;
+    WebDavsCombo->ItemIndex = FtpsIndex;
+    EncryptionView->Text =
+      DebugAlwaysTrue(FtpsCombo->ItemIndex >= WebDavsCombo->ItemIndex) ? FtpsCombo->Text : WebDavsCombo->Text;
+
+    bool AllowScpFallback;
+    TransferProtocolCombo->ItemIndex = FSProtocolToIndex(SessionData->FSProtocol, AllowScpFallback);
+    TransferProtocolView->Text = TransferProtocolCombo->Text;
+
+    // Only after loading TransferProtocolCombo, so that we do not overwrite it with S3 defaults in TransferProtocolComboChange
+    HostNameEdit->Text = SessionData->HostName;
+    UserNameEdit->Text = SessionData->UserName;
+
     bool Editable = IsEditable();
     if (Editable)
     {
@@ -513,18 +528,8 @@ void __fastcall TLoginDialog::LoadSession(TSessionData * SessionData)
           UnicodeString::StringOfChar(L'?', 16) : UnicodeString();
     }
 
-    int FtpsIndex = FtpsToIndex(SessionData->Ftps);
-    FtpsCombo->ItemIndex = FtpsIndex;
-    WebDavsCombo->ItemIndex = FtpsIndex;
-    EncryptionView->Text =
-      DebugAlwaysTrue(FtpsCombo->ItemIndex >= WebDavsCombo->ItemIndex) ? FtpsCombo->Text : WebDavsCombo->Text;
-
-    bool AllowScpFallback;
-    TransferProtocolCombo->ItemIndex = FSProtocolToIndex(SessionData->FSProtocol, AllowScpFallback);
-    TransferProtocolView->Text = TransferProtocolCombo->Text;
-
-    // Only after loading TransferProtocolCombo, so that we do not overwrite it with default S3 hostname
-    HostNameEdit->Text = SessionData->HostName;
+    S3CredentialsEnvCheck->Checked = SessionData->S3CredentialsEnv;
+    UpdateS3Credentials();
 
     NoteGroup->Visible = !Trim(SessionData->Note).IsEmpty();
     NoteMemo->Lines->Text = SessionData->Note;
@@ -567,18 +572,33 @@ void __fastcall TLoginDialog::SaveSession(TSessionData * SessionData)
     SessionData->Assign(FSessionData);
   }
 
-  // Basic page
-  SessionData->UserName = UserNameEdit->Text.Trim();
-  SessionData->PortNumber = PortNumberEdit->AsInteger;
-  // must be loaded after UserName, because HostName may be in format user@host
-  SessionData->HostName = HostNameEdit->Text.Trim();
-  SessionData->Password = PasswordEdit->Text;
-  SessionData->Ftps = GetFtps();
-
   SessionData->FSProtocol =
     // requiring SCP fallback distinction
     GetFSProtocol(true);
 
+  if (SessionData->FSProtocol == fsS3)
+  {
+    SessionData->S3CredentialsEnv = S3CredentialsEnvCheck->Checked;
+  }
+
+  if (SessionData->HasAutoCredentials())
+  {
+    SessionData->UserName = UnicodeString();
+    SessionData->Password = UnicodeString();
+    SessionData->S3SessionToken = UnicodeString();
+  }
+  else
+  {
+    SessionData->UserName = UserNameEdit->Text.Trim();
+    SessionData->Password = PasswordEdit->Text;
+  }
+
+  SessionData->PortNumber = PortNumberEdit->AsInteger;
+  // Must be set after UserName, because HostName may be in format user@host,
+  // Though now we parse the hostname right on this dialog (see HostNameEdit), this is unlikely to ever be triggered.
+  SessionData->HostName = HostNameEdit->Text.Trim();
+  SessionData->Ftps = GetFtps();
+
   TSessionData * EditingSessionData = GetEditingSessionData();
   SessionData->Name =
     (EditingSessionData != NULL) ? EditingSessionData->Name :
@@ -611,12 +631,14 @@ void __fastcall TLoginDialog::UpdateControls()
 
     BasicSshPanel->Visible = SshProtocol;
     BasicFtpPanel->Visible = FtpProtocol && Editable;
-    // we do not support both at the same time
-    DebugAssert(!BasicSshPanel->Visible || !BasicFtpPanel->Visible);
+    BasicS3Panel->Visible = S3Protocol && Editable;
+    // we do not support more than one at the same time
+    DebugAssert((int(BasicSshPanel->Visible) + int(BasicFtpPanel->Visible) + int(BasicS3Panel->Visible)) <= 1);
     BasicGroup->Height =
       FBasicGroupBaseHeight +
       (BasicSshPanel->Visible ? BasicSshPanel->Height : 0) +
-      (BasicFtpPanel->Visible ? BasicFtpPanel->Height : 0);
+      (BasicFtpPanel->Visible ? BasicFtpPanel->Height : 0) +
+      (BasicS3Panel->Visible ? BasicS3Panel->Height : 0);
     int NoteGroupTop = (BasicGroup->Top + BasicGroup->Height) + FNoteGroupOffset;
     NoteGroup->SetBounds(
       NoteGroup->Left, (BasicGroup->Top + BasicGroup->Height) + FNoteGroupOffset,
@@ -630,7 +652,10 @@ void __fastcall TLoginDialog::UpdateControls()
     ReadOnlyControl(PortNumberEdit, !Editable);
     PortNumberEdit->ButtonsVisible = Editable;
     // FSessionData may be NULL temporary even when Editable while switching nodes
-    bool NoAuth = Editable && SshProtocol && (FSessionData != NULL) && FSessionData->SshNoUserAuth;
+    bool NoAuth =
+      Editable && (FSessionData != NULL) &&
+      ((SshProtocol && FSessionData->SshNoUserAuth) ||
+       (S3Protocol && S3CredentialsEnvCheck->Checked));
     ReadOnlyAndEnabledControl(UserNameEdit, !Editable, !NoAuth);
     EnableControl(UserNameLabel, UserNameEdit->Enabled);
     ReadOnlyAndEnabledControl(PasswordEdit, !Editable, !NoAuth);
@@ -2097,15 +2122,60 @@ int __fastcall TLoginDialog::DefaultPort()
   return ::DefaultPort(GetFSProtocol(false), GetFtps());
 }
 //---------------------------------------------------------------------------
+void TLoginDialog::UpdateS3Credentials()
+{
+  if (S3CredentialsEnvCheck->Checked)
+  {
+    UserNameEdit->Text = S3EnvUserName();
+    PasswordEdit->Text = S3EnvPassword();
+    // 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();
+    }
+  }
+}
+//---------------------------------------------------------------------------
 void __fastcall TLoginDialog::TransferProtocolComboChange(TObject * Sender)
 {
   if (!NoUpdate)
   {
     if (GetFSProtocol(false) == fsS3)
     {
+      // Note that this happens even when loading the session
+      // But the values will get overwritten.
       FtpsCombo->ItemIndex = FtpsToIndex(ftpsImplicit);
       HostNameEdit->Text = S3HostName;
     }
+    else
+    {
+      try
+      {
+        if (HostNameEdit->Text == S3HostName)
+        {
+          HostNameEdit->Clear();
+        }
+        if (UserNameEdit->Text == S3EnvUserName())
+        {
+          UserNameEdit->Clear();
+        }
+        if (PasswordEdit->Text == S3EnvPassword())
+        {
+          PasswordEdit->Clear();
+        }
+        if ((FSessionData != NULL) && (FSessionData->S3SessionToken == S3EnvSessionToken()))
+        {
+          FSessionData->S3SessionToken = UnicodeString();
+        }
+      }
+      catch (...)
+      {
+        // noop
+      }
+    }
+
+    S3CredentialsEnvCheck->Checked = false;
   }
 
   int ADefaultPort = DefaultPort();
@@ -3135,3 +3205,9 @@ void __fastcall TLoginDialog::PanelMouseDown(TObject *, TMouseButton, TShiftStat
   CountClicksForWindowPrint(this);
 }
 //---------------------------------------------------------------------------
+void __fastcall TLoginDialog::S3CredentialsEnvCheckClick(TObject *)
+{
+  UpdateS3Credentials();
+  UpdateControls();
+}
+//---------------------------------------------------------------------------

+ 47 - 25
source/forms/Login.dfm

@@ -5,7 +5,7 @@ object LoginDialog: TLoginDialog
   HelpKeyword = 'ui_login'
   BorderIcons = [biSystemMenu, biMinimize, biHelp]
   Caption = 'Login'
-  ClientHeight = 385
+  ClientHeight = 411
   ClientWidth = 873
   Color = clBtnFace
   Constraints.MinHeight = 399
@@ -22,7 +22,7 @@ object LoginDialog: TLoginDialog
     Left = 512
     Top = 0
     Width = 361
-    Height = 361
+    Height = 387
     Align = alRight
     BevelOuter = bvNone
     TabOrder = 0
@@ -30,25 +30,25 @@ object LoginDialog: TLoginDialog
       Left = 0
       Top = 0
       Width = 361
-      Height = 320
+      Height = 346
       Align = alClient
       BevelOuter = bvNone
       TabOrder = 0
       Visible = False
       DesignSize = (
         361
-        320)
+        346)
       object ContentsGroupBox: TGroupBox
         Left = 2
         Top = 12
         Width = 347
-        Height = 305
+        Height = 331
         Anchors = [akLeft, akTop, akBottom]
         Caption = 'ContentsGroupBox'
         TabOrder = 0
         DesignSize = (
           347
-          305)
+          331)
         object ContentsLabel: TLabel
           Left = 12
           Top = 20
@@ -70,7 +70,7 @@ object LoginDialog: TLoginDialog
           Left = 12
           Top = 42
           Width = 324
-          Height = 251
+          Height = 277
           Anchors = [akLeft, akTop, akRight, akBottom]
           Lines.Strings = (
             'ContentsMemo')
@@ -82,25 +82,25 @@ object LoginDialog: TLoginDialog
       Left = 0
       Top = 0
       Width = 361
-      Height = 320
+      Height = 346
       Align = alClient
       Anchors = [akTop, akRight, akBottom]
       BevelOuter = bvNone
       TabOrder = 1
       DesignSize = (
         361
-        320)
+        346)
       object BasicGroup: TGroupBox
         Left = 2
         Top = 12
         Width = 347
-        Height = 229
+        Height = 255
         Anchors = [akLeft, akTop, akRight]
         Caption = 'Session'
         TabOrder = 0
         DesignSize = (
           347
-          229)
+          255)
         object Label1: TLabel
           Left = 12
           Top = 72
@@ -158,6 +158,28 @@ object LoginDialog: TLoginDialog
           Caption = '&Encryption:'
           FocusControl = WebDavsCombo
         end
+        object BasicS3Panel: TPanel
+          Left = 12
+          Top = 195
+          Width = 324
+          Height = 26
+          Anchors = [akLeft, akTop, akRight]
+          BevelOuter = bvNone
+          TabOrder = 15
+          DesignSize = (
+            324
+            26)
+          object S3CredentialsEnvCheck: TCheckBox
+            Left = 0
+            Top = 0
+            Width = 324
+            Height = 17
+            Anchors = [akLeft, akTop, akRight]
+            Caption = '&Read credentials from AWS CLI configuration'
+            TabOrder = 0
+            OnClick = S3CredentialsEnvCheckClick
+          end
+        end
         object EncryptionView: TEdit
           Left = 163
           Top = 39
@@ -288,7 +310,7 @@ object LoginDialog: TLoginDialog
         end
         object AdvancedButton: TButton
           Left = 238
-          Top = 193
+          Top = 219
           Width = 98
           Height = 25
           Action = SessionAdvancedAction
@@ -299,7 +321,7 @@ object LoginDialog: TLoginDialog
         end
         object SaveButton: TButton
           Left = 12
-          Top = 193
+          Top = 219
           Width = 98
           Height = 25
           Action = SaveSessionAction
@@ -310,7 +332,7 @@ object LoginDialog: TLoginDialog
         end
         object EditCancelButton: TButton
           Left = 116
-          Top = 193
+          Top = 219
           Width = 82
           Height = 25
           Action = EditCancelAction
@@ -320,7 +342,7 @@ object LoginDialog: TLoginDialog
         end
         object EditButton: TButton
           Left = 12
-          Top = 193
+          Top = 219
           Width = 98
           Height = 25
           Action = EditSessionAction
@@ -331,7 +353,7 @@ object LoginDialog: TLoginDialog
       end
       object NoteGroup: TGroupBox
         Left = 2
-        Top = 247
+        Top = 273
         Width = 347
         Height = 70
         Anchors = [akLeft, akTop, akRight, akBottom]
@@ -359,7 +381,7 @@ object LoginDialog: TLoginDialog
     end
     object ButtonPanel: TPanel
       Left = 0
-      Top = 320
+      Top = 346
       Width = 361
       Height = 41
       Align = alBottom
@@ -410,18 +432,18 @@ object LoginDialog: TLoginDialog
     Left = 0
     Top = 0
     Width = 512
-    Height = 361
+    Height = 387
     Align = alClient
     BevelOuter = bvNone
     TabOrder = 2
     DesignSize = (
       512
-      361)
+      387)
     object SessionTree: TTreeView
       Left = 11
       Top = 12
       Width = 490
-      Height = 312
+      Height = 338
       Anchors = [akLeft, akTop, akRight, akBottom]
       DoubleBuffered = True
       DragMode = dmAutomatic
@@ -457,8 +479,8 @@ object LoginDialog: TLoginDialog
     end
     object SitesIncrementalSearchLabel: TStaticText
       Left = 14
-      Top = 304
-      Width = 385
+      Top = 330
+      Width = 142
       Height = 17
       Anchors = [akLeft, akRight, akBottom]
       BorderStyle = sbsSingle
@@ -469,7 +491,7 @@ object LoginDialog: TLoginDialog
     end
     object ManageButton: TButton
       Left = 403
-      Top = 330
+      Top = 356
       Width = 98
       Height = 25
       Anchors = [akRight, akBottom]
@@ -479,7 +501,7 @@ object LoginDialog: TLoginDialog
     end
     object ToolsMenuButton: TButton
       Left = 11
-      Top = 330
+      Top = 356
       Width = 98
       Height = 25
       Anchors = [akLeft, akBottom]
@@ -490,7 +512,7 @@ object LoginDialog: TLoginDialog
   end
   object ShowAgainPanel: TPanel
     Left = 0
-    Top = 361
+    Top = 387
     Width = 873
     Height = 24
     Align = alBottom

+ 4 - 0
source/forms/Login.h

@@ -209,6 +209,8 @@ __published:
   TMenuItem *EditRawSettings1;
   TPanel *ShowAgainPanel;
   TCheckBox *ShowAgainCheck;
+  TPanel *BasicS3Panel;
+  TCheckBox *S3CredentialsEnvCheck;
   void __fastcall DataChange(TObject *Sender);
   void __fastcall FormShow(TObject *Sender);
   void __fastcall SessionTreeDblClick(TObject *Sender);
@@ -286,6 +288,7 @@ __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);
 
 private:
   int NoUpdate;
@@ -401,6 +404,7 @@ private:
   void __fastcall CMDpiChanged(TMessage & Message);
   void __fastcall GenerateImages();
   void __fastcall CMVisibleChanged(TMessage & Message);
+  void UpdateS3Credentials();
 
 protected:
   void __fastcall Default();

+ 26 - 3
source/forms/SiteAdvanced.cpp

@@ -11,6 +11,7 @@
 #include <HelpWin.h>
 #include <VCLCommon.h>
 #include <Cryptography.h>
+#include <S3FileSystem.h>
 
 #include "WinInterface.h"
 #include "SiteAdvanced.h"
@@ -223,7 +224,20 @@ void __fastcall TSiteAdvancedDialog::LoadSession()
     {
       S3UrlStyleCombo->ItemIndex = 0;
     }
-    S3SessionTokenMemo->Lines->Text = FSessionData->S3SessionToken;
+
+    UnicodeString S3SessionToken = FSessionData->S3SessionToken;
+    if (FSessionData->HasAutoCredentials())
+    {
+      try
+      {
+        S3SessionToken = S3EnvSessionToken();
+      }
+      catch (...)
+      {
+        // noop
+      }
+    }
+    S3SessionTokenMemo->Lines->Text = S3SessionToken;
 
     // Authentication page
     SshNoUserAuthCheck->Checked = FSessionData->SshNoUserAuth;
@@ -634,8 +648,15 @@ void __fastcall TSiteAdvancedDialog::SaveSession(TSessionData * SessionData)
   {
     SessionData->S3UrlStyle = s3usVirtualHost;
   }
-  // Trim not to try to authenticate with a stray new-line
-  SessionData->S3SessionToken = S3SessionTokenMemo->Lines->Text.Trim();
+  if (SessionData->HasAutoCredentials())
+  {
+    SessionData->S3SessionToken = EmptyStr;
+  }
+  else
+  {
+    // Trim not to try to authenticate with a stray new-line
+    SessionData->S3SessionToken = S3SessionTokenMemo->Lines->Text.Trim();
+  }
 
   // Proxy page
   SessionData->ProxyMethod = GetProxyMethod();
@@ -1050,6 +1071,8 @@ void __fastcall TSiteAdvancedDialog::UpdateControls()
 
     // environment/s3
     S3Sheet->Enabled = S3Protocol;
+    EnableControl(S3SessionTokenMemo, S3Sheet->Enabled && !FSessionData->HasAutoCredentials());
+    EnableControl(S3SessionTokenLabel, S3SessionTokenMemo->Enabled);
 
     // tunnel sheet
     TunnelSheet->Enabled = SshProtocol;

+ 1 - 1
source/forms/SiteAdvanced.dfm

@@ -1078,7 +1078,7 @@ object SiteAdvancedDialog: TSiteAdvancedDialog
           DesignSize = (
             393
             143)
-          object Label5: TLabel
+          object S3SessionTokenLabel: TLabel
             Left = 12
             Top = 20
             Width = 73

+ 1 - 1
source/forms/SiteAdvanced.h

@@ -285,7 +285,7 @@ __published:
   TGroupBox *WebdavGroup;
   TCheckBox *WebDavLiberalEscapingCheck;
   TGroupBox *S3AuthenticationGroup;
-  TLabel *Label5;
+  TLabel *S3SessionTokenLabel;
   TMemo *S3SessionTokenMemo;
   void __fastcall DataChange(TObject *Sender);
   void __fastcall FormShow(TObject *Sender);

+ 1 - 0
source/resource/TextsCore.h

@@ -274,6 +274,7 @@
 #define STORE_NEW_HOSTKEY_ERROR 750
 #define STREAM_IN_SCRIPT_ERROR  751
 #define STREAM_READ_ERROR       752
+#define S3_CONFIG_ERROR         753
 
 #define CORE_CONFIRMATION_STRINGS 300
 #define CONFIRM_PROLONG_TIMEOUT3 301

+ 1 - 0
source/resource/TextsCore1.rc

@@ -245,6 +245,7 @@ BEGIN
   STORE_NEW_HOSTKEY_ERROR, "Failed to store new host key."
   STREAM_IN_SCRIPT_ERROR, "When uploading streamed data, only one source can be specified and the target must specify a filename."
   STREAM_READ_ERROR, "Error reading input stream."
+  S3_CONFIG_ERROR, "Error reading AWS configuration parameter %s"
 
   CORE_CONFIRMATION_STRINGS, "CORE_CONFIRMATION"
   CONFIRM_PROLONG_TIMEOUT3, "Host is not communicating for %d seconds.\n\nWait for another %0:d seconds?"