Files
dohertj2 c95824a65d Initial commit: managed .NET 10 AVEVA Historian SDK + reverse-engineering toolkit
Full read-only SDK (src/AVEVA.Historian.Client) implementing the CLAUDE.md required
surface against AVEVA Historian's binary WCF protocol — no native AVEVA runtime
dependency. All operations live-verified against a local Historian:

- ProbeAsync, ReadRawAsync, ReadAggregateAsync, ReadAtTimeAsync, ReadEventsAsync
- BrowseTagNamesAsync, GetTagMetadataAsync (17 native data-type codes mapped)
- GetConnectionStatusAsync, GetStoreForwardStatusAsync, GetSystemParameterAsync
- 108/108 unit + integration tests pass

Includes the reverse-engineering toolkit (tools/AVEVA.Historian.ReverseEngineering)
used to decode the protocol: WCF probes, IL inspection via dnlib, and IL-rewrite
instrumentation (instrument-wcf-{write,read}message etc.) plus the .NET Framework
trace harness (tools/AVEVA.Historian.NativeTraceHarness) for parity testing.

Sanitized handoff evidence under docs/reverse-engineering/. Native AVEVA binaries
(current/, aveva-install-x64/, aveva-install-x86/) are gitignored — fetch separately
from the AVEVA installer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 06:31:48 -04:00

675 lines
23 KiB
C#

using System;
using System.IO;
using System.IO.Compression;
using System.Net.Security;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Text;
using System.Threading;
using System.Xml;
namespace AVEVA.Historian.WcfCaptureServer;
internal static class Program
{
private static int Main(string[] args)
{
int port = args.Length > 0 && int.TryParse(args[0], out int parsedPort) ? parsedPort : 33268;
string hostName = args.Length > 1 && !string.IsNullOrWhiteSpace(args[1]) ? args[1] : "localhost";
Uri baseAddress = new($"net.tcp://{hostName}:{port}/");
using ServiceHost host = new(typeof(HistoryCaptureService), baseAddress);
host.AddServiceEndpoint(typeof(IHistoryServiceContract2), MdasBinding.Create(TimeSpan.FromSeconds(30)), "Hist");
host.AddServiceEndpoint(typeof(IHistoryServiceContract2), MdasBinding.CreateWindows(TimeSpan.FromSeconds(30)), "Hist-Integrated");
host.AddServiceEndpoint(typeof(IRetrievalServiceContract4), MdasBinding.Create(TimeSpan.FromSeconds(30)), "Retr");
host.Open();
Console.WriteLine($"READY net.tcp://{hostName}:{port}/Hist");
Console.WriteLine($"READY net.tcp://{hostName}:{port}/Hist-Integrated");
Console.WriteLine($"READY net.tcp://{hostName}:{port}/Retr");
Console.Out.Flush();
Thread.Sleep(Timeout.Infinite);
return 0;
}
}
[ServiceContract(Name = "Hist", Namespace = "aa")]
public interface IHistoryServiceContract
{
[OperationContract(Name = "GetV")]
uint GetInterfaceVersion(out uint version);
[OperationContract(Name = "Open")]
uint OpenConnection(
string HostName,
string ProcessName,
uint ProcessId,
string UserName,
byte[] Password,
ushort pwdLength,
byte clientType,
ushort clientVersion,
uint ConnectionMode,
uint ConnectionTimeout,
ref string StorageSessionId,
out uint Handle,
out long ConnectTime,
out uint ServerStatus);
[OperationContract(Name = "Close")]
uint CloseConnection(uint handle);
[OperationContract(Name = "AddT")]
uint AddTags(uint handle, uint elementCount, uint inByteCount, byte[] inputBuffer, out uint outByteCount, out byte[] outputBuffer);
[OperationContract(Name = "RTag")]
uint RegisterTags(uint handle, uint elementCount, uint inByteCount, byte[] inputBuffer, out uint outByteCount, out byte[] outputBuffer);
}
[ServiceContract(Name = "Hist", Namespace = "aa")]
public interface IHistoryServiceContract2 : IHistoryServiceContract
{
[OperationContract(Name = "Open2")]
bool OpenConnection2(ref byte[] inParameters, out byte[] outParameters, out byte[] err);
[OperationContract(Name = "RTag2")]
bool RegisterTags2(string handle, uint elementCount, byte[] inputBuffer, out byte[] outputBuffer, out byte[] errorBuffer);
}
[ServiceContract(Name = "Retr", Namespace = "aa")]
public interface IRetrievalServiceContract
{
[OperationContract(Name = "GetV")]
uint GetInterfaceVersion(out uint version);
[OperationContract]
uint IsOriginalAllowed(uint clientHandle, out bool isAllowed);
}
[ServiceContract(Name = "Retr", Namespace = "aa")]
public interface IRetrievalServiceContract2 : IRetrievalServiceContract
{
[OperationContract]
bool StartQuery2(
uint clientHandle,
ushort queryRequestType,
uint requestSize,
byte[] requestBuffer,
out uint responseSize,
out byte[] responseBuffer,
ref uint queryHandle,
out uint errorSize,
out byte[] errorBuffer);
[OperationContract]
bool GetNextQueryResultBuffer2(
uint clientHandle,
uint queryHandle,
out uint resultSize,
out byte[] resultBuffer,
out uint errorSize,
out byte[] errorBuffer);
[OperationContract]
bool EndQuery2(uint clientHandle, uint queryHandle, out uint errorSize, out byte[] errorBuffer);
}
[ServiceContract(Name = "Retr", Namespace = "aa")]
public interface IRetrievalServiceContract3 : IRetrievalServiceContract2
{
[OperationContract]
uint StartQuery(
uint clientHandle,
ushort queryRequestType,
uint requestSize,
byte[] requestBuffer,
out uint responseSize,
out byte[] responseBuffer,
ref uint queryHandle);
[OperationContract]
uint GetNextQueryResultBuffer(
uint clientHandle,
uint queryHandle,
out uint resultSize,
out byte[] resultBuffer);
[OperationContract]
uint EndQuery(uint clientHandle, uint queryHandle);
}
[ServiceContract(Name = "Retr", Namespace = "aa")]
public interface IRetrievalServiceContract4 : IRetrievalServiceContract3
{
[OperationContract]
bool StartEventQuery(
uint clientHandle,
ushort queryRequestType,
uint requestSize,
byte[] requestBuffer,
out uint responseSize,
out byte[] responseBuffer,
ref uint queryHandle,
out uint errorSize,
out byte[] errorBuffer);
[OperationContract]
bool GetNextEventQueryResultBuffer(
uint clientHandle,
uint queryHandle,
out uint resultSize,
out byte[] resultBuffer,
out uint errorSize,
out byte[] errorBuffer);
[OperationContract]
bool EndEventQuery(uint clientHandle, uint queryHandle, out uint errorSize, out byte[] errorBuffer);
}
public sealed class HistoryCaptureService : IHistoryServiceContract2, IRetrievalServiceContract4
{
private const uint Handle = 1234;
public uint GetInterfaceVersion(out uint version)
{
version = 11;
Console.WriteLine("{\"Operation\":\"GetV\",\"Version\":11}");
return 0;
}
public uint OpenConnection(
string HostName,
string ProcessName,
uint ProcessId,
string UserName,
byte[] Password,
ushort pwdLength,
byte clientType,
ushort clientVersion,
uint ConnectionMode,
uint ConnectionTimeout,
ref string StorageSessionId,
out uint Handle,
out long ConnectTime,
out uint ServerStatus)
{
string storageSessionIdIn = StorageSessionId ?? string.Empty;
StorageSessionId = Guid.Empty.ToString("D");
Handle = HistoryCaptureService.Handle;
ConnectTime = DateTime.UtcNow.ToFileTimeUtc();
ServerStatus = 0;
Console.WriteLine("{" +
"\"Operation\":\"Open\"," +
$"\"HostName\":\"{Escape(HostName)}\"," +
$"\"ProcessName\":\"{Escape(ProcessName)}\"," +
$"\"ProcessId\":{ProcessId}," +
$"\"UserName\":\"{Escape(UserName)}\"," +
$"\"PasswordLength\":{(Password?.Length ?? 0)}," +
$"\"pwdLength\":{pwdLength}," +
$"\"clientType\":{clientType}," +
$"\"clientVersion\":{clientVersion}," +
$"\"ConnectionMode\":{ConnectionMode}," +
$"\"ConnectionTimeout\":{ConnectionTimeout}," +
$"\"StorageSessionIdIn\":\"{Escape(storageSessionIdIn)}\"," +
$"\"StorageSessionIdOut\":\"{Escape(StorageSessionId)}\"" +
"}");
Console.Out.Flush();
return 0;
}
public uint CloseConnection(uint handle)
{
Console.WriteLine($"{{\"Operation\":\"Close\",\"Handle\":{handle}}}");
return 0;
}
public uint AddTags(uint handle, uint elementCount, uint inByteCount, byte[] inputBuffer, out uint outByteCount, out byte[] outputBuffer)
{
byte[] request = inputBuffer ?? Array.Empty<byte>();
outByteCount = 0;
outputBuffer = Array.Empty<byte>();
bool includeBuffers = string.Equals(Environment.GetEnvironmentVariable("WCF_CAPTURE_INCLUDE_BUFFERS"), "1", StringComparison.Ordinal);
Console.WriteLine("{" +
"\"Operation\":\"AddT\"," +
$"\"Handle\":{handle}," +
$"\"ElementCount\":{elementCount}," +
$"\"InputByteCount\":{inByteCount}," +
$"\"InputByteArrayLength\":{request.Length}," +
$"\"InputSha256\":\"{Hash(request)}\"" +
(includeBuffers ? $",\"InputBase64\":\"{Convert.ToBase64String(request)}\"" : string.Empty) +
"}");
Console.Out.Flush();
return 0;
}
public uint RegisterTags(uint handle, uint elementCount, uint inByteCount, byte[] inputBuffer, out uint outByteCount, out byte[] outputBuffer)
{
byte[] request = inputBuffer ?? Array.Empty<byte>();
outByteCount = 0;
outputBuffer = Array.Empty<byte>();
bool includeBuffers = string.Equals(Environment.GetEnvironmentVariable("WCF_CAPTURE_INCLUDE_BUFFERS"), "1", StringComparison.Ordinal);
Console.WriteLine("{" +
"\"Operation\":\"RTag\"," +
$"\"Handle\":{handle}," +
$"\"ElementCount\":{elementCount}," +
$"\"InputByteCount\":{inByteCount}," +
$"\"InputByteArrayLength\":{request.Length}," +
$"\"InputSha256\":\"{Hash(request)}\"" +
(includeBuffers ? $",\"InputBase64\":\"{Convert.ToBase64String(request)}\"" : string.Empty) +
"}");
Console.Out.Flush();
return 0;
}
public bool OpenConnection2(ref byte[] inParameters, out byte[] outParameters, out byte[] err)
{
byte[] input = inParameters ?? Array.Empty<byte>();
Console.WriteLine("{" +
"\"Operation\":\"Open2\"," +
$"\"InputByteCount\":{input.Length}," +
$"\"InputSha256\":\"{Hash(input)}\"" +
"}");
Console.Out.Flush();
outParameters = CreateOpen2Output();
err = Array.Empty<byte>();
return true;
}
public bool RegisterTags2(string handle, uint elementCount, byte[] inputBuffer, out byte[] outputBuffer, out byte[] errorBuffer)
{
byte[] request = inputBuffer ?? Array.Empty<byte>();
outputBuffer = Array.Empty<byte>();
errorBuffer = Array.Empty<byte>();
bool includeBuffers = string.Equals(Environment.GetEnvironmentVariable("WCF_CAPTURE_INCLUDE_BUFFERS"), "1", StringComparison.Ordinal);
Console.WriteLine("{" +
"\"Operation\":\"RTag2\"," +
$"\"Handle\":\"{Escape(handle)}\"," +
$"\"ElementCount\":{elementCount}," +
$"\"InputByteArrayLength\":{request.Length}," +
$"\"InputSha256\":\"{Hash(request)}\"" +
(includeBuffers ? $",\"InputBase64\":\"{Convert.ToBase64String(request)}\"" : string.Empty) +
"}");
Console.Out.Flush();
return true;
}
public uint IsOriginalAllowed(uint clientHandle, out bool isAllowed)
{
isAllowed = true;
Console.WriteLine($"{{\"Operation\":\"IsOriginalAllowed\",\"ClientHandle\":{clientHandle}}}");
Console.Out.Flush();
return clientHandle == Handle ? 0u : 4u;
}
public bool StartQuery2(
uint clientHandle,
ushort queryRequestType,
uint requestSize,
byte[] requestBuffer,
out uint responseSize,
out byte[] responseBuffer,
ref uint queryHandle,
out uint errorSize,
out byte[] errorBuffer)
{
byte[] request = requestBuffer ?? Array.Empty<byte>();
queryHandle = 1;
responseSize = 0;
responseBuffer = Array.Empty<byte>();
errorSize = 5;
errorBuffer = new byte[] { 4, 1, 0, 0, 0 };
bool includeBuffers = string.Equals(Environment.GetEnvironmentVariable("WCF_CAPTURE_INCLUDE_BUFFERS"), "1", StringComparison.Ordinal);
Console.WriteLine("{" +
"\"Operation\":\"StartQuery2\"," +
$"\"ClientHandle\":{clientHandle}," +
$"\"QueryRequestType\":{queryRequestType}," +
$"\"RequestSize\":{requestSize}," +
$"\"RequestByteCount\":{request.Length}," +
$"\"RequestSha256\":\"{Hash(request)}\"" +
(includeBuffers ? $",\"RequestBase64\":\"{Convert.ToBase64String(request)}\"" : string.Empty) +
"}");
Console.Out.Flush();
return false;
}
public uint StartQuery(
uint clientHandle,
ushort queryRequestType,
uint requestSize,
byte[] requestBuffer,
out uint responseSize,
out byte[] responseBuffer,
ref uint queryHandle)
{
byte[] request = requestBuffer ?? Array.Empty<byte>();
queryHandle = 1;
responseSize = 0;
responseBuffer = Array.Empty<byte>();
bool includeBuffers = string.Equals(Environment.GetEnvironmentVariable("WCF_CAPTURE_INCLUDE_BUFFERS"), "1", StringComparison.Ordinal);
Console.WriteLine("{" +
"\"Operation\":\"StartQuery\"," +
$"\"ClientHandle\":{clientHandle}," +
$"\"QueryRequestType\":{queryRequestType}," +
$"\"RequestSize\":{requestSize}," +
$"\"RequestByteCount\":{request.Length}," +
$"\"RequestSha256\":\"{Hash(request)}\"" +
(includeBuffers ? $",\"RequestBase64\":\"{Convert.ToBase64String(request)}\"" : string.Empty) +
"}");
Console.Out.Flush();
return 4;
}
public bool GetNextQueryResultBuffer2(
uint clientHandle,
uint queryHandle,
out uint resultSize,
out byte[] resultBuffer,
out uint errorSize,
out byte[] errorBuffer)
{
resultSize = 0;
resultBuffer = Array.Empty<byte>();
errorSize = 5;
errorBuffer = new byte[] { 4, 1, 0, 0, 0 };
Console.WriteLine($"{{\"Operation\":\"GetNextQueryResultBuffer2\",\"ClientHandle\":{clientHandle},\"QueryHandle\":{queryHandle}}}");
Console.Out.Flush();
return false;
}
public uint GetNextQueryResultBuffer(
uint clientHandle,
uint queryHandle,
out uint resultSize,
out byte[] resultBuffer)
{
resultSize = 0;
resultBuffer = Array.Empty<byte>();
Console.WriteLine($"{{\"Operation\":\"GetNextQueryResultBuffer\",\"ClientHandle\":{clientHandle},\"QueryHandle\":{queryHandle}}}");
Console.Out.Flush();
return 4;
}
public bool EndQuery2(uint clientHandle, uint queryHandle, out uint errorSize, out byte[] errorBuffer)
{
errorSize = 0;
errorBuffer = Array.Empty<byte>();
Console.WriteLine($"{{\"Operation\":\"EndQuery2\",\"ClientHandle\":{clientHandle},\"QueryHandle\":{queryHandle}}}");
Console.Out.Flush();
return true;
}
public uint EndQuery(uint clientHandle, uint queryHandle)
{
Console.WriteLine($"{{\"Operation\":\"EndQuery\",\"ClientHandle\":{clientHandle},\"QueryHandle\":{queryHandle}}}");
Console.Out.Flush();
return 0;
}
public bool StartEventQuery(
uint clientHandle,
ushort queryRequestType,
uint requestSize,
byte[] requestBuffer,
out uint responseSize,
out byte[] responseBuffer,
ref uint queryHandle,
out uint errorSize,
out byte[] errorBuffer)
{
byte[] request = requestBuffer ?? Array.Empty<byte>();
queryHandle = 1;
responseSize = 0;
responseBuffer = Array.Empty<byte>();
errorSize = 5;
errorBuffer = new byte[] { 4, 1, 0, 0, 0 };
bool includeBuffers = string.Equals(Environment.GetEnvironmentVariable("WCF_CAPTURE_INCLUDE_BUFFERS"), "1", StringComparison.Ordinal);
Console.WriteLine("{" +
"\"Operation\":\"StartEventQuery\"," +
$"\"ClientHandle\":{clientHandle}," +
$"\"QueryRequestType\":{queryRequestType}," +
$"\"RequestSize\":{requestSize}," +
$"\"RequestByteCount\":{request.Length}," +
$"\"RequestSha256\":\"{Hash(request)}\"" +
(includeBuffers ? $",\"RequestBase64\":\"{Convert.ToBase64String(request)}\"" : string.Empty) +
"}");
Console.Out.Flush();
return false;
}
public bool GetNextEventQueryResultBuffer(
uint clientHandle,
uint queryHandle,
out uint resultSize,
out byte[] resultBuffer,
out uint errorSize,
out byte[] errorBuffer)
{
resultSize = 0;
resultBuffer = Array.Empty<byte>();
errorSize = 5;
errorBuffer = new byte[] { 4, 1, 0, 0, 0 };
Console.WriteLine($"{{\"Operation\":\"GetNextEventQueryResultBuffer\",\"ClientHandle\":{clientHandle},\"QueryHandle\":{queryHandle}}}");
Console.Out.Flush();
return false;
}
public bool EndEventQuery(uint clientHandle, uint queryHandle, out uint errorSize, out byte[] errorBuffer)
{
errorSize = 0;
errorBuffer = Array.Empty<byte>();
Console.WriteLine($"{{\"Operation\":\"EndEventQuery\",\"ClientHandle\":{clientHandle},\"QueryHandle\":{queryHandle}}}");
Console.Out.Flush();
return true;
}
private static byte[] CreateOpen2Output()
{
using MemoryStream stream = new MemoryStream();
using BinaryWriter writer = new BinaryWriter(stream);
writer.Write(Handle);
writer.Write(Guid.Empty.ToByteArray());
writer.Write(DateTime.UtcNow.ToFileTimeUtc());
writer.Write(0u);
return stream.ToArray();
}
private static string Escape(string? value)
{
return (value ?? string.Empty).Replace("\\", "\\\\").Replace("\"", "\\\"");
}
private static string Hash(byte[] value)
{
using System.Security.Cryptography.SHA256 sha256 = System.Security.Cryptography.SHA256.Create();
byte[] hash = sha256.ComputeHash(value);
StringBuilder builder = new(hash.Length * 2);
foreach (byte b in hash)
{
builder.Append(b.ToString("x2"));
}
return builder.ToString();
}
}
internal static class MdasBinding
{
public static Binding Create(TimeSpan timeout)
{
CustomBinding binding = new(
new MdasMessageEncodingBindingElement(new BinaryMessageEncodingBindingElement
{
MessageVersion = MessageVersion.Soap12WSAddressing10
}),
new TcpTransportBindingElement
{
MaxReceivedMessageSize = 64 * 1024 * 1024,
TransferMode = TransferMode.Buffered
})
{
OpenTimeout = timeout,
CloseTimeout = timeout,
SendTimeout = timeout,
ReceiveTimeout = timeout
};
return binding;
}
public static Binding CreateWindows(TimeSpan timeout)
{
NetTcpBinding nativeShape = new NetTcpBinding(SecurityMode.Transport)
{
MaxReceivedMessageSize = 64 * 1024 * 1024,
MaxBufferSize = 64 * 1024 * 1024
};
nativeShape.ReaderQuotas.MaxArrayLength = nativeShape.MaxBufferSize;
nativeShape.Security.Transport.ClientCredentialType = TcpClientCredentialType.Windows;
nativeShape.Security.Transport.ProtectionLevel = ProtectionLevel.None;
BindingElementCollection elements = nativeShape.CreateBindingElements();
for (int i = 0; i < elements.Count; i++)
{
if (elements[i] is MessageEncodingBindingElement encoding)
{
elements[i] = new MdasMessageEncodingBindingElement(encoding);
break;
}
}
return new CustomBinding(elements)
{
OpenTimeout = timeout,
CloseTimeout = timeout,
SendTimeout = timeout,
ReceiveTimeout = timeout
};
}
}
internal sealed class MdasMessageEncodingBindingElement : MessageEncodingBindingElement
{
private readonly MessageEncodingBindingElement inner;
public MdasMessageEncodingBindingElement(MessageEncodingBindingElement inner)
{
this.inner = inner ?? throw new ArgumentNullException(nameof(inner));
}
private MdasMessageEncodingBindingElement(MdasMessageEncodingBindingElement source)
{
inner = (MessageEncodingBindingElement)source.inner.Clone();
}
public override MessageVersion MessageVersion
{
get => inner.MessageVersion;
set => inner.MessageVersion = value;
}
public override MessageEncoderFactory CreateMessageEncoderFactory()
{
return new MdasMessageEncoderFactory(inner.CreateMessageEncoderFactory());
}
public override BindingElement Clone()
{
return new MdasMessageEncodingBindingElement(this);
}
public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)
{
context.BindingParameters.Add(this);
return context.BuildInnerChannelFactory<TChannel>();
}
public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context)
{
context.BindingParameters.Add(this);
return context.BuildInnerChannelListener<TChannel>();
}
public override bool CanBuildChannelFactory<TChannel>(BindingContext context)
{
context.BindingParameters.Add(this);
return context.CanBuildInnerChannelFactory<TChannel>();
}
public override bool CanBuildChannelListener<TChannel>(BindingContext context)
{
context.BindingParameters.Add(this);
return context.CanBuildInnerChannelListener<TChannel>();
}
public override T GetProperty<T>(BindingContext context)
{
return inner.GetProperty<T>(context) ?? context.GetInnerProperty<T>();
}
}
internal sealed class MdasMessageEncoderFactory : MessageEncoderFactory
{
private readonly MessageEncoderFactory inner;
private readonly MessageEncoder encoder;
public MdasMessageEncoderFactory(MessageEncoderFactory inner)
{
this.inner = inner ?? throw new ArgumentNullException(nameof(inner));
encoder = new MdasMessageEncoder(inner.Encoder);
}
public override MessageEncoder Encoder => encoder;
public override MessageVersion MessageVersion => inner.MessageVersion;
}
internal sealed class MdasMessageEncoder : MessageEncoder
{
private const string MdasContentType = "application/x-mdas";
private readonly MessageEncoder inner;
public MdasMessageEncoder(MessageEncoder inner)
{
this.inner = inner ?? throw new ArgumentNullException(nameof(inner));
}
public override string ContentType => MdasContentType;
public override string MediaType => MdasContentType;
public override MessageVersion MessageVersion => inner.MessageVersion;
public override bool IsContentTypeSupported(string contentType)
{
return contentType.StartsWith(MdasContentType, StringComparison.OrdinalIgnoreCase)
|| inner.IsContentTypeSupported(contentType);
}
public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType)
{
Message message = inner.ReadMessage(buffer, bufferManager);
message.Properties.Encoder = this;
return message;
}
public override Message ReadMessage(Stream stream, int maxSizeOfHeaders, string contentType)
{
return inner.ReadMessage(stream, maxSizeOfHeaders);
}
public override void WriteMessage(Message message, Stream stream)
{
inner.WriteMessage(message, stream);
}
public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)
{
return inner.WriteMessage(message, maxMessageSize, bufferManager, messageOffset);
}
}