using Grpc.Core; using Grpc.Net.Client; using Microsoft.Extensions.Logging; using MxGateway.Contracts.Proto; using Polly; using System.Net.Http; using System.Net.Security; using System.Security.Cryptography.X509Certificates; namespace MxGateway.Client; /// /// Provides the .NET client entry point for the public MXAccess Gateway gRPC API. /// public sealed class MxGatewayClient : IAsyncDisposable { private readonly GrpcChannel _channel; private readonly IMxGatewayClientTransport _transport; private readonly ResiliencePipeline _safeUnaryRetryPipeline; private bool _disposed; internal MxGatewayClient( MxGatewayClientOptions options, IMxGatewayClientTransport transport) { ArgumentNullException.ThrowIfNull(options); options.Validate(); Options = options; _transport = transport ?? throw new ArgumentNullException(nameof(transport)); _safeUnaryRetryPipeline = MxGatewayClientRetryPolicy.Create( options.Retry, options.LoggerFactory?.CreateLogger()); _channel = null!; } private MxGatewayClient( GrpcChannel channel, IMxGatewayClientTransport transport) { _channel = channel; _transport = transport; Options = transport.Options; _safeUnaryRetryPipeline = MxGatewayClientRetryPolicy.Create( Options.Retry, Options.LoggerFactory?.CreateLogger()); } public MxGatewayClientOptions Options { get; } public MxAccessGateway.MxAccessGatewayClient RawClient => _transport.RawClient ?? throw new InvalidOperationException("The raw generated gRPC client is not available for this client instance."); public static MxGatewayClient Create(MxGatewayClientOptions options) { ArgumentNullException.ThrowIfNull(options); options.Validate(); HttpMessageHandler handler = CreateHttpHandler(options); var channel = GrpcChannel.ForAddress( options.Endpoint, new GrpcChannelOptions { HttpHandler = handler, LoggerFactory = options.LoggerFactory, }); return new MxGatewayClient( channel, new GrpcMxGatewayClientTransport( options, new MxAccessGateway.MxAccessGatewayClient(channel))); } public async Task OpenSessionAsync( OpenSessionRequest? request = null, CancellationToken cancellationToken = default) { OpenSessionReply reply = await OpenSessionRawAsync( request ?? new OpenSessionRequest(), cancellationToken) .ConfigureAwait(false); return new MxGatewaySession(this, reply); } public Task OpenSessionRawAsync( OpenSessionRequest request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); ThrowIfDisposed(); return _transport.OpenSessionAsync(request, CreateCallOptions(cancellationToken)); } public Task CloseSessionRawAsync( CloseSessionRequest request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); ThrowIfDisposed(); return ExecuteSafeUnaryAsync( token => _transport.CloseSessionAsync(request, CreateCallOptions(token)), cancellationToken); } public Task InvokeAsync( MxCommandRequest request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); ThrowIfDisposed(); if (MxGatewayClientRetryPolicy.IsRetryableCommand(request.Command?.Kind ?? MxCommandKind.Unspecified)) { return ExecuteSafeUnaryAsync( token => _transport.InvokeAsync(request, CreateCallOptions(token)), cancellationToken); } return _transport.InvokeAsync(request, CreateCallOptions(cancellationToken)); } public IAsyncEnumerable StreamEventsAsync( StreamEventsRequest request, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); ThrowIfDisposed(); return _transport.StreamEventsAsync(request, CreateStreamCallOptions(cancellationToken)); } public ValueTask DisposeAsync() { if (_disposed) { return ValueTask.CompletedTask; } _disposed = true; _channel?.Dispose(); return ValueTask.CompletedTask; } internal CallOptions CreateCallOptions(CancellationToken cancellationToken) { return CreateCallOptions(cancellationToken, Options.DefaultCallTimeout); } internal CallOptions CreateStreamCallOptions(CancellationToken cancellationToken) { return CreateCallOptions(cancellationToken, Options.StreamTimeout); } internal CallOptions CreateCallOptions( CancellationToken cancellationToken, TimeSpan? timeout) { Metadata headers = new() { { "authorization", $"Bearer {Options.ApiKey}" }, }; return new CallOptions( headers, timeout is null ? null : DateTime.UtcNow.Add(timeout.Value), cancellationToken); } private async Task ExecuteSafeUnaryAsync( Func> call, CancellationToken cancellationToken) { using CancellationTokenSource timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeout.CancelAfter(Options.DefaultCallTimeout); return await _safeUnaryRetryPipeline.ExecuteAsync( async token => await call(token).ConfigureAwait(false), timeout.Token) .ConfigureAwait(false); } private static HttpMessageHandler CreateHttpHandler(MxGatewayClientOptions options) { SocketsHttpHandler handler = new() { ConnectTimeout = options.ConnectTimeout, }; if (options.UseTls) { handler.SslOptions = new SslClientAuthenticationOptions(); if (!string.IsNullOrWhiteSpace(options.ServerNameOverride)) { handler.SslOptions.TargetHost = options.ServerNameOverride; } if (!string.IsNullOrWhiteSpace(options.CaCertificatePath)) { X509Certificate2 trustedRoot = X509CertificateLoader.LoadCertificateFromFile(options.CaCertificatePath); handler.SslOptions.RemoteCertificateValidationCallback = (_, certificate, chain, errors) => { if (certificate is null) { return false; } using X509Chain customChain = new(); customChain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; customChain.ChainPolicy.CustomTrustStore.Add(trustedRoot); customChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; customChain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag; X509Certificate2 certificateToValidate = certificate as X509Certificate2 ?? X509CertificateLoader.LoadCertificate(certificate.Export(X509ContentType.Cert)); return customChain.Build(certificateToValidate); }; } } return handler; } private void ThrowIfDisposed() { ObjectDisposedException.ThrowIf(_disposed, this); } }