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,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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user