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