0b1e9d0a7f
Builds the native 2023 R2 version-8 OpenConnection format, which (unlike v6) carries a ConnectionType byte (Event vs Process) — required because the 2023 R2 server returns event rows only on an Event connection. - HistorianOpen2Protocol.SerializeNativeOpenConnectionVersion8: reproduces the 302-byte v8 layout decoded from a live capture (version 8, markers, client-key GUID, username HString, length-prefixed credential token, ClientType / ConnectionType / flag / constant word / compact metadata / two empty strings; the tail reuses WriteClientCommonInfo). Golden-tested (Version8EventSerializerReproducesCapturedNativeStructure). - HistorianNativeHandshake.BuildEventOpenConnectionVersion8Request: ConnectionType= Event, zeroed credential token (mirroring how v6 zeros its credential block and relies on the separate ValidateClientCredential handshake). - HistorianGrpcHandshake.OpenSession: optional eventConnection switch; the OpenConnection failure path now decodes the native error (type/code/ASCII). Path A (reuse ValidateClientCredential + zeroed token) was live-tested and DISPROVEN: the server parses the v8 buffer but rejects it at the auth check with native 132/34 "EstablishConnection Failed to get client key" — the v8 path looks up the client key in the registry HistoryService.ExchangeKey (ECDH) populates, not the one ValidateClientCredential does. The event orchestrator is therefore reverted to the v6 session (gated test still pins the no-row throw). The v8 serializer/builder are retained for Path B (implement ExchangeKey). 323/323 offline tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
319 lines
12 KiB
C#
319 lines
12 KiB
C#
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Builds the native 2023 R2 <b>version-8</b> OpenConnection request — the format the stock client
|
|
/// uses, which (unlike v6) carries a <paramref name="connectionType"/> byte (Event vs Process). The
|
|
/// tail is the same <see cref="HistorianClientCommonInfo"/> 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 <c>HistorianString</c>, 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).
|
|
/// </summary>
|
|
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<byte> 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<byte> 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<byte> 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);
|