using System.Buffers.Binary; using System.Text; namespace AVEVA.Historian.Client.Wcf; internal static class HistorianOpen2Protocol { public static byte[] SerializeLegacyVersion1(HistorianOpen2Request request) { using MemoryStream stream = new(); using BinaryWriter writer = new(stream, Encoding.Unicode, leaveOpen: true); writer.Write((ushort)1); WriteHistorianString(writer, request.HostName); WriteHistorianString(writer, request.ProcessName); writer.Write(request.ProcessId); WriteHistorianString(writer, request.UserName); writer.Write((uint)request.Password.Length); writer.Write(request.Password); writer.Write(request.ClientType); writer.Write(request.ClientVersion); writer.Write(request.ConnectionMode); WriteMetadataNamespace(writer, request.MetadataNamespace); return stream.ToArray(); } public static byte[] SerializeNativeVersion3(HistorianOpen2Request request, HistorianClientCommonInfo commonInfo) { using MemoryStream stream = new(); using BinaryWriter writer = new(stream, Encoding.Unicode, leaveOpen: true); writer.Write((byte)3); WriteNativeOpenConnectionContent(writer, request, commonInfo); return stream.ToArray(); } public static byte[] SerializeNativeOpenConnection3Version6( HistorianOpen2Request request, HistorianClientCommonInfo commonInfo, Guid clientKey, byte[]? credentialBlock = null) { using MemoryStream stream = new(); using BinaryWriter writer = new(stream, Encoding.Unicode, leaveOpen: true); writer.Write((byte)6); writer.Write(clientKey.ToByteArray()); writer.Write((byte)0); WriteNativeOpenConnectionContent(writer, request, commonInfo, credentialBlock, useCompactMetadataNamespace: true); return stream.ToArray(); } /// /// Builds the native 2023 R2 version-8 OpenConnection request — the format the stock client /// uses, which (unlike v6) carries a byte (Event vs Process). The /// tail is the same v6 emits; the head is the v8 layout /// decoded from a live capture (docs/reverse-engineering/grpc-event-query-capture.md): version 8, /// two marker bytes, the client-key GUID, a username HistorianString, a length-prefixed /// credential token, then ClientType / ConnectionType / flag / a constant word / compact metadata /// namespace / two empty strings. The credential token is normally an ECDH-derived blob in the /// native cert path; this serializer sends it as supplied (zeroed for the Negotiate-auth path, /// mirroring how the v6 request already sends a zeroed credential block and relies on the separate /// ValidateClientCredential handshake for authentication). /// public static byte[] SerializeNativeOpenConnectionVersion8( HistorianClientCommonInfo commonInfo, Guid clientKey, byte connectionType, byte connectionFlag, string userName, byte[] credentialToken) { using MemoryStream stream = new(); using BinaryWriter writer = new(stream, Encoding.Unicode, leaveOpen: true); writer.Write((byte)8); // [0] format version 8 writer.Write((byte)0xF0); // [1] constant marker writer.Write(new byte[19]); // [2..20] zero padding writer.Write((byte)1); // [21] constant marker writer.Write(clientKey.ToByteArray()); // [22..37] per-session client key WriteHistorianString(writer, userName); // username (UTF-16 HistorianString) writer.Write((ushort)credentialToken.Length); // credential-token length (u16) writer.Write(credentialToken); // credential token (zeroed for the Negotiate path) writer.Write((byte)4); // ClientType = 4 writer.Write(connectionType); // ConnectionType (Event = 1 / Process = 2) writer.Write(connectionFlag); // type flag (Event = 1 / Process = 0) writer.Write((ushort)3); // constant word observed at [97..98] WriteCompactMetadataNamespace(writer, HistorianMetadataNamespace.Empty); WriteHistorianString(writer, string.Empty); WriteHistorianString(writer, string.Empty); WriteClientCommonInfo(writer, commonInfo); // tail: FormatVersion 3 => no ClientDllVersion writer.Write(0u); // trailing terminator return stream.ToArray(); } private static void WriteNativeOpenConnectionContent( BinaryWriter writer, HistorianOpen2Request request, HistorianClientCommonInfo commonInfo, byte[]? credentialBlock = null, bool useCompactMetadataNamespace = false) { byte[] secretBytes = credentialBlock ?? request.Password; WriteHistorianString(writer, request.HostName); checked { writer.Write((ushort)secretBytes.Length); } writer.Write(secretBytes); writer.Write(request.ClientType); writer.Write(request.ConnectionMode); if (useCompactMetadataNamespace) { WriteCompactMetadataNamespace(writer, request.MetadataNamespace); } else { WriteMetadataNamespace(writer, request.MetadataNamespace); } WriteHistorianString(writer, string.Empty); WriteHistorianString(writer, string.Empty); WriteClientCommonInfo(writer, commonInfo); } public static HistorianNativeError? TryReadNativeError(ReadOnlySpan buffer) { if (buffer.Length < 5) { return null; } byte type = buffer[0]; uint code = BinaryPrimitives.ReadUInt32LittleEndian(buffer[1..5]); return new HistorianNativeError(type, code, GetKnownErrorName(code)); } public static HistorianLegacyOpen2Output? TryReadLegacyOpen2Output(ReadOnlySpan buffer) { if (buffer.Length != 32) { return null; } uint handle = BinaryPrimitives.ReadUInt32LittleEndian(buffer[..4]); Guid storageSessionId = new(buffer.Slice(4, 16)); long connectTime = BinaryPrimitives.ReadInt64LittleEndian(buffer.Slice(20, 8)); uint serverStatus = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(28, 4)); return new HistorianLegacyOpen2Output(handle, storageSessionId, connectTime, serverStatus); } public static HistorianNativeOpen3Output? TryReadNativeOpen3Output(ReadOnlySpan buffer) { if (buffer.Length < 29) { return null; } byte protocolVersion = buffer[0]; if (protocolVersion is not (2 or 3)) { return null; } int minimumLength = protocolVersion >= 3 ? 37 : 29; if (buffer.Length < minimumLength) { return null; } uint handle = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(1, 4)); Guid storageSessionId = new(buffer.Slice(5, 16)); long connectTime = BinaryPrimitives.ReadInt64LittleEndian(buffer.Slice(21, 8)); long? serverTime = null; if (protocolVersion >= 3) { serverTime = BinaryPrimitives.ReadInt64LittleEndian(buffer.Slice(29, 8)); } byte[] trailingBytes = buffer[minimumLength..].ToArray(); return new HistorianNativeOpen3Output( protocolVersion, handle, storageSessionId, connectTime, serverTime, trailingBytes); } public static byte[] EncodeWidePassword(string password) { return string.IsNullOrEmpty(password) ? [] : Encoding.Unicode.GetBytes(password); } private static void WriteMetadataNamespace(BinaryWriter writer, HistorianMetadataNamespace metadataNamespace) { writer.Write(metadataNamespace.HasValue ? (byte)1 : (byte)0); WriteHistorianString(writer, metadataNamespace.Namespace); WriteHistorianString(writer, metadataNamespace.TagPrefix); WriteHistorianString(writer, metadataNamespace.PropertyPrefix); } private static void WriteCompactMetadataNamespace(BinaryWriter writer, HistorianMetadataNamespace metadataNamespace) { if (!metadataNamespace.HasValue || metadataNamespace.Namespace.Length != 0 || metadataNamespace.TagPrefix.Length != 0 || metadataNamespace.PropertyPrefix.Length != 0) { throw new ProtocolEvidenceMissingException("OpenConnection3 non-empty metadata namespace"); } writer.Write((byte)1); WriteCompactEmptyString(writer); WriteCompactEmptyString(writer); WriteCompactEmptyString(writer); } private static void WriteCompactEmptyString(BinaryWriter writer) { writer.Write((ushort)1); writer.Write((byte)0); } private static void WriteHistorianString(BinaryWriter writer, string value) { writer.Write((uint)value.Length); if (value.Length > 0) { writer.Write(Encoding.Unicode.GetBytes(value)); } } private static void WriteClientCommonInfo(BinaryWriter writer, HistorianClientCommonInfo commonInfo) { writer.Write(commonInfo.FormatVersion); WriteHistorianString(writer, commonInfo.ServerNodeName); WriteHistorianString(writer, commonInfo.ClientNodeName); writer.Write(commonInfo.ProcessId); writer.Write(commonInfo.HcalVersion); WriteHistorianString(writer, commonInfo.ProcessName); WriteHistorianString(writer, commonInfo.Proxy); WriteHistorianString(writer, commonInfo.DataSourceId); writer.Write(commonInfo.ShardId.ToByteArray()); writer.Write(commonInfo.ClientVersion); if (commonInfo.FormatVersion >= 3) { writer.Write(commonInfo.ClientTimestamp); } if (commonInfo.FormatVersion >= 4) { WriteHistorianString(writer, commonInfo.ClientDllVersion); } } private static string? GetKnownErrorName(uint code) { return code switch { 1 => "Failure", 73 => "InvalidPacketVersion", 171 => "AuthenticationFailed", _ => null }; } } internal sealed record HistorianOpen2Request( string HostName, string ProcessName, uint ProcessId, string UserName, byte[] Password, byte ClientType, ushort ClientVersion, uint ConnectionMode, HistorianMetadataNamespace MetadataNamespace); internal sealed record HistorianMetadataNamespace( bool HasValue, string Namespace, string TagPrefix, string PropertyPrefix) { public static HistorianMetadataNamespace Empty { get; } = new(true, string.Empty, string.Empty, string.Empty); } internal sealed record HistorianNativeError(byte Type, uint Code, string? Name); internal sealed record HistorianLegacyOpen2Output( uint Handle, Guid StorageSessionId, long ConnectTimeFileTimeUtc, uint ServerStatus); internal sealed record HistorianNativeOpen3Output( byte ProtocolVersion, uint Handle, Guid StorageSessionId, long ConnectTimeFileTimeUtc, long? ServerTimeFileTimeUtc, byte[] TrailingBytes); internal sealed record HistorianClientCommonInfo( byte FormatVersion, string ServerNodeName, string ClientNodeName, uint ProcessId, ushort HcalVersion, string ProcessName, string Proxy, string DataSourceId, Guid ShardId, uint ClientVersion, ulong ClientTimestamp, string ClientDllVersion);