using System.Buffers.Binary; using System.Diagnostics; using System.Runtime.Versioning; using System.ServiceModel; using System.ServiceModel.Channels; using AVEVA.Historian.Client.Wcf.Contracts; namespace AVEVA.Historian.Client.Wcf; [SupportedOSPlatform("windows")] internal static class HistorianWcfAuthChainHelper { private const int OpenConnection3MinResponseLength = 5; private const int CredentialBlockSizeBytes = 1026; private const int MaxValClRounds = 8; private const string ClientNodeNameFallback = "AVEVA.Historian.Client"; private const string ClientDataSourceId = "2020.406.2652.2"; private const string ClientDllVersionString = "2020.406.2652.2"; private const byte NativeClientType = 4; 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; private const byte NativeClientCommonInfoFormatVersion = 4; private const ushort NativeHcalVersion = 17; private const uint NativeClientVersionInt = 999_999; private const ushort NativeOpen2ClientVersion = 9; /// /// 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); 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 = 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 = BinaryPrimitives.ReadUInt32LittleEndian(open2Response.AsSpan(1, 4)); Guid storageSessionId = open2Response.Length >= 21 ? new Guid(open2Response.AsSpan(5, 16)) : Guid.Empty; 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) { using HistorianSspiClient sspi = options.IntegratedSecurity ? new HistorianSspiClient(options.TargetSpn) : new HistorianSspiClient(options.TargetSpn, ParseDomain(options.UserName), ParseUserName(options.UserName), options.Password); string handle = contextKey.ToString("D").ToUpperInvariant(); byte[] incoming = []; for (int round = 0; round < MaxValClRounds; round++) { cancellationToken.ThrowIfCancellationRequested(); HistorianSspiStepResult step = sspi.Next(incoming); byte[] outgoing = step.Token; HistorianWcfAuthenticationProtocol.TryApplyNativeNtlmNegotiateVersionFlag(outgoing); byte[] wrapped = HistorianWcfAuthenticationProtocol.WrapValidateClientCredentialToken(round == 0, outgoing); bool serverSuccess = channel.ValidateClientCredential(handle, wrapped, out byte[] serverOutput, out byte[] errorBuffer); serverOutput ??= []; errorBuffer ??= []; if (!serverSuccess) { throw new InvalidOperationException($"ValCl round {round} rejected (errorLen={errorBuffer.Length})."); } ValidateClientCredentialResponse? response = HistorianWcfAuthenticationProtocol.TryReadValidateClientCredentialResponse(serverOutput); if (response is null || !response.Continue) { return; } incoming = response.Token; if (step.IsCompleted && incoming.Length == 0) { return; } } throw new InvalidOperationException($"ValCl exceeded {MaxValClRounds} rounds without terminal success."); } private static string ParseDomain(string userName) { if (string.IsNullOrEmpty(userName)) return string.Empty; int slash = userName.IndexOf('\\'); return slash > 0 ? userName[..slash] : string.Empty; } private static string ParseUserName(string userName) { if (string.IsNullOrEmpty(userName)) return string.Empty; int slash = userName.IndexOf('\\'); return slash > 0 ? userName[(slash + 1)..] : userName; } private static byte[] BuildOpenConnection3Request(string host, Guid contextKey, uint connectionMode) { Process current = Process.GetCurrentProcess(); string machineName = Environment.MachineName; string processName = string.IsNullOrEmpty(current.ProcessName) ? ClientNodeNameFallback : current.ProcessName; _ = host; // host reserved for remote-orchestrator extension HistorianOpen2Request open2 = new( HostName: machineName, ProcessName: string.Empty, ProcessId: checked((uint)current.Id), UserName: string.Empty, Password: [], ClientType: NativeClientType, ClientVersion: NativeOpen2ClientVersion, ConnectionMode: connectionMode, MetadataNamespace: HistorianMetadataNamespace.Empty); HistorianClientCommonInfo commonInfo = new( FormatVersion: NativeClientCommonInfoFormatVersion, ServerNodeName: machineName, ClientNodeName: processName, ProcessId: checked((uint)current.Id), HcalVersion: NativeHcalVersion, ProcessName: string.Empty, Proxy: string.Empty, DataSourceId: ClientDataSourceId, ShardId: Guid.Empty, ClientVersion: NativeClientVersionInt, ClientTimestamp: (ulong)DateTime.UtcNow.ToFileTimeUtc(), ClientDllVersion: ClientDllVersionString); return HistorianOpen2Protocol.SerializeNativeOpenConnection3Version6( open2, commonInfo, contextKey, credentialBlock: new byte[CredentialBlockSizeBytes]); } 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 { } } } }