Skip to content

Commit

Permalink
Support configuring the client in code using ssh_config options.
Browse files Browse the repository at this point in the history
  • Loading branch information
tmds committed Sep 19, 2024
1 parent 1d63109 commit 3b62d4f
Show file tree
Hide file tree
Showing 12 changed files with 540 additions and 328 deletions.
5 changes: 3 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
<ItemGroup>
<!-- Tmds.Ssh dependencies -->
<PackageVersion Include="BouncyCastle.Cryptography" Version="2.4.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<!-- Test dependencies -->
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageVersion Include="xunit" Version="2.9.0" />
Expand All @@ -13,6 +12,8 @@
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="Tmds.ExecFunction" Version="0.7.1" />
<!-- Example dependencies -->
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageVersion Include="Azure.Identity" Version="1.12.0" />
<PackageVersion Include="Azure.Security.KeyVault.Keys" Version="4.6.0" />
</ItemGroup>
Expand Down
55 changes: 51 additions & 4 deletions examples/ssh/Program.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,34 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

namespace Tmds.Ssh;

class Program
{
static async Task Main(string[] args)
static Task<int> Main(string[] args)
{
var destinationArg = new Argument<string>(name: "destination", () => "localhost")
{ Arity = ArgumentArity.ZeroOrOne };
var commandArg = new Argument<string>(name: "command", () => "echo 'hello world'")
{ Arity = ArgumentArity.ZeroOrOne };
var sshConfigOptions = new Option<string[]>(new[] { "-o", "--option" },
description: $"Set an SSH Config option, for example: [email protected].{Environment.NewLine}Supported options: {string.Join(", ", Enum.GetValues<SshConfigOption>().Select(o => o.ToString()))}.")
{ Arity = ArgumentArity.ZeroOrMore };

var rootCommand = new RootCommand("Execute a command on a remote system over SSH.");
rootCommand.AddOption(sshConfigOptions);
rootCommand.AddArgument(destinationArg);
rootCommand.AddArgument(commandArg);
rootCommand.SetHandler(ExecuteAsync, destinationArg, commandArg, sshConfigOptions);

return rootCommand.InvokeAsync(args);
}

static async Task ExecuteAsync(string destination, string command, string[] options)
{
bool trace = IsEnvvarTrue("TRACE");
bool log = trace || IsEnvvarTrue("LOG");
Expand All @@ -21,10 +43,9 @@ static async Task Main(string[] args)
}
});

string destination = args.Length >= 1 ? args[0] : "localhost";
string command = args.Length >= 2 ? args[1] : "echo 'hello world'";
SshConfigOptions configOptions = CreateSshConfigOptions(options);

using SshClient client = new SshClient(destination, loggerFactory);
using SshClient client = new SshClient(destination, configOptions, loggerFactory);

using var process = await client.ExecuteAsync(command);
Task[] tasks = new[]
Expand Down Expand Up @@ -73,6 +94,32 @@ static void PrintExceptions(Task[] tasks)
}
}

private static SshConfigOptions CreateSshConfigOptions(string[] options)
{
SshConfigOptions configOptions = new SshConfigOptions(SshConfigOptions.DefaultConfigFilePaths);

Dictionary<SshConfigOption, SshConfigOptionValue> optionsDict = new();
foreach (var option in options)
{
string[] split = option.Split('=', 2);
if (split.Length != 2)
{
throw new ArgumentException($"Option '{option}' is not in the <Key>=<Value> format.");
}
if (Enum.TryParse<SshConfigOption>(split[0], ignoreCase: true, out var key))
{
optionsDict[key] = split[1];
}
else
{
throw new ArgumentException($"Unsupported option: {option}.");
}
}
configOptions.Options = optionsDict;

return configOptions;
}

static bool IsEnvvarTrue(string variableName)
{
string? value = Environment.GetEnvironmentVariable(variableName);
Expand Down
1 change: 1 addition & 0 deletions examples/ssh/ssh.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Console" />
<PackageReference Include="System.CommandLine" />
</ItemGroup>

</Project>
8 changes: 1 addition & 7 deletions src/Tmds.Ssh/SshClientSettings.SshConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,9 @@ partial class SshClientSettings
AlgorithmNames.Password
];

internal static ValueTask<SshClientSettings> LoadFromConfigAsync(string destination, SshConfigOptions options, CancellationToken cancellationToken = default)
{
(string? userName, string host, int? port) = ParseDestination(destination);
return LoadFromConfigAsync(userName, host, port, options, cancellationToken);
}

internal static async ValueTask<SshClientSettings> LoadFromConfigAsync(string? userName, string host, int? port, SshConfigOptions options, CancellationToken cancellationToken = default)
{
SshConfig sshConfig = await SshConfig.DetermineConfigForHost(userName, host, port, options.ConfigFilePaths, cancellationToken);
SshConfig sshConfig = await SshConfig.DetermineConfigForHost(userName, host, port, options.Options, options.ConfigFilePaths, cancellationToken);

List<Name> ciphers = DetermineAlgorithms(sshConfig.Ciphers, DefaultEncryptionAlgorithms, SupportedEncryptionAlgorithms);
List<Name> hostKeyAlgorithms = DetermineAlgorithms(sshConfig.HostKeyAlgorithms, DefaultServerHostKeyAlgorithms, SupportedServerHostKeyAlgorithms);
Expand Down
Loading

0 comments on commit 3b62d4f

Please sign in to comment.