c95824a65d
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>
881 lines
33 KiB
C#
881 lines
33 KiB
C#
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);
|
|
}
|
|
}
|