using System.Net.Security; using System.Security.Cryptography.X509Certificates; using Grpc.Core; using Grpc.Net.Client; using Grpc.Net.Client.Web; namespace AVEVA.Historian.Client.Grpc; /// /// Builds a for the 2023 R2 Historian Client Access Point, /// replicating the stock Archestra.Historian.GrpcClient.GrpcClientBase.InitializeBase /// transport shape: gRPC-Web (binary) over HTTP/1.1, optional TLS with an /// untrusted-certificate bypass, and gzip request encoding. /// internal static class HistorianGrpcChannelFactory { /// /// Resolves the effective gRPC port: when the caller left /// at the WCF default (32568), the 2023 R2 gRPC default (32565) is substituted; otherwise the /// explicit value is honoured. /// internal static int ResolvePort(HistorianClientOptions options) => options.Port == HistorianClientOptions.DefaultPort ? HistorianClientOptions.DefaultGrpcPort : options.Port; /// /// Builds the channel address. TLS uses https://{ServerDnsIdentity|Host}:{port} (the /// DNS-identity override lets the URL match the server certificate name when connecting by IP); /// plaintext uses http://{Host}:{port}. /// internal static string ResolveAddress(HistorianClientOptions options) { int port = ResolvePort(options); if (options.GrpcUseTls) { string tlsHost = !string.IsNullOrEmpty(options.ServerDnsIdentity) ? options.ServerDnsIdentity! : options.Host; return $"https://{tlsHost}:{port}"; } return $"http://{options.Host}:{port}"; } public static HistorianGrpcConnection Create(HistorianClientOptions options) { string address = ResolveAddress(options); var httpHandler = new HttpClientHandler(); if (options.AllowUntrustedServerCertificate) { httpHandler.ServerCertificateCustomValidationCallback = AcceptAnyCertificate; } // gRPC-Web binary mode over HTTP/1.1 — matches the stock client (GrpcWebMode.GrpcWeb, // HttpVersion 1.1). The 2023 R2 HCAP endpoint speaks gRPC-Web, not bare HTTP/2 gRPC. var webHandler = new GrpcWebHandler(GrpcWebMode.GrpcWeb, httpHandler) { HttpVersion = new Version(1, 1) }; var channelOptions = new GrpcChannelOptions { HttpHandler = webHandler }; GrpcChannel channel = GrpcChannel.ForAddress(address, channelOptions); // The stock client always advertises gzip request encoding; honour the option so // bandwidth-limited links can disable it. var metadata = new Metadata(); if (options.Compression) { metadata.Add("grpc-internal-encoding-request", "gzip"); } return new HistorianGrpcConnection(channel, metadata); } private static bool AcceptAnyCertificate( HttpRequestMessage request, X509Certificate2? certificate, X509Chain? chain, SslPolicyErrors errors) => true; } /// A live gRPC channel plus the per-call metadata header set. internal sealed class HistorianGrpcConnection(GrpcChannel channel, Metadata metadata) : IDisposable { public GrpcChannel Channel { get; } = channel; public Metadata Metadata { get; } = metadata; public void Dispose() => Channel.Dispose(); }