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; /// /// 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. /// public const uint NativeIntegratedWriteEnabledConnectionMode = 0x401; /// /// 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. /// public static uint OpenAuthenticatedConnection( HistorianClientOptions options, Binding historyBinding, EndpointAddress historyEndpoint, Guid contextKey, CancellationToken cancellationToken, uint connectionMode = NativeIntegratedReadOnlyConnectionMode, Action? additionalSetup = null) { ChannelFactory 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(ChannelFactory factory) { try { if (factory.State == CommunicationState.Faulted) factory.Abort(); else factory.Close(); } catch { try { factory.Abort(); } catch { } } } }