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();
}