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 { } }
}
}