mirror of
https://github.com/microsoft/terminal.git
synced 2025-12-19 18:11:39 -05:00
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:
1
.github/actions/spelling/allow/names.txt
vendored
1
.github/actions/spelling/allow/names.txt
vendored
@@ -77,6 +77,7 @@ sonpham
|
|||||||
stakx
|
stakx
|
||||||
talo
|
talo
|
||||||
thereses
|
thereses
|
||||||
|
Thysell
|
||||||
Walisch
|
Walisch
|
||||||
WDX
|
WDX
|
||||||
Wellons
|
Wellons
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
161
src/cascadia/TerminalSettingsModel/SshHostGenerator.cpp
Normal file
161
src/cascadia/TerminalSettingsModel/SshHostGenerator.cpp
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/cascadia/TerminalSettingsModel/SshHostGenerator.h
Normal file
40
src/cascadia/TerminalSettingsModel/SshHostGenerator.h
Normal 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;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user