Dynamically generate profiles from hosts in OpenSSH config files (#14042)

This PR adds a new dynamic profile generator which creates profiles to
quickly connect to detected SSH hosts.

This PR adds a new `SshHostGenerator` inbox dynamic profile generator.
When run, it looks for an install of our
[Win32-OpenSSH](https://github.com/PowerShell/Win32-OpenSSH) client app
`ssh.exe` in all of the (official) places it gets installed. If the exe
is found, the generator then looks for and parses both the user and
system OpenSSH config files for valid SSH hosts. Each host is then
converted into a profiles to call `ssh.exe` and connect to those hosts.

VALIDATION
Installed OpenSSH, configured host for alt.org NetHack server, connected
and played some NetHack from the created profile.

* [x] When OpenSSH is not installed, don't add profiles
* [x] Detected when installed via Optional Features (installs in
  `System32\OpenSSH`, added to PATH)
* [x] Detected when installed via the 32-Bit OpenSSH MSI from GitHub
  (installs in `Program Files (x86)\OpenSSH`, not added to PATH)
* [x] Detected when installed via the 64-Bit OpenSSH MSI from GitHub
  (installs in `Program Files\OpenSSH`, not added to PATH)
* [x] Detected when installed via `winget install
  Microsoft.OpenSSH.Beta` (uses MSI from GitHub)
* [x] With `"disabledProfileSources": ["Windows.Terminal.SSH"]` the
  profiles are not generated

Closes #9031

Co-authored-by: Carlos Zamora <carlos.zamora@microsoft.com>
Co-authored-by: Mike Griese <migrie@microsoft.com>
Closes https://github.com/microsoft/terminal/issues/9031
This commit is contained in:
Jon Thysell
2022-12-09 14:01:53 -08:00
committed by GitHub
parent 84bb98bc81
commit a5f9c85c39
7 changed files with 223 additions and 0 deletions

View File

@@ -77,6 +77,7 @@ sonpham
stakx stakx
talo talo
thereses thereses
Thysell
Walisch Walisch
WDX WDX
Wellons Wellons

View File

@@ -13,6 +13,9 @@
#include "PowershellCoreProfileGenerator.h" #include "PowershellCoreProfileGenerator.h"
#include "VisualStudioGenerator.h" #include "VisualStudioGenerator.h"
#include "WslDistroGenerator.h" #include "WslDistroGenerator.h"
#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED
#include "SshHostGenerator.h"
#endif
// The following files are generated at build time into the "Generated Files" directory. // The following files are generated at build time into the "Generated Files" directory.
// defaults(-universal).h is a file containing the default json settings in a std::string_view. // defaults(-universal).h is a file containing the default json settings in a std::string_view.
@@ -148,6 +151,9 @@ void SettingsLoader::GenerateProfiles()
_executeGenerator(WslDistroGenerator{}); _executeGenerator(WslDistroGenerator{});
_executeGenerator(AzureCloudShellGenerator{}); _executeGenerator(AzureCloudShellGenerator{});
_executeGenerator(VisualStudioGenerator{}); _executeGenerator(VisualStudioGenerator{});
#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED
_executeGenerator(SshHostGenerator{});
#endif
} }
// A new settings.json gets a special treatment: // A new settings.json gets a special treatment:

View File

@@ -92,6 +92,7 @@
<ClInclude Include="VsDevShellGenerator.h" /> <ClInclude Include="VsDevShellGenerator.h" />
<ClInclude Include="VsSetupConfiguration.h" /> <ClInclude Include="VsSetupConfiguration.h" />
<ClInclude Include="WslDistroGenerator.h" /> <ClInclude Include="WslDistroGenerator.h" />
<ClInclude Include="SshHostGenerator.h" />
<ClInclude Include="ModelSerializationHelpers.h" /> <ClInclude Include="ModelSerializationHelpers.h" />
</ItemGroup> </ItemGroup>
<!-- ========================= Cpp Files ======================== --> <!-- ========================= Cpp Files ======================== -->
@@ -166,6 +167,7 @@
<ClCompile Include="VsDevShellGenerator.cpp" /> <ClCompile Include="VsDevShellGenerator.cpp" />
<ClCompile Include="VsSetupConfiguration.cpp" /> <ClCompile Include="VsSetupConfiguration.cpp" />
<ClCompile Include="WslDistroGenerator.cpp" /> <ClCompile Include="WslDistroGenerator.cpp" />
<ClCompile Include="SshHostGenerator.cpp" />
<!-- You _NEED_ to include this file and the jsoncpp IncludePath (below) if <!-- You _NEED_ to include this file and the jsoncpp IncludePath (below) if
you want to use jsoncpp --> you want to use jsoncpp -->
<ClCompile Include="$(OpenConsoleDir)\dep\jsoncpp\jsoncpp.cpp"> <ClCompile Include="$(OpenConsoleDir)\dep\jsoncpp\jsoncpp.cpp">

View File

@@ -18,6 +18,9 @@
<ClCompile Include="WslDistroGenerator.cpp"> <ClCompile Include="WslDistroGenerator.cpp">
<Filter>profileGeneration</Filter> <Filter>profileGeneration</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="SshHostGenerator.cpp">
<Filter>profileGeneration</Filter>
</ClCompile>
<ClCompile Include="CascadiaSettings.cpp" /> <ClCompile Include="CascadiaSettings.cpp" />
<ClCompile Include="CascadiaSettingsSerialization.cpp" /> <ClCompile Include="CascadiaSettingsSerialization.cpp" />
<ClCompile Include="GlobalAppSettings.cpp" /> <ClCompile Include="GlobalAppSettings.cpp" />
@@ -61,6 +64,9 @@
<ClInclude Include="WslDistroGenerator.h"> <ClInclude Include="WslDistroGenerator.h">
<Filter>profileGeneration</Filter> <Filter>profileGeneration</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="SshHostGenerator.h">
<Filter>profileGeneration</Filter>
</ClInclude>
<ClInclude Include="CascadiaSettings.h" /> <ClInclude Include="CascadiaSettings.h" />
<ClInclude Include="GlobalAppSettings.h" /> <ClInclude Include="GlobalAppSettings.h" />
<ClInclude Include="TerminalSettingsSerializationHelpers.h" /> <ClInclude Include="TerminalSettingsSerializationHelpers.h" />

View File

@@ -0,0 +1,161 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "SshHostGenerator.h"
#include "../../inc/DefaultSettings.h"
#include "DynamicProfileUtils.h"
static constexpr std::wstring_view SshHostGeneratorNamespace{ L"Windows.Terminal.SSH" };
static constexpr std::wstring_view PROFILE_TITLE_PREFIX = L"SSH - ";
static constexpr std::wstring_view PROFILE_ICON_PATH = L"ms-appx:///ProfileIcons/{550ce7b8-d500-50ad-8a1a-c400c3262db3}.png";
// OpenSSH is installed under System32 when installed via Optional Features
static constexpr std::wstring_view SSH_EXE_PATH1 = L"%SystemRoot%\\System32\\OpenSSH\\ssh.exe";
// OpenSSH (x86/x64) is installed under Program Files when installed via MSI
static constexpr std::wstring_view SSH_EXE_PATH2 = L"%ProgramFiles%\\OpenSSH\\ssh.exe";
// OpenSSH (x86) is installed under Program Files x86 when installed via MSI on x64 machine
static constexpr std::wstring_view SSH_EXE_PATH3 = L"%ProgramFiles(x86)%\\OpenSSH\\ssh.exe";
static constexpr std::wstring_view SSH_SYSTEM_CONFIG_PATH = L"%ProgramData%\\ssh\\ssh_config";
static constexpr std::wstring_view SSH_USER_CONFIG_PATH = L"%UserProfile%\\.ssh\\config";
static constexpr std::wstring_view SSH_CONFIG_HOST_KEY{ L"Host" };
static constexpr std::wstring_view SSH_CONFIG_HOSTNAME_KEY{ L"HostName" };
using namespace ::Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Settings::Model;
/*static*/ const std::wregex SshHostGenerator::_configKeyValueRegex{ LR"(^\s*(\w+)\s+([^\s]+.*[^\s])\s*$)" };
/*static*/ std::wstring_view SshHostGenerator::_getProfileName(const std::wstring_view& hostName) noexcept
{
return std::wstring_view{ L"" + PROFILE_TITLE_PREFIX + hostName };
}
/*static*/ std::wstring_view SshHostGenerator::_getProfileIconPath() noexcept
{
return PROFILE_ICON_PATH;
}
/*static*/ std::wstring_view SshHostGenerator::_getProfileCommandLine(const std::wstring_view& sshExePath, const std::wstring_view& hostName) noexcept
{
return std::wstring_view{ L"\"" + sshExePath + L"\" " + hostName };
}
/*static*/ bool SshHostGenerator::_tryFindSshExePath(std::wstring& sshExePath) noexcept
{
try
{
for (const auto& path : { SSH_EXE_PATH1, SSH_EXE_PATH2, SSH_EXE_PATH3 })
{
if (std::filesystem::exists(wil::ExpandEnvironmentStringsW<std::wstring>(path.data())))
{
sshExePath = path;
return true;
}
}
}
CATCH_LOG();
return false;
}
/*static*/ bool SshHostGenerator::_tryParseConfigKeyValue(const std::wstring_view& line, std::wstring& key, std::wstring& value) noexcept
{
try
{
if (!line.empty() && !line.starts_with(L"#"))
{
std::wstring input{ line };
std::wsmatch match;
if (std::regex_search(input, match, SshHostGenerator::_configKeyValueRegex))
{
key = match[1];
value = match[2];
return true;
}
}
}
CATCH_LOG();
return false;
}
/*static*/ void SshHostGenerator::_getHostNamesFromConfigFile(const std::wstring_view& configPath, std::vector<std::wstring>& hostNames) noexcept
{
try
{
const std::filesystem::path resolvedConfigPath{ wil::ExpandEnvironmentStringsW<std::wstring>(configPath.data()) };
if (std::filesystem::exists(resolvedConfigPath))
{
std::wifstream inputStream(resolvedConfigPath);
std::wstring line;
std::wstring key;
std::wstring value;
std::wstring lastHost;
while (std::getline(inputStream, line))
{
if (_tryParseConfigKeyValue(line, key, value))
{
if (til::equals_insensitive_ascii(key, SSH_CONFIG_HOST_KEY))
{
// Save potential Host value for later
lastHost = value;
}
else if (til::equals_insensitive_ascii(key, SSH_CONFIG_HOSTNAME_KEY))
{
// HostName was specified
if (!lastHost.empty())
{
hostNames.emplace_back(lastHost);
lastHost = L"";
}
}
}
}
}
}
CATCH_LOG();
}
std::wstring_view SshHostGenerator::GetNamespace() const noexcept
{
return SshHostGeneratorNamespace;
}
// Method Description:
// - Generate a list of profiles for each detected OpenSSH host.
// Arguments:
// - <none>
// Return Value:
// - <A list of SSH host profiles.>
void SshHostGenerator::GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const
{
std::wstring sshExePath;
if (_tryFindSshExePath(sshExePath))
{
std::vector<std::wstring> hostNames;
_getHostNamesFromConfigFile(SSH_SYSTEM_CONFIG_PATH, hostNames);
_getHostNamesFromConfigFile(SSH_USER_CONFIG_PATH, hostNames);
for (const auto& hostName : hostNames)
{
const auto profile{ CreateDynamicProfile(_getProfileName(hostName)) };
profile->Commandline(winrt::hstring{ _getProfileCommandLine(sshExePath, hostName) });
profile->Icon(winrt::hstring{ _getProfileIconPath() });
profiles.emplace_back(profile);
}
}
}

View File

@@ -0,0 +1,40 @@
/*++
Copyright (c) Microsoft Corporation
Licensed under the MIT license.
Module Name:
- SshHostGenerator
Abstract:
- This is the dynamic profile generator for SSH connections. Enumerates all the
SSH hosts to create profiles for them.
Author(s):
- Jon Thysell - September 2022
--*/
#pragma once
#include "IDynamicProfileGenerator.h"
namespace winrt::Microsoft::Terminal::Settings::Model
{
class SshHostGenerator final : public IDynamicProfileGenerator
{
public:
std::wstring_view GetNamespace() const noexcept override;
void GenerateProfiles(std::vector<winrt::com_ptr<implementation::Profile>>& profiles) const override;
private:
static const std::wregex _configKeyValueRegex;
static std::wstring_view _getProfileName(const std::wstring_view& hostName) noexcept;
static std::wstring_view _getProfileIconPath() noexcept;
static std::wstring_view _getProfileCommandLine(const std::wstring_view& sshExePath, const std::wstring_view& hostName) noexcept;
static bool _tryFindSshExePath(std::wstring& sshExePath) noexcept;
static bool _tryParseConfigKeyValue(const std::wstring_view& line, std::wstring& key, std::wstring& value) noexcept;
static void _getHostNamesFromConfigFile(const std::wstring_view& configPath, std::vector<std::wstring>& hostNames) noexcept;
};
};

View File

@@ -138,4 +138,11 @@
</alwaysEnabledBrandingTokens> </alwaysEnabledBrandingTokens>
</feature> </feature>
<feature>
<name>Feature_DynamicSSHProfiles</name>
<description>Enables the dynamic profile generator for OpenSSH config files</description>
<id>9031</id>
<stage>AlwaysDisabled</stage>
</feature>
</featureStaging> </featureStaging>