diff --git a/.github/actions/spelling/allow/names.txt b/.github/actions/spelling/allow/names.txt index 1c6ef9a373c..56ae1aa03ac 100644 --- a/.github/actions/spelling/allow/names.txt +++ b/.github/actions/spelling/allow/names.txt @@ -77,6 +77,7 @@ sonpham stakx talo thereses +Thysell Walisch WDX Wellons diff --git a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp index da175280cb0..39124e2f0d5 100644 --- a/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp +++ b/src/cascadia/TerminalSettingsModel/CascadiaSettingsSerialization.cpp @@ -13,6 +13,9 @@ #include "PowershellCoreProfileGenerator.h" #include "VisualStudioGenerator.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. // 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(AzureCloudShellGenerator{}); _executeGenerator(VisualStudioGenerator{}); +#if TIL_FEATURE_DYNAMICSSHPROFILES_ENABLED + _executeGenerator(SshHostGenerator{}); +#endif } // A new settings.json gets a special treatment: diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj index a1aaf301ce8..6231c3664a9 100644 --- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj +++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj @@ -92,6 +92,7 @@ + @@ -166,6 +167,7 @@ + diff --git a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters index fb21fccdc31..3a4aab82329 100644 --- a/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters +++ b/src/cascadia/TerminalSettingsModel/Microsoft.Terminal.Settings.ModelLib.vcxproj.filters @@ -18,6 +18,9 @@ profileGeneration + + profileGeneration + @@ -61,6 +64,9 @@ profileGeneration + + profileGeneration + diff --git a/src/cascadia/TerminalSettingsModel/SshHostGenerator.cpp b/src/cascadia/TerminalSettingsModel/SshHostGenerator.cpp new file mode 100644 index 00000000000..30dc19039f0 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/SshHostGenerator.cpp @@ -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(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& hostNames) noexcept +{ + try + { + const std::filesystem::path resolvedConfigPath{ wil::ExpandEnvironmentStringsW(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: +// - +// Return Value: +// - +void SshHostGenerator::GenerateProfiles(std::vector>& profiles) const +{ + std::wstring sshExePath; + if (_tryFindSshExePath(sshExePath)) + { + std::vector 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); + } + } +} diff --git a/src/cascadia/TerminalSettingsModel/SshHostGenerator.h b/src/cascadia/TerminalSettingsModel/SshHostGenerator.h new file mode 100644 index 00000000000..a355b0c4302 --- /dev/null +++ b/src/cascadia/TerminalSettingsModel/SshHostGenerator.h @@ -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>& 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& hostNames) noexcept; + }; +}; diff --git a/src/features.xml b/src/features.xml index 9d3c36cf7d3..e26c217d3bf 100644 --- a/src/features.xml +++ b/src/features.xml @@ -138,4 +138,11 @@ + + Feature_DynamicSSHProfiles + Enables the dynamic profile generator for OpenSSH config files + 9031 + AlwaysDisabled + +