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>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net481</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.ServiceModel" />
|
||||
<Reference Include="System.Web.Extensions" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<system.diagnostics>
|
||||
<sources>
|
||||
<source name="System.ServiceModel" switchValue="Verbose">
|
||||
<listeners>
|
||||
<add name="xml" />
|
||||
</listeners>
|
||||
</source>
|
||||
<source name="System.ServiceModel.MessageLogging" switchValue="Verbose">
|
||||
<listeners>
|
||||
<add name="xml" />
|
||||
</listeners>
|
||||
</source>
|
||||
</sources>
|
||||
<sharedListeners>
|
||||
<add name="xml"
|
||||
type="System.Diagnostics.XmlWriterTraceListener"
|
||||
initializeData="docs\reverse-engineering\native-wcf-message-log.svclog" />
|
||||
</sharedListeners>
|
||||
<trace autoflush="true" />
|
||||
</system.diagnostics>
|
||||
<system.serviceModel>
|
||||
<diagnostics>
|
||||
<messageLogging
|
||||
logEntireMessage="true"
|
||||
logMalformedMessages="true"
|
||||
logMessagesAtServiceLevel="true"
|
||||
logMessagesAtTransportLevel="true"
|
||||
maxMessagesToLog="3000"
|
||||
maxSizeOfMessageToLog="67108864" />
|
||||
</diagnostics>
|
||||
</system.serviceModel>
|
||||
</configuration>
|
||||
@@ -0,0 +1,968 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Web.Script.Serialization;
|
||||
|
||||
namespace AVEVA.Historian.NativeTraceHarness;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
string repoRoot = FindRepoRoot();
|
||||
Directory.SetCurrentDirectory(repoRoot);
|
||||
|
||||
string tagName = GetArg(args, "--tag") ?? "OtOpcUaParityTest_001.Counter";
|
||||
string serverName = GetArg(args, "--server-name") ?? "localhost";
|
||||
int tcpPort = int.TryParse(GetArg(args, "--tcp-port"), out int parsedTcpPort) ? parsedTcpPort : 32568;
|
||||
int lookbackMinutes = int.TryParse(GetArg(args, "--lookback-minutes"), out int parsedLookback) ? parsedLookback : 1440;
|
||||
int maxRows = int.TryParse(GetArg(args, "--max-rows"), out int parsedMaxRows) ? parsedMaxRows : 1;
|
||||
int waitSeconds = int.TryParse(GetArg(args, "--connection-wait-seconds"), out int parsedWait) ? parsedWait : 15;
|
||||
int preLoadSleepSeconds = int.TryParse(GetArg(args, "--pre-load-sleep-seconds"), out int parsedPreLoadSleep) ? parsedPreLoadSleep : 0;
|
||||
int preOpenSleepSeconds = int.TryParse(GetArg(args, "--pre-open-sleep-seconds"), out int parsedPreOpenSleep) ? parsedPreOpenSleep : 0;
|
||||
int preStartSleepSeconds = int.TryParse(GetArg(args, "--pre-start-sleep-seconds"), out int parsedPreStartSleep) ? parsedPreStartSleep : 0;
|
||||
string scenario = GetArg(args, "--scenario") ?? "history";
|
||||
string retrievalModeName = GetArg(args, "--retrieval-mode") ?? "Full";
|
||||
bool directConnection = HasFlag(args, "--direct-connection");
|
||||
bool integratedSecurity = !HasFlag(args, "--no-integrated-security");
|
||||
string? proxyServer = GetArg(args, "--proxy-server");
|
||||
string? runtimeMethodPointerOutput = GetArg(args, "--runtime-method-pointer-output");
|
||||
string runtimeMethodPointerFilters = GetArg(args, "--runtime-method-pointer-filters")
|
||||
?? "StartDataQuery;StartQuery;GetNextRow;StartEventQuery";
|
||||
ulong resolutionTicks = ulong.TryParse(GetArg(args, "--resolution-ticks"), out ulong parsedResolutionTicks) ? parsedResolutionTicks : 0;
|
||||
DateTime endUtc = TryParseUtc(GetArg(args, "--end-utc")) ?? DateTime.UtcNow;
|
||||
DateTime startUtc = TryParseUtc(GetArg(args, "--start-utc")) ?? endUtc.AddMinutes(-lookbackMinutes);
|
||||
|
||||
string current = Path.GetFullPath(GetArg(args, "--current-dir") ?? Path.Combine(repoRoot, "current"));
|
||||
string managedDll = Path.GetFullPath(GetArg(args, "--managed-dll-path") ?? Path.Combine(current, "aahClientManaged.dll"));
|
||||
if (!File.Exists(managedDll))
|
||||
{
|
||||
throw new FileNotFoundException("Missing aahClientManaged.dll.", managedDll);
|
||||
}
|
||||
if (!Directory.Exists(current))
|
||||
{
|
||||
throw new DirectoryNotFoundException($"Missing dependency folder: {current}");
|
||||
}
|
||||
|
||||
AppDomain.CurrentDomain.AssemblyResolve += (_, eventArgs) =>
|
||||
{
|
||||
AssemblyName name = new(eventArgs.Name);
|
||||
string candidate = Path.Combine(current, name.Name + ".dll");
|
||||
return File.Exists(candidate) ? Assembly.LoadFrom(candidate) : null!;
|
||||
};
|
||||
|
||||
Directory.CreateDirectory(Path.Combine(repoRoot, "docs", "reverse-engineering"));
|
||||
TryDelete(Path.Combine(repoRoot, "docs", "reverse-engineering", "native-wcf-message-log.svclog"));
|
||||
TraceSource diagnosticProbe = new("System.ServiceModel");
|
||||
diagnosticProbe.TraceInformation("NativeTraceHarness diagnostics probe");
|
||||
diagnosticProbe.Flush();
|
||||
|
||||
Directory.SetCurrentDirectory(current);
|
||||
if (preLoadSleepSeconds > 0)
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromSeconds(preLoadSleepSeconds));
|
||||
}
|
||||
|
||||
Assembly assembly = Assembly.LoadFrom(managedDll);
|
||||
string? methodPointerFilter = GetArg(args, "--dump-method-pointers");
|
||||
if (methodPointerFilter is not null)
|
||||
{
|
||||
Console.WriteLine(Serialize(DumpRuntimeMethodPointers(assembly, methodPointerFilter)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
Type accessType = GetType(assembly, "ArchestrA.HistorianAccess");
|
||||
Type connectionArgsType = GetType(assembly, "ArchestrA.HistorianConnectionArgs");
|
||||
Type connectionStatusType = GetType(assembly, "ArchestrA.HistorianConnectionStatus");
|
||||
Type connectionType = GetType(assembly, "ArchestrA.HistorianConnectionType");
|
||||
Type historyQueryArgsType = GetType(assembly, "ArchestrA.HistoryQueryArgs");
|
||||
Type eventQueryArgsType = GetType(assembly, "ArchestrA.EventQueryArgs");
|
||||
Type tagQueryArgsType = GetType(assembly, "ArchestrA.TagQueryArgs");
|
||||
Type eventQueryTypeType = GetType(assembly, "ArchestrA.HistorianEventQueryType");
|
||||
Type eventOrderType = GetType(assembly, "ArchestrA.HistorianEventOrder");
|
||||
Type errorType = GetType(assembly, "ArchestrA.HistorianAccessError");
|
||||
Type retrievalModeType = GetType(assembly, "ArchestrA.HistorianRetrievalMode");
|
||||
|
||||
object access = Activator.CreateInstance(accessType)!;
|
||||
object connectionArgs = Activator.CreateInstance(connectionArgsType)!;
|
||||
SetProperty(connectionArgs, "ServerName", serverName);
|
||||
SetProperty(connectionArgs, "TcpPort", checked((ushort)tcpPort));
|
||||
SetProperty(connectionArgs, "ReadOnly", true);
|
||||
SetProperty(connectionArgs, "IntegratedSecurity", integratedSecurity);
|
||||
SetProperty(connectionArgs, "ConnectionType", Enum.Parse(connectionType, IsEventScenario(scenario) ? "Event" : "Process"));
|
||||
if (directConnection)
|
||||
{
|
||||
SetProperty(connectionArgs, "DirectConnection", true);
|
||||
SetField(connectionArgs, "directConnection", true);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(proxyServer))
|
||||
{
|
||||
SetProperty(connectionArgs, "ProxyServer", proxyServer!);
|
||||
}
|
||||
|
||||
Dictionary<string, object?> snapshots = [];
|
||||
if (preOpenSleepSeconds > 0)
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromSeconds(preOpenSleepSeconds));
|
||||
}
|
||||
|
||||
object openError = Activator.CreateInstance(errorType)!;
|
||||
MethodInfo openMethod = accessType.GetMethod("OpenConnection", new[] { connectionArgsType, errorType.MakeByRefType() })
|
||||
?? throw new MissingMethodException("HistorianAccess.OpenConnection");
|
||||
object?[] openArgs = [connectionArgs, openError];
|
||||
bool openSuccess = (bool)openMethod.Invoke(access, openArgs)!;
|
||||
openError = openArgs[1]!;
|
||||
snapshots["ConnectionArgs"] = SnapshotObject(connectionArgs);
|
||||
snapshots["AccessAfterOpen"] = SnapshotObject(access);
|
||||
|
||||
ConnectionStatusSnapshot status = WaitForConnection(access, accessType, connectionStatusType, waitSeconds);
|
||||
|
||||
bool startSuccess = false;
|
||||
object? startError = null;
|
||||
string? startQueryException = null;
|
||||
string? moveTerminalDescription = null;
|
||||
List<object> rows = [];
|
||||
|
||||
if (openSuccess && status.ConnectedToServer && IsEventScenario(scenario))
|
||||
{
|
||||
object query = accessType.GetMethod("CreateEventQuery", Type.EmptyTypes)!.Invoke(access, Array.Empty<object>())!;
|
||||
Type queryType = query.GetType();
|
||||
snapshots["EventQueryAfterCreate"] = SnapshotObject(query);
|
||||
|
||||
object queryArgs = Activator.CreateInstance(eventQueryArgsType)!;
|
||||
SetProperty(queryArgs, "StartDateTime", startUtc);
|
||||
SetProperty(queryArgs, "EndDateTime", endUtc);
|
||||
SetProperty(queryArgs, "EventCount", checked((uint)Math.Max(maxRows, 1)));
|
||||
SetProperty(queryArgs, "QueryType", Enum.Parse(eventQueryTypeType, "Events"));
|
||||
SetProperty(queryArgs, "EventOrder", Enum.Parse(eventOrderType, "Ascending"));
|
||||
snapshots["EventQueryArgsBeforeStart"] = SnapshotObject(queryArgs);
|
||||
|
||||
startError = Activator.CreateInstance(errorType)!;
|
||||
MethodInfo startMethod = queryType.GetMethod("StartQuery", new[] { eventQueryArgsType, errorType.MakeByRefType() })
|
||||
?? throw new MissingMethodException("EventQuery.StartQuery");
|
||||
WriteRuntimeMethodPointerSnapshot(assembly, runtimeMethodPointerOutput, runtimeMethodPointerFilters, repoRoot, scenario, "before-event-start");
|
||||
if (preStartSleepSeconds > 0)
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromSeconds(preStartSleepSeconds));
|
||||
}
|
||||
|
||||
object?[] startArgs = [queryArgs, startError];
|
||||
try
|
||||
{
|
||||
startSuccess = (bool)startMethod.Invoke(query, startArgs)!;
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
startQueryException = FormatException(ex.InnerException ?? ex);
|
||||
}
|
||||
|
||||
startError = startArgs[1];
|
||||
snapshots["EventQueryAfterStart"] = SnapshotObject(query);
|
||||
snapshots["EventQueryArgsAfterStart"] = SnapshotObject(queryArgs);
|
||||
|
||||
if (startSuccess)
|
||||
{
|
||||
MethodInfo moveMethod = queryType.GetMethod("MoveNext", new[] { errorType.MakeByRefType() })
|
||||
?? throw new MissingMethodException("EventQuery.MoveNext");
|
||||
for (int i = 0; i < maxRows; i++)
|
||||
{
|
||||
object moveError = Activator.CreateInstance(errorType)!;
|
||||
object?[] moveArgs = [moveError];
|
||||
bool hasRow = (bool)moveMethod.Invoke(query, moveArgs)!;
|
||||
moveError = moveArgs[0]!;
|
||||
if (!hasRow)
|
||||
{
|
||||
moveTerminalDescription = GetPropertyText(moveError, "ErrorDescription");
|
||||
break;
|
||||
}
|
||||
|
||||
object result = GetPropertyValue(query, "QueryResult")!;
|
||||
snapshots["EventQueryAfterFirstMove"] = SnapshotObject(query);
|
||||
snapshots["EventResultAfterFirstMove"] = SnapshotObject(result);
|
||||
rows.Add(new
|
||||
{
|
||||
EventTime = FormatDateProperty(result, "EventTime"),
|
||||
ReceivedTime = FormatDateProperty(result, "ReceivedTime"),
|
||||
EventType = TryGetPropertyValue(result, "EventType"),
|
||||
Type = TryGetPropertyValue(result, "Type"),
|
||||
DisplayText = TryGetPropertyValue(result, "DisplayText"),
|
||||
Area = TryGetPropertyValue(result, "Area"),
|
||||
Source = TryGetPropertyValue(result, "Source"),
|
||||
System = TryGetPropertyValue(result, "System"),
|
||||
Severity = TryGetPropertyValue(result, "Severity"),
|
||||
Priority = TryGetPropertyValue(result, "Priority"),
|
||||
IsAlarm = TryGetPropertyValue(result, "IsAlarm")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MethodInfo? endMethod = queryType.GetMethod("EndQuery", new[] { errorType.MakeByRefType() });
|
||||
if (endMethod is not null)
|
||||
{
|
||||
object endError = Activator.CreateInstance(errorType)!;
|
||||
object?[] endArgs = [endError];
|
||||
_ = endMethod.Invoke(query, endArgs);
|
||||
}
|
||||
|
||||
if (query is IDisposable disposableQuery)
|
||||
{
|
||||
disposableQuery.Dispose();
|
||||
}
|
||||
}
|
||||
else if (openSuccess && status.ConnectedToServer && IsTagScenario(scenario))
|
||||
{
|
||||
object query = accessType.GetMethod("CreateTagQuery", Type.EmptyTypes)!.Invoke(access, Array.Empty<object>())!;
|
||||
Type queryType = query.GetType();
|
||||
snapshots["TagQueryAfterCreate"] = SnapshotObject(query);
|
||||
|
||||
object queryArgs = Activator.CreateInstance(tagQueryArgsType)!;
|
||||
SetProperty(queryArgs, "TagFilter", tagName);
|
||||
SetProperty(queryArgs, "CacheTagInfo", true);
|
||||
SetProperty(queryArgs, "RetrieveTagExtendedPropertyInfo", false);
|
||||
snapshots["TagQueryArgsBeforeStart"] = SnapshotObject(queryArgs);
|
||||
|
||||
startError = Activator.CreateInstance(errorType)!;
|
||||
MethodInfo startMethod = queryType.GetMethods().First(method =>
|
||||
method.Name == "StartQuery" && method.GetParameters().Length == 3);
|
||||
WriteRuntimeMethodPointerSnapshot(assembly, runtimeMethodPointerOutput, runtimeMethodPointerFilters, repoRoot, scenario, "before-tag-start");
|
||||
if (preStartSleepSeconds > 0)
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromSeconds(preStartSleepSeconds));
|
||||
}
|
||||
|
||||
object?[] startArgs = [queryArgs, 0u, startError];
|
||||
try
|
||||
{
|
||||
startSuccess = (bool)startMethod.Invoke(query, startArgs)!;
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
startQueryException = FormatException(ex.InnerException ?? ex);
|
||||
}
|
||||
|
||||
uint tagCount = startArgs[1] is uint count ? count : 0;
|
||||
startError = startArgs[2];
|
||||
snapshots["TagQueryAfterStart"] = SnapshotObject(query);
|
||||
snapshots["TagQueryArgsAfterStart"] = SnapshotObject(queryArgs);
|
||||
|
||||
if (startSuccess)
|
||||
{
|
||||
uint requestedRows = checked((uint)Math.Max(maxRows, 1));
|
||||
|
||||
MethodInfo? getTagNamesMethod = queryType.GetMethods().FirstOrDefault(method =>
|
||||
method.Name == "GetTagNames" && method.GetParameters().Length == 4);
|
||||
if (getTagNamesMethod is not null)
|
||||
{
|
||||
object tagNamesError = Activator.CreateInstance(errorType)!;
|
||||
object?[] tagNameArgs = [0u, requestedRows, null, tagNamesError];
|
||||
bool namesSuccess = (bool)getTagNamesMethod.Invoke(query, tagNameArgs)!;
|
||||
tagNamesError = tagNameArgs[3]!;
|
||||
rows.Add(new
|
||||
{
|
||||
Kind = "TagNames",
|
||||
Success = namesSuccess,
|
||||
ErrorDescription = GetPropertyText(tagNamesError, "ErrorDescription"),
|
||||
Names = ToSerializableValue(tagNameArgs[2])
|
||||
});
|
||||
}
|
||||
|
||||
MethodInfo? getTagInfoMethod = queryType.GetMethods().FirstOrDefault(method =>
|
||||
method.Name == "GetTagInfo" && method.GetParameters().Length == 4);
|
||||
if (getTagInfoMethod is not null)
|
||||
{
|
||||
object tagInfoError = Activator.CreateInstance(errorType)!;
|
||||
object?[] tagInfoArgs = [0u, requestedRows, null, tagInfoError];
|
||||
bool infoSuccess = (bool)getTagInfoMethod.Invoke(query, tagInfoArgs)!;
|
||||
tagInfoError = tagInfoArgs[3]!;
|
||||
rows.Add(new
|
||||
{
|
||||
Kind = "TagInfo",
|
||||
Success = infoSuccess,
|
||||
ErrorDescription = GetPropertyText(tagInfoError, "ErrorDescription"),
|
||||
Tags = SummarizeTagList(tagInfoArgs[2])
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MethodInfo? endMethod = queryType.GetMethod("EndQuery", new[] { errorType.MakeByRefType() });
|
||||
if (endMethod is not null)
|
||||
{
|
||||
object endError = Activator.CreateInstance(errorType)!;
|
||||
object?[] endArgs = [endError];
|
||||
_ = endMethod.Invoke(query, endArgs);
|
||||
}
|
||||
|
||||
if (query is IDisposable disposableQuery)
|
||||
{
|
||||
disposableQuery.Dispose();
|
||||
}
|
||||
}
|
||||
else if (openSuccess && status.ConnectedToServer)
|
||||
{
|
||||
object query = accessType.GetMethod("CreateHistoryQuery", Type.EmptyTypes)!.Invoke(access, Array.Empty<object>())!;
|
||||
Type queryType = query.GetType();
|
||||
snapshots["QueryAfterCreate"] = SnapshotObject(query);
|
||||
object queryArgs = Activator.CreateInstance(historyQueryArgsType)!;
|
||||
StringCollection tags = [];
|
||||
tags.Add(tagName);
|
||||
|
||||
SetProperty(queryArgs, "TagNames", tags);
|
||||
SetProperty(queryArgs, "StartDateTime", startUtc);
|
||||
SetProperty(queryArgs, "EndDateTime", endUtc);
|
||||
SetProperty(queryArgs, "BatchSize", checked((uint)Math.Max(maxRows, 1)));
|
||||
SetProperty(queryArgs, "RetrievalMode", Enum.Parse(retrievalModeType, retrievalModeName, ignoreCase: true));
|
||||
if (resolutionTicks > 0)
|
||||
{
|
||||
SetProperty(queryArgs, "Resolution", resolutionTicks);
|
||||
}
|
||||
snapshots["QueryArgsBeforeStart"] = SnapshotObject(queryArgs);
|
||||
|
||||
startError = Activator.CreateInstance(errorType)!;
|
||||
MethodInfo startMethod = queryType.GetMethod("StartQuery", new[] { historyQueryArgsType, errorType.MakeByRefType() })
|
||||
?? throw new MissingMethodException("HistoryQuery.StartQuery");
|
||||
WriteRuntimeMethodPointerSnapshot(assembly, runtimeMethodPointerOutput, runtimeMethodPointerFilters, repoRoot, scenario, "before-history-start");
|
||||
if (preStartSleepSeconds > 0)
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromSeconds(preStartSleepSeconds));
|
||||
}
|
||||
|
||||
object?[] startArgs = [queryArgs, startError];
|
||||
try
|
||||
{
|
||||
startSuccess = (bool)startMethod.Invoke(query, startArgs)!;
|
||||
}
|
||||
catch (TargetInvocationException ex)
|
||||
{
|
||||
startQueryException = FormatException(ex.InnerException ?? ex);
|
||||
}
|
||||
|
||||
startError = startArgs[1];
|
||||
snapshots["QueryAfterStart"] = SnapshotObject(query);
|
||||
snapshots["QueryArgsAfterStart"] = SnapshotObject(queryArgs);
|
||||
|
||||
if (startSuccess)
|
||||
{
|
||||
MethodInfo moveMethod = queryType.GetMethod("MoveNext", new[] { errorType.MakeByRefType() })
|
||||
?? throw new MissingMethodException("HistoryQuery.MoveNext");
|
||||
for (int i = 0; i < maxRows; i++)
|
||||
{
|
||||
object moveError = Activator.CreateInstance(errorType)!;
|
||||
object?[] moveArgs = [moveError];
|
||||
bool hasRow = (bool)moveMethod.Invoke(query, moveArgs)!;
|
||||
moveError = moveArgs[0]!;
|
||||
if (!hasRow)
|
||||
{
|
||||
moveTerminalDescription = GetPropertyText(moveError, "ErrorDescription");
|
||||
break;
|
||||
}
|
||||
|
||||
object result = GetPropertyValue(query, "QueryResult")!;
|
||||
snapshots["QueryAfterFirstMove"] = SnapshotObject(query);
|
||||
snapshots["QueryResultAfterFirstMove"] = SnapshotObject(result);
|
||||
rows.Add(new
|
||||
{
|
||||
StartDateTime = ((DateTime)GetPropertyValue(result, "StartDateTime")!).ToString("O"),
|
||||
EndDateTime = ((DateTime)GetPropertyValue(result, "EndDateTime")!).ToString("O"),
|
||||
Quality = GetPropertyValue(result, "Quality"),
|
||||
OpcQuality = GetPropertyValue(result, "OpcQuality"),
|
||||
QualityDetail = GetPropertyValue(result, "QualityDetail"),
|
||||
Value = GetPropertyValue(result, "Value"),
|
||||
PercentGood = GetPropertyValue(result, "PercentGood")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MethodInfo? endMethod = queryType.GetMethod("EndQuery", new[] { errorType.MakeByRefType() });
|
||||
if (endMethod is not null)
|
||||
{
|
||||
object endError = Activator.CreateInstance(errorType)!;
|
||||
object?[] endArgs = [endError];
|
||||
_ = endMethod.Invoke(query, endArgs);
|
||||
}
|
||||
|
||||
if (query is IDisposable disposableQuery)
|
||||
{
|
||||
disposableQuery.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
if (openSuccess)
|
||||
{
|
||||
object closeError = Activator.CreateInstance(errorType)!;
|
||||
MethodInfo? closeMethod = accessType.GetMethod("CloseConnection", new[] { errorType.MakeByRefType() });
|
||||
if (closeMethod is not null)
|
||||
{
|
||||
object?[] closeArgs = [closeError];
|
||||
_ = closeMethod.Invoke(access, closeArgs);
|
||||
}
|
||||
}
|
||||
|
||||
if (access is IDisposable disposableAccess)
|
||||
{
|
||||
disposableAccess.Dispose();
|
||||
}
|
||||
|
||||
Trace.Flush();
|
||||
|
||||
string tracePath = Path.Combine(repoRoot, "docs", "reverse-engineering", "native-wcf-message-log.svclog");
|
||||
var output = new
|
||||
{
|
||||
Operation = "NativeTraceHarness.IntegratedRead",
|
||||
Scenario = scenario,
|
||||
ServerName = serverName,
|
||||
DirectConnection = directConnection,
|
||||
ProxyServer = proxyServer,
|
||||
TagName = tagName,
|
||||
LookbackMinutes = lookbackMinutes,
|
||||
RetrievalMode = retrievalModeName,
|
||||
ResolutionTicks = resolutionTicks,
|
||||
StartUtc = startUtc.ToString("O"),
|
||||
EndUtc = endUtc.ToString("O"),
|
||||
OpenSuccess = openSuccess,
|
||||
OpenErrorType = GetPropertyText(openError, "ErrorType"),
|
||||
OpenErrorCode = GetPropertyText(openError, "ErrorCode"),
|
||||
OpenErrorDescription = GetPropertyText(openError, "ErrorDescription"),
|
||||
status.ConnectedToServer,
|
||||
status.Pending,
|
||||
status.ErrorOccurred,
|
||||
StartQuerySuccess = startSuccess,
|
||||
StartQueryErrorType = GetPropertyText(startError, "ErrorType"),
|
||||
StartQueryErrorCode = GetPropertyText(startError, "ErrorCode"),
|
||||
StartQueryErrorDescription = GetPropertyText(startError, "ErrorDescription"),
|
||||
StartQueryException = startQueryException,
|
||||
MoveTerminalDescription = moveTerminalDescription,
|
||||
RowCount = rows.Count,
|
||||
Rows = rows,
|
||||
Snapshots = snapshots,
|
||||
TracePath = tracePath,
|
||||
TraceExists = File.Exists(tracePath),
|
||||
TraceBytes = File.Exists(tracePath) ? new FileInfo(tracePath).Length : 0
|
||||
};
|
||||
|
||||
Console.WriteLine(Serialize(output));
|
||||
return openSuccess && startSuccess ? 0 : 1;
|
||||
}
|
||||
|
||||
private static ConnectionStatusSnapshot WaitForConnection(object access, Type accessType, Type statusType, int waitSeconds)
|
||||
{
|
||||
MethodInfo method = accessType.GetMethod("GetConnectionStatus", new[] { statusType.MakeByRefType() })
|
||||
?? throw new MissingMethodException("HistorianAccess.GetConnectionStatus");
|
||||
DateTime deadline = DateTime.UtcNow.AddSeconds(Math.Max(waitSeconds, 1));
|
||||
ConnectionStatusSnapshot snapshot;
|
||||
do
|
||||
{
|
||||
object status = Activator.CreateInstance(statusType)!;
|
||||
object?[] args = [status];
|
||||
_ = method.Invoke(access, args);
|
||||
status = args[0]!;
|
||||
snapshot = new ConnectionStatusSnapshot(
|
||||
(bool)GetPropertyValue(status, "ConnectedToServer")!,
|
||||
(bool)GetPropertyValue(status, "Pending")!,
|
||||
(bool)GetPropertyValue(status, "ErrorOccurred")!);
|
||||
|
||||
if ((snapshot.ConnectedToServer && !snapshot.Pending) || snapshot.ErrorOccurred || (!snapshot.ConnectedToServer && !snapshot.Pending))
|
||||
{
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
Thread.Sleep(250);
|
||||
} while (DateTime.UtcNow < deadline);
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<object> DumpRuntimeMethodPointers(Assembly assembly, string filter)
|
||||
{
|
||||
List<object> results = [];
|
||||
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance;
|
||||
LoadedModuleInfo? moduleInfo = FindLoadedModule(Path.GetFileName(assembly.Location));
|
||||
foreach (MethodInfo method in assembly.ManifestModule.GetMethods(flags))
|
||||
{
|
||||
AddRuntimeMethodPointer(results, "<Module>", method, filter, moduleInfo);
|
||||
}
|
||||
|
||||
foreach (Type type in assembly.GetTypes())
|
||||
{
|
||||
foreach (MethodInfo method in type.GetMethods(flags))
|
||||
{
|
||||
AddRuntimeMethodPointer(results, type.FullName ?? type.Name, method, filter, moduleInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static void WriteRuntimeMethodPointerSnapshot(
|
||||
Assembly assembly,
|
||||
string? outputPath,
|
||||
string filtersText,
|
||||
string repoRoot,
|
||||
string scenario,
|
||||
string phase)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(outputPath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string resolvedPath = Path.IsPathRooted(outputPath!)
|
||||
? outputPath!
|
||||
: Path.Combine(repoRoot, outputPath!);
|
||||
string? directory = Path.GetDirectoryName(resolvedPath);
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
List<object> methodPointers = [];
|
||||
foreach (string rawFilter in filtersText.Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
string filter = rawFilter.Trim();
|
||||
if (filter.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
methodPointers.Add(new
|
||||
{
|
||||
Filter = filter,
|
||||
Methods = DumpRuntimeMethodPointers(assembly, filter)
|
||||
});
|
||||
}
|
||||
|
||||
var snapshot = new
|
||||
{
|
||||
Operation = "NativeTraceHarness.RuntimeMethodPointerSnapshot",
|
||||
ProcessId = Process.GetCurrentProcess().Id,
|
||||
Scenario = scenario,
|
||||
Phase = phase,
|
||||
TimestampUtc = DateTime.UtcNow.ToString("O"),
|
||||
AssemblyPath = assembly.Location,
|
||||
MethodPointers = methodPointers
|
||||
};
|
||||
|
||||
File.WriteAllText(resolvedPath, Serialize(snapshot));
|
||||
}
|
||||
|
||||
private static void AddRuntimeMethodPointer(List<object> results, string declaringType, MethodInfo method, string filter, LoadedModuleInfo? moduleInfo)
|
||||
{
|
||||
string fullName = declaringType + "." + method.Name;
|
||||
if (fullName.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string? pointer = null;
|
||||
string? prepareError = null;
|
||||
try
|
||||
{
|
||||
RuntimeHelpers.PrepareMethod(method.MethodHandle);
|
||||
long pointerValue = method.MethodHandle.GetFunctionPointer().ToInt64();
|
||||
pointer = "0x" + pointerValue.ToString("X");
|
||||
bool pointerInModule = moduleInfo is not null && pointerValue >= moduleInfo.BaseAddress && pointerValue < moduleInfo.EndAddress;
|
||||
long? pointerRva = pointerInModule ? pointerValue - moduleInfo!.BaseAddress : null;
|
||||
results.Add(new
|
||||
{
|
||||
DeclaringType = declaringType,
|
||||
method.Name,
|
||||
MetadataToken = "0x" + method.MetadataToken.ToString("X8"),
|
||||
IsStatic = method.IsStatic,
|
||||
IsPublic = method.IsPublic,
|
||||
ModuleBase = moduleInfo is not null ? "0x" + moduleInfo.BaseAddress.ToString("X") : null,
|
||||
ModuleSize = moduleInfo is not null ? "0x" + moduleInfo.Size.ToString("X") : null,
|
||||
FunctionPointer = pointer,
|
||||
FunctionPointerInModule = pointerInModule,
|
||||
FunctionPointerRva = pointerRva.HasValue ? "0x" + pointerRva.Value.ToString("X") : null,
|
||||
PrepareError = prepareError
|
||||
});
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
prepareError = FormatException(ex);
|
||||
}
|
||||
|
||||
results.Add(new
|
||||
{
|
||||
DeclaringType = declaringType,
|
||||
method.Name,
|
||||
MetadataToken = "0x" + method.MetadataToken.ToString("X8"),
|
||||
IsStatic = method.IsStatic,
|
||||
IsPublic = method.IsPublic,
|
||||
ModuleBase = moduleInfo is not null ? "0x" + moduleInfo.BaseAddress.ToString("X") : null,
|
||||
ModuleSize = moduleInfo is not null ? "0x" + moduleInfo.Size.ToString("X") : null,
|
||||
FunctionPointer = pointer,
|
||||
FunctionPointerInModule = false,
|
||||
FunctionPointerRva = (string?)null,
|
||||
PrepareError = prepareError
|
||||
});
|
||||
}
|
||||
|
||||
private static LoadedModuleInfo? FindLoadedModule(string moduleName)
|
||||
{
|
||||
foreach (ProcessModule module in Process.GetCurrentProcess().Modules)
|
||||
{
|
||||
if (string.Equals(module.ModuleName, moduleName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new LoadedModuleInfo(module.BaseAddress.ToInt64(), module.ModuleMemorySize);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private sealed class LoadedModuleInfo
|
||||
{
|
||||
public LoadedModuleInfo(long baseAddress, int size)
|
||||
{
|
||||
BaseAddress = baseAddress;
|
||||
Size = size;
|
||||
}
|
||||
|
||||
public long BaseAddress { get; }
|
||||
|
||||
public int Size { get; }
|
||||
|
||||
public long EndAddress => BaseAddress + Size;
|
||||
}
|
||||
|
||||
private static string Serialize(object value)
|
||||
{
|
||||
return new JavaScriptSerializer { MaxJsonLength = int.MaxValue }.Serialize(value);
|
||||
}
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
string? directory = AppContext.BaseDirectory;
|
||||
while (!string.IsNullOrEmpty(directory))
|
||||
{
|
||||
if (File.Exists(Path.Combine(directory, "Histsdk.slnx")))
|
||||
{
|
||||
return directory!;
|
||||
}
|
||||
|
||||
directory = Directory.GetParent(directory)?.FullName;
|
||||
}
|
||||
|
||||
return Directory.GetCurrentDirectory();
|
||||
}
|
||||
|
||||
private static string? GetArg(string[] args, string name)
|
||||
{
|
||||
for (int i = 0; i < args.Length - 1; i++)
|
||||
{
|
||||
if (args[i].Equals(name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return args[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool HasFlag(string[] args, string name)
|
||||
{
|
||||
foreach (string arg in args)
|
||||
{
|
||||
if (arg.Equals(name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static DateTime? TryParseUtc(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!DateTime.TryParse(
|
||||
value,
|
||||
null,
|
||||
System.Globalization.DateTimeStyles.AssumeUniversal | System.Globalization.DateTimeStyles.AdjustToUniversal,
|
||||
out DateTime parsed))
|
||||
{
|
||||
throw new ArgumentException("Invalid UTC timestamp: " + value);
|
||||
}
|
||||
|
||||
return DateTime.SpecifyKind(parsed, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
private static Type GetType(Assembly assembly, string name)
|
||||
{
|
||||
return assembly.GetType(name, throwOnError: true)!;
|
||||
}
|
||||
|
||||
private static void SetProperty(object target, string name, object value)
|
||||
{
|
||||
PropertyInfo? property = target.GetType().GetProperty(name);
|
||||
if (property is not null && property.CanWrite)
|
||||
{
|
||||
MethodInfo? setter = property.GetSetMethod(nonPublic: true);
|
||||
if (setter is not null)
|
||||
{
|
||||
setter.Invoke(target, [value]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetField(object target, string name, object value)
|
||||
{
|
||||
FieldInfo? field = target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
if (field is not null)
|
||||
{
|
||||
field.SetValue(target, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static object? GetPropertyValue(object target, string name)
|
||||
{
|
||||
PropertyInfo? property = target.GetType().GetProperty(name);
|
||||
return property is null || !property.CanRead ? null : property.GetValue(target);
|
||||
}
|
||||
|
||||
private static object? TryGetPropertyValue(object target, string name)
|
||||
{
|
||||
return TryRead(() => GetPropertyValue(target, name));
|
||||
}
|
||||
|
||||
private static string? FormatDateProperty(object target, string name)
|
||||
{
|
||||
object? value = TryGetPropertyValue(target, name);
|
||||
return value is DateTime dateTime ? dateTime.ToString("O") : value?.ToString();
|
||||
}
|
||||
|
||||
private static string? GetPropertyText(object? target, string name)
|
||||
{
|
||||
if (target is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return GetPropertyValue(target, name)?.ToString();
|
||||
}
|
||||
|
||||
private static string FormatException(Exception ex)
|
||||
{
|
||||
return ex.GetType().Name + ": " + ex.Message;
|
||||
}
|
||||
|
||||
private static bool IsEventScenario(string scenario)
|
||||
{
|
||||
return scenario.Equals("event", StringComparison.OrdinalIgnoreCase)
|
||||
|| scenario.Equals("events", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsTagScenario(string scenario)
|
||||
{
|
||||
return scenario.Equals("tag", StringComparison.OrdinalIgnoreCase)
|
||||
|| scenario.Equals("tags", StringComparison.OrdinalIgnoreCase)
|
||||
|| scenario.Equals("tag-query", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static Dictionary<string, object?> SnapshotObject(object target)
|
||||
{
|
||||
Dictionary<string, object?> snapshot = new(StringComparer.OrdinalIgnoreCase);
|
||||
BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||
|
||||
foreach (FieldInfo field in target.GetType().GetFields(flags))
|
||||
{
|
||||
if (ShouldSkipMember(field.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
snapshot["field:" + field.Name] = TryRead(() => ToSerializableValue(field.GetValue(target)));
|
||||
}
|
||||
|
||||
foreach (PropertyInfo property in target.GetType().GetProperties(flags))
|
||||
{
|
||||
if (!property.CanRead || property.GetIndexParameters().Length != 0 || ShouldSkipMember(property.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
snapshot["property:" + property.Name] = TryRead(() => ToSerializableValue(property.GetValue(target)));
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static bool ShouldSkipMember(string name)
|
||||
{
|
||||
return name.IndexOf("password", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| name.IndexOf("user", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| name.IndexOf("security", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private static object? TryRead(Func<object?> read)
|
||||
{
|
||||
try
|
||||
{
|
||||
return read();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return "<unreadable:" + ex.GetType().Name + ">";
|
||||
}
|
||||
}
|
||||
|
||||
private static object? ToSerializableValue(object? value)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Type type = value.GetType();
|
||||
if (type.IsEnum)
|
||||
{
|
||||
return value.ToString();
|
||||
}
|
||||
|
||||
if (value is DateTime dateTime)
|
||||
{
|
||||
return dateTime.ToString("O");
|
||||
}
|
||||
|
||||
if (value is string or bool or byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value is StringCollection strings)
|
||||
{
|
||||
List<string> result = [];
|
||||
foreach (string? item in strings)
|
||||
{
|
||||
if (item is not null)
|
||||
{
|
||||
result.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (value is Array array)
|
||||
{
|
||||
List<object?> result = [];
|
||||
int count = Math.Min(array.Length, 8);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
result.Add(ToSerializableValue(array.GetValue(i)));
|
||||
}
|
||||
|
||||
if (array.Length > count)
|
||||
{
|
||||
result.Add("<truncated:" + array.Length + ">");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
return "<" + type.FullName + ">";
|
||||
}
|
||||
|
||||
private static List<object?> SummarizeTagList(object? tagList)
|
||||
{
|
||||
List<object?> result = [];
|
||||
if (tagList is null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
object? lengthValue = TryGetPropertyValue(tagList, "Length") ?? TryGetPropertyValue(tagList, "Count");
|
||||
int length = Convert.ToInt32(lengthValue);
|
||||
int count = Math.Min(length, 8);
|
||||
MethodInfo? itemMethod = tagList.GetType().GetMethods().FirstOrDefault(method =>
|
||||
method.Name == "Item" && method.GetParameters().Length == 1);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
object? tag = null;
|
||||
try
|
||||
{
|
||||
tag = itemMethod?.Invoke(tagList, [checked((uint)i)]);
|
||||
}
|
||||
catch
|
||||
{
|
||||
tag = null;
|
||||
}
|
||||
|
||||
if (tag is null)
|
||||
{
|
||||
try
|
||||
{
|
||||
tag = itemMethod?.Invoke(tagList, [checked((uint)(i + 1))]);
|
||||
}
|
||||
catch
|
||||
{
|
||||
tag = null;
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(tag is null ? null : new
|
||||
{
|
||||
TagName = TryGetPropertyValue(tag, "TagName"),
|
||||
TagDescription = TryGetPropertyValue(tag, "TagDescription"),
|
||||
EngineeringUnit = TryGetPropertyValue(tag, "EngineeringUnit"),
|
||||
TagKey = TryGetPropertyValue(tag, "TagKey"),
|
||||
TagDataType = TryGetPropertyValue(tag, "TagDataType"),
|
||||
TagStorageType = TryGetPropertyValue(tag, "TagStorageType"),
|
||||
SourceTag = TryGetPropertyValue(tag, "SourceTag"),
|
||||
SourceServer = TryGetPropertyValue(tag, "SourceServer"),
|
||||
IsInActive = TryGetPropertyValue(tag, "IsInActive")
|
||||
});
|
||||
}
|
||||
|
||||
if (length > count)
|
||||
{
|
||||
result.Add("<truncated:" + length + ">");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TryDelete(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort cleanup; WCF may still hold the previous trace file.
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ConnectionStatusSnapshot
|
||||
{
|
||||
public ConnectionStatusSnapshot(bool connectedToServer, bool pending, bool errorOccurred)
|
||||
{
|
||||
ConnectedToServer = connectedToServer;
|
||||
Pending = pending;
|
||||
ErrorOccurred = errorOccurred;
|
||||
}
|
||||
|
||||
public bool ConnectedToServer { get; }
|
||||
|
||||
public bool Pending { get; }
|
||||
|
||||
public bool ErrorOccurred { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net481</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.ServiceModel" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,880 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Security;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.ServiceModel;
|
||||
using System.ServiceModel.Channels;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
private const string Namespace = "aa";
|
||||
private const string HistoryService = "Hist";
|
||||
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
string targetName = GetArg(args, "--target") ?? @"NT SERVICE\aahClientAccessPoint";
|
||||
string endpoint = GetArg(args, "--endpoint") ?? "net.pipe://localhost/Hist";
|
||||
string retrievalEndpoint = GetArg(args, "--retr-endpoint") ?? "net.pipe://localhost/Retr";
|
||||
string? open2ReplayPath = GetArg(args, "--open2-replay");
|
||||
string? dataQueryReplayPath = GetArg(args, "--data-query-replay");
|
||||
int maxBufferSize = int.TryParse(GetArg(args, "--max-buffer-size"), out int parsedMaxBufferSize)
|
||||
? parsedMaxBufferSize
|
||||
: 66303;
|
||||
|
||||
try
|
||||
{
|
||||
IHistoryServiceContract2 channel = CreatePipeChannel(endpoint, maxBufferSize);
|
||||
uint getVersionReturn = channel.GetInterfaceVersion(out uint interfaceVersion);
|
||||
|
||||
using SspiClient sspi = new("Negotiate", targetName);
|
||||
byte[] incoming = Array.Empty<byte>();
|
||||
List<string> roundJson = new();
|
||||
bool? finalServerSuccess = null;
|
||||
string? finalStatus = null;
|
||||
int? finalServerOutputLength = null;
|
||||
NativeError? finalNativeError = null;
|
||||
|
||||
Guid contextKey = Guid.NewGuid();
|
||||
string handle = contextKey.ToString("D").ToUpperInvariant();
|
||||
for (int round = 0; round < 8; round++)
|
||||
{
|
||||
SspiStepResult clientStep = sspi.Next(incoming);
|
||||
ApplyNativeNtlmNegotiateVersionFlag(clientStep.Token);
|
||||
byte[] wrapped = WrapValidateClientCredentialToken(round == 0, clientStep.Token);
|
||||
|
||||
bool serverSuccess = channel.ValidateClientCredential(handle, wrapped, out byte[] serverOutput, out byte[] errorBuffer);
|
||||
serverOutput ??= Array.Empty<byte>();
|
||||
errorBuffer ??= Array.Empty<byte>();
|
||||
NativeError? nativeError = TryReadNativeError(errorBuffer);
|
||||
bool serverContinue = serverOutput.Length > 0 && serverOutput[0] != 0;
|
||||
byte[] serverToken = serverContinue && serverOutput.Length > 1
|
||||
? serverOutput.Skip(1).ToArray()
|
||||
: Array.Empty<byte>();
|
||||
|
||||
roundJson.Add("{"
|
||||
+ JsonProp("Round", round) + ","
|
||||
+ JsonProp("ClientStatus", clientStep.Status) + ","
|
||||
+ JsonProp("OutgoingLength", clientStep.Token.Length) + ","
|
||||
+ JsonProp("OutgoingSha256", HashBytesOrNull(clientStep.Token)) + ","
|
||||
+ JsonProp("OutgoingPrefixHex", ToPrefixHex(clientStep.Token, 32)) + ","
|
||||
+ JsonProp("WrappedOutgoingLength", wrapped.Length) + ","
|
||||
+ JsonProp("WrappedOutgoingSha256", HashBytesOrNull(wrapped)) + ","
|
||||
+ JsonProp("WrappedOutgoingPrefixHex", ToPrefixHex(wrapped, 32)) + ","
|
||||
+ JsonProp("ServerSuccess", serverSuccess) + ","
|
||||
+ JsonProp("ServerOutputLength", serverOutput.Length) + ","
|
||||
+ JsonProp("ServerOutputSha256", HashBytesOrNull(serverOutput)) + ","
|
||||
+ JsonProp("ServerOutputPrefixHex", ToPrefixHex(serverOutput, 32)) + ","
|
||||
+ JsonProp("ServerContinue", serverContinue) + ","
|
||||
+ JsonProp("ServerTokenLength", serverToken.Length) + ","
|
||||
+ JsonProp("ErrorLength", errorBuffer.Length) + ","
|
||||
+ "\"NativeError\":" + FormatNativeError(nativeError)
|
||||
+ "}");
|
||||
|
||||
finalServerSuccess = serverSuccess;
|
||||
finalStatus = clientStep.Status;
|
||||
finalServerOutputLength = serverOutput.Length;
|
||||
finalNativeError = nativeError;
|
||||
|
||||
if (!serverSuccess || clientStep.Done || !serverContinue)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
incoming = serverToken;
|
||||
}
|
||||
|
||||
string? chainJson = null;
|
||||
if (finalServerSuccess == true && finalNativeError is null && open2ReplayPath != null)
|
||||
{
|
||||
chainJson = RunOpen2AndQueryChain(channel, retrievalEndpoint, contextKey, open2ReplayPath, dataQueryReplayPath, maxBufferSize);
|
||||
}
|
||||
|
||||
Console.WriteLine("{"
|
||||
+ JsonProp("Runtime", ".NET Framework") + ","
|
||||
+ JsonProp("Endpoint", endpoint) + ","
|
||||
+ JsonProp("Operation", "ValCl") + ","
|
||||
+ JsonProp("Transport", "NamedPipeNone") + ","
|
||||
+ JsonProp("GetVersionReturnCode", getVersionReturn) + ","
|
||||
+ JsonProp("InterfaceVersion", interfaceVersion) + ","
|
||||
+ JsonProp("TargetName", targetName) + ","
|
||||
+ JsonProp("HandleSha256", Sha256Utf8(handle)) + ","
|
||||
+ JsonProp("HandleLength", handle.Length) + ","
|
||||
+ JsonProp("FinalStatus", finalStatus) + ","
|
||||
+ JsonProp("FinalServerSuccess", finalServerSuccess) + ","
|
||||
+ JsonProp("FinalServerOutputLength", finalServerOutputLength) + ","
|
||||
+ "\"FinalNativeError\":" + FormatNativeError(finalNativeError) + ","
|
||||
+ "\"Rounds\":[" + string.Join(",", roundJson) + "]"
|
||||
+ (chainJson is null ? string.Empty : "," + chainJson)
|
||||
+ "}");
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine(Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_STACK") == "1" ? ex.ToString() : ex.Message);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static IHistoryServiceContract2 CreatePipeChannel(string endpoint, int maxBufferSize)
|
||||
{
|
||||
ChannelFactory<IHistoryServiceContract2> factory = new(BuildMdasPipeBinding(maxBufferSize), new EndpointAddress(endpoint));
|
||||
return factory.CreateChannel();
|
||||
}
|
||||
|
||||
private static IRetrievalServiceContract2 CreateRetrievalPipeChannel(string endpoint, int maxBufferSize)
|
||||
{
|
||||
ChannelFactory<IRetrievalServiceContract2> factory = new(BuildMdasPipeBinding(maxBufferSize), new EndpointAddress(endpoint));
|
||||
return factory.CreateChannel();
|
||||
}
|
||||
|
||||
private static CustomBinding BuildMdasPipeBinding(int maxBufferSize)
|
||||
{
|
||||
NetNamedPipeBinding nativeShape = new()
|
||||
{
|
||||
MaxBufferSize = maxBufferSize,
|
||||
MaxReceivedMessageSize = maxBufferSize
|
||||
};
|
||||
nativeShape.Security.Mode = NetNamedPipeSecurityMode.None;
|
||||
nativeShape.ReaderQuotas.MaxArrayLength = maxBufferSize;
|
||||
|
||||
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 = TimeSpan.FromSeconds(10),
|
||||
CloseTimeout = TimeSpan.FromSeconds(10),
|
||||
SendTimeout = TimeSpan.FromSeconds(10),
|
||||
ReceiveTimeout = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
}
|
||||
|
||||
private static string RunOpen2AndQueryChain(
|
||||
IHistoryServiceContract2 historyChannel,
|
||||
string retrievalEndpoint,
|
||||
Guid contextKey,
|
||||
string open2ReplayPath,
|
||||
string? dataQueryReplayPath,
|
||||
int maxBufferSize)
|
||||
{
|
||||
StringBuilder json = new();
|
||||
json.Append("\"Chain\":{");
|
||||
|
||||
byte[] open2RequestRaw = File.ReadAllBytes(open2ReplayPath);
|
||||
if (open2RequestRaw.Length < 17 || open2RequestRaw[0] != 6)
|
||||
{
|
||||
json.Append(JsonProp("Open2ReplaySource", open2ReplayPath));
|
||||
json.Append("," + JsonProp("Open2ReplayLength", open2RequestRaw.Length));
|
||||
json.Append("," + JsonProp("Open2Skipped", "replay must be a v6 OpenConnection3 buffer of at least 17 bytes"));
|
||||
json.Append("}");
|
||||
return json.ToString();
|
||||
}
|
||||
|
||||
byte[] open2Request = (byte[])open2RequestRaw.Clone();
|
||||
byte[] keyBytes = contextKey.ToByteArray();
|
||||
Buffer.BlockCopy(keyBytes, 0, open2Request, 1, 16);
|
||||
|
||||
json.Append(JsonProp("Open2RequestLength", open2Request.Length));
|
||||
json.Append("," + JsonProp("Open2RequestOriginalSha256", Sha256(open2RequestRaw)));
|
||||
json.Append("," + JsonProp("Open2RequestSplicedSha256", Sha256(open2Request)));
|
||||
|
||||
byte[] open2In = open2Request;
|
||||
bool open2Success;
|
||||
byte[] open2Out;
|
||||
byte[] open2Err;
|
||||
try
|
||||
{
|
||||
open2Success = historyChannel.OpenConnection2(ref open2In, out open2Out, out open2Err);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
json.Append("," + JsonProp("Open2Exception", ex.GetType().Name + ": " + ex.Message));
|
||||
json.Append("}");
|
||||
return json.ToString();
|
||||
}
|
||||
|
||||
open2Out ??= Array.Empty<byte>();
|
||||
open2Err ??= Array.Empty<byte>();
|
||||
json.Append("," + JsonProp("Open2Success", open2Success));
|
||||
json.Append("," + JsonProp("Open2ResponseLength", open2Out.Length));
|
||||
json.Append("," + JsonProp("Open2ResponseSha256", HashBytesOrNull(open2Out)));
|
||||
json.Append("," + JsonProp("Open2ResponsePrefixHex", ToPrefixHex(open2Out, 8)));
|
||||
json.Append("," + JsonProp("Open2ErrorLength", open2Err.Length));
|
||||
json.Append("," + "\"Open2NativeError\":" + FormatNativeError(TryReadNativeError(open2Err)));
|
||||
|
||||
uint clientHandle = 0;
|
||||
byte responseVersion = 0;
|
||||
if (open2Success && open2Out.Length >= 5)
|
||||
{
|
||||
responseVersion = open2Out[0];
|
||||
clientHandle = ReadUInt32LittleEndian(open2Out, 1);
|
||||
json.Append("," + JsonProp("Open2ResponseVersion", responseVersion));
|
||||
json.Append("," + JsonProp("Open2ClientHandlePresent", true));
|
||||
}
|
||||
|
||||
if (!open2Success || clientHandle == 0)
|
||||
{
|
||||
json.Append("}");
|
||||
return json.ToString();
|
||||
}
|
||||
|
||||
IRetrievalServiceContract2 retrChannel = CreateRetrievalPipeChannel(retrievalEndpoint, maxBufferSize);
|
||||
try
|
||||
{
|
||||
uint retrGetVersionReturn = retrChannel.GetInterfaceVersion(out uint retrInterfaceVersion);
|
||||
json.Append("," + JsonProp("RetrGetVersionReturnCode", retrGetVersionReturn));
|
||||
json.Append("," + JsonProp("RetrInterfaceVersion", retrInterfaceVersion));
|
||||
|
||||
uint isOriginalReturn = retrChannel.IsOriginalAllowed(clientHandle, out bool isAllowed);
|
||||
json.Append("," + JsonProp("IsOriginalAllowedReturnCode", isOriginalReturn));
|
||||
json.Append("," + JsonProp("IsOriginalAllowedIsAllowed", isAllowed));
|
||||
|
||||
if (dataQueryReplayPath is null)
|
||||
{
|
||||
json.Append("," + JsonProp("StartQuery2Skipped", "no --data-query-replay path"));
|
||||
json.Append("}");
|
||||
return json.ToString();
|
||||
}
|
||||
|
||||
byte[] dataQueryRequest = File.ReadAllBytes(dataQueryReplayPath);
|
||||
json.Append("," + JsonProp("StartQuery2RequestLength", dataQueryRequest.Length));
|
||||
json.Append("," + JsonProp("StartQuery2RequestSha256", Sha256(dataQueryRequest)));
|
||||
|
||||
uint queryHandle = 0;
|
||||
bool startQuerySuccess;
|
||||
uint responseSize;
|
||||
byte[] responseBuffer;
|
||||
uint errorSize;
|
||||
byte[] errorBuffer;
|
||||
try
|
||||
{
|
||||
startQuerySuccess = retrChannel.StartQuery2(
|
||||
clientHandle,
|
||||
queryRequestType: 1,
|
||||
requestSize: (uint)dataQueryRequest.Length,
|
||||
requestBuffer: dataQueryRequest,
|
||||
out responseSize,
|
||||
out responseBuffer,
|
||||
ref queryHandle,
|
||||
out errorSize,
|
||||
out errorBuffer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
json.Append("," + JsonProp("StartQuery2Exception", ex.GetType().Name + ": " + ex.Message));
|
||||
json.Append("}");
|
||||
return json.ToString();
|
||||
}
|
||||
|
||||
responseBuffer ??= Array.Empty<byte>();
|
||||
errorBuffer ??= Array.Empty<byte>();
|
||||
json.Append("," + JsonProp("StartQuery2Success", startQuerySuccess));
|
||||
json.Append("," + JsonProp("StartQuery2ResponseSize", (int)responseSize));
|
||||
json.Append("," + JsonProp("StartQuery2ResponseSha256", HashBytesOrNull(responseBuffer)));
|
||||
json.Append("," + JsonProp("StartQuery2ResponsePrefixHex", ToPrefixHex(responseBuffer, 8)));
|
||||
json.Append("," + JsonProp("StartQuery2ErrorSize", (int)errorSize));
|
||||
json.Append("," + JsonProp("StartQuery2QueryHandlePresent", queryHandle != 0));
|
||||
json.Append("," + "\"StartQuery2NativeError\":" + FormatNativeError(TryReadNativeError(errorBuffer)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { ((ICommunicationObject)retrChannel).Close(); }
|
||||
catch { try { ((ICommunicationObject)retrChannel).Abort(); } catch { } }
|
||||
}
|
||||
|
||||
json.Append("}");
|
||||
return json.ToString();
|
||||
}
|
||||
|
||||
private static byte[] WrapValidateClientCredentialToken(bool isFirstRound, byte[] token)
|
||||
{
|
||||
byte[] buffer = new byte[checked(1 + sizeof(uint) + token.Length)];
|
||||
buffer[0] = isFirstRound ? (byte)1 : (byte)0;
|
||||
WriteUInt32LittleEndian(buffer, 1, checked((uint)token.Length));
|
||||
Buffer.BlockCopy(token, 0, buffer, 1 + sizeof(uint), token.Length);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private static void ApplyNativeNtlmNegotiateVersionFlag(byte[] token)
|
||||
{
|
||||
byte[] ntlmSignature = Encoding.ASCII.GetBytes("NTLMSSP\0");
|
||||
if (token.Length >= 16 && token.Take(ntlmSignature.Length).SequenceEqual(ntlmSignature)
|
||||
&& ReadUInt32LittleEndian(token, 8) == 1)
|
||||
{
|
||||
uint flags = ReadUInt32LittleEndian(token, 12);
|
||||
flags |= 0x0010_0000;
|
||||
WriteUInt32LittleEndian(token, 12, flags);
|
||||
}
|
||||
}
|
||||
|
||||
private static NativeError? TryReadNativeError(byte[] bytes)
|
||||
{
|
||||
if (bytes.Length < 5)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
uint type = bytes[0];
|
||||
uint code = ReadUInt32LittleEndian(bytes, 1);
|
||||
return new NativeError(type, code, code == 1 ? "Failure" : null);
|
||||
}
|
||||
|
||||
private static uint ReadUInt32LittleEndian(byte[] bytes, int offset)
|
||||
{
|
||||
return (uint)(bytes[offset]
|
||||
| bytes[offset + 1] << 8
|
||||
| bytes[offset + 2] << 16
|
||||
| bytes[offset + 3] << 24);
|
||||
}
|
||||
|
||||
private static void WriteUInt32LittleEndian(byte[] bytes, int offset, uint value)
|
||||
{
|
||||
bytes[offset] = (byte)value;
|
||||
bytes[offset + 1] = (byte)(value >> 8);
|
||||
bytes[offset + 2] = (byte)(value >> 16);
|
||||
bytes[offset + 3] = (byte)(value >> 24);
|
||||
}
|
||||
|
||||
private static string? GetArg(string[] args, string name)
|
||||
{
|
||||
for (int i = 0; i < args.Length - 1; i++)
|
||||
{
|
||||
if (string.Equals(args[i], name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return args[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string JsonProp(string name, string? value)
|
||||
{
|
||||
return "\"" + JsonEscape(name) + "\":" + (value is null ? "null" : "\"" + JsonEscape(value) + "\"");
|
||||
}
|
||||
|
||||
private static string JsonProp(string name, bool value) => "\"" + JsonEscape(name) + "\":" + (value ? "true" : "false");
|
||||
|
||||
private static string JsonProp(string name, bool? value) => "\"" + JsonEscape(name) + "\":" + (value.HasValue ? value.Value ? "true" : "false" : "null");
|
||||
|
||||
private static string JsonProp(string name, int value) => "\"" + JsonEscape(name) + "\":" + value;
|
||||
|
||||
private static string JsonProp(string name, int? value) => "\"" + JsonEscape(name) + "\":" + (value.HasValue ? value.Value.ToString() : "null");
|
||||
|
||||
private static string JsonProp(string name, uint value) => "\"" + JsonEscape(name) + "\":" + value;
|
||||
|
||||
private static string FormatNativeError(NativeError? error)
|
||||
{
|
||||
return error is null
|
||||
? "null"
|
||||
: "{" + JsonProp("Type", error.Value.Type) + "," + JsonProp("Code", error.Value.Code) + "," + JsonProp("Name", error.Value.Name) + "}";
|
||||
}
|
||||
|
||||
private static string? HashBytesOrNull(byte[] bytes)
|
||||
{
|
||||
return bytes.Length == 0 ? null : Sha256(bytes);
|
||||
}
|
||||
|
||||
private static string Sha256Utf8(string value) => Sha256(Encoding.UTF8.GetBytes(value));
|
||||
|
||||
private static string Sha256(byte[] bytes)
|
||||
{
|
||||
using SHA256 sha256 = SHA256.Create();
|
||||
return BitConverter.ToString(sha256.ComputeHash(bytes)).Replace("-", string.Empty).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string ToPrefixHex(byte[] bytes, int maxBytes)
|
||||
{
|
||||
int count = Math.Min(bytes.Length, maxBytes);
|
||||
StringBuilder builder = new(count * 2);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
builder.Append(bytes[i].ToString("x2"));
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string JsonEscape(string value)
|
||||
{
|
||||
return value
|
||||
.Replace("\\", "\\\\")
|
||||
.Replace("\"", "\\\"")
|
||||
.Replace("\r", "\\r")
|
||||
.Replace("\n", "\\n");
|
||||
}
|
||||
|
||||
private readonly struct NativeError
|
||||
{
|
||||
public NativeError(uint type, uint code, string? name)
|
||||
{
|
||||
Type = type;
|
||||
Code = code;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public uint Type { get; }
|
||||
public uint Code { get; }
|
||||
public string? Name { get; }
|
||||
}
|
||||
|
||||
private readonly struct SspiStepResult
|
||||
{
|
||||
public SspiStepResult(byte[] token, string status, bool done)
|
||||
{
|
||||
Token = token;
|
||||
Status = status;
|
||||
Done = done;
|
||||
}
|
||||
|
||||
public byte[] Token { get; }
|
||||
public string Status { get; }
|
||||
public bool Done { get; }
|
||||
}
|
||||
|
||||
private sealed class SspiClient : IDisposable
|
||||
{
|
||||
private const int SECPKG_CRED_OUTBOUND = 2;
|
||||
private const int SECBUFFER_TOKEN = 2;
|
||||
private const int ISC_REQ_REPLAY_DETECT = 0x4;
|
||||
private const int ISC_REQ_SEQUENCE_DETECT = 0x8;
|
||||
private const int ISC_REQ_CONFIDENTIALITY = 0x10;
|
||||
private const int ISC_REQ_CONNECTION = 0x800;
|
||||
private const int ISC_REQ_IDENTIFY = 0x20000;
|
||||
private const int ISC_REQ_ALLOCATE_MEMORY = 0x100;
|
||||
private const int SEC_E_OK = 0;
|
||||
private const int SEC_I_CONTINUE_NEEDED = 0x00090312;
|
||||
|
||||
private readonly string targetName;
|
||||
private SecHandle credential;
|
||||
private SecHandle context;
|
||||
private bool haveContext;
|
||||
private int roundIndex;
|
||||
|
||||
public SspiClient(string package, string targetName)
|
||||
{
|
||||
this.targetName = targetName;
|
||||
credential = default;
|
||||
long expiry;
|
||||
int status = AcquireCredentialsHandle(null, package, SECPKG_CRED_OUTBOUND, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, ref credential, out expiry);
|
||||
ThrowIfFailed(status, "AcquireCredentialsHandle");
|
||||
}
|
||||
|
||||
public SspiStepResult Next(byte[] incoming)
|
||||
{
|
||||
SecBufferDesc outBufferDesc = CreateOutputBufferDesc();
|
||||
SecBufferDesc? inBufferDesc = incoming.Length == 0 ? null : CreateInputBufferDesc(incoming);
|
||||
try
|
||||
{
|
||||
uint contextAttributes;
|
||||
long expiry;
|
||||
SecHandle newContext = default;
|
||||
int status;
|
||||
int nativeBase = ISC_REQ_REPLAY_DETECT | ISC_REQ_SEQUENCE_DETECT | ISC_REQ_CONFIDENTIALITY | ISC_REQ_CONNECTION;
|
||||
int contextRequirements = ISC_REQ_ALLOCATE_MEMORY | nativeBase | (roundIndex == 0 ? ISC_REQ_IDENTIFY : 0);
|
||||
if (inBufferDesc.HasValue)
|
||||
{
|
||||
SecBufferDesc input = inBufferDesc.Value;
|
||||
status = InitializeSecurityContext(
|
||||
ref credential,
|
||||
ref context,
|
||||
targetName,
|
||||
contextRequirements,
|
||||
0,
|
||||
0,
|
||||
ref input,
|
||||
0,
|
||||
ref newContext,
|
||||
ref outBufferDesc,
|
||||
out contextAttributes,
|
||||
out expiry);
|
||||
}
|
||||
else
|
||||
{
|
||||
status = InitializeSecurityContext(
|
||||
ref credential,
|
||||
IntPtr.Zero,
|
||||
targetName,
|
||||
contextRequirements,
|
||||
0,
|
||||
0,
|
||||
IntPtr.Zero,
|
||||
0,
|
||||
ref newContext,
|
||||
ref outBufferDesc,
|
||||
out contextAttributes,
|
||||
out expiry);
|
||||
}
|
||||
|
||||
if (!haveContext)
|
||||
{
|
||||
context = newContext;
|
||||
haveContext = true;
|
||||
}
|
||||
|
||||
ThrowIfFailed(status, "InitializeSecurityContext", allowContinue: true);
|
||||
byte[] token = ReadTokenAndFree(outBufferDesc);
|
||||
roundIndex++;
|
||||
return new SspiStepResult(token, status == SEC_E_OK ? "Completed" : "ContinueNeeded", status == SEC_E_OK);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (inBufferDesc.HasValue)
|
||||
{
|
||||
FreeBufferDesc(inBufferDesc.Value, freeToken: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (haveContext)
|
||||
{
|
||||
DeleteSecurityContext(ref context);
|
||||
}
|
||||
|
||||
FreeCredentialsHandle(ref credential);
|
||||
}
|
||||
|
||||
private static byte[] ReadTokenAndFree(SecBufferDesc desc)
|
||||
{
|
||||
try
|
||||
{
|
||||
SecBuffer buffer = Marshal.PtrToStructure<SecBuffer>(desc.pBuffers);
|
||||
if (buffer.cbBuffer == 0 || buffer.pvBuffer == IntPtr.Zero)
|
||||
{
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
byte[] bytes = new byte[buffer.cbBuffer];
|
||||
Marshal.Copy(buffer.pvBuffer, bytes, 0, bytes.Length);
|
||||
FreeContextBuffer(buffer.pvBuffer);
|
||||
return bytes;
|
||||
}
|
||||
finally
|
||||
{
|
||||
FreeBufferDesc(desc, freeToken: false);
|
||||
}
|
||||
}
|
||||
|
||||
private static SecBufferDesc CreateOutputBufferDesc()
|
||||
{
|
||||
SecBuffer buffer = new() { BufferType = SECBUFFER_TOKEN, cbBuffer = 0, pvBuffer = IntPtr.Zero };
|
||||
IntPtr bufferPtr = Marshal.AllocHGlobal(Marshal.SizeOf<SecBuffer>());
|
||||
Marshal.StructureToPtr(buffer, bufferPtr, false);
|
||||
return new SecBufferDesc { ulVersion = 0, cBuffers = 1, pBuffers = bufferPtr };
|
||||
}
|
||||
|
||||
private static SecBufferDesc CreateInputBufferDesc(byte[] token)
|
||||
{
|
||||
IntPtr tokenPtr = Marshal.AllocHGlobal(token.Length);
|
||||
Marshal.Copy(token, 0, tokenPtr, token.Length);
|
||||
SecBuffer buffer = new() { BufferType = SECBUFFER_TOKEN, cbBuffer = token.Length, pvBuffer = tokenPtr };
|
||||
IntPtr bufferPtr = Marshal.AllocHGlobal(Marshal.SizeOf<SecBuffer>());
|
||||
Marshal.StructureToPtr(buffer, bufferPtr, false);
|
||||
return new SecBufferDesc { ulVersion = 0, cBuffers = 1, pBuffers = bufferPtr };
|
||||
}
|
||||
|
||||
private static void FreeBufferDesc(SecBufferDesc desc, bool freeToken)
|
||||
{
|
||||
if (desc.pBuffers == IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (freeToken)
|
||||
{
|
||||
SecBuffer buffer = Marshal.PtrToStructure<SecBuffer>(desc.pBuffers);
|
||||
if (buffer.pvBuffer != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(buffer.pvBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
Marshal.FreeHGlobal(desc.pBuffers);
|
||||
}
|
||||
|
||||
private static void ThrowIfFailed(int status, string operation, bool allowContinue = false)
|
||||
{
|
||||
if (status == SEC_E_OK || allowContinue && status == SEC_I_CONTINUE_NEEDED)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Win32Exception(status, operation + " failed with 0x" + status.ToString("X8"));
|
||||
}
|
||||
|
||||
[DllImport("secur32.dll", CharSet = CharSet.Unicode, SetLastError = false)]
|
||||
private static extern int AcquireCredentialsHandle(
|
||||
string? pszPrincipal,
|
||||
string pszPackage,
|
||||
int fCredentialUse,
|
||||
IntPtr pvLogonId,
|
||||
IntPtr pAuthData,
|
||||
IntPtr pGetKeyFn,
|
||||
IntPtr pvGetKeyArgument,
|
||||
ref SecHandle phCredential,
|
||||
out long ptsExpiry);
|
||||
|
||||
[DllImport("secur32.dll", CharSet = CharSet.Unicode, SetLastError = false)]
|
||||
private static extern int InitializeSecurityContext(
|
||||
ref SecHandle phCredential,
|
||||
IntPtr phContext,
|
||||
string pszTargetName,
|
||||
int fContextReq,
|
||||
int Reserved1,
|
||||
int TargetDataRep,
|
||||
IntPtr pInput,
|
||||
int Reserved2,
|
||||
ref SecHandle phNewContext,
|
||||
ref SecBufferDesc pOutput,
|
||||
out uint pfContextAttr,
|
||||
out long ptsExpiry);
|
||||
|
||||
[DllImport("secur32.dll", CharSet = CharSet.Unicode, SetLastError = false)]
|
||||
private static extern int InitializeSecurityContext(
|
||||
ref SecHandle phCredential,
|
||||
ref SecHandle phContext,
|
||||
string pszTargetName,
|
||||
int fContextReq,
|
||||
int Reserved1,
|
||||
int TargetDataRep,
|
||||
ref SecBufferDesc pInput,
|
||||
int Reserved2,
|
||||
ref SecHandle phNewContext,
|
||||
ref SecBufferDesc pOutput,
|
||||
out uint pfContextAttr,
|
||||
out long ptsExpiry);
|
||||
|
||||
[DllImport("secur32.dll", CharSet = CharSet.Unicode, SetLastError = false)]
|
||||
private static extern int InitializeSecurityContext(
|
||||
ref SecHandle phCredential,
|
||||
ref SecHandle phContext,
|
||||
string pszTargetName,
|
||||
int fContextReq,
|
||||
int Reserved1,
|
||||
int TargetDataRep,
|
||||
IntPtr pInput,
|
||||
int Reserved2,
|
||||
ref SecHandle phNewContext,
|
||||
ref SecBufferDesc pOutput,
|
||||
out uint pfContextAttr,
|
||||
out long ptsExpiry);
|
||||
|
||||
[DllImport("secur32.dll", SetLastError = false)]
|
||||
private static extern int DeleteSecurityContext(ref SecHandle phContext);
|
||||
|
||||
[DllImport("secur32.dll", SetLastError = false)]
|
||||
private static extern int FreeCredentialsHandle(ref SecHandle phCredential);
|
||||
|
||||
[DllImport("secur32.dll", SetLastError = false)]
|
||||
private static extern int FreeContextBuffer(IntPtr pvContextBuffer);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct SecHandle
|
||||
{
|
||||
public IntPtr dwLower;
|
||||
public IntPtr dwUpper;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct SecBuffer
|
||||
{
|
||||
public int cbBuffer;
|
||||
public int BufferType;
|
||||
public IntPtr pvBuffer;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct SecBufferDesc
|
||||
{
|
||||
public int ulVersion;
|
||||
public int cBuffers;
|
||||
public IntPtr pBuffers;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ServiceContract(Name = "Hist", Namespace = "aa")]
|
||||
internal interface IHistoryServiceContract
|
||||
{
|
||||
[OperationContract(Name = "GetV")]
|
||||
uint GetInterfaceVersion(out uint version);
|
||||
}
|
||||
|
||||
[ServiceContract(Name = "Hist", Namespace = "aa")]
|
||||
internal interface IHistoryServiceContract2 : IHistoryServiceContract
|
||||
{
|
||||
[OperationContract(Name = "ValCl")]
|
||||
[return: MarshalAs(UnmanagedType.U1)]
|
||||
bool ValidateClientCredential(
|
||||
string handle,
|
||||
[MessageParameter(Name = "inBuff")] byte[] inputBuffer,
|
||||
[MessageParameter(Name = "outBuff")] out byte[] outputBuffer,
|
||||
out byte[] errorBuffer);
|
||||
|
||||
[OperationContract(Name = "Open2")]
|
||||
[return: MarshalAs(UnmanagedType.U1)]
|
||||
bool OpenConnection2(
|
||||
[MessageParameter(Name = "inParameters")] ref byte[] inParameters,
|
||||
[MessageParameter(Name = "outParameters")] out byte[] outParameters,
|
||||
[MessageParameter(Name = "err")] out byte[] err);
|
||||
}
|
||||
|
||||
[ServiceContract(Name = "Retr", Namespace = "aa")]
|
||||
internal interface IRetrievalServiceContract
|
||||
{
|
||||
[OperationContract(Name = "GetV")]
|
||||
uint GetInterfaceVersion(out uint version);
|
||||
|
||||
[OperationContract]
|
||||
uint IsOriginalAllowed(uint clientHandle, out bool isAllowed);
|
||||
}
|
||||
|
||||
[ServiceContract(Name = "Retr", Namespace = "aa")]
|
||||
internal interface IRetrievalServiceContract2 : IRetrievalServiceContract
|
||||
{
|
||||
[OperationContract]
|
||||
[return: MarshalAs(UnmanagedType.U1)]
|
||||
bool StartQuery2(
|
||||
uint clientHandle,
|
||||
ushort queryRequestType,
|
||||
uint requestSize,
|
||||
[MessageParameter(Name = "pRequestBuff")] byte[] requestBuffer,
|
||||
out uint responseSize,
|
||||
[MessageParameter(Name = "pResponseBuff")] out byte[] responseBuffer,
|
||||
ref uint queryHandle,
|
||||
[MessageParameter(Name = "errSize")] out uint errorSize,
|
||||
[MessageParameter(Name = "err")] out byte[] errorBuffer);
|
||||
|
||||
[OperationContract]
|
||||
[return: MarshalAs(UnmanagedType.U1)]
|
||||
bool GetNextQueryResultBuffer2(
|
||||
uint clientHandle,
|
||||
uint queryHandle,
|
||||
out uint resultSize,
|
||||
[MessageParameter(Name = "pResultBuff")] out byte[] resultBuffer,
|
||||
[MessageParameter(Name = "errSize")] out uint errorSize,
|
||||
[MessageParameter(Name = "err")] out byte[] errorBuffer);
|
||||
}
|
||||
|
||||
internal sealed class MdasMessageEncodingBindingElement : MessageEncodingBindingElement
|
||||
{
|
||||
private readonly MessageEncodingBindingElement inner;
|
||||
|
||||
public MdasMessageEncodingBindingElement(MessageEncodingBindingElement inner)
|
||||
{
|
||||
this.inner = 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 bool CanBuildChannelFactory<TChannel>(BindingContext context)
|
||||
{
|
||||
context.BindingParameters.Add(this);
|
||||
return context.CanBuildInnerChannelFactory<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;
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return inner.ReadMessage(buffer, bufferManager, inner.ContentType);
|
||||
}
|
||||
|
||||
public override Message ReadMessage(Stream stream, int maxSizeOfHeaders, string contentType)
|
||||
{
|
||||
return inner.ReadMessage(stream, maxSizeOfHeaders, inner.ContentType);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\AVEVA.Historian.Client\AVEVA.Historian.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="dnlib" Version="4.5.0" />
|
||||
<PackageReference Include="System.ServiceModel.NetNamedPipe" Version="10.0.652802" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
File diff suppressed because it is too large
Load Diff
+12
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net481</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>ReverseInstrumentation.snk</AssemblyOriginatorKeyFile>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,501 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace AVEVA.Historian.ReverseInstrumentation
|
||||
{
|
||||
public static class CaptureLogger
|
||||
{
|
||||
private static readonly object Gate = new object();
|
||||
|
||||
public static void LogBuffer(string phase, IntPtr data, ulong length)
|
||||
{
|
||||
try
|
||||
{
|
||||
int byteCount = checked((int)Math.Min(length, 1024UL * 1024UL));
|
||||
byte[] bytes = new byte[byteCount];
|
||||
if (data != IntPtr.Zero && byteCount > 0 && IsReadableMemoryRange(data, byteCount))
|
||||
{
|
||||
Marshal.Copy(data, bytes, 0, byteCount);
|
||||
}
|
||||
|
||||
string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE");
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson");
|
||||
}
|
||||
|
||||
string directory = Path.GetDirectoryName(Path.GetFullPath(path));
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
string json = "{"
|
||||
+ "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\","
|
||||
+ "\"Phase\":\"" + JsonEscape(phase) + "\","
|
||||
+ "\"Length\":" + length + ","
|
||||
+ "\"CapturedLength\":" + byteCount + ","
|
||||
+ "\"Sha256\":\"" + ComputeSha256(bytes) + "\","
|
||||
+ "\"Base64\":\"" + Convert.ToBase64String(bytes) + "\""
|
||||
+ "}";
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Reverse-engineering instrumentation must not perturb the native query path.
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsReadableMemoryRange(IntPtr address, int byteCount)
|
||||
{
|
||||
if (address == IntPtr.Zero || byteCount <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
long current = address.ToInt64();
|
||||
long end = checked(current + byteCount);
|
||||
while (current < end)
|
||||
{
|
||||
if (VirtualQuery(new IntPtr(current), out MEMORY_BASIC_INFORMATION info, (UIntPtr)Marshal.SizeOf(typeof(MEMORY_BASIC_INFORMATION))) == UIntPtr.Zero)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (info.State != MEM_COMMIT || (info.Protect & PAGE_GUARD) != 0 || (info.Protect & PAGE_NOACCESS) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
long regionEnd = checked(info.BaseAddress.ToInt64() + (long)info.RegionSize.ToUInt64());
|
||||
if (regionEnd <= current)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
current = regionEnd;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private const uint MEM_COMMIT = 0x1000;
|
||||
private const uint PAGE_NOACCESS = 0x01;
|
||||
private const uint PAGE_GUARD = 0x100;
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern UIntPtr VirtualQuery(IntPtr lpAddress, out MEMORY_BASIC_INFORMATION lpBuffer, UIntPtr dwLength);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MEMORY_BASIC_INFORMATION
|
||||
{
|
||||
public IntPtr BaseAddress;
|
||||
public IntPtr AllocationBase;
|
||||
public uint AllocationProtect;
|
||||
public UIntPtr RegionSize;
|
||||
public uint State;
|
||||
public uint Protect;
|
||||
public uint Type;
|
||||
}
|
||||
|
||||
public static void LogByteArraySegment(string phase, byte[] bytes, int offset, int count)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (bytes == null || count <= 0 || offset < 0 || offset > bytes.Length)
|
||||
{
|
||||
WriteRecord(phase, 0UL, new byte[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
int captureCount = Math.Min(count, bytes.Length - offset);
|
||||
if (captureCount > 1024 * 1024)
|
||||
{
|
||||
captureCount = 1024 * 1024;
|
||||
}
|
||||
|
||||
byte[] captured = new byte[captureCount];
|
||||
Buffer.BlockCopy(bytes, offset, captured, 0, captureCount);
|
||||
WriteRecord(phase, (ulong)count, captured);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Reverse-engineering instrumentation must not perturb the native query path.
|
||||
}
|
||||
}
|
||||
|
||||
public static void LogByteArray(string phase, byte[] bytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] captured = bytes ?? new byte[0];
|
||||
if (captured.Length > 1024 * 1024)
|
||||
{
|
||||
byte[] truncated = new byte[1024 * 1024];
|
||||
Buffer.BlockCopy(captured, 0, truncated, 0, truncated.Length);
|
||||
captured = truncated;
|
||||
}
|
||||
|
||||
WriteRecord(phase, bytes == null ? 0UL : (ulong)bytes.Length, captured);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Reverse-engineering instrumentation must not perturb the native query path.
|
||||
}
|
||||
}
|
||||
|
||||
public static void LogByteArraySummary(string phase, byte[] bytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
byte[] captured = bytes ?? new byte[0];
|
||||
if (captured.Length > 1024 * 1024)
|
||||
{
|
||||
byte[] truncated = new byte[1024 * 1024];
|
||||
Buffer.BlockCopy(captured, 0, truncated, 0, truncated.Length);
|
||||
captured = truncated;
|
||||
}
|
||||
|
||||
WriteSummaryRecord(phase, bytes == null ? 0UL : (ulong)bytes.Length, captured);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Reverse-engineering instrumentation must not perturb the native query path.
|
||||
}
|
||||
}
|
||||
|
||||
public static void LogString(string phase, string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
string captured = value ?? string.Empty;
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(captured);
|
||||
|
||||
string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE");
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson");
|
||||
}
|
||||
|
||||
string directory = Path.GetDirectoryName(Path.GetFullPath(path));
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
string json = "{"
|
||||
+ "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\","
|
||||
+ "\"Phase\":\"" + JsonEscape(phase) + "\","
|
||||
+ "\"Length\":" + captured.Length + ","
|
||||
+ "\"Sha256\":\"" + ComputeSha256(bytes) + "\","
|
||||
+ "\"Value\":\"" + JsonEscape(captured) + "\""
|
||||
+ "}";
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Reverse-engineering instrumentation must not perturb the native query path.
|
||||
}
|
||||
}
|
||||
|
||||
public static void LogStringSummary(string phase, string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
string captured = value ?? string.Empty;
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(captured);
|
||||
int hyphenCount = 0;
|
||||
int uppercaseCount = 0;
|
||||
int lowercaseCount = 0;
|
||||
int digitCount = 0;
|
||||
for (int index = 0; index < captured.Length; index++)
|
||||
{
|
||||
char current = captured[index];
|
||||
if (current == '-')
|
||||
{
|
||||
hyphenCount++;
|
||||
}
|
||||
else if (current >= 'A' && current <= 'Z')
|
||||
{
|
||||
uppercaseCount++;
|
||||
}
|
||||
else if (current >= 'a' && current <= 'z')
|
||||
{
|
||||
lowercaseCount++;
|
||||
}
|
||||
else if (current >= '0' && current <= '9')
|
||||
{
|
||||
digitCount++;
|
||||
}
|
||||
}
|
||||
|
||||
string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE");
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson");
|
||||
}
|
||||
|
||||
string directory = Path.GetDirectoryName(Path.GetFullPath(path));
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
string json = "{"
|
||||
+ "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\","
|
||||
+ "\"Phase\":\"" + JsonEscape(phase) + "\","
|
||||
+ "\"Length\":" + captured.Length + ","
|
||||
+ "\"Sha256\":\"" + ComputeSha256(bytes) + "\","
|
||||
+ "\"HyphenCount\":" + hyphenCount + ","
|
||||
+ "\"UppercaseCount\":" + uppercaseCount + ","
|
||||
+ "\"LowercaseCount\":" + lowercaseCount + ","
|
||||
+ "\"DigitCount\":" + digitCount + ","
|
||||
+ "\"StartsWithBrace\":" + (captured.StartsWith("{", StringComparison.Ordinal) ? "true" : "false") + ","
|
||||
+ "\"EndsWithBrace\":" + (captured.EndsWith("}", StringComparison.Ordinal) ? "true" : "false") + ","
|
||||
+ "\"ContainsBackslash\":" + (captured.IndexOf('\\') >= 0 ? "true" : "false")
|
||||
+ "}";
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Reverse-engineering instrumentation must not perturb the native query path.
|
||||
}
|
||||
}
|
||||
|
||||
public static void LogUInt32(string phase, uint value)
|
||||
{
|
||||
try
|
||||
{
|
||||
string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE");
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson");
|
||||
}
|
||||
|
||||
string directory = Path.GetDirectoryName(Path.GetFullPath(path));
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
string json = "{"
|
||||
+ "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\","
|
||||
+ "\"Phase\":\"" + JsonEscape(phase) + "\","
|
||||
+ "\"Value\":" + value
|
||||
+ "}";
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Reverse-engineering instrumentation must not perturb the native query path.
|
||||
}
|
||||
}
|
||||
|
||||
public static void LogStdVector(string phase, IntPtr vector, ulong elementSize, ulong maxElements)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (vector == IntPtr.Zero || elementSize == 0 || maxElements == 0)
|
||||
{
|
||||
WriteVectorRecord(phase, elementSize, 0, 0, 0, new byte[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
IntPtr begin = Marshal.ReadIntPtr(vector, 0);
|
||||
IntPtr end = Marshal.ReadIntPtr(vector, IntPtr.Size);
|
||||
if (begin == IntPtr.Zero || end == IntPtr.Zero)
|
||||
{
|
||||
WriteVectorRecord(phase, elementSize, 0, 0, 0, new byte[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
long byteLength = end.ToInt64() - begin.ToInt64();
|
||||
if (byteLength <= 0 || byteLength % checked((long)elementSize) != 0)
|
||||
{
|
||||
WriteVectorRecord(phase, elementSize, 0, 0, 0, new byte[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
ulong count = checked((ulong)byteLength / elementSize);
|
||||
ulong capturedCount = Math.Min(count, maxElements);
|
||||
ulong capturedLength = Math.Min(checked(capturedCount * elementSize), 1024UL * 1024UL);
|
||||
byte[] captured = new byte[checked((int)capturedLength)];
|
||||
if (captured.Length > 0)
|
||||
{
|
||||
Marshal.Copy(begin, captured, 0, captured.Length);
|
||||
}
|
||||
|
||||
WriteVectorRecord(phase, elementSize, count, capturedCount, (ulong)byteLength, captured);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Reverse-engineering instrumentation must not perturb the native query path.
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteSummaryRecord(string phase, ulong length, byte[] bytes)
|
||||
{
|
||||
string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE");
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson");
|
||||
}
|
||||
|
||||
string directory = Path.GetDirectoryName(Path.GetFullPath(path));
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
string json = "{"
|
||||
+ "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\","
|
||||
+ "\"Phase\":\"" + JsonEscape(phase) + "\","
|
||||
+ "\"Length\":" + length + ","
|
||||
+ "\"CapturedLength\":" + bytes.Length + ","
|
||||
+ "\"Sha256\":\"" + ComputeSha256(bytes) + "\","
|
||||
+ "\"PrefixHex\":\"" + ToPrefixHex(bytes, 32) + "\","
|
||||
+ "\"PrefixAscii\":\"" + JsonEscape(ToSafeAscii(bytes, 32)) + "\""
|
||||
+ "}";
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteRecord(string phase, ulong length, byte[] bytes)
|
||||
{
|
||||
string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE");
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson");
|
||||
}
|
||||
|
||||
string directory = Path.GetDirectoryName(Path.GetFullPath(path));
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
string json = "{"
|
||||
+ "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\","
|
||||
+ "\"Phase\":\"" + JsonEscape(phase) + "\","
|
||||
+ "\"Length\":" + length + ","
|
||||
+ "\"CapturedLength\":" + bytes.Length + ","
|
||||
+ "\"Sha256\":\"" + ComputeSha256(bytes) + "\","
|
||||
+ "\"Base64\":\"" + Convert.ToBase64String(bytes) + "\""
|
||||
+ "}";
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteVectorRecord(
|
||||
string phase,
|
||||
ulong elementSize,
|
||||
ulong count,
|
||||
ulong capturedCount,
|
||||
ulong length,
|
||||
byte[] bytes)
|
||||
{
|
||||
string path = Environment.GetEnvironmentVariable("AVEVA_HISTORIAN_RE_CAPTURE");
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
path = Path.Combine(Path.GetTempPath(), "aveva-historian-re-capture.ndjson");
|
||||
}
|
||||
|
||||
string directory = Path.GetDirectoryName(Path.GetFullPath(path));
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
string json = "{"
|
||||
+ "\"TimestampUtc\":\"" + JsonEscape(DateTimeOffset.UtcNow.ToString("O")) + "\","
|
||||
+ "\"Phase\":\"" + JsonEscape(phase) + "\","
|
||||
+ "\"ElementSize\":" + elementSize + ","
|
||||
+ "\"Count\":" + count + ","
|
||||
+ "\"CapturedCount\":" + capturedCount + ","
|
||||
+ "\"Length\":" + length + ","
|
||||
+ "\"CapturedLength\":" + bytes.Length + ","
|
||||
+ "\"Sha256\":\"" + ComputeSha256(bytes) + "\","
|
||||
+ "\"Base64\":\"" + Convert.ToBase64String(bytes) + "\""
|
||||
+ "}";
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
File.AppendAllText(path, json + Environment.NewLine, Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ComputeSha256(byte[] bytes)
|
||||
{
|
||||
using (SHA256 sha256 = SHA256.Create())
|
||||
{
|
||||
byte[] hash = sha256.ComputeHash(bytes);
|
||||
StringBuilder builder = new StringBuilder(hash.Length * 2);
|
||||
foreach (byte value in hash)
|
||||
{
|
||||
builder.Append(value.ToString("x2"));
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private static string ToPrefixHex(byte[] bytes, int maxBytes)
|
||||
{
|
||||
int count = Math.Min(bytes.Length, maxBytes);
|
||||
StringBuilder builder = new StringBuilder(count * 2);
|
||||
for (int index = 0; index < count; index++)
|
||||
{
|
||||
builder.Append(bytes[index].ToString("x2"));
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string ToSafeAscii(byte[] bytes, int maxBytes)
|
||||
{
|
||||
int count = Math.Min(bytes.Length, maxBytes);
|
||||
StringBuilder builder = new StringBuilder(count);
|
||||
for (int index = 0; index < count; index++)
|
||||
{
|
||||
byte value = bytes[index];
|
||||
builder.Append(value >= 32 && value <= 126 ? (char)value : '.');
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string JsonEscape(string value)
|
||||
{
|
||||
return value
|
||||
.Replace("\\", "\\\\")
|
||||
.Replace("\"", "\\\"")
|
||||
.Replace("\r", "\\r")
|
||||
.Replace("\n", "\\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net481</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.ServiceModel" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,674 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user