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
322 lines
12 KiB
C#
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)]);
|
|
}
|
|
}
|