Files
histsdk/tests/AVEVA.Historian.Client.Tests/WcfOpen2ProtocolTests.cs
T
Joseph Doherty 0b1e9d0a7f feat(grpc-events): v8 OpenConnection serializer + native error decode (Path A disproven)
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
2026-06-23 09:45:52 -04:00

322 lines
12 KiB
C#

using System.Text;
using AVEVA.Historian.Client.Wcf;
namespace AVEVA.Historian.Client.Tests;
public sealed class WcfOpen2ProtocolTests
{
[Fact]
public void Version8EventSerializerReproducesCapturedNativeStructure()
{
// Field sizes chosen to match the live 2023 R2 Event-connection capture (302 bytes):
// username=12, token=26, serverNode=15, clientNode=32, datasource=16.
// See docs/reverse-engineering/grpc-event-query-capture.md.
var clientKey = new Guid("11223344-5566-7788-99aa-bbccddeeff00");
byte[] actual = HistorianOpen2Protocol.SerializeNativeOpenConnectionVersion8(
new HistorianClientCommonInfo(
FormatVersion: 3,
ServerNodeName: new string('S', 15),
ClientNodeName: new string('C', 32),
ProcessId: 0x11223344,
HcalVersion: 18,
ProcessName: string.Empty,
Proxy: string.Empty,
DataSourceId: new string('D', 16),
ShardId: new Guid(Enumerable.Repeat((byte)0xFF, 16).ToArray()),
ClientVersion: 999_999,
ClientTimestamp: 0x01DD02426F9B6F6C,
ClientDllVersion: string.Empty),
clientKey,
connectionType: 1, // Event
connectionFlag: 1, // Event
userName: new string('U', 12),
credentialToken: new byte[26]);
Assert.Equal(302, actual.Length); // exact native length for these field sizes
Assert.Equal(0x08, actual[0]); // format version 8
Assert.Equal(0xF0, actual[1]); // marker
Assert.Equal(0x01, actual[21]); // marker
Assert.Equal(clientKey.ToByteArray(), actual[22..38]);
Assert.Equal(0x04, actual[94]); // ClientType
Assert.Equal(0x01, actual[95]); // ConnectionType = Event
Assert.Equal(0x01, actual[96]); // type flag = Event
Assert.Equal([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF], actual[270..286]); // ShardId
}
[Fact]
public void LegacyVersion1SerializerMatchesDecompiledSaveOpenConnectionParamsLayout()
{
byte[] actual = HistorianOpen2Protocol.SerializeLegacyVersion1(new HistorianOpen2Request(
HostName: "H",
ProcessName: "P",
ProcessId: 0x01020304,
UserName: "U",
Password: Encoding.Unicode.GetBytes("pw"),
ClientType: 4,
ClientVersion: 11,
ConnectionMode: 2,
MetadataNamespace: HistorianMetadataNamespace.Empty));
byte[] expected =
[
0x01, 0x00,
0x01, 0x00, 0x00, 0x00, 0x48, 0x00,
0x01, 0x00, 0x00, 0x00, 0x50, 0x00,
0x04, 0x03, 0x02, 0x01,
0x01, 0x00, 0x00, 0x00, 0x55, 0x00,
0x04, 0x00, 0x00, 0x00, 0x70, 0x00, 0x77, 0x00,
0x04,
0x0B, 0x00,
0x02, 0x00, 0x00, 0x00,
0x01,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
];
Assert.Equal(expected, actual);
}
[Fact]
public void LegacyVersion1SerializerUsesUtf16CodeUnitStringLengths()
{
byte[] actual = HistorianOpen2Protocol.SerializeLegacyVersion1(new HistorianOpen2Request(
HostName: "A\ud83d\ude00",
ProcessName: string.Empty,
ProcessId: 0,
UserName: string.Empty,
Password: [],
ClientType: 4,
ClientVersion: 0,
ConnectionMode: 2,
MetadataNamespace: HistorianMetadataNamespace.Empty));
Assert.Equal([0x03, 0x00, 0x00, 0x00], actual[2..6]);
Assert.Equal(Encoding.Unicode.GetBytes("A\ud83d\ude00"), actual[6..12]);
}
[Fact]
public void NativeErrorParserReadsObservedFiveByteBuffers()
{
HistorianNativeError? error = HistorianOpen2Protocol.TryReadNativeError([0x04, 0xAB, 0x00, 0x00, 0x00]);
Assert.NotNull(error);
Assert.Equal(4, error.Type);
Assert.Equal<uint>(171, error.Code);
Assert.Equal("AuthenticationFailed", error.Name);
}
[Fact]
public void NativeErrorParserRejectsShortBuffers()
{
Assert.Null(HistorianOpen2Protocol.TryReadNativeError([0x04, 0xAB, 0x00, 0x00]));
}
[Fact]
public void LegacyOpen2OutputParserReadsObservedWcfLayout()
{
byte[] buffer =
[
0x78, 0x56, 0x34, 0x12,
0x33, 0x22, 0x11, 0x00,
0x55, 0x44,
0x77, 0x66,
0x88, 0x99, 0xAA, 0xBB,
0xCC, 0xDD, 0xEE, 0xFF,
0x08, 0x07, 0x06, 0x05,
0x04, 0x03, 0x02, 0x01,
0x44, 0x33, 0x22, 0x11
];
HistorianLegacyOpen2Output? output = HistorianOpen2Protocol.TryReadLegacyOpen2Output(buffer);
Assert.NotNull(output);
Assert.Equal<uint>(0x12345678, output.Handle);
Assert.Equal(new Guid("00112233-4455-6677-8899-aabbccddeeff"), output.StorageSessionId);
Assert.Equal(0x0102030405060708, output.ConnectTimeFileTimeUtc);
Assert.Equal<uint>(0x11223344, output.ServerStatus);
}
[Fact]
public void LegacyOpen2OutputParserRejectsNonLegacyLength()
{
Assert.Null(HistorianOpen2Protocol.TryReadLegacyOpen2Output([0x00]));
}
[Fact]
public void NativeOpen3OutputParserReadsObservedDeserializerLayout()
{
byte[] buffer =
[
0x03,
0x78, 0x56, 0x34, 0x12,
0x33, 0x22, 0x11, 0x00,
0x55, 0x44,
0x77, 0x66,
0x88, 0x99, 0xAA, 0xBB,
0xCC, 0xDD, 0xEE, 0xFF,
0x08, 0x07, 0x06, 0x05,
0x04, 0x03, 0x02, 0x01,
0x18, 0x17, 0x16, 0x15,
0x14, 0x13, 0x12, 0x11,
0x44, 0x33, 0x22, 0x11,
0x00
];
HistorianNativeOpen3Output? output = HistorianOpen2Protocol.TryReadNativeOpen3Output(buffer);
Assert.NotNull(output);
Assert.Equal(3, output.ProtocolVersion);
Assert.Equal<uint>(0x12345678, output.Handle);
Assert.Equal(new Guid("00112233-4455-6677-8899-aabbccddeeff"), output.StorageSessionId);
Assert.Equal(0x0102030405060708, output.ConnectTimeFileTimeUtc);
Assert.Equal(0x1112131415161718, output.ServerTimeFileTimeUtc);
Assert.Equal([0x44, 0x33, 0x22, 0x11, 0x00], output.TrailingBytes);
}
[Fact]
public void NativeOpen3OutputParserRejectsUnsupportedVersion()
{
Assert.Null(HistorianOpen2Protocol.TryReadNativeOpen3Output([0x01, 0x00, 0x00, 0x00]));
}
[Fact]
public void NativeVersion3SerializerMatchesDecompiledFieldOrder()
{
byte[] actual = HistorianOpen2Protocol.SerializeNativeVersion3(
new HistorianOpen2Request(
HostName: "H",
ProcessName: "P",
ProcessId: 0x01020304,
UserName: string.Empty,
Password: [0xAA, 0xBB],
ClientType: 4,
ClientVersion: 11,
ConnectionMode: 1026,
MetadataNamespace: HistorianMetadataNamespace.Empty),
new HistorianClientCommonInfo(
FormatVersion: 3,
ServerNodeName: "S",
ClientNodeName: "C",
ProcessId: 0x11223344,
HcalVersion: 17,
ProcessName: "Proc",
Proxy: string.Empty,
DataSourceId: string.Empty,
ShardId: new Guid("00112233-4455-6677-8899-aabbccddeeff"),
ClientVersion: 0x55667788,
ClientTimestamp: 0x0102030405060708,
ClientDllVersion: string.Empty));
byte[] expectedPrefix =
[
0x03,
0x01, 0x00, 0x00, 0x00, 0x48, 0x00,
0x02, 0x00, 0xAA, 0xBB,
0x04,
0x02, 0x04, 0x00, 0x00
];
Assert.Equal(expectedPrefix, actual[..expectedPrefix.Length]);
Assert.Contains<byte>(0x03, actual);
byte[] expectedSuffix = [0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01];
Assert.Equal(expectedSuffix, actual[^expectedSuffix.Length..]);
}
[Fact]
public void NativeOpenConnection3Version6SerializerAddsObservedPrefixBeforeContent()
{
HistorianOpen2Request request = new(
HostName: "H",
ProcessName: "P",
ProcessId: 0x01020304,
UserName: string.Empty,
Password: [0xAA, 0xBB],
ClientType: 4,
ClientVersion: 11,
ConnectionMode: 1026,
MetadataNamespace: HistorianMetadataNamespace.Empty);
HistorianClientCommonInfo commonInfo = new(
FormatVersion: 3,
ServerNodeName: "S",
ClientNodeName: "C",
ProcessId: 0x11223344,
HcalVersion: 17,
ProcessName: "Proc",
Proxy: string.Empty,
DataSourceId: string.Empty,
ShardId: new Guid("00112233-4455-6677-8899-aabbccddeeff"),
ClientVersion: 0x55667788,
ClientTimestamp: 0x0102030405060708,
ClientDllVersion: string.Empty);
byte[] actual = HistorianOpen2Protocol.SerializeNativeOpenConnection3Version6(
request,
commonInfo,
new Guid("00112233-4455-6677-8899-aabbccddeeff"));
byte[] expectedPrefix =
[
0x06,
0x33, 0x22, 0x11, 0x00,
0x55, 0x44,
0x77, 0x66,
0x88, 0x99, 0xAA, 0xBB,
0xCC, 0xDD, 0xEE, 0xFF,
0x00
];
Assert.Equal(expectedPrefix, actual[..expectedPrefix.Length]);
byte[] expectedContentPrefix =
[
0x01, 0x00, 0x00, 0x00, 0x48, 0x00,
0x02, 0x00, 0xAA, 0xBB,
0x04,
0x02, 0x04, 0x00, 0x00,
0x01,
0x01, 0x00, 0x00,
0x01, 0x00, 0x00,
0x01, 0x00, 0x00
];
Assert.Equal(expectedContentPrefix, actual[expectedPrefix.Length..(expectedPrefix.Length + expectedContentPrefix.Length)]);
}
[Fact]
public void NativeOpenConnection3Version6SerializerCanUseSeparateCredentialBlock()
{
HistorianOpen2Request request = new(
HostName: "H",
ProcessName: "P",
ProcessId: 0x01020304,
UserName: string.Empty,
Password: [0xAA, 0xBB],
ClientType: 4,
ClientVersion: 11,
ConnectionMode: 1026,
MetadataNamespace: HistorianMetadataNamespace.Empty);
HistorianClientCommonInfo commonInfo = new(
FormatVersion: 2,
ServerNodeName: string.Empty,
ClientNodeName: string.Empty,
ProcessId: 0,
HcalVersion: 17,
ProcessName: string.Empty,
Proxy: string.Empty,
DataSourceId: string.Empty,
ShardId: Guid.Empty,
ClientVersion: 0,
ClientTimestamp: 0,
ClientDllVersion: string.Empty);
byte[] actual = HistorianOpen2Protocol.SerializeNativeOpenConnection3Version6(
request,
commonInfo,
Guid.Empty,
[0x00, 0x00, 0x00, 0x00]);
int hostLengthOffset = 18;
int credentialLengthOffset = hostLengthOffset + 4 + Encoding.Unicode.GetByteCount("H");
Assert.Equal([0x04, 0x00], actual[credentialLengthOffset..(credentialLengthOffset + 2)]);
Assert.Equal([0x00, 0x00, 0x00, 0x00], actual[(credentialLengthOffset + 2)..(credentialLengthOffset + 6)]);
}
}