mirror of
https://github.com/esiur/esiur-dotnet.git
synced 2026-07-30 17:30:40 +00:00
Esiur CLI
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
using Esiur.CLI.Authentication;
|
||||
using Esiur.CLI.Configuration;
|
||||
using Esiur.Protocol;
|
||||
using Esiur.Resource;
|
||||
using Esiur.Security.Authority;
|
||||
using Esiur.Security.Authority.Providers;
|
||||
|
||||
namespace Esiur.CLI.Client;
|
||||
|
||||
public interface IEsiurSessionFactory
|
||||
{
|
||||
Task<EsiurSession> ConnectAsync(
|
||||
ResolvedConnection settings,
|
||||
bool passwordFromStandardInput,
|
||||
TextReader input,
|
||||
TextWriter error,
|
||||
CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public sealed class EsiurSessionFactory(ICredentialService credentials) : IEsiurSessionFactory
|
||||
{
|
||||
public async Task<EsiurSession> ConnectAsync(
|
||||
ResolvedConnection settings,
|
||||
bool passwordFromStandardInput,
|
||||
TextReader input,
|
||||
TextWriter error,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var warehouse = new Warehouse();
|
||||
byte[]? password = null;
|
||||
try
|
||||
{
|
||||
var profile = settings.Profile ?? new ConnectionProfile
|
||||
{
|
||||
Name = settings.DisplayName,
|
||||
Endpoint = settings.Endpoint,
|
||||
Provider = settings.Provider,
|
||||
Identity = settings.Identity,
|
||||
Domain = settings.Domain ?? string.Empty,
|
||||
};
|
||||
var context = new EpConnectionContext
|
||||
{
|
||||
AutoReconnect = false,
|
||||
AuthenticationTimeout = settings.Timeout,
|
||||
Domain = settings.Domain ?? string.Empty,
|
||||
};
|
||||
|
||||
if (PromptCredentialService.IsPasswordProvider(settings.Provider))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(settings.Identity))
|
||||
throw new CliException("Password authentication requires an identity.", ExitCodes.InvalidArguments);
|
||||
password = await credentials.GetPasswordAsync(
|
||||
profile, passwordFromStandardInput, input, error, cancellationToken);
|
||||
warehouse.RegisterAuthenticationProvider(
|
||||
new PasswordClientProvider(settings.Identity, password!));
|
||||
context.AuthenticationMode = AuthenticationMode.InitializerIdentity;
|
||||
context.AuthenticationProtocol = PasswordAuthenticationProvider.ProtocolName;
|
||||
context.Identity = settings.Identity;
|
||||
}
|
||||
|
||||
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
if (settings.Timeout > TimeSpan.Zero) timeout.CancelAfter(settings.Timeout);
|
||||
var endpoint = EndpointParser.ConnectionEndpoint(settings.Endpoint);
|
||||
var connectTask = AwaitReply(warehouse.Get<EpConnection>(endpoint, context), timeout.Token);
|
||||
var connection = await connectTask;
|
||||
if (connection is null)
|
||||
throw new CliException($"Could not connect to {endpoint}.", ExitCodes.ConnectionFailed);
|
||||
return new EsiurSession(warehouse, connection, settings);
|
||||
}
|
||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try { await warehouse.Close(); } catch { }
|
||||
throw new CliException("The connection timed out.", ExitCodes.Timeout);
|
||||
}
|
||||
catch (CliException)
|
||||
{
|
||||
try { await warehouse.Close(); } catch { }
|
||||
throw;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
try { await warehouse.Close(); } catch { }
|
||||
var authentication = PromptCredentialService.IsPasswordProvider(settings.Provider)
|
||||
&& exception.Message.Contains("auth", StringComparison.OrdinalIgnoreCase);
|
||||
throw new CliException(
|
||||
authentication ? "Authentication failed." : $"Connection failed: {exception.Message}",
|
||||
authentication ? ExitCodes.AuthenticationFailed : ExitCodes.ConnectionFailed,
|
||||
exception);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (password is not null) Array.Clear(password, 0, password.Length);
|
||||
}
|
||||
}
|
||||
|
||||
internal static Task<T> AwaitReply<T>(Esiur.Core.AsyncReply<T> reply, CancellationToken cancellationToken)
|
||||
{
|
||||
var completion = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
reply.Then(value => completion.TrySetResult((T)value))
|
||||
.Error(exception => completion.TrySetException(exception));
|
||||
var registration = cancellationToken.Register(() => completion.TrySetCanceled(cancellationToken));
|
||||
_ = completion.Task.ContinueWith(_ => registration.Dispose(), TaskScheduler.Default);
|
||||
return completion.Task;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EsiurSession : IAsyncDisposable
|
||||
{
|
||||
internal EsiurSession(Warehouse warehouse, EpConnection connection, ResolvedConnection settings)
|
||||
{
|
||||
Warehouse = warehouse;
|
||||
Connection = connection;
|
||||
Settings = settings;
|
||||
}
|
||||
|
||||
public Warehouse Warehouse { get; }
|
||||
public EpConnection Connection { get; }
|
||||
public ResolvedConnection Settings { get; }
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
try { Connection.Destroy(); } catch { }
|
||||
try { await Warehouse.Close(); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
public static class EndpointParser
|
||||
{
|
||||
public static string ConnectionEndpoint(string endpoint)
|
||||
{
|
||||
ConfigurationResolver.ValidateEndpoint(endpoint);
|
||||
var uri = new Uri(endpoint);
|
||||
return $"ep://{uri.Authority}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user