Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamically generate profiles from hosts in OpenSSH config files #14042

Merged
merged 10 commits into from
Dec 9, 2022
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
<ClInclude Include="VsDevShellGenerator.h" />
<ClInclude Include="VsSetupConfiguration.h" />
<ClInclude Include="WslDistroGenerator.h" />
<ClInclude Include="SshHostGenerator.h" />
<ClInclude Include="ModelSerializationHelpers.h" />
</ItemGroup>
<!-- ========================= Cpp Files ======================== -->
Expand Down Expand Up @@ -166,6 +167,7 @@
<ClCompile Include="VsDevShellGenerator.cpp" />
<ClCompile Include="VsSetupConfiguration.cpp" />
<ClCompile Include="WslDistroGenerator.cpp" />
<ClCompile Include="SshHostGenerator.cpp" />
<!-- You _NEED_ to include this file and the jsoncpp IncludePath (below) if
you want to use jsoncpp -->
<ClCompile Include="$(OpenConsoleDir)\dep\jsoncpp\jsoncpp.cpp">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
<ClCompile Include="WslDistroGenerator.cpp">
<Filter>profileGeneration</Filter>
</ClCompile>
<ClCompile Include="SshHostGenerator.cpp">
<Filter>profileGeneration</Filter>
</ClCompile>
<ClCompile Include="CascadiaSettings.cpp" />
<ClCompile Include="CascadiaSettingsSerialization.cpp" />
<ClCompile Include="GlobalAppSettings.cpp" />
Expand Down Expand Up @@ -61,6 +64,9 @@
<ClInclude Include="WslDistroGenerator.h">
<Filter>profileGeneration</Filter>
</ClInclude>
<ClInclude Include="SshHostGenerator.h">
<Filter>profileGeneration</Filter>
</ClInclude>
<ClInclude Include="CascadiaSettings.h" />
<ClInclude Include="GlobalAppSettings.h" />
<ClInclude Include="TerminalSettingsSerializationHelpers.h" />
Expand Down
161 changes: 161 additions & 0 deletions src/cascadia/TerminalSettingsModel/SshHostGenerator.cpp
Original file line number Diff line number Diff line change
@@ -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_SYSTEMCONFIG_PATH = L"%ProgramData%\\ssh\\ssh_config";
static constexpr std::wstring_view SSH_USERCONFIG_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_SYSTEMCONFIG_PATH, hostNames);
_getHostNamesFromConfigFile(SSH_USERCONFIG_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 changes: 40 additions & 0 deletions src/cascadia/TerminalSettingsModel/SshHostGenerator.h
Original file line number Diff line number Diff line change
@@ -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;
};
};
7 changes: 7 additions & 0 deletions src/features.xml
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,11 @@
</alwaysEnabledBrandingTokens>
</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>