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); } }