1
0
Эх сурвалжийг харах

Bug 1896: Importing sessions from OpenSSH config file

https://winscp.net/tracker/1896
(cherry picked from commit c3305ab60b515db6fedc4744ced833ed35ceb111)

# Conflicts:
#	source/resource/TextsWin.h
#	source/resource/TextsWin1.rc

Source commit: 23408008c3c6d52a7a9628e4e46593f695ca2a78
Martin Prikryl 4 жил өмнө
parent
commit
755f6f9147

+ 21 - 12
source/core/Common.cpp

@@ -879,21 +879,30 @@ UnicodeString __fastcall ExpandEnvironmentVariables(const UnicodeString & Str)
   return Buf;
 }
 //---------------------------------------------------------------------------
-bool __fastcall IsPathToSameFile(const UnicodeString & Path1, const UnicodeString & Path2)
+UnicodeString GetNormalizedPath(const UnicodeString & Path)
 {
-  UnicodeString ShortPath1 = ExtractShortPathName(Path1);
-  UnicodeString ShortPath2 = ExtractShortPathName(Path2);
-
-  bool Result;
-  // ExtractShortPathName returns empty string if file does not exist
-  if (ShortPath1.IsEmpty() || ShortPath2.IsEmpty())
-  {
-    Result = AnsiSameText(Path1, Path2);
-  }
-  else
+  UnicodeString Result = ExcludeTrailingBackslash(Path);
+  Result = ReplaceChar(Result, L'/', L'\\');
+  return Result;
+}
+//---------------------------------------------------------------------------
+UnicodeString GetCanonicalPath(const UnicodeString & Path)
+{
+  UnicodeString Result = ExtractShortPathName(Path);
+  if (Result.IsEmpty())
   {
-    Result = AnsiSameText(ShortPath1, ShortPath2);
+    Result = Path;
   }
+  Result = GetNormalizedPath(Result);
+  return Result;
+}
+//---------------------------------------------------------------------------
+bool __fastcall IsPathToSameFile(const UnicodeString & Path1, const UnicodeString & Path2)
+{
+  UnicodeString CanonicalPath1 = GetCanonicalPath(Path1);
+  UnicodeString CanonicalPath2 = GetCanonicalPath(Path2);
+
+  bool Result = SameText(CanonicalPath1, CanonicalPath2);
   return Result;
 }
 //---------------------------------------------------------------------------

+ 2 - 0
source/core/Common.h

@@ -95,6 +95,8 @@ UnicodeString __fastcall EscapePuttyCommandParam(UnicodeString Param);
 UnicodeString __fastcall StringsToParams(TStrings * Strings);
 UnicodeString __fastcall ExpandEnvironmentVariables(const UnicodeString & Str);
 bool __fastcall SamePaths(const UnicodeString & Path1, const UnicodeString & Path2);
+UnicodeString GetNormalizedPath(const UnicodeString & Path);
+UnicodeString GetCanonicalPath(const UnicodeString & Path);
 bool __fastcall IsPathToSameFile(const UnicodeString & Path1, const UnicodeString & Path2);
 int __fastcall CompareLogicalText(
   const UnicodeString & S1, const UnicodeString & S2, bool NaturalOrderNumericalSorting);

+ 56 - 13
source/core/Configuration.cpp

@@ -1413,17 +1413,21 @@ TStorage __fastcall TConfiguration::GetStorage()
   return FStorage;
 }
 //---------------------------------------------------------------------
+static TStoredSessionList * CreateSessionsForImport(TStoredSessionList * Sessions)
+{
+  std::unique_ptr<TStoredSessionList> Result(new TStoredSessionList(true));
+  Result->DefaultSettings = Sessions->DefaultSettings;
+  return Result.release();
+}
+//---------------------------------------------------------------------
 TStoredSessionList * __fastcall TConfiguration::SelectFilezillaSessionsForImport(
   TStoredSessionList * Sessions, UnicodeString & Error)
 {
-  std::unique_ptr<TStoredSessionList> ImportSessionList(new TStoredSessionList(true));
-  ImportSessionList->DefaultSettings = Sessions->DefaultSettings;
+  std::unique_ptr<TStoredSessionList> ImportSessionList(CreateSessionsForImport(Sessions));
 
   UnicodeString AppDataPath = GetShellFolderPath(CSIDL_APPDATA);
-  UnicodeString FilezillaSiteManagerFile =
-    IncludeTrailingBackslash(AppDataPath) + L"FileZilla\\sitemanager.xml";
-  UnicodeString FilezillaConfigurationFile =
-    IncludeTrailingBackslash(AppDataPath) + L"FileZilla\\filezilla.xml";
+  UnicodeString FilezillaSiteManagerFile = TPath::Combine(AppDataPath, L"FileZilla\\sitemanager.xml");
+  UnicodeString FilezillaConfigurationFile = TPath::Combine(AppDataPath, L"FileZilla\\filezilla.xml");
 
   if (FileExists(ApiPath(FilezillaSiteManagerFile)))
   {
@@ -1460,14 +1464,18 @@ bool __fastcall TConfiguration::AnyFilezillaSessionForImport(TStoredSessionList
   }
 }
 //---------------------------------------------------------------------
+static UnicodeString GetOpensshFolder()
+{
+  UnicodeString ProfilePath = GetShellFolderPath(CSIDL_PROFILE);
+  UnicodeString Result = TPath::Combine(ProfilePath, L".ssh");
+  return Result;
+}
+//---------------------------------------------------------------------
 TStoredSessionList * __fastcall TConfiguration::SelectKnownHostsSessionsForImport(
   TStoredSessionList * Sessions, UnicodeString & Error)
 {
-  std::unique_ptr<TStoredSessionList> ImportSessionList(new TStoredSessionList(true));
-  ImportSessionList->DefaultSettings = Sessions->DefaultSettings;
-
-  UnicodeString ProfilePath = GetShellFolderPath(CSIDL_PROFILE);
-  UnicodeString KnownHostsFile = IncludeTrailingBackslash(ProfilePath) + L".ssh\\known_hosts";
+  std::unique_ptr<TStoredSessionList> ImportSessionList(CreateSessionsForImport(Sessions));
+  UnicodeString KnownHostsFile = TPath::Combine(GetOpensshFolder(), L"known_hosts");
 
   try
   {
@@ -1493,8 +1501,7 @@ TStoredSessionList * __fastcall TConfiguration::SelectKnownHostsSessionsForImpor
 TStoredSessionList * __fastcall TConfiguration::SelectKnownHostsSessionsForImport(
   TStrings * Lines, TStoredSessionList * Sessions, UnicodeString & Error)
 {
-  std::unique_ptr<TStoredSessionList> ImportSessionList(new TStoredSessionList(true));
-  ImportSessionList->DefaultSettings = Sessions->DefaultSettings;
+  std::unique_ptr<TStoredSessionList> ImportSessionList(CreateSessionsForImport(Sessions));
 
   try
   {
@@ -1508,6 +1515,42 @@ TStoredSessionList * __fastcall TConfiguration::SelectKnownHostsSessionsForImpor
   return ImportSessionList.release();
 }
 //---------------------------------------------------------------------------
+TStoredSessionList * TConfiguration::SelectOpensshSessionsForImport(
+  TStoredSessionList * Sessions, UnicodeString & Error)
+{
+  std::unique_ptr<TStoredSessionList> ImportSessionList(CreateSessionsForImport(Sessions));
+  UnicodeString ConfigFile = TPath::Combine(GetOpensshFolder(), L"config");
+
+  try
+  {
+    if (FileExists(ApiPath(ConfigFile)))
+    {
+      std::unique_ptr<TStrings> Lines(new TStringList());
+      LoadScriptFromFile(ConfigFile, Lines.get(), true);
+      ImportSessionList->ImportFromOpenssh(Lines.get());
+
+      if (ImportSessionList->Count > 0)
+      {
+        ImportSessionList->SelectSessionsToImport(Sessions, true);
+      }
+      else
+      {
+        throw Exception(LoadStr(OPENSSH_CONFIG_NO_SITES));
+      }
+    }
+    else
+    {
+      throw Exception(LoadStr(OPENSSH_CONFIG_NOT_FOUND));
+    }
+  }
+  catch (Exception & E)
+  {
+    Error = FORMAT(L"%s\n(%s)", (E.Message, ConfigFile));
+  }
+
+  return ImportSessionList.release();
+}
+//---------------------------------------------------------------------------
 void __fastcall TConfiguration::SetRandomSeedFile(UnicodeString value)
 {
   if (RandomSeedFile != value)

+ 1 - 0
source/core/Configuration.h

@@ -275,6 +275,7 @@ public:
     TStoredSessionList * Sessions, UnicodeString & Error);
   TStoredSessionList * __fastcall SelectKnownHostsSessionsForImport(
     TStrings * Lines, TStoredSessionList * Sessions, UnicodeString & Error);
+  TStoredSessionList * SelectOpensshSessionsForImport(TStoredSessionList * Sessions, UnicodeString & Error);
 
   __property TVSFixedFileInfo *FixedApplicationInfo  = { read=GetFixedApplicationInfo };
   __property void * ApplicationInfo  = { read=GetApplicationInfo };

+ 221 - 2
source/core/SessionData.cpp

@@ -13,6 +13,7 @@
 #include "RemoteFiles.h"
 #include "SFTPFileSystem.h"
 #include "S3FileSystem.h"
+#include "FileMasks.h"
 #include <Soap.EncdDecd.hpp>
 #include <StrUtils.hpp>
 #include <XMLDoc.hpp>
@@ -75,11 +76,45 @@ const UnicodeString UrlRawSettingsParamNamePrefix(L"x-");
 const UnicodeString PassphraseOption(L"passphrase");
 const UnicodeString RawSettingsOption(L"rawsettings");
 const UnicodeString S3HostName(S3LibDefaultHostName());
+const UnicodeString OpensshHostDirective(L"Host");
 //---------------------------------------------------------------------
 TDateTime __fastcall SecToDateTime(int Sec)
 {
   return TDateTime(double(Sec) / SecsPerDay);
 }
+//---------------------------------------------------------------------
+static bool IsValidOpensshLine(const UnicodeString & Line)
+{
+  return !Line.IsEmpty() && (Line[1] != L'#');
+}
+//---------------------------------------------------------------------
+static bool ParseOpensshDirective(const UnicodeString & ALine, UnicodeString & Directive, UnicodeString & Value)
+{
+  bool Result = IsValidOpensshLine(ALine);
+  if (Result)
+  {
+    int From = 1;
+    wchar_t Equal = L'=';
+    UnicodeString Delimiters(UnicodeString(L" \t") + Equal);
+    wchar_t Delimiter;
+    UnicodeString Line = Trim(ALine);
+    Directive = CopyToChars(Line, From, Delimiters, false, &Delimiter, false);
+    Result = !Directive.IsEmpty();
+    if (Result)
+    {
+      Value = Line;
+      Value.Delete(1, Directive.Length() + 1);
+      Value = Trim(Value);
+      if ((Delimiter != Equal) && !Value.IsEmpty() && (Value[1] == Equal))
+      {
+        Value.Delete(1, 1);
+        Value = Trim(Value); // not sure about this, but for the directives we support it does not matter
+      }
+      Result = !Value.IsEmpty();
+    }
+  }
+  return Result;
+}
 //--- TSessionData ----------------------------------------------------
 __fastcall TSessionData::TSessionData(UnicodeString aName):
   TNamedObject(aName)
@@ -1549,6 +1584,135 @@ void __fastcall TSessionData::ImportFromFilezilla(
 
 }
 //---------------------------------------------------------------------
+bool OpensshBoolValue(const UnicodeString & Value)
+{
+  return SameText(Value, L"yes");
+}
+//---------------------------------------------------------------------
+UnicodeString CutOpensshToken(UnicodeString & S)
+{
+  DebugAssert(!S.IsEmpty() && (S == Trim(S)));
+  int From = 1;
+  UnicodeString Result = CopyToChars(S, From, L" \t", false);
+  S.Delete(1, Result.Length());
+  S = Trim(S);
+  DebugAssert(!Result.IsEmpty() && (Result == Trim(Result)));
+  Result = Trim(Result);
+  return Result;
+}
+//---------------------------------------------------------------------
+void TSessionData::ImportFromOpenssh(TStrings * Lines)
+{
+  bool SkippingSection = false;
+  std::unique_ptr<TStrings> UsedDirectives(CreateSortedStringList());
+  for (int Index = 0; Index < Lines->Count; Index++)
+  {
+    UnicodeString Line = Lines->Strings[Index];
+    UnicodeString Directive, Value;
+    if (ParseOpensshDirective(Line, Directive, Value))
+    {
+      if (SameText(Directive, OpensshHostDirective))
+      {
+        DebugAssert(!Value.IsEmpty() && (Value == Trim(Value)));
+
+        SkippingSection = true;
+        while (!Value.IsEmpty())
+        {
+          UnicodeString M = CutOpensshToken(Value);
+          bool Negated = DebugAlwaysTrue(!M.IsEmpty()) && (M[1] == L'!');
+          if (Negated)
+          {
+            M.Delete(1, 1);
+          }
+          TFileMasks Mask;
+          Mask.SetMask(M);
+          // This does way more than OpenSSH, but on the other hand, the special characters of our file masks,
+          // should not be present in hostnames.
+          if (Mask.Matches(Name, false, UnicodeString(), NULL))
+          {
+            if (Negated)
+            {
+              // Skip even if matched by other possitive patterns
+              SkippingSection = true;
+              break;
+            }
+            else
+            {
+              // Keep looking, in case if negated
+              SkippingSection = false;
+            }
+          }
+        }
+      }
+      else if (SameText(Directive, L"Match"))
+      {
+        SkippingSection = true;
+      }
+      else if (!SkippingSection && (UsedDirectives->IndexOf(Directive) < 0))
+      {
+        if (SameText(Directive, L"AddressFamily"))
+        {
+          if (SameText(Value, L"inet"))
+          {
+            AddressFamily = afIPv4;
+          }
+          else if (SameText(Value, L"inet6"))
+          {
+            AddressFamily = afIPv6;
+          }
+          else
+          {
+            AddressFamily = afAuto;
+          }
+        }
+        else if (SameText(Directive, L"BindAddress"))
+        {
+          SourceAddress = Value;
+        }
+        else if (SameText(Directive, L"Compression"))
+        {
+          Compression = OpensshBoolValue(Value);
+        }
+        else if (SameText(Directive, L"ForwardAgent"))
+        {
+          AgentFwd = OpensshBoolValue(Value);
+        }
+        else if (SameText(Directive, L"GSSAPIAuthentication"))
+        {
+          AuthGSSAPI = OpensshBoolValue(Value);
+        }
+        else if (SameText(Directive, L"GSSAPIDelegateCredentials"))
+        {
+          AuthGSSAPIKEX = OpensshBoolValue(Value);
+        }
+        else if (SameText(Directive, L"Hostname"))
+        {
+          HostName = Value;
+        }
+        else if (SameText(Directive, L"IdentityFile"))
+        {
+          // It's likely there would be forward slashes in OpenSSH config file and our load/save dialogs
+          // (e.g. when converting keys) work suboptimally when working with forward slashes.
+          PublicKeyFile = GetNormalizedPath(Value);
+        }
+        else if (SameText(Directive, L"KbdInteractiveAuthentication"))
+        {
+          AuthKI = OpensshBoolValue(Value);
+        }
+        else if (SameText(Directive, L"Port"))
+        {
+          PortNumber = StrToInt(Value);
+        }
+        else if (SameText(Directive, L"User"))
+        {
+          UserName = Value;
+        }
+        UsedDirectives->Add(Directive);
+      }
+    }
+  }
+}
+//---------------------------------------------------------------------
 void __fastcall TSessionData::SavePasswords(THierarchicalStorage * Storage, bool PuttyExport, bool DoNotEncryptPasswords, bool SaveAll)
 {
   if (!Configuration->DisablePasswordStoring && !PuttyExport && (!FPassword.IsEmpty() || SaveAll))
@@ -4607,6 +4771,11 @@ void __fastcall TStoredSessionList::ImportFromFilezilla(
   }
 }
 //---------------------------------------------------------------------
+UnicodeString FormatKnownHostName(const UnicodeString & HostName, int PortNumber)
+{
+  return FORMAT(L"%s:%d", (HostName, PortNumber));
+}
+//---------------------------------------------------------------------
 void __fastcall TStoredSessionList::ImportFromKnownHosts(TStrings * Lines)
 {
   bool SessionList = false;
@@ -4625,7 +4794,7 @@ void __fastcall TStoredSessionList::ImportFromKnownHosts(TStrings * Lines)
     {
       UnicodeString Line = Lines->Strings[Index];
       Line = Trim(Line);
-      if (!Line.IsEmpty() && (Line[1] != L';'))
+      if (IsValidOpensshLine(Line))
       {
         int P = Pos(L' ', Line);
         if (P > 0)
@@ -4654,7 +4823,7 @@ void __fastcall TStoredSessionList::ImportFromKnownHosts(TStrings * Lines)
           UnicodeString NameStr = HostNameStr;
           if (PortNumber >= 0)
           {
-            NameStr = FORMAT(L"%s:%d", (NameStr, PortNumber));
+            NameStr = FormatKnownHostName(NameStr, PortNumber);
           }
 
           std::unique_ptr<TSessionData> SessionDataOwner;
@@ -4715,6 +4884,34 @@ void __fastcall TStoredSessionList::ImportFromKnownHosts(TStrings * Lines)
   }
 }
 //---------------------------------------------------------------------
+void TStoredSessionList::ImportFromOpenssh(TStrings * Lines)
+{
+  std::unique_ptr<TStrings> Hosts(CreateSortedStringList());
+  for (int Index = 0; Index < Lines->Count; Index++)
+  {
+    UnicodeString Line = Lines->Strings[Index];
+    UnicodeString Directive, Value;
+    if (ParseOpensshDirective(Line, Directive, Value) &&
+        SameText(Directive, OpensshHostDirective))
+    {
+      while (!Value.IsEmpty())
+      {
+        UnicodeString Name = CutOpensshToken(Value);
+        if ((Hosts->IndexOf(Name) < 0) && (Name.LastDelimiter(L"*?") == 0))
+        {
+          std::unique_ptr<TSessionData> Data(new TSessionData(EmptyStr));
+          Data->CopyData(DefaultSettings);
+          Data->Name = Name;
+          Data->HostName = Name;
+          Data->ImportFromOpenssh(Lines);
+          Add(Data.release());
+          Hosts->Add(Name);
+        }
+      }
+    }
+  }
+}
+//---------------------------------------------------------------------
 void __fastcall TStoredSessionList::Export(const UnicodeString FileName)
 {
   THierarchicalStorage * Storage = TIniFileStorage::CreateFromPath(FileName);
@@ -5068,6 +5265,28 @@ void __fastcall TStoredSessionList::ImportSelectedKnownHosts(TStoredSessionList
   }
 }
 //---------------------------------------------------------------------------
+void TStoredSessionList::SelectKnownHostsForSelectedSessions(
+  TStoredSessionList * KnownHosts, TStoredSessionList * Sessions)
+{
+  for (int SessionIndex = 0; SessionIndex < Sessions->Count; SessionIndex++)
+  {
+    TSessionData * Session = Sessions->Sessions[SessionIndex];
+    if (Session->Selected)
+    {
+      UnicodeString Key = Session->HostName;
+      if (Session->PortNumber != Session->GetDefaultPort())
+      {
+        Key = FormatKnownHostName(Key, Session->PortNumber);
+      }
+      TSessionData * KnownHost = dynamic_cast<TSessionData *>(KnownHosts->FindByName(Key));
+      if (KnownHost != NULL)
+      {
+        KnownHost->Selected = true;
+      }
+    }
+  }
+}
+//---------------------------------------------------------------------------
 bool __fastcall TStoredSessionList::IsFolderOrWorkspace(
   const UnicodeString & Name, bool Workspace)
 {

+ 3 - 0
source/core/SessionData.h

@@ -486,6 +486,7 @@ public:
   void __fastcall ApplyRawSettings(TStrings * RawSettings, bool Unsafe);
   void __fastcall ApplyRawSettings(THierarchicalStorage * Storage, bool Unsafe);
   void __fastcall ImportFromFilezilla(_di_IXMLNode Node, const UnicodeString & Path, _di_IXMLNode SettingsNode);
+  void ImportFromOpenssh(TStrings * Lines);
   void __fastcall Save(THierarchicalStorage * Storage, bool PuttyExport,
     const TSessionData * Default = NULL);
   void __fastcall SaveRecryptedPasswords(THierarchicalStorage * Storage);
@@ -714,6 +715,7 @@ public:
   void __fastcall Saved();
   void __fastcall ImportFromFilezilla(const UnicodeString FileName, const UnicodeString ConfigurationFileName);
   void __fastcall ImportFromKnownHosts(TStrings * Lines);
+  void ImportFromOpenssh(TStrings * Lines);
   void __fastcall Export(const UnicodeString FileName);
   void __fastcall Load(THierarchicalStorage * Storage, bool AsModified = false,
     bool UseDefaults = false, bool PuttyImport = false);
@@ -751,6 +753,7 @@ public:
     const UnicodeString & SourceKey, TStoredSessionList * Sessions, bool OnlySelected);
   static void __fastcall ImportSelectedKnownHosts(TStoredSessionList * Sessions);
   static bool __fastcall OpenHostKeysSubKey(THierarchicalStorage * Storage, bool CanCreate);
+  static void SelectKnownHostsForSelectedSessions(TStoredSessionList * KnownHosts, TStoredSessionList * Sessions);
 
 private:
   TSessionData * FDefaultSettings;

+ 117 - 16
source/forms/ImportSessions.cpp

@@ -19,26 +19,37 @@
 //---------------------------------------------------------------------
 #pragma resource "*.dfm"
 //---------------------------------------------------------------------
-const int KnownHostsIndex = 2;
+const int OpensshIndex = 2;
+const int KnownHostsIndex = 3;
 //---------------------------------------------------------------------
 bool __fastcall DoImportSessionsDialog(TList * Imported)
 {
   std::unique_ptr<TStrings> Errors(new TStringList());
+  std::unique_ptr<TList> SessionListsList(new TList());
   UnicodeString Error;
+
   std::unique_ptr<TStoredSessionList> PuttyImportSessionList(
     GUIConfiguration->SelectPuttySessionsForImport(StoredSessions, Error));
+  SessionListsList->Add(PuttyImportSessionList.get());
   Errors->Add(Error);
+
   std::unique_ptr<TStoredSessionList> FilezillaImportSessionList(
     Configuration->SelectFilezillaSessionsForImport(StoredSessions, Error));
+  SessionListsList->Add(FilezillaImportSessionList.get());
+  Errors->Add(Error);
+
+  std::unique_ptr<TStoredSessionList> OpensshImportSessionList(
+    Configuration->SelectOpensshSessionsForImport(StoredSessions, Error));
+  SessionListsList->Add(OpensshImportSessionList.get());
   Errors->Add(Error);
+
   std::unique_ptr<TStoredSessionList> KnownHostsImportSessionList(
     Configuration->SelectKnownHostsSessionsForImport(StoredSessions, Error));
+  DebugAssert(KnownHostsIndex == SessionListsList->Count);
+  SessionListsList->Add(KnownHostsImportSessionList.get());
   Errors->Add(Error);
 
-  std::unique_ptr<TList> SessionListsList(new TList());
-  SessionListsList->Add(PuttyImportSessionList.get());
-  SessionListsList->Add(FilezillaImportSessionList.get());
-  SessionListsList->Add(KnownHostsImportSessionList.get());
+  DebugAssert(SessionListsList->Count == Errors->Count);
 
   std::unique_ptr<TImportSessionsDialog> ImportSessionsDialog(
     SafeFormCreate<TImportSessionsDialog>(Application));
@@ -52,18 +63,21 @@ bool __fastcall DoImportSessionsDialog(TList * Imported)
     // Particularly when importing known_hosts, there is no feedback.
     TInstantOperationVisualizer Visualizer;
 
-    StoredSessions->Import(PuttyImportSessionList.get(), true, Imported);
-    StoredSessions->Import(FilezillaImportSessionList.get(), true, Imported);
+    UnicodeString PuttyHostKeysSourceKey = Configuration->PuttyRegistryStorageKey;
+    TStoredSessionList * AKnownHostsImportSessionList =
+      static_cast<TStoredSessionList *>(SessionListsList->Items[KnownHostsIndex]);
 
-    UnicodeString SourceKey = Configuration->PuttyRegistryStorageKey;
+    StoredSessions->Import(PuttyImportSessionList.get(), true, Imported);
+    TStoredSessionList::ImportHostKeys(PuttyHostKeysSourceKey, PuttyImportSessionList.get(), true);
 
-    TStoredSessionList::ImportHostKeys(SourceKey, PuttyImportSessionList.get(), true);
+    StoredSessions->Import(FilezillaImportSessionList.get(), true, Imported);
+    // FileZilla uses PuTTY's host key store
+    TStoredSessionList::ImportHostKeys(PuttyHostKeysSourceKey, FilezillaImportSessionList.get(), true);
 
-    // Filezilla uses PuTTY's host key store
-    TStoredSessionList::ImportHostKeys(SourceKey, FilezillaImportSessionList.get(), true);
+    StoredSessions->Import(OpensshImportSessionList.get(), true, Imported);
+    // The actual import will be done by ImportSelectedKnownHosts
+    TStoredSessionList::SelectKnownHostsForSelectedSessions(AKnownHostsImportSessionList, OpensshImportSessionList.get());
 
-    TStoredSessionList * AKnownHostsImportSessionList =
-      static_cast<TStoredSessionList *>(SessionListsList->Items[KnownHostsIndex]);
     TStoredSessionList::ImportSelectedKnownHosts(AKnownHostsImportSessionList);
   }
   return Result;
@@ -129,12 +143,17 @@ void __fastcall TImportSessionsDialog::ClearSelections()
   }
 }
 //---------------------------------------------------------------------
+TSessionData * TImportSessionsDialog::GetSessionData(TListItem * Item)
+{
+  return DebugNotNull(static_cast<TSessionData *>(Item->Data));
+}
+//---------------------------------------------------------------------
 void __fastcall TImportSessionsDialog::SaveSelection()
 {
   for (int Index = 0; Index < SessionListView2->Items->Count; Index++)
   {
-    ((TSessionData*)SessionListView2->Items->Item[Index]->Data)->Selected =
-      SessionListView2->Items->Item[Index]->Checked;
+    TListItem * Item = SessionListView2->Items->Item[Index];
+    GetSessionData(Item)->Selected = Item->Checked;
   }
 }
 //---------------------------------------------------------------------
@@ -177,7 +196,7 @@ void __fastcall TImportSessionsDialog::LoadSessions()
 void __fastcall TImportSessionsDialog::SessionListView2InfoTip(
       TObject * /*Sender*/, TListItem * Item, UnicodeString & InfoTip)
 {
-  TSessionData * Data = DebugNotNull(reinterpret_cast<TSessionData *>(Item->Data));
+  TSessionData * Data = GetSessionData(Item);
   if (SourceComboBox->ItemIndex == KnownHostsIndex)
   {
     UnicodeString Algs;
@@ -235,6 +254,88 @@ bool __fastcall TImportSessionsDialog::Execute()
   {
     ClearSelections();
     SaveSelection();
+
+    if (SourceComboBox->ItemIndex == OpensshIndex)
+    {
+      int ConvertedKeys = 0;
+      std::unique_ptr<TStrings> ConvertedSessions(new TStringList());
+      std::unique_ptr<TStrings> ConvertedKeyFiles(new TStringList());
+      std::unique_ptr<TStrings> NotConvertedKeyFiles(CreateSortedStringList());
+      for (int Index = 0; Index < SessionListView2->Items->Count; Index++)
+      {
+        TListItem * Item = SessionListView2->Items->Item[Index];
+        if (Item->Checked)
+        {
+          TSessionData * Data = GetSessionData(Item);
+          if (!Data->PublicKeyFile.IsEmpty() &&
+              FileExists(ApiPath(Data->PublicKeyFile)))
+          {
+            UnicodeString CanonicalPath = GetCanonicalPath(Data->PublicKeyFile);
+            // Reuses the already converted keys saved under a custom name
+            // (when saved under the default name they would be captured by the later condition based on GetConvertedKeyFileName)
+            int CanonicalIndex = ConvertedKeyFiles->IndexOfName(CanonicalPath);
+            bool ConvertedSession = false;
+            if (CanonicalIndex >= 0)
+            {
+              Data->PublicKeyFile = ConvertedKeyFiles->ValueFromIndex[CanonicalIndex];
+              ConvertedSession = true;
+            }
+            // Prevents asking about converting the same key again, when the user refuses the conversion.
+            else if (NotConvertedKeyFiles->IndexOf(CanonicalPath) >= 0)
+            {
+              // noop
+            }
+            else
+            {
+              UnicodeString ConvertedFilename = GetConvertedKeyFileName(Data->PublicKeyFile);
+              UnicodeString FileName;
+              if (FileExists(ApiPath(ConvertedFilename)))
+              {
+                FileName = ConvertedFilename;
+                ConvertedSession = true;
+              }
+              else
+              {
+                FileName = Data->PublicKeyFile;
+                TDateTime TimestampBefore, TimestampAfter;
+                FileAge(FileName, TimestampBefore);
+                try
+                {
+                  VerifyAndConvertKey(FileName, true);
+                  FileAge(FileName, TimestampAfter);
+                  if ((Data->PublicKeyFile != FileName) ||
+                      // should never happen as cancelling the saving throws EAbort
+                      DebugAlwaysTrue(TimestampBefore != TimestampAfter))
+                  {
+                    ConvertedSession = true;
+                  }
+                }
+                catch (EAbort &)
+                {
+                  NotConvertedKeyFiles->Add(CanonicalPath);
+                }
+              }
+
+              if (ConvertedSession)
+              {
+                Data->PublicKeyFile = FileName;
+                ConvertedKeyFiles->Values[CanonicalPath] = FileName;
+              }
+            }
+            if (ConvertedSession)
+            {
+              ConvertedSessions->Add(FORMAT(L"%s (%s)", (Data->Name, Data->PublicKeyFile)));
+            }
+          }
+        }
+      }
+
+      if (ConvertedSessions->Count > 0)
+      {
+        UnicodeString Message = MainInstructions(FMTLOAD(IMPORT_CONVERTED_KEYS, (ConvertedKeyFiles->Count, ConvertedSessions->Count)));
+        MoreMessageDialog(Message, ConvertedSessions.get(), qtInformation, qaOK, HelpKeyword);
+      }
+    }
   }
 
   return Result;

+ 1 - 0
source/forms/ImportSessions.dfm

@@ -104,6 +104,7 @@ object ImportSessionsDialog: TImportSessionsDialog
     Items.Strings = (
       'PuTTY'
       'FileZilla'
+      'OpenSSH'
       'known_hosts')
   end
   object ErrorPanel: TPanel

+ 2 - 0
source/forms/ImportSessions.h

@@ -40,11 +40,13 @@ private:
   TList * FSessionListsList;
   TStrings * FErrors;
   std::unique_ptr<TStoredSessionList> FPastedKnownHosts;
+  int FKnownHostsIndex;
   void __fastcall UpdateControls();
   void __fastcall LoadSessions();
   void __fastcall ClearSelections();
   void __fastcall SaveSelection();
   TStoredSessionList * __fastcall GetSessionList(int Index);
+  TSessionData * GetSessionData(TListItem * Item);
   virtual void __fastcall CreateHandle();
   virtual void __fastcall DestroyHandle();
   virtual void __fastcall Dispatch(void * Message);

+ 3 - 1
source/resource/TextsCore.h

@@ -276,6 +276,8 @@
 #define STREAM_READ_ERROR       752
 #define S3_CONFIG_ERROR         753
 #define CREATE_TEMP_DIR_ERROR   754
+#define OPENSSH_CONFIG_NOT_FOUND 755
+#define OPENSSH_CONFIG_NO_SITES 756
 
 #define CORE_CONFIRMATION_STRINGS 300
 #define CONFIRM_PROLONG_TIMEOUT3 301
@@ -321,7 +323,7 @@
 #define CERT_TEXT2              349
 #define CERTIFICATE_PASSPHRASE_PROMPT 350
 #define CERTIFICATE_PASSPHRASE_TITLE 351
-#define KEY_TYPE_CONVERT3       352
+#define KEY_TYPE_CONVERT4       352
 #define MULTI_FILES_TO_ONE      353
 #define KEY_EXCHANGE_ALG        354
 #define KEYKEY_TYPE             355

+ 3 - 1
source/resource/TextsCore1.rc

@@ -248,6 +248,8 @@ BEGIN
   S3_CONFIG_ERROR, "Error reading AWS configuration parameter %s"
   TIMEOUT_ERROR, "Timeout waiting for server to respond."
   CREATE_TEMP_DIR_ERROR, "Cannot create temporary directory '%s'. You may change root directory to store temporary files in Preferences."
+  OPENSSH_CONFIG_NOT_FOUND, "OpenSSH config file not found."
+  OPENSSH_CONFIG_NO_SITES, "No Host directives for specific hosts found in OpenSSH config file (%s)."
 
   CORE_CONFIRMATION_STRINGS, "CORE_CONFIRMATION"
   CONFIRM_PROLONG_TIMEOUT3, "Host is not communicating for %d seconds.\n\nWait for another %0:d seconds?"
@@ -293,7 +295,7 @@ BEGIN
   CERT_TEXT2, "Issuer:\n%s\nSubject:\n%s\nValid: %s - %s\n\nFingerprints:\n- SHA-256: %s\n- SHA-1: %s\n\nSummary: %s"
   CERTIFICATE_PASSPHRASE_PROMPT, "&Passphrase for client certificate:"
   CERTIFICATE_PASSPHRASE_TITLE, "Client certificate passphrase"
-  KEY_TYPE_CONVERT3, "**Do you want to convert this %s private key to PuTTY format?**\n\n%s"
+  KEY_TYPE_CONVERT4, "**Do you want to convert %s private key to PuTTY format?**\n\n%s"
   MULTI_FILES_TO_ONE, "**Are you sure you want to transfer multiple files to a single file '%s' in a directory '%s'?**\n\nThe files will overwrite one another.\n\nIf you actually want to transfer all files to a directory '%s', keeping their name, make sure you terminate the path with a slash."
   KEY_EXCHANGE_ALG, "key-exchange algorithm"
   KEYKEY_TYPE, "host key type"

+ 1 - 0
source/resource/TextsWin.h

@@ -639,6 +639,7 @@
 #define SYNCHRONIZE_CHECKLIST_DELETE_REMOTE 6031
 #define SYNCHRONIZE_CHECKLIST_DELETE_LOCAL 6032
 #define SYNCHRONIZE_CHECKLIST_REVERSE 6033
+#define IMPORT_CONVERTED_KEYS   6045
 
 // 2xxx is reserved for TextsFileZilla.h
 

+ 1 - 0
source/resource/TextsWin1.rc

@@ -644,6 +644,7 @@ BEGIN
         SYNCHRONIZE_CHECKLIST_DELETE_REMOTE, "Delete obsolete remote file"
         SYNCHRONIZE_CHECKLIST_DELETE_LOCAL, "Delete obsolete local file"
         SYNCHRONIZE_CHECKLIST_REVERSE, "Click to reverse"
+        IMPORT_CONVERTED_KEYS, "%d key files in %d imported sessions were converted or replaced with existing keys in supported format."
 
         WIN_VARIABLE_STRINGS, "WIN_VARIABLE"
         WINSCP_COPYRIGHT, "Copyright © 2000–2021 Martin Prikryl"

+ 7 - 2
source/windows/Tools.cpp

@@ -1244,6 +1244,11 @@ void __fastcall EditSelectBaseName(HWND Edit)
   }
 }
 //---------------------------------------------------------------------------
+UnicodeString GetConvertedKeyFileName(const UnicodeString & FileName)
+{
+  return ChangeFileExt(FileName, FORMAT(L".%s", (PuttyKeyExt)));
+}
+//---------------------------------------------------------------------------
 static void __fastcall ConvertKey(UnicodeString & FileName, TKeyType Type)
 {
   UnicodeString Passphrase;
@@ -1264,7 +1269,7 @@ static void __fastcall ConvertKey(UnicodeString & FileName, TKeyType Type)
 
   try
   {
-    FileName = ChangeFileExt(FileName, FORMAT(L".%s", (PuttyKeyExt)));
+    FileName = GetConvertedKeyFileName(FileName);
 
     if (!SaveDialog(LoadStr(CONVERTKEY_SAVE_TITLE), LoadStr(CONVERTKEY_SAVE_FILTER), PuttyKeyExt, FileName))
     {
@@ -1304,7 +1309,7 @@ void DoVerifyKey(UnicodeString & FileName, bool Convert, UnicodeString & Message
           if (Convert)
           {
             Configuration->Usage->Inc(L"PrivateKeyConvertSuggestionsNative");
-            UnicodeString ConvertMessage = FMTLOAD(KEY_TYPE_CONVERT3, (TypeName, RemoveMainInstructionsTag(Message)));
+            UnicodeString ConvertMessage = FMTLOAD(KEY_TYPE_CONVERT4, (TypeName, RemoveMainInstructionsTag(Message)));
             Message = EmptyStr;
             if (MoreMessageDialog(ConvertMessage, NULL, qtConfirmation, qaOK | qaCancel, HelpKeyword) == qaOK)
             {

+ 1 - 0
source/windows/Tools.h

@@ -70,6 +70,7 @@ void __fastcall CopyToClipboard(TStrings * Strings);
 void __fastcall ShutDownWindows();
 void __fastcall SuspendWindows();
 void __fastcall EditSelectBaseName(HWND Edit);
+UnicodeString GetConvertedKeyFileName(const UnicodeString & FileName);
 void __fastcall VerifyAndConvertKey(UnicodeString & FileName, bool CanIgnore);
 void __fastcall VerifyKey(const UnicodeString & FileName);
 void __fastcall VerifyCertificate(const UnicodeString & FileName);