using Google.Protobuf;
using Grpc.Core;
using AVEVA.Historian.Client.Wcf;
using GrpcHistory = ArchestrA.Grpc.Contract.History;
using GrpcStorage = ArchestrA.Grpc.Contract.Storage;
namespace AVEVA.Historian.Client.Grpc;
///
/// Shared 2023 R2 gRPC authentication handshake. Opens an authenticated History session over an
/// existing and returns the transient client handle used by
/// the Retrieval/Status services. Extracted from so the
/// read, status, and (future) browse/metadata gRPC paths all drive the identical chain:
/// HistoryService.GetInterfaceVersion → StorageService.ValidateClientCredential (token loop) →
/// HistoryService.OpenConnection. The byte payloads (OpenConnection3 v6 request, NTLM token
/// framing) are the proven 2020 protocol and transfer unchanged inside protobuf bytes fields.
///
/// See for the op-routing rationale (the Negotiate loop
/// belongs on StorageService.ValidateClientCredential, NOT HistoryService.ExchangeKey).
///
internal static class HistorianGrpcHandshake
{
///
/// The handles produced by a successful OpenConnection. is the
/// transient uint session token used by StartQuery/GetSystemParameter and the other
/// uint-handle ops. is the storage-session GUID used (formatted
/// uppercase via ) by the string-handle ops
/// (GetTagInfosFromName, GetTagExtendedPropertiesFromName, ExecuteSqlCommand, ...).
///
internal readonly record struct Session(uint ClientHandle, Guid StorageSessionId)
{
/// The storage GUID in the uppercase "D" form the native string-handle ops require.
public string StringHandle => StorageSessionId.ToString("D").ToUpperInvariant();
}
/// Convenience overload for callers that only need the uint client handle.
public static uint OpenAuthenticatedConnection(
HistorianGrpcConnection connection,
HistorianClientOptions options,
CancellationToken cancellationToken)
=> OpenSession(connection, options, cancellationToken).ClientHandle;
///
/// The native Open2 connection mode. Defaults to read-only (0x402); pass
///
/// (0x401) for write-enabled sessions (e.g. the non-streamed/revision Transaction path,
/// which the read-only mode silently rejects with err 132 OperationNotEnabled).
///
public static Session OpenSession(
HistorianGrpcConnection connection,
HistorianClientOptions options,
CancellationToken cancellationToken,
uint connectionMode = HistorianWcfAuthChainHelper.NativeIntegratedReadOnlyConnectionMode)
{
DateTime Deadline() => DateTime.UtcNow.Add(options.RequestTimeout);
Guid contextKey = Guid.NewGuid();
var historyClient = new GrpcHistory.HistoryService.HistoryServiceClient(connection.Channel);
GrpcHistory.GetInterfaceVersionResponse historyVersion = historyClient.GetInterfaceVersion(
new GrpcHistory.GetInterfaceVersionRequest(), connection.Metadata, Deadline(), cancellationToken);
HistorianServerVersionGate.Validate(HistorianServiceInterface.History, historyVersion.UiVersion, options);
var storageClient = new GrpcStorage.StorageService.StorageServiceClient(connection.Channel);
HistorianNativeHandshake.RunTokenRounds(
(handle, wrapped, _) =>
{
GrpcStorage.ValidateClientCredentialResponse response = storageClient.ValidateClientCredential(
new GrpcStorage.ValidateClientCredentialRequest { Handle = handle, InBuff = ByteString.CopyFrom(wrapped) },
connection.Metadata,
Deadline(),
cancellationToken);
byte[] serverOutput = response.OutBuff?.ToByteArray() ?? [];
byte[] error = response.Status?.BtError?.ToByteArray() ?? [];
bool success = response.Status?.BSuccess ?? false;
return new HistorianNativeHandshake.TokenExchangeResult(success, serverOutput, error);
},
contextKey,
options,
cancellationToken);
byte[] open2Request = HistorianNativeHandshake.BuildOpenConnection3Request(
options.Host, contextKey, connectionMode);
GrpcHistory.OpenConnectionResponse open2 = historyClient.OpenConnection(
new GrpcHistory.OpenConnectionRequest { BtConnectionRequest = ByteString.CopyFrom(open2Request) },
connection.Metadata,
Deadline(),
cancellationToken);
byte[] open2Response = open2.BtConnectionResponse?.ToByteArray() ?? [];
if (!(open2.Status?.BSuccess ?? false))
{
byte[] err = open2.Status?.BtError?.ToByteArray() ?? [];
throw new InvalidOperationException($"gRPC OpenConnection failed (errorLen={err.Length}, responseLen={open2Response.Length}).");
}
(uint clientHandle, Guid storageSessionId) = HistorianNativeHandshake.ParseOpenConnectionResponse(open2Response);
return new Session(clientHandle, storageSessionId);
}
}