1e9a87fce9
Stands up HistorianTransport.RemoteGrpc end-to-end for the read path, built on the recovered 2023 R2 gRPC contract (gRPC-Web/HTTP-1.1, port 32565, gzip). The opaque protobuf `bytes` fields carry the SAME native binary payloads as the 2020 WCF/MDAS path, so the proven serializers and parsers are reused unchanged. - Grpc/Protos/*.proto: 6 protoc-validated contracts recovered from embedded FileDescriptors (authoritative, not guessed). - Grpc/HistorianGrpcChannelFactory: GrpcWebHandler/HTTP-1.1 channel, ResolvePort/ResolveAddress, optional TLS + gzip. - Grpc/HistorianGrpcReadOrchestrator: mirrors the WCF read chain over gRPC; auth uses HistoryService.ExchangeKey (the gRPC ValCl op). - Wcf/HistorianNativeHandshake: transport-agnostic Open2 request builder + SSPI/Negotiate token loop + response decode, shared by WCF and gRPC. - Op map (2020 -> gRPC): ValCl->ExchangeKey, Open2->OpenConnection, StartQuery2->StartQuery, GetNextQueryResultBuffer2->GetNextQueryResultBuffer. - HistorianClientOptions: DefaultGrpcPort=32565, GrpcUseTls. - csproj: Google.Protobuf, Grpc.Net.Client(.Web), Grpc.Tools codegen. Not yet live-verified against a 2023 R2 server: ExchangeKey is the first thing to revisit if a live server rejects the handshake; the inner byte payloads are the proven 2020 protocol. Gated live test via HISTORIAN_GRPC_HOST. 188 unit tests green; build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
116 lines
4.8 KiB
C#
116 lines
4.8 KiB
C#
using System.ServiceModel;
|
||
using System.ServiceModel.Channels;
|
||
using AVEVA.Historian.Client.Wcf.Contracts;
|
||
|
||
namespace AVEVA.Historian.Client.Wcf;
|
||
|
||
internal static class HistorianWcfAuthChainHelper
|
||
{
|
||
private const int OpenConnection3MinResponseLength = 5;
|
||
public const uint NativeIntegratedReadOnlyConnectionMode = 0x402;
|
||
public const uint NativeIntegratedEventConnectionMode = 0x501;
|
||
/// <summary>
|
||
/// Process + write-enabled + integrated security. Per native ilspy
|
||
/// (HistorianAccessUtil.SetConnectionMode): Process=1, OR 0x400 for integratedSecurity.
|
||
/// EnsT2 and DelT silently return false with err code 132 (OperationNotEnabled) when
|
||
/// Open2 is opened with 0x402 (read-only); 0x401 unlocks write capability.
|
||
/// </summary>
|
||
public const uint NativeIntegratedWriteEnabledConnectionMode = 0x401;
|
||
|
||
/// <summary>
|
||
/// Runs Hist.GetV → Hist.ValCl × N → Hist.Open2 against the configured /Hist endpoint and
|
||
/// returns the transient /Retr client handle decoded from the OpenConnection3 response.
|
||
/// Caller is responsible for opening the matching /Retr channel.
|
||
/// </summary>
|
||
public static uint OpenAuthenticatedConnection(
|
||
HistorianClientOptions options,
|
||
Binding historyBinding,
|
||
EndpointAddress historyEndpoint,
|
||
Guid contextKey,
|
||
CancellationToken cancellationToken,
|
||
uint connectionMode = NativeIntegratedReadOnlyConnectionMode,
|
||
Action<IHistoryServiceContract2, OpenConnectionContext>? additionalSetup = null)
|
||
{
|
||
ChannelFactory<IHistoryServiceContract2> historyFactory = new(historyBinding, historyEndpoint);
|
||
HistorianWcfClientCredentialsHelper.Configure(historyFactory, options);
|
||
historyFactory.Endpoint.EndpointBehaviors.Add(new HistorianWcfHistAddressingBehavior());
|
||
if (HistorianWcfMessageCaptureBehavior.IsEnabled)
|
||
{
|
||
historyFactory.Endpoint.EndpointBehaviors.Add(new HistorianWcfMessageCaptureBehavior());
|
||
}
|
||
|
||
try
|
||
{
|
||
IHistoryServiceContract2 historyChannel = historyFactory.CreateChannel();
|
||
ICommunicationObject historyChannelCo = (ICommunicationObject)historyChannel;
|
||
try
|
||
{
|
||
historyChannel.GetInterfaceVersion(out _);
|
||
RunValClRounds(historyChannel, contextKey, options, cancellationToken);
|
||
|
||
byte[] open2Request = HistorianNativeHandshake.BuildOpenConnection3Request(options.Host, contextKey, connectionMode);
|
||
bool open2Success = historyChannel.OpenConnection2(ref open2Request, out byte[] open2Response, out byte[] open2Error);
|
||
open2Response ??= [];
|
||
open2Error ??= [];
|
||
if (!open2Success || open2Response.Length < OpenConnection3MinResponseLength)
|
||
{
|
||
throw new InvalidOperationException(
|
||
$"Open2 failed (Success={open2Success}, ResponseLen={open2Response.Length}, ErrorLen={open2Error.Length}).");
|
||
}
|
||
|
||
(uint clientHandle, Guid storageSessionId) = HistorianNativeHandshake.ParseOpenConnectionResponse(open2Response);
|
||
|
||
if (additionalSetup is not null)
|
||
{
|
||
additionalSetup(historyChannel, new OpenConnectionContext(contextKey, clientHandle, storageSessionId));
|
||
}
|
||
|
||
return clientHandle;
|
||
}
|
||
finally
|
||
{
|
||
CloseChannelSafely(historyChannelCo);
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
CloseFactorySafely(historyFactory);
|
||
}
|
||
}
|
||
|
||
public readonly record struct OpenConnectionContext(Guid ContextKey, uint ClientHandle, Guid StorageSessionId);
|
||
|
||
private static void RunValClRounds(IHistoryServiceContract2 channel, Guid contextKey, HistorianClientOptions options, CancellationToken cancellationToken)
|
||
{
|
||
HistorianNativeHandshake.RunTokenRounds(
|
||
(handle, wrapped, _) =>
|
||
{
|
||
bool serverSuccess = channel.ValidateClientCredential(handle, wrapped, out byte[] serverOutput, out byte[] errorBuffer);
|
||
return new HistorianNativeHandshake.TokenExchangeResult(serverSuccess, serverOutput ?? [], errorBuffer ?? []);
|
||
},
|
||
contextKey,
|
||
options,
|
||
cancellationToken);
|
||
}
|
||
|
||
private static void CloseChannelSafely(ICommunicationObject channel)
|
||
{
|
||
try
|
||
{
|
||
if (channel.State == CommunicationState.Faulted) channel.Abort();
|
||
else channel.Close();
|
||
}
|
||
catch { try { channel.Abort(); } catch { } }
|
||
}
|
||
|
||
private static void CloseFactorySafely<TChannel>(ChannelFactory<TChannel> factory)
|
||
{
|
||
try
|
||
{
|
||
if (factory.State == CommunicationState.Faulted) factory.Abort();
|
||
else factory.Close();
|
||
}
|
||
catch { try { factory.Abort(); } catch { } }
|
||
}
|
||
}
|