Initial project state: .NET reference, design, Rust port (M0+M1), evidence
rust / build / test / clippy / fmt (push) Has been cancelled

Layout:
- src/                    .NET 10 x64 reference: MxNativeCodec, MxNativeClient,
                          MxAsbClient, probes, tests, harnesses. Executable spec.
- design/                 Architectural plan for the Rust port (M0–M6), error
                          model, protocol invariants, risks (R1–R16), adversarial
                          review log (review.md).
- rust/                   Rust workspace. M0 skeleton + M1 codec parity.
                          mxaccess-codec: 215 unit tests + 2 cross-implementation
                          parity tests (byte-identical against .NET reference).
                          Other crates are M0 stubs awaiting M2+.
- captures/               Frida + netsh + pcap evidence per CLAUDE.md
                          ("captures are evidence, not throwaway logs").
- analysis/               Decompiled C# (frida/proxy/decompiled-*),
                          Ghidra exports for native DLLs (`exports/` only —
                          working state at `projects/` and AVEVA's input
                          binaries at `input/` are gitignored).
- docs/                   Reverse-engineering reference docs.
- tools/                  Setup-LiveProbeEnv.ps1 (Infisical credential fetcher),
                          Compute-Crc.ps1 (.NET parity helper).
- .github/workflows/      Rust CI: fmt + build + test + clippy on Windows.
- LICENSE                 MIT (Joseph Doherty, 2026).

Verified:
- cargo test --workspace → 217 passed (215 unit + 2 .NET parity), 0 failed
- cargo clippy --workspace -- -D warnings → clean
- cargo fmt --all -- --check → clean
- cargo publish --dry-run -p mxaccess-codec → packages cleanly

Excluded from history (see .gitignore):
- **/bin, **/obj, **/target — build artifacts
- analysis/ghidra/projects/ — Ghidra working state (regenerable)
- analysis/ghidra/input/ — AVEVA proprietary DLLs (vendor IP)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-05 06:21:00 -04:00
parent 43733699b0
commit fe2a6db786
3849 changed files with 352975 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
using System.Globalization;
namespace MxNativeClient;
public sealed record ComObjRef(
uint Signature,
uint Flags,
Guid Iid,
uint StandardFlags,
uint PublicRefs,
ulong Oxid,
ulong Oid,
Guid Ipid,
ushort DualStringEntries,
ushort DualStringSecurityOffset,
IReadOnlyList<ComDualStringEntry> DualStringEntriesDecoded)
{
public static ComObjRef Parse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < 68)
{
throw new ArgumentException("OBJREF buffer is too short.", nameof(buffer));
}
ushort dualStringEntries = ReadUInt16(buffer, 64);
ushort securityOffset = ReadUInt16(buffer, 66);
return new ComObjRef(
Signature: ReadUInt32(buffer, 0),
Flags: ReadUInt32(buffer, 4),
Iid: new Guid(buffer.Slice(8, 16)),
StandardFlags: ReadUInt32(buffer, 24),
PublicRefs: ReadUInt32(buffer, 28),
Oxid: ReadUInt64(buffer, 32),
Oid: ReadUInt64(buffer, 40),
Ipid: new Guid(buffer.Slice(48, 16)),
DualStringEntries: dualStringEntries,
DualStringSecurityOffset: securityOffset,
DualStringEntriesDecoded: DecodeDualStringArray(buffer[68..], dualStringEntries, securityOffset));
}
public IEnumerable<string> ToDiagnosticLines()
{
yield return $"objref_signature=0x{Signature:X8}";
yield return $"objref_flags=0x{Flags:X8}";
yield return $"objref_iid={Iid}";
yield return $"std_flags=0x{StandardFlags:X8}";
yield return $"std_public_refs={PublicRefs}";
yield return $"std_oxid=0x{Oxid:X16}";
yield return $"std_oid=0x{Oid:X16}";
yield return $"std_ipid={Ipid}";
yield return $"dual_string_entries={DualStringEntries}";
yield return $"dual_string_security_offset={DualStringSecurityOffset}";
yield return $"dual_strings={string.Join("|", DualStringEntriesDecoded.Select(static entry => entry.ToDiagnosticString()))}";
}
private static IReadOnlyList<ComDualStringEntry> DecodeDualStringArray(ReadOnlySpan<byte> data, ushort entries, ushort securityOffset)
{
int count = Math.Min(entries, data.Length / 2);
var strings = new List<ComDualStringEntry>();
for (int i = 0; i < count;)
{
int entryStart = i;
ushort towerId = ReadUInt16(data, i * 2);
i++;
if (towerId == 0)
{
continue;
}
var text = new List<char>();
while (i < count)
{
ushort value = ReadUInt16(data, i * 2);
i++;
if (value == 0)
{
break;
}
if (value >= 0x20 && value <= 0x7e)
{
text.Add((char)value);
}
else
{
text.Add('<');
text.AddRange(value.ToString("x4", CultureInfo.InvariantCulture));
text.Add('>');
}
}
strings.Add(new ComDualStringEntry(
towerId,
ProtocolTowerName(towerId),
new string(text.ToArray()),
IsSecurityBinding: entryStart >= securityOffset));
}
return strings;
}
private static string ProtocolTowerName(ushort towerId)
{
return towerId switch
{
0x0007 => "ncacn_ip_tcp",
0x0008 => "ncadg_ip_udp",
0x0009 => "ncacn_np",
0x000f => "ncacn_spx",
0x0010 => "ncacn_nb_nb",
0x0016 => "ncadg_ip_udp_or_netbios",
0x001f => "ncalrpc",
_ => "unknown",
};
}
private static ushort ReadUInt16(ReadOnlySpan<byte> buffer, int offset)
{
return (ushort)(buffer[offset] | (buffer[offset + 1] << 8));
}
private static uint ReadUInt32(ReadOnlySpan<byte> buffer, int offset)
{
return (uint)(buffer[offset]
| (buffer[offset + 1] << 8)
| (buffer[offset + 2] << 16)
| (buffer[offset + 3] << 24));
}
private static ulong ReadUInt64(ReadOnlySpan<byte> buffer, int offset)
{
return ReadUInt32(buffer, offset) | ((ulong)ReadUInt32(buffer, offset + 4) << 32);
}
}
public sealed record ComDualStringEntry(ushort TowerId, string Protocol, string Value, bool IsSecurityBinding)
{
public string ToDiagnosticString()
{
string kind = IsSecurityBinding ? "security" : "string";
return $"{kind}:0x{TowerId:x4}:{Protocol}:{Value}";
}
}
+100
View File
@@ -0,0 +1,100 @@
using System.Runtime.InteropServices;
using ComTypes = System.Runtime.InteropServices.ComTypes;
namespace MxNativeClient;
public static class ComObjRefProvider
{
public const uint MarshalContextInProcess = 0;
public const uint MarshalContextLocal = 1;
public const uint MarshalContextDifferentMachine = 2;
public static byte[] MarshalActivatedIUnknownObjRef(string progId, uint destinationContext)
{
var type = Type.GetTypeFromProgID(progId, throwOnError: true)
?? throw new InvalidOperationException($"ProgID {progId} was not resolved.");
object instance = Activator.CreateInstance(type)
?? throw new InvalidOperationException($"ProgID {progId} activation returned null.");
try
{
return MarshalIUnknownObjRef(instance, destinationContext);
}
finally
{
if (Marshal.IsComObject(instance))
{
_ = Marshal.ReleaseComObject(instance);
}
}
}
public static byte[] MarshalIUnknownObjRef(object comObject, uint destinationContext)
{
return MarshalInterfaceObjRef(comObject, new Guid("00000000-0000-0000-C000-000000000046"), destinationContext);
}
public static byte[] MarshalInterfaceObjRef(object comObject, Guid iid, uint destinationContext)
{
IntPtr unknown = IntPtr.Zero;
ComTypes.IStream? stream = null;
try
{
unknown = Marshal.GetIUnknownForObject(comObject);
Marshal.ThrowExceptionForHR(CreateStreamOnHGlobal(IntPtr.Zero, true, out stream));
Marshal.ThrowExceptionForHR(CoMarshalInterface(stream, ref iid, unknown, destinationContext, IntPtr.Zero, 0));
Marshal.ThrowExceptionForHR(GetHGlobalFromStream(stream, out var hglobal));
nuint size = GlobalSize(hglobal);
IntPtr pointer = GlobalLock(hglobal);
if (pointer == IntPtr.Zero)
{
throw new InvalidOperationException("GlobalLock failed.");
}
try
{
byte[] buffer = new byte[(int)size];
Marshal.Copy(pointer, buffer, 0, buffer.Length);
return buffer;
}
finally
{
_ = GlobalUnlock(hglobal);
}
}
finally
{
if (unknown != IntPtr.Zero)
{
Marshal.Release(unknown);
}
if (stream is not null)
{
_ = Marshal.ReleaseComObject(stream);
}
}
}
[DllImport("ole32.dll")]
private static extern int CreateStreamOnHGlobal(IntPtr hGlobal, [MarshalAs(UnmanagedType.Bool)] bool deleteOnRelease, out ComTypes.IStream stream);
[DllImport("ole32.dll")]
private static extern int CoMarshalInterface(ComTypes.IStream stream, ref Guid iid, IntPtr unknown, uint destinationContext, IntPtr destinationContextPointer, uint marshalFlags);
[DllImport("ole32.dll")]
private static extern int GetHGlobalFromStream(ComTypes.IStream stream, out IntPtr hGlobal);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GlobalLock(IntPtr hMem);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GlobalUnlock(IntPtr hMem);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nuint GlobalSize(IntPtr hMem);
}
+135
View File
@@ -0,0 +1,135 @@
using System.Buffers.Binary;
using System.Buffers;
using System.Net;
using System.Net.Security;
namespace MxNativeClient;
public enum DceRpcAuthType : byte
{
None = 0,
GssNegotiate = 9,
WinNt = 10,
}
public enum DceRpcAuthLevel : byte
{
None = 1,
Connect = 2,
PacketIntegrity = 5,
PacketPrivacy = 6,
}
public sealed record DceRpcAuthTrailer(
DceRpcAuthType AuthType,
DceRpcAuthLevel AuthLevel,
byte AuthPadLength,
byte AuthReserved,
uint AuthContextId)
{
public const int Length = 8;
public static DceRpcAuthTrailer Parse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < Length)
{
throw new ArgumentException("DCE/RPC auth trailer is too short.", nameof(buffer));
}
return new DceRpcAuthTrailer(
(DceRpcAuthType)buffer[0],
(DceRpcAuthLevel)buffer[1],
buffer[2],
buffer[3],
BinaryPrimitives.ReadUInt32LittleEndian(buffer[4..8]));
}
public void WriteTo(Span<byte> buffer)
{
if (buffer.Length < Length)
{
throw new ArgumentException("DCE/RPC auth trailer buffer is too short.", nameof(buffer));
}
buffer[0] = (byte)AuthType;
buffer[1] = (byte)AuthLevel;
buffer[2] = AuthPadLength;
buffer[3] = AuthReserved;
BinaryPrimitives.WriteUInt32LittleEndian(buffer[4..8], AuthContextId);
}
}
public sealed record DceRpcAuthValue(DceRpcAuthTrailer Trailer, ReadOnlyMemory<byte> Token);
public sealed class SspiClientContext : IDisposable
{
private readonly NegotiateAuthentication _authentication;
public SspiClientContext(string package, string targetName, ProtectionLevel protectionLevel = ProtectionLevel.None)
{
var credential = CreateCredentialFromEnvironment();
_authentication = new NegotiateAuthentication(new NegotiateAuthenticationClientOptions
{
Package = package,
TargetName = targetName,
Credential = credential ?? CredentialCache.DefaultNetworkCredentials,
RequiredProtectionLevel = protectionLevel,
});
}
public byte[] GetOutgoingBlob(ReadOnlySpan<byte> incomingToken)
{
byte[]? input = incomingToken.IsEmpty ? Array.Empty<byte>() : incomingToken.ToArray();
byte[]? output = _authentication.GetOutgoingBlob(input, out var status);
if (status is not (NegotiateAuthenticationStatusCode.Completed or NegotiateAuthenticationStatusCode.ContinueNeeded))
{
throw new InvalidOperationException($"SSPI negotiation failed with {status}.");
}
return output ?? [];
}
public byte[] Wrap(ReadOnlySpan<byte> input, bool requestEncryption, out bool isEncrypted)
{
var writer = new ArrayBufferWriter<byte>(input.Length + 64);
var status = _authentication.Wrap(input, writer, requestEncryption, out isEncrypted);
if (status != NegotiateAuthenticationStatusCode.Completed)
{
throw new InvalidOperationException($"SSPI wrap failed with {status}.");
}
return writer.WrittenSpan.ToArray();
}
public byte[] Unwrap(ReadOnlySpan<byte> input, out bool wasEncrypted)
{
var writer = new ArrayBufferWriter<byte>(input.Length);
var status = _authentication.Unwrap(input, writer, out wasEncrypted);
if (status != NegotiateAuthenticationStatusCode.Completed)
{
throw new InvalidOperationException($"SSPI unwrap failed with {status}.");
}
return writer.WrittenSpan.ToArray();
}
public void Dispose()
{
_authentication.Dispose();
}
private static NetworkCredential? CreateCredentialFromEnvironment()
{
string? user = Environment.GetEnvironmentVariable("MX_RPC_USER");
string? password = Environment.GetEnvironmentVariable("MX_RPC_PASSWORD");
if (string.IsNullOrWhiteSpace(user) || password is null)
{
return null;
}
string? domain = Environment.GetEnvironmentVariable("MX_RPC_DOMAIN");
return string.IsNullOrWhiteSpace(domain)
? new NetworkCredential(user, password)
: new NetworkCredential(user, password, domain);
}
}
+380
View File
@@ -0,0 +1,380 @@
using System.Buffers.Binary;
namespace MxNativeClient;
public enum DceRpcPacketType : byte
{
Request = 0,
Response = 2,
Fault = 3,
Bind = 11,
BindAck = 12,
AlterContext = 14,
AlterContextResponse = 15,
Auth3 = 16,
}
public readonly record struct DceRpcSyntaxId(Guid Uuid, ushort VersionMajor, ushort VersionMinor)
{
public static DceRpcSyntaxId Ndr20 { get; } = new(
new Guid("8A885D04-1CEB-11C9-9FE8-08002B104860"),
2,
0);
}
public sealed record DceRpcPresentationContext(
ushort ContextId,
DceRpcSyntaxId AbstractSyntax,
IReadOnlyList<DceRpcSyntaxId> TransferSyntaxes);
public sealed record DceRpcPduHeader(
byte Version,
byte VersionMinor,
DceRpcPacketType PacketType,
byte PacketFlags,
uint DataRepresentation,
ushort FragmentLength,
ushort AuthLength,
uint CallId)
{
public const int Length = 16;
public static DceRpcPduHeader Parse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < Length)
{
throw new ArgumentException("DCE/RPC PDU header is too short.", nameof(buffer));
}
return new DceRpcPduHeader(
Version: buffer[0],
VersionMinor: buffer[1],
PacketType: (DceRpcPacketType)buffer[2],
PacketFlags: buffer[3],
DataRepresentation: BinaryPrimitives.ReadUInt32LittleEndian(buffer[4..8]),
FragmentLength: BinaryPrimitives.ReadUInt16LittleEndian(buffer[8..10]),
AuthLength: BinaryPrimitives.ReadUInt16LittleEndian(buffer[10..12]),
CallId: BinaryPrimitives.ReadUInt32LittleEndian(buffer[12..16]));
}
public void WriteTo(Span<byte> buffer)
{
if (buffer.Length < Length)
{
throw new ArgumentException("DCE/RPC PDU header buffer is too short.", nameof(buffer));
}
buffer[0] = Version;
buffer[1] = VersionMinor;
buffer[2] = (byte)PacketType;
buffer[3] = PacketFlags;
BinaryPrimitives.WriteUInt32LittleEndian(buffer[4..8], DataRepresentation);
BinaryPrimitives.WriteUInt16LittleEndian(buffer[8..10], FragmentLength);
BinaryPrimitives.WriteUInt16LittleEndian(buffer[10..12], AuthLength);
BinaryPrimitives.WriteUInt32LittleEndian(buffer[12..16], CallId);
}
}
public sealed record DceRpcRequestPdu(
DceRpcPduHeader Header,
uint AllocationHint,
ushort ContextId,
ushort Opnum,
ReadOnlyMemory<byte> StubData)
{
public static DceRpcRequestPdu Parse(ReadOnlyMemory<byte> pdu)
{
var span = pdu.Span;
var header = DceRpcPduHeader.Parse(span);
if (header.PacketType != DceRpcPacketType.Request)
{
throw new ArgumentException("PDU is not a request.", nameof(pdu));
}
if (span.Length < 24 || header.FragmentLength > span.Length)
{
throw new ArgumentException("DCE/RPC request PDU is truncated.", nameof(pdu));
}
int trailerLength = header.AuthLength == 0 ? 0 : header.AuthLength + 8;
int stubLength = header.FragmentLength - 24 - trailerLength;
if (stubLength < 0)
{
throw new ArgumentException("DCE/RPC request PDU has invalid auth/trailer length.", nameof(pdu));
}
return new DceRpcRequestPdu(
Header: header,
AllocationHint: BinaryPrimitives.ReadUInt32LittleEndian(span[16..20]),
ContextId: BinaryPrimitives.ReadUInt16LittleEndian(span[20..22]),
Opnum: BinaryPrimitives.ReadUInt16LittleEndian(span[22..24]),
StubData: pdu.Slice(24, stubLength));
}
public byte[] Encode()
{
int length = 24 + StubData.Length;
byte[] pdu = new byte[length];
var header = Header with
{
PacketType = DceRpcPacketType.Request,
FragmentLength = (ushort)length,
AuthLength = 0,
PacketFlags = Header.PacketFlags == 0 ? (byte)0x03 : Header.PacketFlags,
};
header.WriteTo(pdu);
BinaryPrimitives.WriteUInt32LittleEndian(pdu.AsSpan(16, 4), AllocationHint);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(20, 2), ContextId);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(22, 2), Opnum);
StubData.Span.CopyTo(pdu.AsSpan(24));
return pdu;
}
}
public sealed record DceRpcResponsePdu(
DceRpcPduHeader Header,
uint AllocationHint,
ushort ContextId,
byte CancelCount,
ReadOnlyMemory<byte> StubData)
{
public static DceRpcResponsePdu Parse(ReadOnlyMemory<byte> pdu)
{
var span = pdu.Span;
var header = DceRpcPduHeader.Parse(span);
if (header.PacketType != DceRpcPacketType.Response)
{
throw new ArgumentException("PDU is not a response.", nameof(pdu));
}
if (span.Length < 24 || header.FragmentLength > span.Length)
{
throw new ArgumentException("DCE/RPC response PDU is truncated.", nameof(pdu));
}
int trailerLength = header.AuthLength == 0 ? 0 : header.AuthLength + 8;
int stubLength = header.FragmentLength - 24 - trailerLength;
if (stubLength < 0)
{
throw new ArgumentException("DCE/RPC response PDU has invalid auth/trailer length.", nameof(pdu));
}
return new DceRpcResponsePdu(
Header: header,
AllocationHint: BinaryPrimitives.ReadUInt32LittleEndian(span[16..20]),
ContextId: BinaryPrimitives.ReadUInt16LittleEndian(span[20..22]),
CancelCount: span[22],
StubData: pdu.Slice(24, stubLength));
}
}
public sealed record DceRpcFaultPdu(
DceRpcPduHeader Header,
uint AllocationHint,
ushort ContextId,
byte CancelCount,
uint Status,
ReadOnlyMemory<byte> StubData)
{
public static DceRpcFaultPdu Parse(ReadOnlyMemory<byte> pdu)
{
var span = pdu.Span;
var header = DceRpcPduHeader.Parse(span);
if (header.PacketType != DceRpcPacketType.Fault)
{
throw new ArgumentException("PDU is not a fault.", nameof(pdu));
}
if (span.Length < 28 || header.FragmentLength > span.Length)
{
throw new ArgumentException("DCE/RPC fault PDU is truncated.", nameof(pdu));
}
int trailerLength = header.AuthLength == 0 ? 0 : header.AuthLength + 8;
int stubLength = header.FragmentLength - 28 - trailerLength;
if (stubLength < 0)
{
throw new ArgumentException("DCE/RPC fault PDU has invalid auth/trailer length.", nameof(pdu));
}
return new DceRpcFaultPdu(
Header: header,
AllocationHint: BinaryPrimitives.ReadUInt32LittleEndian(span[16..20]),
ContextId: BinaryPrimitives.ReadUInt16LittleEndian(span[20..22]),
CancelCount: span[22],
Status: BinaryPrimitives.ReadUInt32LittleEndian(span[24..28]),
StubData: pdu.Slice(28, stubLength));
}
}
public sealed record DceRpcBindPdu(
DceRpcPduHeader Header,
ushort MaxTransmitFragment,
ushort MaxReceiveFragment,
uint AssociationGroupId,
IReadOnlyList<DceRpcPresentationContext> PresentationContexts)
{
public static DceRpcBindPdu Parse(ReadOnlyMemory<byte> pdu)
{
var span = pdu.Span;
var header = DceRpcPduHeader.Parse(span);
if (header.PacketType is not (DceRpcPacketType.Bind or DceRpcPacketType.AlterContext))
{
throw new ArgumentException("PDU is not a bind or alter-context PDU.", nameof(pdu));
}
if (span.Length < 28 || header.FragmentLength > span.Length)
{
throw new ArgumentException("DCE/RPC bind PDU is truncated.", nameof(pdu));
}
int contextCount = span[24];
int offset = 28;
var contexts = new List<DceRpcPresentationContext>(contextCount);
for (int i = 0; i < contextCount; i++)
{
if (offset + 24 > header.FragmentLength)
{
throw new ArgumentException("DCE/RPC bind context is truncated.", nameof(pdu));
}
ushort contextId = BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(offset, 2));
byte transferSyntaxCount = span[offset + 2];
offset += 4;
var abstractSyntax = ReadSyntax(span, ref offset);
var transferSyntaxes = new List<DceRpcSyntaxId>(transferSyntaxCount);
for (int j = 0; j < transferSyntaxCount; j++)
{
transferSyntaxes.Add(ReadSyntax(span, ref offset));
}
contexts.Add(new DceRpcPresentationContext(contextId, abstractSyntax, transferSyntaxes));
}
return new DceRpcBindPdu(
Header: header,
MaxTransmitFragment: BinaryPrimitives.ReadUInt16LittleEndian(span[16..18]),
MaxReceiveFragment: BinaryPrimitives.ReadUInt16LittleEndian(span[18..20]),
AssociationGroupId: BinaryPrimitives.ReadUInt32LittleEndian(span[20..24]),
PresentationContexts: contexts);
}
public byte[] Encode()
{
int length = 28 + PresentationContexts.Sum(static context => 24 + 20 * context.TransferSyntaxes.Count);
byte[] pdu = new byte[length];
var header = Header with { FragmentLength = (ushort)length, AuthLength = 0 };
header.WriteTo(pdu);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(16, 2), MaxTransmitFragment);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(18, 2), MaxReceiveFragment);
BinaryPrimitives.WriteUInt32LittleEndian(pdu.AsSpan(20, 4), AssociationGroupId);
pdu[24] = (byte)PresentationContexts.Count;
int offset = 28;
foreach (var context in PresentationContexts)
{
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(offset, 2), context.ContextId);
pdu[offset + 2] = (byte)context.TransferSyntaxes.Count;
offset += 4;
WriteSyntax(pdu.AsSpan(), ref offset, context.AbstractSyntax);
foreach (var transferSyntax in context.TransferSyntaxes)
{
WriteSyntax(pdu.AsSpan(), ref offset, transferSyntax);
}
}
return pdu;
}
public byte[] EncodeWithAuth(DceRpcAuthTrailer trailer, ReadOnlySpan<byte> authToken)
{
byte[] unauthenticated = Encode();
int padLength = Align(unauthenticated.Length, 4) - unauthenticated.Length;
int length = unauthenticated.Length + padLength + DceRpcAuthTrailer.Length + authToken.Length;
byte[] pdu = new byte[length];
unauthenticated.CopyTo(pdu.AsSpan());
var header = Header with
{
FragmentLength = (ushort)length,
AuthLength = (ushort)authToken.Length,
};
header.WriteTo(pdu);
var alignedTrailer = trailer with { AuthPadLength = (byte)padLength };
alignedTrailer.WriteTo(pdu.AsSpan(unauthenticated.Length + padLength, DceRpcAuthTrailer.Length));
authToken.CopyTo(pdu.AsSpan(length - authToken.Length));
return pdu;
}
public static byte[] EncodeAuth3(DceRpcPduHeader header, DceRpcAuthTrailer trailer, ReadOnlySpan<byte> authToken)
{
byte[] body = [(byte)' ', (byte)' ', (byte)' ', (byte)' '];
int padLength = Align(DceRpcPduHeader.Length + body.Length, 4) - (DceRpcPduHeader.Length + body.Length);
int length = DceRpcPduHeader.Length + body.Length + padLength + DceRpcAuthTrailer.Length + authToken.Length;
byte[] pdu = new byte[length];
var authHeader = header with
{
PacketType = DceRpcPacketType.Auth3,
FragmentLength = (ushort)length,
AuthLength = (ushort)authToken.Length,
};
authHeader.WriteTo(pdu);
body.CopyTo(pdu.AsSpan(DceRpcPduHeader.Length));
var alignedTrailer = trailer with { AuthPadLength = (byte)padLength };
alignedTrailer.WriteTo(pdu.AsSpan(DceRpcPduHeader.Length + body.Length + padLength, DceRpcAuthTrailer.Length));
authToken.CopyTo(pdu.AsSpan(length - authToken.Length));
return pdu;
}
public static DceRpcAuthValue ReadAuthValue(ReadOnlyMemory<byte> pdu)
{
var header = DceRpcPduHeader.Parse(pdu.Span);
if (header.AuthLength == 0)
{
throw new ArgumentException("PDU has no auth value.", nameof(pdu));
}
int trailerOffset = header.FragmentLength - header.AuthLength - DceRpcAuthTrailer.Length;
if (trailerOffset < DceRpcPduHeader.Length)
{
throw new ArgumentException("PDU auth trailer offset is invalid.", nameof(pdu));
}
return new DceRpcAuthValue(
DceRpcAuthTrailer.Parse(pdu.Span.Slice(trailerOffset, DceRpcAuthTrailer.Length)),
pdu.Slice(header.FragmentLength - header.AuthLength, header.AuthLength));
}
private static DceRpcSyntaxId ReadSyntax(ReadOnlySpan<byte> span, ref int offset)
{
if (offset + 20 > span.Length)
{
throw new ArgumentException("DCE/RPC syntax identifier is truncated.", nameof(span));
}
var syntax = new DceRpcSyntaxId(
new Guid(span.Slice(offset, 16)),
BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(offset + 16, 2)),
BinaryPrimitives.ReadUInt16LittleEndian(span.Slice(offset + 18, 2)));
offset += 20;
return syntax;
}
private static void WriteSyntax(Span<byte> span, ref int offset, DceRpcSyntaxId syntax)
{
syntax.Uuid.TryWriteBytes(span.Slice(offset, 16));
BinaryPrimitives.WriteUInt16LittleEndian(span.Slice(offset + 16, 2), syntax.VersionMajor);
BinaryPrimitives.WriteUInt16LittleEndian(span.Slice(offset + 18, 2), syntax.VersionMinor);
offset += 20;
}
private static int Align(int value, int alignment)
{
int remainder = value % alignment;
return remainder == 0 ? value : value + alignment - remainder;
}
}
+420
View File
@@ -0,0 +1,420 @@
using System.Buffers.Binary;
using System.Net.Sockets;
using System.Net.Security;
namespace MxNativeClient;
public sealed class DceRpcTcpClient : IDisposable
{
private readonly string _host;
private readonly int _port;
private TcpClient? _client;
private NetworkStream? _stream;
private uint _nextCallId = 1;
private ushort _boundContextId;
private SspiClientContext? _sspi;
private ManagedNtlmClientContext? _managedNtlm;
private DceRpcAuthTrailer? _authTrailer;
private DceRpcAuthLevel _authLevel = DceRpcAuthLevel.None;
public DceRpcTcpClient(string host, int port)
{
_host = host;
_port = port;
}
public void Connect()
{
_client = new TcpClient();
_client.Connect(_host, _port);
_stream = _client.GetStream();
}
public DceRpcPduHeader Bind(Guid interfaceId, ushort versionMajor, ushort versionMinor)
{
EnsureConnected();
uint callId = _nextCallId++;
var pdu = new DceRpcBindPdu(
Header: CreateHeader(DceRpcPacketType.Bind, callId),
MaxTransmitFragment: 4280,
MaxReceiveFragment: 4280,
AssociationGroupId: 0,
PresentationContexts:
[
new DceRpcPresentationContext(
ContextId: 0,
AbstractSyntax: new DceRpcSyntaxId(interfaceId, versionMajor, versionMinor),
TransferSyntaxes: [DceRpcSyntaxId.Ndr20]),
]);
Write(pdu.Encode());
byte[] response = ReadPdu();
return DceRpcPduHeader.Parse(response);
}
public DceRpcPduHeader BindWithNtlmConnect(Guid interfaceId, ushort versionMajor, ushort versionMinor, string targetName = "localhost")
{
return BindWithNtlm(interfaceId, versionMajor, versionMinor, DceRpcAuthLevel.Connect, targetName);
}
public DceRpcPduHeader BindWithNtlmPacketIntegrity(Guid interfaceId, ushort versionMajor, ushort versionMinor, string targetName = "localhost")
{
return BindWithNtlm(interfaceId, versionMajor, versionMinor, DceRpcAuthLevel.PacketIntegrity, targetName);
}
public DceRpcPduHeader BindWithManagedNtlmPacketIntegrity(Guid interfaceId, ushort versionMajor, ushort versionMinor)
{
EnsureConnected();
_sspi?.Dispose();
_sspi = null;
_managedNtlm = ManagedNtlmClientContext.FromEnvironment();
byte[] type1 = _managedNtlm.CreateType1();
uint callId = _nextCallId++;
var pdu = new DceRpcBindPdu(
Header: CreateHeader(DceRpcPacketType.Bind, callId),
MaxTransmitFragment: 4280,
MaxReceiveFragment: 4280,
AssociationGroupId: 0,
PresentationContexts:
[
new DceRpcPresentationContext(
ContextId: 0,
AbstractSyntax: new DceRpcSyntaxId(interfaceId, versionMajor, versionMinor),
TransferSyntaxes: [DceRpcSyntaxId.Ndr20]),
]);
var trailer = new DceRpcAuthTrailer(
AuthType: DceRpcAuthType.WinNt,
AuthLevel: DceRpcAuthLevel.PacketIntegrity,
AuthPadLength: 0,
AuthReserved: 0,
AuthContextId: 79232);
Write(pdu.EncodeWithAuth(trailer, type1));
byte[] response = ReadPdu();
var responseHeader = DceRpcPduHeader.Parse(response);
var challenge = DceRpcBindPdu.ReadAuthValue(response);
byte[] type3 = _managedNtlm.CreateType3(challenge.Token.Span);
byte[] auth3 = DceRpcBindPdu.EncodeAuth3(
CreateHeader(DceRpcPacketType.Auth3, responseHeader.CallId),
trailer,
type3);
Write(auth3);
_boundContextId = 0;
_authTrailer = trailer;
_authLevel = DceRpcAuthLevel.PacketIntegrity;
return responseHeader;
}
public DceRpcPduHeader BindWithNtlm(Guid interfaceId, ushort versionMajor, ushort versionMinor, DceRpcAuthLevel authLevel, string targetName = "localhost")
{
EnsureConnected();
_sspi?.Dispose();
_managedNtlm = null;
_sspi = new SspiClientContext("NTLM", targetName, ProtectionLevelFor(authLevel));
byte[] type1 = _sspi.GetOutgoingBlob(ReadOnlySpan<byte>.Empty);
uint callId = _nextCallId++;
var pdu = new DceRpcBindPdu(
Header: CreateHeader(DceRpcPacketType.Bind, callId),
MaxTransmitFragment: 4280,
MaxReceiveFragment: 4280,
AssociationGroupId: 0,
PresentationContexts:
[
new DceRpcPresentationContext(
ContextId: 0,
AbstractSyntax: new DceRpcSyntaxId(interfaceId, versionMajor, versionMinor),
TransferSyntaxes: [DceRpcSyntaxId.Ndr20]),
]);
var trailer = new DceRpcAuthTrailer(
AuthType: DceRpcAuthType.WinNt,
AuthLevel: authLevel,
AuthPadLength: 0,
AuthReserved: 0,
AuthContextId: 79232);
Write(pdu.EncodeWithAuth(trailer, type1));
byte[] response = ReadPdu();
var responseHeader = DceRpcPduHeader.Parse(response);
var challenge = DceRpcBindPdu.ReadAuthValue(response);
byte[] type3 = _sspi.GetOutgoingBlob(challenge.Token.Span);
byte[] auth3 = DceRpcBindPdu.EncodeAuth3(
CreateHeader(DceRpcPacketType.Auth3, responseHeader.CallId),
trailer,
type3);
Write(auth3);
_boundContextId = 0;
_authTrailer = trailer;
_authLevel = authLevel;
return responseHeader;
}
public DceRpcResponsePdu Call(ushort contextId, ushort opnum, ReadOnlyMemory<byte> stubData)
{
return CallCore(contextId, opnum, stubData, objectUuid: null);
}
public DceRpcResponsePdu CallBoundObject(Guid objectUuid, ushort opnum, ReadOnlyMemory<byte> stubData)
{
return CallCore(_boundContextId, opnum, stubData, objectUuid);
}
private DceRpcResponsePdu CallCore(ushort contextId, ushort opnum, ReadOnlyMemory<byte> stubData, Guid? objectUuid)
{
EnsureConnected();
uint callId = _nextCallId++;
byte[] request = EncodeRequestBytes(CreateHeader(DceRpcPacketType.Request, callId), contextId, opnum, stubData, objectUuid);
Write(EncodeRequest(request));
byte[] response = ReadPdu();
var header = DceRpcPduHeader.Parse(response);
if (header.PacketType == DceRpcPacketType.Fault)
{
var fault = DceRpcFaultPdu.Parse(response);
throw new DceRpcFaultException(fault.Status);
}
return DceRpcResponsePdu.Parse(response);
}
public DceRpcResponsePdu CallBound(ushort opnum, ReadOnlyMemory<byte> stubData)
{
return Call(_boundContextId, opnum, stubData);
}
public void Dispose()
{
_sspi?.Dispose();
_stream?.Dispose();
_client?.Dispose();
}
private byte[] EncodeRequest(byte[] request)
{
if (_authLevel == DceRpcAuthLevel.PacketIntegrity)
{
return EncodePacketIntegrityRequest(request);
}
return request;
}
private byte[] EncodePacketIntegrityRequest(byte[] unauthenticated)
{
if (_authTrailer is null)
{
throw new InvalidOperationException("Packet-integrity auth was requested without an auth trailer.");
}
int padLength = Align(unauthenticated.Length, 4) - unauthenticated.Length;
int signatureLength = 16;
int length = unauthenticated.Length + padLength + DceRpcAuthTrailer.Length + signatureLength;
byte[] pdu = new byte[length];
unauthenticated.CopyTo(pdu.AsSpan());
if (padLength > 0)
{
pdu.AsSpan(unauthenticated.Length, padLength).Fill(0xbb);
}
var parsedHeader = DceRpcPduHeader.Parse(unauthenticated);
var header = parsedHeader with
{
PacketType = DceRpcPacketType.Request,
PacketFlags = parsedHeader.PacketFlags == 0 ? (byte)0x03 : parsedHeader.PacketFlags,
FragmentLength = (ushort)length,
AuthLength = (ushort)signatureLength,
};
header.WriteTo(pdu);
var trailer = _authTrailer with { AuthPadLength = (byte)padLength };
int trailerOffset = unauthenticated.Length + padLength;
trailer.WriteTo(pdu.AsSpan(trailerOffset, DceRpcAuthTrailer.Length));
pdu.AsSpan(length - signatureLength, signatureLength).Fill(0x20);
byte[] verifier;
if (_managedNtlm is not null)
{
verifier = _managedNtlm.Sign(pdu.AsSpan(0, length - signatureLength));
}
else if (_sspi is not null)
{
byte[] wrapped = _sspi.Wrap(pdu.AsSpan(0, length - signatureLength), requestEncryption: false, out _);
verifier = ExtractNtlmVerifier(wrapped, pdu.AsSpan(0, length - signatureLength));
}
else
{
throw new InvalidOperationException("Packet-integrity auth was requested without an auth context.");
}
verifier.CopyTo(pdu.AsSpan(length - signatureLength, signatureLength));
return pdu;
}
private static byte[] EncodeRequestBytes(
DceRpcPduHeader header,
ushort contextId,
ushort opnum,
ReadOnlyMemory<byte> stubData,
Guid? objectUuid)
{
int objectLength = objectUuid.HasValue ? 16 : 0;
int fixedOffset = DceRpcPduHeader.Length;
int stubOffset = fixedOffset + 8 + objectLength;
int length = stubOffset + stubData.Length;
byte[] pdu = new byte[length];
var requestHeader = header with
{
PacketType = DceRpcPacketType.Request,
FragmentLength = (ushort)length,
AuthLength = 0,
PacketFlags = (byte)((header.PacketFlags == 0 ? 0x03 : header.PacketFlags) | (objectUuid.HasValue ? 0x80 : 0x00)),
};
requestHeader.WriteTo(pdu);
BinaryPrimitives.WriteUInt32LittleEndian(pdu.AsSpan(fixedOffset, 4), (uint)stubData.Length);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(fixedOffset + 4, 2), contextId);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(fixedOffset + 6, 2), opnum);
if (objectUuid.HasValue)
{
objectUuid.Value.TryWriteBytes(pdu.AsSpan(fixedOffset + 8, 16));
}
stubData.Span.CopyTo(pdu.AsSpan(stubOffset));
return pdu;
}
private static byte[] ExtractNtlmVerifier(ReadOnlySpan<byte> wrapped, ReadOnlySpan<byte> signedInput)
{
const int signatureLength = 16;
TraceWrapShape(wrapped, signedInput, signatureLength);
if (wrapped.Length == signatureLength)
{
return wrapped.ToArray();
}
if (wrapped.Length == signedInput.Length + signatureLength)
{
if (wrapped[signatureLength..].SequenceEqual(signedInput))
{
return wrapped[..signatureLength].ToArray();
}
if (wrapped[..signedInput.Length].SequenceEqual(signedInput))
{
return wrapped[^signatureLength..].ToArray();
}
return wrapped[..signatureLength].ToArray();
}
throw new InvalidOperationException($"Unexpected SSPI wrap verifier length {wrapped.Length} for input length {signedInput.Length}.");
}
private static void TraceWrapShape(ReadOnlySpan<byte> wrapped, ReadOnlySpan<byte> signedInput, int signatureLength)
{
string? tracePath = Environment.GetEnvironmentVariable("MX_RPC_WRAP_TRACE");
if (string.IsNullOrWhiteSpace(tracePath))
{
return;
}
bool tokenThenInput = wrapped.Length >= signatureLength
&& wrapped.Length == signedInput.Length + signatureLength
&& wrapped[signatureLength..].SequenceEqual(signedInput);
bool inputThenToken = wrapped.Length >= signatureLength
&& wrapped.Length == signedInput.Length + signatureLength
&& wrapped[..signedInput.Length].SequenceEqual(signedInput);
string text =
$"signed_input_length={signedInput.Length}{Environment.NewLine}" +
$"wrapped_length={wrapped.Length}{Environment.NewLine}" +
$"token_then_input={tokenThenInput}{Environment.NewLine}" +
$"input_then_token={inputThenToken}{Environment.NewLine}" +
$"wrapped_first16={Convert.ToHexString(wrapped[..Math.Min(signatureLength, wrapped.Length)])}{Environment.NewLine}" +
$"wrapped_last16={Convert.ToHexString(wrapped[^Math.Min(signatureLength, wrapped.Length)..])}{Environment.NewLine}";
File.AppendAllText(tracePath, text);
}
private static DceRpcPduHeader CreateHeader(DceRpcPacketType packetType, uint callId)
{
return new DceRpcPduHeader(
Version: 5,
VersionMinor: 0,
PacketType: packetType,
PacketFlags: 0x03,
DataRepresentation: 0x10,
FragmentLength: 0,
AuthLength: 0,
CallId: callId);
}
private static int Align(int value, int alignment)
{
int remainder = value % alignment;
return remainder == 0 ? value : value + alignment - remainder;
}
private static ProtectionLevel ProtectionLevelFor(DceRpcAuthLevel authLevel)
{
return authLevel switch
{
DceRpcAuthLevel.PacketPrivacy => ProtectionLevel.EncryptAndSign,
DceRpcAuthLevel.PacketIntegrity => ProtectionLevel.Sign,
_ => ProtectionLevel.None,
};
}
private void Write(byte[] pdu)
{
EnsureConnected();
_stream!.Write(pdu);
_stream.Flush();
}
private byte[] ReadPdu()
{
EnsureConnected();
byte[] headerBytes = ReadExact(DceRpcPduHeader.Length);
var header = DceRpcPduHeader.Parse(headerBytes);
byte[] pdu = new byte[header.FragmentLength];
headerBytes.CopyTo(pdu, 0);
byte[] body = ReadExact(header.FragmentLength - DceRpcPduHeader.Length);
body.CopyTo(pdu.AsSpan(DceRpcPduHeader.Length));
return pdu;
}
private byte[] ReadExact(int length)
{
byte[] buffer = new byte[length];
int offset = 0;
while (offset < length)
{
int read = _stream!.Read(buffer, offset, length - offset);
if (read == 0)
{
throw new IOException("DCE/RPC socket closed while reading.");
}
offset += read;
}
return buffer;
}
private void EnsureConnected()
{
if (_stream is null)
{
throw new InvalidOperationException("DCE/RPC TCP client is not connected.");
}
}
}
public sealed class DceRpcFaultException : Exception
{
public DceRpcFaultException(uint status)
: base($"DCE/RPC fault 0x{status:x8}")
{
Status = status;
}
public uint Status { get; }
}
@@ -0,0 +1,433 @@
using Microsoft.Data.SqlClient;
using MxNativeCodec;
namespace MxNativeClient;
public sealed record GalaxyTagMetadata(
string ObjectTagName,
string AttributeName,
string? PrimitiveName,
ushort PlatformId,
ushort EngineId,
ushort ObjectId,
short PrimitiveId,
short AttributeId,
short PropertyId,
short MxDataType,
bool IsArray,
short SecurityClassification,
string AttributeSource)
{
public const short ValuePropertyId = 10;
public const short BufferPropertyId = 50;
public bool IsBufferProperty => PropertyId == BufferPropertyId;
public MxReferenceHandle ToReferenceHandle(byte galaxyId = 1)
{
return MxReferenceHandle.Create(
galaxyId,
PlatformId,
EngineId,
ObjectId,
ObjectTagName,
PrimitiveId,
AttributeId,
PropertyId,
AttributeName,
IsArray);
}
public MxValueKind ToValueKind()
{
return NmxWriteMessage.GetValueKind(MxDataType, IsArray);
}
public bool TryGetValueKind(out MxValueKind valueKind)
{
return NmxWriteMessage.TryGetValueKind(MxDataType, IsArray, out valueKind);
}
public bool IsSupportedValueKind => TryGetValueKind(out _);
public (MxValueKind ValueKind, object Value) ProjectWriteValue(object value)
{
if (TryGetValueKind(out MxValueKind valueKind))
{
return (valueKind, value);
}
if (IsArray)
{
throw new ArgumentOutOfRangeException(nameof(value), $"Unsupported MX array data type {MxDataType}.");
}
return (MxDataType, value) switch
{
((short)MxNativeCodec.MxDataType.ElapsedTime, TimeSpan timeSpan) => (MxValueKind.Int32, checked((int)timeSpan.TotalMilliseconds)),
((short)MxNativeCodec.MxDataType.ElapsedTime, _) => (MxValueKind.Int32, value),
((short)MxNativeCodec.MxDataType.InternationalizedString, _) => (MxValueKind.String, value),
_ => throw new ArgumentOutOfRangeException(nameof(value), $"Unsupported MX data type {MxDataType}."),
};
}
}
public sealed class GalaxyRepositoryTagResolver
{
private const string DefaultConnectionString =
"Server=localhost;Database=ZB;Integrated Security=True;Encrypt=False;TrustServerCertificate=True";
private readonly string _connectionString;
public GalaxyRepositoryTagResolver(string connectionString = DefaultConnectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
_connectionString = connectionString;
}
public async Task<GalaxyTagMetadata> ResolveAsync(
string tagReference,
CancellationToken cancellationToken = default)
{
IReadOnlyList<ParsedTagReference> candidates = ParsedTagReference.ParseCandidates(tagReference);
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
foreach (ParsedTagReference parsed in candidates)
{
await using var command = connection.CreateCommand();
command.CommandText = ResolveSql;
command.Parameters.AddWithValue("@objectTagName", parsed.ObjectTagName);
command.Parameters.AddWithValue("@attributeName", parsed.AttributeName);
command.Parameters.AddWithValue("@primitiveName", (object?)parsed.PrimitiveName ?? DBNull.Value);
await using SqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
GalaxyTagMetadata metadata = ReadMetadata(reader);
return parsed.PropertyIdOverride.HasValue
? metadata with { PropertyId = parsed.PropertyIdOverride.Value }
: metadata;
}
}
throw new InvalidOperationException($"Galaxy tag reference '{tagReference}' was not found in the deployed repository metadata.");
}
public async Task<IReadOnlyList<GalaxyTagMetadata>> BrowseAsync(
string objectTagLike = "%",
string attributeLike = "%",
int maxRows = 100,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(objectTagLike);
ArgumentException.ThrowIfNullOrWhiteSpace(attributeLike);
if (maxRows <= 0)
{
throw new ArgumentOutOfRangeException(nameof(maxRows), "Maximum row count must be positive.");
}
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var command = connection.CreateCommand();
command.CommandText = BrowseSql;
command.Parameters.AddWithValue("@objectTagLike", objectTagLike);
command.Parameters.AddWithValue("@attributeLike", attributeLike);
command.Parameters.AddWithValue("@maxRows", Math.Min(maxRows, 1000));
List<GalaxyTagMetadata> results = [];
await using SqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
results.Add(ReadMetadata(reader));
}
return results;
}
private static GalaxyTagMetadata ReadMetadata(SqlDataReader reader)
{
return new GalaxyTagMetadata(
ObjectTagName: reader.GetString(0),
AttributeName: reader.GetString(1),
PrimitiveName: reader.IsDBNull(2) ? null : reader.GetString(2),
PlatformId: checked((ushort)reader.GetInt16(3)),
EngineId: checked((ushort)reader.GetInt16(4)),
ObjectId: checked((ushort)reader.GetInt16(5)),
PrimitiveId: reader.GetInt16(6),
AttributeId: reader.GetInt16(7),
PropertyId: checked((short)reader.GetInt32(8)),
MxDataType: reader.GetInt16(9),
IsArray: reader.GetBoolean(10),
SecurityClassification: reader.GetInt16(11),
AttributeSource: reader.GetString(12));
}
private sealed record ParsedTagReference(
string ObjectTagName,
string? PrimitiveName,
string AttributeName,
short? PropertyIdOverride)
{
public static IReadOnlyList<ParsedTagReference> ParseCandidates(string tagReference)
{
ArgumentException.ThrowIfNullOrWhiteSpace(tagReference);
var property = ParsePropertySuffix(tagReference);
string[] parts = property.BaseReference.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
return parts.Length switch
{
2 => [new ParsedTagReference(parts[0], null, parts[1], property.PropertyIdOverride)],
>= 3 =>
[
new ParsedTagReference(parts[0], parts[1], string.Join('.', parts.Skip(2)), property.PropertyIdOverride),
new ParsedTagReference(parts[0], null, string.Join('.', parts.Skip(1)), property.PropertyIdOverride),
],
_ => throw new ArgumentException("Tag reference must be Object.Attribute or Object.Primitive.Attribute.", nameof(tagReference)),
};
}
private static (string BaseReference, short? PropertyIdOverride) ParsePropertySuffix(string tagReference)
{
const string bufferSuffix = ".property(buffer)";
if (tagReference.EndsWith(bufferSuffix, StringComparison.OrdinalIgnoreCase))
{
string baseReference = tagReference[..^bufferSuffix.Length];
if (string.IsNullOrWhiteSpace(baseReference))
{
throw new ArgumentException("Property references must include a base tag reference.", nameof(tagReference));
}
return (baseReference, GalaxyTagMetadata.BufferPropertyId);
}
return (tagReference, null);
}
}
private const string ResolveSql = """
;WITH deployed_package_chain AS (
SELECT
g.gobject_id,
p.package_id,
p.derived_from_package_id,
0 AS depth
FROM dbo.gobject g
INNER JOIN dbo.package p
ON p.package_id = g.deployed_package_id
WHERE g.is_template = 0
AND g.deployed_package_id <> 0
AND g.tag_name = @objectTagName
UNION ALL
SELECT
dpc.gobject_id,
p.package_id,
p.derived_from_package_id,
dpc.depth + 1
FROM deployed_package_chain dpc
INNER JOIN dbo.package p
ON p.package_id = dpc.derived_from_package_id
WHERE dpc.derived_from_package_id <> 0
AND dpc.depth < 10
),
ranked_dynamic AS (
SELECT
g.tag_name AS object_tag_name,
da.attribute_name,
CAST(NULL AS nvarchar(329)) AS primitive_name,
i.mx_platform_id,
i.mx_engine_id,
i.mx_object_id,
da.mx_primitive_id,
da.mx_attribute_id,
CAST(10 AS int) AS property_id,
da.mx_data_type,
da.is_array,
da.security_classification,
CAST(N'dynamic' AS nvarchar(16)) AS attribute_source,
ROW_NUMBER() OVER (
PARTITION BY dpc.gobject_id, da.attribute_name
ORDER BY dpc.depth
) AS rn
FROM deployed_package_chain dpc
INNER JOIN dbo.dynamic_attribute da
ON da.package_id = dpc.package_id
INNER JOIN dbo.gobject g
ON g.gobject_id = dpc.gobject_id
INNER JOIN dbo.instance i
ON i.gobject_id = g.gobject_id
WHERE da.attribute_name = @attributeName
AND @primitiveName IS NULL
),
primitive_attributes AS (
SELECT
g.tag_name AS object_tag_name,
ad.attribute_name,
NULLIF(pi.primitive_name, N'') AS primitive_name,
i.mx_platform_id,
i.mx_engine_id,
i.mx_object_id,
pi.mx_primitive_id,
ad.mx_attribute_id,
CAST(10 AS int) AS property_id,
ad.mx_data_type,
ad.is_array,
ad.security_classification,
CAST(N'primitive' AS nvarchar(16)) AS attribute_source,
1 AS rn
FROM dbo.gobject g
INNER JOIN dbo.instance i
ON i.gobject_id = g.gobject_id
INNER JOIN dbo.primitive_instance pi
ON pi.gobject_id = g.gobject_id
AND pi.package_id = g.deployed_package_id
AND pi.property_bitmask & 0x10 <> 0x10
INNER JOIN dbo.attribute_definition ad
ON ad.primitive_definition_id = pi.primitive_definition_id
WHERE g.tag_name = @objectTagName
AND ad.attribute_name = @attributeName
AND (
(@primitiveName IS NULL AND pi.primitive_name = N'')
OR (@primitiveName IS NOT NULL AND pi.primitive_name = @primitiveName)
)
)
SELECT TOP (1)
object_tag_name,
attribute_name,
primitive_name,
mx_platform_id,
mx_engine_id,
mx_object_id,
mx_primitive_id,
mx_attribute_id,
property_id,
mx_data_type,
is_array,
security_classification,
attribute_source
FROM (
SELECT * FROM ranked_dynamic WHERE rn = 1
UNION ALL
SELECT * FROM primitive_attributes
) resolved
ORDER BY CASE attribute_source WHEN N'dynamic' THEN 0 ELSE 1 END
""";
private const string BrowseSql = """
;WITH deployed_objects AS (
SELECT
g.gobject_id,
g.tag_name,
g.deployed_package_id,
i.mx_platform_id,
i.mx_engine_id,
i.mx_object_id
FROM dbo.gobject g
INNER JOIN dbo.instance i
ON i.gobject_id = g.gobject_id
WHERE g.is_template = 0
AND g.deployed_package_id <> 0
AND g.tag_name LIKE @objectTagLike
),
deployed_package_chain AS (
SELECT
d.gobject_id,
d.tag_name,
d.mx_platform_id,
d.mx_engine_id,
d.mx_object_id,
p.package_id,
p.derived_from_package_id,
0 AS depth
FROM deployed_objects d
INNER JOIN dbo.package p
ON p.package_id = d.deployed_package_id
UNION ALL
SELECT
dpc.gobject_id,
dpc.tag_name,
dpc.mx_platform_id,
dpc.mx_engine_id,
dpc.mx_object_id,
p.package_id,
p.derived_from_package_id,
dpc.depth + 1
FROM deployed_package_chain dpc
INNER JOIN dbo.package p
ON p.package_id = dpc.derived_from_package_id
WHERE dpc.derived_from_package_id <> 0
AND dpc.depth < 10
),
ranked_dynamic AS (
SELECT
dpc.tag_name AS object_tag_name,
da.attribute_name,
CAST(NULL AS nvarchar(329)) AS primitive_name,
dpc.mx_platform_id,
dpc.mx_engine_id,
dpc.mx_object_id,
da.mx_primitive_id,
da.mx_attribute_id,
CAST(10 AS int) AS property_id,
da.mx_data_type,
da.is_array,
da.security_classification,
CAST(N'dynamic' AS nvarchar(16)) AS attribute_source,
ROW_NUMBER() OVER (
PARTITION BY dpc.gobject_id, da.attribute_name
ORDER BY dpc.depth
) AS rn
FROM deployed_package_chain dpc
INNER JOIN dbo.dynamic_attribute da
ON da.package_id = dpc.package_id
WHERE da.attribute_name LIKE @attributeLike
),
primitive_attributes AS (
SELECT
d.tag_name AS object_tag_name,
ad.attribute_name,
NULLIF(pi.primitive_name, N'') AS primitive_name,
d.mx_platform_id,
d.mx_engine_id,
d.mx_object_id,
pi.mx_primitive_id,
ad.mx_attribute_id,
CAST(10 AS int) AS property_id,
ad.mx_data_type,
ad.is_array,
ad.security_classification,
CAST(N'primitive' AS nvarchar(16)) AS attribute_source,
1 AS rn
FROM deployed_objects d
INNER JOIN dbo.gobject g
ON g.gobject_id = d.gobject_id
INNER JOIN dbo.primitive_instance pi
ON pi.gobject_id = g.gobject_id
AND pi.package_id = g.deployed_package_id
AND pi.property_bitmask & 0x10 <> 0x10
INNER JOIN dbo.attribute_definition ad
ON ad.primitive_definition_id = pi.primitive_definition_id
WHERE ad.attribute_name LIKE @attributeLike
)
SELECT TOP (@maxRows)
object_tag_name,
attribute_name,
primitive_name,
mx_platform_id,
mx_engine_id,
mx_object_id,
mx_primitive_id,
mx_attribute_id,
property_id,
mx_data_type,
is_array,
security_classification,
attribute_source
FROM (
SELECT * FROM ranked_dynamic WHERE rn = 1
UNION ALL
SELECT * FROM primitive_attributes
) resolved
ORDER BY object_tag_name, primitive_name, attribute_name
""";
}
@@ -0,0 +1,149 @@
using Microsoft.Data.SqlClient;
namespace MxNativeClient;
public sealed record GalaxyUserProfile(
int UserProfileId,
string UserProfileName,
Guid UserGuid,
string DefaultSecurityGroup,
int? InTouchAccessLevel,
IReadOnlyList<string> Roles);
public sealed class GalaxyRepositoryUserResolver
{
private const string DefaultConnectionString =
"Server=localhost;Database=ZB;Integrated Security=True;Encrypt=False;TrustServerCertificate=True";
private readonly string _connectionString;
public GalaxyRepositoryUserResolver(string connectionString = DefaultConnectionString)
{
ArgumentException.ThrowIfNullOrWhiteSpace(connectionString);
_connectionString = connectionString;
}
public async Task<int> ResolveUserProfileIdByGuidAsync(
Guid userGuid,
CancellationToken cancellationToken = default)
{
GalaxyUserProfile profile = await ResolveByGuidAsync(userGuid, cancellationToken).ConfigureAwait(false);
return profile.UserProfileId;
}
public async Task<GalaxyUserProfile> ResolveByGuidAsync(
Guid userGuid,
CancellationToken cancellationToken = default)
{
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var command = connection.CreateCommand();
command.CommandText = UserByGuidSql;
command.Parameters.AddWithValue("@userGuid", userGuid);
await using SqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
throw new KeyNotFoundException($"Galaxy user GUID {userGuid} was not found in dbo.user_profile.");
}
return ReadProfile(reader);
}
public async Task<GalaxyUserProfile> ResolveByNameAsync(
string userName,
CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(userName);
await using var connection = new SqlConnection(_connectionString);
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var command = connection.CreateCommand();
command.CommandText = UserByNameSql;
command.Parameters.AddWithValue("@userName", userName);
await using SqlDataReader reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
throw new KeyNotFoundException($"Galaxy user {userName} was not found in dbo.user_profile.");
}
return ReadProfile(reader);
}
private static GalaxyUserProfile ReadProfile(SqlDataReader reader)
{
return new GalaxyUserProfile(
reader.GetInt32(0),
reader.GetString(1),
reader.GetGuid(2),
reader.GetString(3),
reader.IsDBNull(4) ? null : reader.GetInt32(4),
reader.IsDBNull(5) ? [] : ParseRoleBlob(reader.GetString(5)));
}
private static IReadOnlyList<string> ParseRoleBlob(string rolesText)
{
if (!rolesText.StartsWith("0x", StringComparison.OrdinalIgnoreCase) || rolesText.Length <= 2)
{
return [];
}
byte[] bytes = Convert.FromHexString(rolesText[2..]);
List<string> roles = [];
for (int offset = 0; offset + 3 < bytes.Length; offset++)
{
List<char> chars = [];
int cursor = offset;
while (cursor + 1 < bytes.Length)
{
char c = (char)(bytes[cursor] | (bytes[cursor + 1] << 8));
if (c == '\0')
{
break;
}
if (c < 0x20 || c > 0x7e)
{
chars.Clear();
break;
}
chars.Add(c);
cursor += 2;
}
if (chars.Count < 2 || cursor + 1 >= bytes.Length || bytes[cursor] != 0 || bytes[cursor + 1] != 0)
{
continue;
}
string role = new(chars.ToArray());
if (!roles.Contains(role, StringComparer.OrdinalIgnoreCase))
{
roles.Add(role);
}
offset = cursor;
}
return roles;
}
private const string UserSelectSql = """
SELECT TOP (1)
user_profile_id,
user_profile_name,
user_guid,
default_security_group,
intouch_access_level,
CONVERT(nvarchar(max), roles) AS roles_text
FROM dbo.user_profile
""";
private const string UserByGuidSql = UserSelectSql + "\nWHERE user_guid = @userGuid\nORDER BY user_profile_id";
private const string UserByNameSql = UserSelectSql + "\nWHERE user_profile_name = @userName\nORDER BY user_profile_id";
}
@@ -0,0 +1,393 @@
using System.Buffers.Binary;
using System.Net;
using System.Net.Sockets;
namespace MxNativeClient;
public sealed class ManagedCallbackExporter : IDisposable
{
private readonly TcpListener _listener;
private readonly CancellationTokenSource _cancellation = new();
private readonly Task _acceptLoop;
private readonly List<string> _events = [];
private readonly object _eventsLock = new();
private readonly ulong _oxid = RandomUInt64();
private readonly ulong _oid = RandomUInt64();
public ManagedCallbackExporter(int port = 0)
{
CallbackIpid = Guid.NewGuid();
RemUnknownIpid = Guid.NewGuid();
_listener = new TcpListener(IPAddress.Any, port);
_listener.Start();
Port = ((IPEndPoint)_listener.LocalEndpoint).Port;
_acceptLoop = Task.Run(AcceptLoopAsync);
}
public int Port { get; }
public Guid CallbackIpid { get; }
public Guid RemUnknownIpid { get; }
public IReadOnlyList<string> Events
{
get
{
lock (_eventsLock)
{
return _events.ToArray();
}
}
}
public byte[] CreateCallbackObjRef(string hostName)
{
return ComObjRefBuilder.CreateStandardObjRef(
NmxProcedureMetadata.INmxSvcCallback,
stdFlags: 0x280,
publicRefs: 5,
oxid: _oxid,
oid: _oid,
ipid: CallbackIpid,
stringBindings: [$"{hostName}[{Port}]"]);
}
public void Dispose()
{
_cancellation.Cancel();
_listener.Stop();
try
{
_acceptLoop.Wait(TimeSpan.FromSeconds(1));
}
catch
{
}
_cancellation.Dispose();
}
private async Task AcceptLoopAsync()
{
while (!_cancellation.IsCancellationRequested)
{
try
{
var client = await _listener.AcceptTcpClientAsync(_cancellation.Token).ConfigureAwait(false);
Record($"accept remote={client.Client.RemoteEndPoint}");
_ = Task.Run(() => ServeClientAsync(client), _cancellation.Token);
}
catch (OperationCanceledException)
{
break;
}
catch (ObjectDisposedException)
{
break;
}
catch (Exception ex)
{
Record($"accept_error {ex.GetType().Name}: {ex.Message}");
}
}
}
private async Task ServeClientAsync(TcpClient client)
{
using (client)
{
NetworkStream stream = client.GetStream();
ushort currentContext = 0;
Guid currentInterface = Guid.Empty;
while (!_cancellation.IsCancellationRequested)
{
byte[]? pdu = await ReadPduAsync(stream, _cancellation.Token).ConfigureAwait(false);
if (pdu is null)
{
Record("client_closed");
return;
}
var header = DceRpcPduHeader.Parse(pdu);
Record($"pdu type={header.PacketType} flags=0x{header.PacketFlags:X2} frag={header.FragmentLength} auth={header.AuthLength} call={header.CallId}");
if (header.PacketType == DceRpcPacketType.Bind || header.PacketType == DceRpcPacketType.AlterContext)
{
var bind = DceRpcBindPdu.Parse(pdu);
currentContext = bind.PresentationContexts.Count == 0 ? (ushort)0 : bind.PresentationContexts[0].ContextId;
currentInterface = bind.PresentationContexts.Count == 0 ? Guid.Empty : bind.PresentationContexts[0].AbstractSyntax.Uuid;
Record($"bind context={currentContext} iid={currentInterface}");
await stream.WriteAsync(EncodeBindAck(header.CallId, currentContext), _cancellation.Token).ConfigureAwait(false);
continue;
}
if (header.PacketType == DceRpcPacketType.Request)
{
byte[] response = HandleRequest(pdu, header, currentContext, currentInterface);
await stream.WriteAsync(response, _cancellation.Token).ConfigureAwait(false);
continue;
}
if (header.PacketType == DceRpcPacketType.Auth3)
{
Record("auth3_ignored");
continue;
}
Record($"unhandled_pdu {header.PacketType}");
}
}
}
private byte[] HandleRequest(byte[] pdu, DceRpcPduHeader header, ushort currentContext, Guid currentInterface)
{
int fixedOffset = DceRpcPduHeader.Length;
uint allocationHint = BinaryPrimitives.ReadUInt32LittleEndian(pdu.AsSpan(fixedOffset, 4));
ushort contextId = BinaryPrimitives.ReadUInt16LittleEndian(pdu.AsSpan(fixedOffset + 4, 2));
ushort opnum = BinaryPrimitives.ReadUInt16LittleEndian(pdu.AsSpan(fixedOffset + 6, 2));
int stubOffset = fixedOffset + 8;
Guid? objectUuid = null;
if ((header.PacketFlags & 0x80) != 0)
{
objectUuid = new Guid(pdu.AsSpan(stubOffset, 16));
stubOffset += 16;
}
int trailerLength = header.AuthLength == 0 ? 0 : DceRpcAuthTrailer.Length + header.AuthLength;
int stubLength = header.FragmentLength - stubOffset - trailerLength;
ReadOnlySpan<byte> stub = pdu.AsSpan(stubOffset, Math.Max(0, stubLength));
Record($"request iid={currentInterface} context={contextId}/{currentContext} opnum={opnum} object={objectUuid} alloc={allocationHint} stub={stub.Length}");
if (currentInterface == RemUnknownMessages.IRemUnknown)
{
return opnum switch
{
RemUnknownMessages.RemQueryInterfaceOpnum => EncodeResponse(header.CallId, contextId, EncodeRemQueryInterfaceResponse(stub)),
RemUnknownMessages.RemAddRefOpnum => EncodeResponse(header.CallId, contextId, EncodeOrpcHResultResponse(0)),
RemUnknownMessages.RemReleaseOpnum => EncodeResponse(header.CallId, contextId, EncodeOrpcHResultResponse(0)),
_ => EncodeFault(header.CallId, contextId, 0x000006F7),
};
}
if (currentInterface == NmxSvcCallbackMessages.InterfaceId)
{
if (opnum is NmxSvcCallbackMessages.DataReceivedOpnum or NmxSvcCallbackMessages.StatusReceivedOpnum)
{
var parsed = NmxSvcCallbackMessages.ParseCallbackRequest(stub);
Record($"callback opnum={opnum} body={parsed.Body.Length}");
return EncodeResponse(header.CallId, contextId, NmxSvcCallbackMessages.EncodeCallbackResponse(0));
}
return EncodeFault(header.CallId, contextId, 0x000006F7);
}
return EncodeFault(header.CallId, contextId, 0x000006F7);
}
private byte[] EncodeRemQueryInterfaceResponse(ReadOnlySpan<byte> request)
{
Guid requestedIid = request.Length >= 76 ? new Guid(request.Slice(60, 16)) : Guid.Empty;
Record($"remqi requested={requestedIid}");
var std = new StdObjRef(0x280, 5, _oxid, _oid, requestedIid == RemUnknownMessages.IRemUnknown ? RemUnknownIpid : CallbackIpid);
int hr = requestedIid == RemUnknownMessages.IRemUnknown
|| requestedIid == NmxProcedureMetadata.INmxSvcCallback
|| requestedIid == new Guid("00000000-0000-0000-C000-000000000046")
? 0
: unchecked((int)0x80004002);
byte[] buffer = new byte[OrpcThat.EncodedLengthWithoutExtensions + 4 + 4 + RemQiResult.EncodedLength + 4];
int offset = 0;
new OrpcThat(0, 0).Encode().CopyTo(buffer.AsSpan(offset));
offset += OrpcThat.EncodedLengthWithoutExtensions;
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(offset, 4), 0x00020000);
offset += 4;
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(offset, 4), 1);
offset += 4;
BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(offset, 4), hr);
offset += 8;
std.Encode().CopyTo(buffer.AsSpan(offset));
offset += StdObjRef.EncodedLength;
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(offset, 4), 0);
return buffer;
}
private static byte[] EncodeOrpcHResultResponse(int hresult)
{
byte[] buffer = new byte[OrpcThat.EncodedLengthWithoutExtensions + 4];
new OrpcThat(0, 0).Encode().CopyTo(buffer.AsSpan());
BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(OrpcThat.EncodedLengthWithoutExtensions, 4), hresult);
return buffer;
}
private static byte[] EncodeBindAck(uint callId, ushort contextId)
{
const string secondaryAddress = "";
byte[] secondary = System.Text.Encoding.ASCII.GetBytes(secondaryAddress + "\0");
int secAddrLength = secondary.Length;
int secPad = Align(28 + 2 + secAddrLength, 4) - (28 + 2 + secAddrLength);
int resultOffset = 28 + 2 + secAddrLength + secPad;
int length = resultOffset + 4 + 24;
byte[] pdu = new byte[length];
new DceRpcPduHeader(5, 0, DceRpcPacketType.BindAck, 0x03, 0x10, (ushort)length, 0, callId).WriteTo(pdu);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(16, 2), 4280);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(18, 2), 4280);
BinaryPrimitives.WriteUInt32LittleEndian(pdu.AsSpan(20, 4), 0x00005353);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(24, 2), (ushort)secAddrLength);
secondary.CopyTo(pdu.AsSpan(26));
int offset = resultOffset;
pdu[offset++] = 1;
pdu[offset++] = 0;
pdu[offset++] = 0;
pdu[offset++] = 0;
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(offset, 2), 0);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(offset + 2, 2), 0);
offset += 4;
DceRpcSyntaxId.Ndr20.Uuid.TryWriteBytes(pdu.AsSpan(offset, 16));
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(offset + 16, 2), DceRpcSyntaxId.Ndr20.VersionMajor);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(offset + 18, 2), DceRpcSyntaxId.Ndr20.VersionMinor);
_ = contextId;
return pdu;
}
private static byte[] EncodeResponse(uint callId, ushort contextId, byte[] stubData)
{
int length = 24 + stubData.Length;
byte[] pdu = new byte[length];
new DceRpcPduHeader(5, 0, DceRpcPacketType.Response, 0x03, 0x10, (ushort)length, 0, callId).WriteTo(pdu);
BinaryPrimitives.WriteUInt32LittleEndian(pdu.AsSpan(16, 4), (uint)stubData.Length);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(20, 2), contextId);
pdu[22] = 0;
pdu[23] = 0;
stubData.CopyTo(pdu.AsSpan(24));
return pdu;
}
private static byte[] EncodeFault(uint callId, ushort contextId, uint status)
{
byte[] pdu = new byte[28];
new DceRpcPduHeader(5, 0, DceRpcPacketType.Fault, 0x03, 0x10, (ushort)pdu.Length, 0, callId).WriteTo(pdu);
BinaryPrimitives.WriteUInt32LittleEndian(pdu.AsSpan(16, 4), 0);
BinaryPrimitives.WriteUInt16LittleEndian(pdu.AsSpan(20, 2), contextId);
BinaryPrimitives.WriteUInt32LittleEndian(pdu.AsSpan(24, 4), status);
return pdu;
}
private async Task<byte[]?> ReadPduAsync(NetworkStream stream, CancellationToken cancellationToken)
{
byte[] header = new byte[DceRpcPduHeader.Length];
if (!await ReadExactAsync(stream, header, cancellationToken).ConfigureAwait(false))
{
return null;
}
var parsed = DceRpcPduHeader.Parse(header);
byte[] pdu = new byte[parsed.FragmentLength];
header.CopyTo(pdu, 0);
if (!await ReadExactAsync(stream, pdu.AsMemory(DceRpcPduHeader.Length), cancellationToken).ConfigureAwait(false))
{
return null;
}
return pdu;
}
private static async Task<bool> ReadExactAsync(NetworkStream stream, Memory<byte> buffer, CancellationToken cancellationToken)
{
int offset = 0;
while (offset < buffer.Length)
{
int read = await stream.ReadAsync(buffer[offset..], cancellationToken).ConfigureAwait(false);
if (read == 0)
{
return false;
}
offset += read;
}
return true;
}
private void Record(string message)
{
lock (_eventsLock)
{
_events.Add($"{DateTimeOffset.UtcNow:O} {message}");
}
}
private static int Align(int value, int alignment)
{
int remainder = value % alignment;
return remainder == 0 ? value : value + alignment - remainder;
}
private static ulong RandomUInt64()
{
Span<byte> bytes = stackalloc byte[8];
System.Security.Cryptography.RandomNumberGenerator.Fill(bytes);
return BinaryPrimitives.ReadUInt64LittleEndian(bytes);
}
}
public static class ComObjRefBuilder
{
public static byte[] CreateStandardObjRef(
Guid iid,
uint stdFlags,
uint publicRefs,
ulong oxid,
ulong oid,
Guid ipid,
IReadOnlyList<string> stringBindings)
{
ushort securityOffset = (ushort)(stringBindings.Sum(binding => 1 + binding.Length + 1) + 1);
var words = new List<ushort>();
foreach (string binding in stringBindings)
{
words.Add(0x0007);
foreach (char ch in binding)
{
words.Add(ch);
}
words.Add(0);
}
words.Add(0);
foreach (ushort authenticationService in new ushort[] { 0x0009, 0x001e, 0x0010, 0x000a, 0x0016, 0x001f, 0x000e })
{
words.Add(authenticationService);
words.Add(0xffff);
words.Add(0);
}
words.Add(0);
ushort entries = (ushort)words.Count;
byte[] buffer = new byte[68 + entries * 2];
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(0, 4), 0x574F454D);
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(4, 4), 1);
iid.TryWriteBytes(buffer.AsSpan(8, 16));
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(24, 4), stdFlags);
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(28, 4), publicRefs);
BinaryPrimitives.WriteUInt64LittleEndian(buffer.AsSpan(32, 8), oxid);
BinaryPrimitives.WriteUInt64LittleEndian(buffer.AsSpan(40, 8), oid);
ipid.TryWriteBytes(buffer.AsSpan(48, 16));
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(64, 2), entries);
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(66, 2), securityOffset);
int offset = 68;
foreach (ushort word in words)
{
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(offset, 2), word);
offset += 2;
}
return buffer;
}
}
@@ -0,0 +1,575 @@
using System.Globalization;
using System.Runtime.InteropServices;
using MxNativeCodec;
namespace MxNativeClient;
public enum DceRpcClientAuthentication
{
ManagedNtlm,
WindowsSspiNtlm,
}
public sealed class ManagedNmxService2Client : IDisposable
{
private readonly object _activatedComObject;
private readonly DceRpcTcpClient _serviceClient;
private readonly Guid _serviceIpid;
private bool _disposed;
private ManagedNmxService2Client(object activatedComObject, DceRpcTcpClient serviceClient, Guid serviceIpid)
{
_activatedComObject = activatedComObject;
_serviceClient = serviceClient;
_serviceIpid = serviceIpid;
}
public string Host { get; private init; } = string.Empty;
public int Port { get; private init; }
public static ManagedNmxService2Client Create(
DceRpcClientAuthentication authentication = DceRpcClientAuthentication.ManagedNtlm)
{
var type = Type.GetTypeFromProgID("NmxSvc.NmxService", throwOnError: true)
?? throw new InvalidOperationException("ProgID NmxSvc.NmxService was not resolved.");
object instance = Activator.CreateInstance(type)
?? throw new InvalidOperationException("ProgID NmxSvc.NmxService activation returned null.");
try
{
var resolved = ResolveService(instance, authentication);
var serviceClient = new DceRpcTcpClient(resolved.Host, resolved.Port);
serviceClient.Connect();
BindWithAuthentication(
serviceClient,
NmxService2Messages.InterfaceId,
authentication,
resolved.Host);
return new ManagedNmxService2Client(instance, serviceClient, resolved.ServiceIpid)
{
Host = resolved.Host,
Port = resolved.Port,
};
}
catch
{
if (Marshal.IsComObject(instance))
{
_ = Marshal.ReleaseComObject(instance);
}
throw;
}
}
public int GetPartnerVersion(int galaxyId, int platformId, int engineId)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] request = NmxService2Messages.EncodeGetPartnerVersionRequest(
OrpcThis.Create(Guid.NewGuid()),
galaxyId,
platformId,
engineId);
var response = _serviceClient.CallBoundObject(_serviceIpid, NmxService2Messages.GetPartnerVersionOpnum, request);
var parsed = NmxService2Messages.ParseGetPartnerVersionResponse(response.StubData.Span);
ThrowIfFailed(parsed.HResult, nameof(GetPartnerVersion));
return parsed.PartnerVersion;
}
public int RegisterEngine2(int localEngineId, string engineName, int version, byte[] callbackObjRef)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] request = NmxService2Messages.EncodeRegisterEngine2Request(
OrpcThis.Create(Guid.NewGuid()),
localEngineId,
engineName,
version,
callbackObjRef);
return CallForHResult(NmxService2Messages.RegisterEngine2Opnum, request);
}
public int RegisterEngine2WithoutCallback(int localEngineId, string engineName, int version)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] request = NmxService2Messages.EncodeRegisterEngine2Request(
OrpcThis.Create(Guid.NewGuid()),
localEngineId,
engineName,
version,
callbackObjRef: null);
return CallForHResult(NmxService2Messages.RegisterEngine2Opnum, request);
}
public int UnregisterEngine(int localEngineId)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] request = NmxService2Messages.EncodeUnregisterEngineRequest(
OrpcThis.Create(Guid.NewGuid()),
localEngineId);
return CallForHResult(NmxService2Messages.UnregisterEngineOpnum, request);
}
public int Connect(int localEngineId, int remoteGalaxyId, int remotePlatformId, int remoteEngineId)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] request = NmxService2Messages.EncodeConnectRequest(
OrpcThis.Create(Guid.NewGuid()),
localEngineId,
remoteGalaxyId,
remotePlatformId,
remoteEngineId);
return CallForHResult(NmxService2Messages.ConnectOpnum, request);
}
public int AddSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] request = NmxService2Messages.EncodeSubscriberEngineRequest(
OrpcThis.Create(Guid.NewGuid()),
localEngineId,
subscriberGalaxyId,
subscriberPlatformId,
subscriberEngineId);
return CallForHResult(NmxService2Messages.AddSubscriberEngineOpnum, request);
}
public int RemoveSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] request = NmxService2Messages.EncodeSubscriberEngineRequest(
OrpcThis.Create(Guid.NewGuid()),
localEngineId,
subscriberGalaxyId,
subscriberPlatformId,
subscriberEngineId);
return CallForHResult(NmxService2Messages.RemoveSubscriberEngineOpnum, request);
}
public int SetHeartbeatSendInterval(int ticksPerBeat, int maxMissedTicks)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] request = NmxService2Messages.EncodeSetHeartbeatSendIntervalRequest(
OrpcThis.Create(Guid.NewGuid()),
ticksPerBeat,
maxMissedTicks);
return CallForHResult(NmxService2Messages.SetHeartbeatSendIntervalOpnum, request);
}
public int TransferData(int remoteGalaxyId, int remotePlatformId, int remoteEngineId, ReadOnlySpan<byte> messageBody)
{
ObjectDisposedException.ThrowIf(_disposed, this);
ValidateTransferDataBody(messageBody, nameof(messageBody));
byte[] request = NmxService2Messages.EncodeTransferDataRequest(
OrpcThis.Create(Guid.NewGuid()),
remoteGalaxyId,
remotePlatformId,
remoteEngineId,
messageBody);
return CallForHResult(NmxService2Messages.TransferDataOpnum, request);
}
private static void ValidateTransferDataBody(ReadOnlySpan<byte> messageBody, string parameterName)
{
if (messageBody.IsEmpty)
{
throw new ArgumentException("TransferData body cannot be empty.", parameterName);
}
NmxTransferEnvelopeTemplate.FromObserved(messageBody);
if (messageBody.Length == NmxTransferEnvelopeTemplate.HeaderLength)
{
throw new ArgumentException("TransferData body must include an inner message after the 46-byte envelope.", parameterName);
}
}
internal static byte[] EncodeWriteTransferBody(
int localEngineId,
GalaxyTagMetadata tag,
object value,
int writeIndex = 1,
uint clientToken = 0,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
var projection = tag.ProjectWriteValue(value);
byte[] innerBody = NmxWriteMessage.Encode(
tag.ToReferenceHandle((byte)galaxyId),
projection.ValueKind,
projection.Value,
writeIndex,
clientToken);
return NmxTransferEnvelope.Encode(
NmxTransferMessageKind.Write,
localEngineId,
galaxyId,
tag.PlatformId,
tag.EngineId,
innerBody,
sourceGalaxyId,
sourcePlatformId);
}
internal static byte[] EncodeWrite2TransferBody(
int localEngineId,
GalaxyTagMetadata tag,
object value,
DateTime timestamp,
int writeIndex = 1,
uint clientToken = 0,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
var projection = tag.ProjectWriteValue(value);
byte[] innerBody = NmxWriteMessage.EncodeTimestamped(
tag.ToReferenceHandle((byte)galaxyId),
projection.ValueKind,
projection.Value,
timestamp,
writeIndex,
clientToken);
return NmxTransferEnvelope.Encode(
NmxTransferMessageKind.Write,
localEngineId,
galaxyId,
tag.PlatformId,
tag.EngineId,
innerBody,
sourceGalaxyId,
sourcePlatformId);
}
internal static byte[] EncodeWriteSecured2TransferBody(
int localEngineId,
GalaxyTagMetadata tag,
object value,
DateTime timestamp,
string clientName,
int currentUserId,
int verifierUserId,
int writeIndex = 1,
uint clientToken = 0,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
var projection = tag.ProjectWriteValue(value);
byte[] innerBody = NmxSecuredWrite2Message.Encode(
tag.ToReferenceHandle((byte)galaxyId),
projection.ValueKind,
projection.Value,
timestamp,
clientName,
NmxSecuredWrite2Message.ResolveObservedUserToken(currentUserId),
NmxSecuredWrite2Message.ResolveObservedUserToken(verifierUserId),
writeIndex,
clientToken);
return NmxTransferEnvelope.Encode(
NmxTransferMessageKind.Write,
localEngineId,
galaxyId,
tag.PlatformId,
tag.EngineId,
innerBody,
sourceGalaxyId,
sourcePlatformId);
}
internal static byte[] EncodeAdviseSupervisoryTransferBody(
int localEngineId,
GalaxyTagMetadata tag,
Guid itemCorrelationId,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
byte[] innerBody = NmxItemControlMessage.FromReferenceHandle(
NmxItemControlCommand.AdviseSupervisory,
itemCorrelationId,
tag.ToReferenceHandle((byte)galaxyId)).Encode();
return NmxTransferEnvelope.Encode(
NmxTransferMessageKind.ItemControl,
localEngineId,
galaxyId,
tag.PlatformId,
tag.EngineId,
innerBody,
sourceGalaxyId,
sourcePlatformId);
}
public int Write(
int localEngineId,
GalaxyTagMetadata tag,
object value,
int writeIndex = 1,
uint clientToken = 0,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] transferBody = EncodeWriteTransferBody(
localEngineId,
tag,
value,
writeIndex,
clientToken,
galaxyId,
sourceGalaxyId,
sourcePlatformId);
return TransferData(galaxyId, tag.PlatformId, tag.EngineId, transferBody);
}
public int Write2(
int localEngineId,
GalaxyTagMetadata tag,
object value,
DateTime timestamp,
int writeIndex = 1,
uint clientToken = 0,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] transferBody = EncodeWrite2TransferBody(
localEngineId,
tag,
value,
timestamp,
writeIndex,
clientToken,
galaxyId,
sourceGalaxyId,
sourcePlatformId);
return TransferData(galaxyId, tag.PlatformId, tag.EngineId, transferBody);
}
public int WriteSecured2(
int localEngineId,
GalaxyTagMetadata tag,
object value,
DateTime timestamp,
string clientName,
int currentUserId,
int verifierUserId,
int writeIndex = 1,
uint clientToken = 0,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] transferBody = EncodeWriteSecured2TransferBody(
localEngineId,
tag,
value,
timestamp,
clientName,
currentUserId,
verifierUserId,
writeIndex,
clientToken,
galaxyId,
sourceGalaxyId,
sourcePlatformId);
return TransferData(galaxyId, tag.PlatformId, tag.EngineId, transferBody);
}
public int AdviseSupervisory(
int localEngineId,
GalaxyTagMetadata tag,
Guid itemCorrelationId,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] transferBody = EncodeAdviseSupervisoryTransferBody(
localEngineId,
tag,
itemCorrelationId,
galaxyId,
sourceGalaxyId,
sourcePlatformId);
return TransferData(galaxyId, tag.PlatformId, tag.EngineId, transferBody);
}
public int SendObservedPreAdviseMetadata(
int localEngineId,
Guid itemCorrelationId,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] innerBody = NmxMetadataQueryMessage.EncodeObservedPreAdvise(itemCorrelationId);
byte[] transferBody = NmxTransferEnvelope.Encode(
NmxTransferMessageKind.Metadata,
localEngineId,
galaxyId,
targetPlatformId: 1,
targetEngineId: 1,
innerBody,
sourceGalaxyId,
sourcePlatformId);
return TransferData(galaxyId, 1, 1, transferBody);
}
public int RegisterReference(
int localEngineId,
GalaxyTagMetadata routeTag,
NmxReferenceRegistrationMessage message,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] transferBody = NmxTransferEnvelope.Encode(
NmxTransferMessageKind.ItemControl,
localEngineId,
galaxyId,
routeTag.PlatformId,
routeTag.EngineId,
message.Encode(),
sourceGalaxyId,
sourcePlatformId);
return TransferData(galaxyId, routeTag.PlatformId, routeTag.EngineId, transferBody);
}
public int UnAdvise(
int localEngineId,
GalaxyTagMetadata tag,
Guid itemCorrelationId,
int galaxyId = 1,
int sourceGalaxyId = 1,
int sourcePlatformId = 1)
{
ObjectDisposedException.ThrowIf(_disposed, this);
byte[] innerBody = NmxItemControlMessage.FromReferenceHandle(
NmxItemControlCommand.UnAdvise,
itemCorrelationId,
tag.ToReferenceHandle((byte)galaxyId)).Encode();
byte[] transferBody = NmxTransferEnvelope.Encode(
NmxTransferMessageKind.Write,
localEngineId,
galaxyId,
tag.PlatformId,
tag.EngineId,
innerBody,
sourceGalaxyId,
sourcePlatformId);
return TransferData(galaxyId, tag.PlatformId, tag.EngineId, transferBody);
}
public void Dispose()
{
if (_disposed)
{
return;
}
_serviceClient.Dispose();
if (Marshal.IsComObject(_activatedComObject))
{
_ = Marshal.ReleaseComObject(_activatedComObject);
}
_disposed = true;
}
private int CallForHResult(ushort opnum, byte[] request)
{
var response = _serviceClient.CallBoundObject(_serviceIpid, opnum, request);
var parsed = NmxService2Messages.ParseHResultResponse(response.StubData.Span);
return parsed.HResult;
}
private static (string Host, int Port, Guid ServiceIpid) ResolveService(
object instance,
DceRpcClientAuthentication authentication)
{
byte[] buffer = ComObjRefProvider.MarshalIUnknownObjRef(
instance,
ComObjRefProvider.MarshalContextDifferentMachine);
var objRef = ComObjRef.Parse(buffer);
var exporter = new ObjectExporterClient();
var oxidResponse = authentication == DceRpcClientAuthentication.ManagedNtlm
? exporter.ResolveOxidWithManagedNtlmPacketIntegrity(objRef.Oxid)
: exporter.ResolveOxidWithNtlmPacketIntegrity(objRef.Oxid);
var resolved = ObjectExporterMessages.ParseResolveOxidResult(oxidResponse.StubData.Span);
var endpoint = resolved.Bindings.First(binding => binding.TowerId == ObjectExporterMessages.ProtseqNcacnIpTcp);
string host = ParseBracketedHost(endpoint.Value);
int port = ParseBracketedPort(endpoint.Value);
using var client = new DceRpcTcpClient(host, port);
client.Connect();
BindWithAuthentication(client, RemUnknownMessages.IRemUnknown, authentication, host);
byte[] request = RemUnknownMessages.EncodeRemQueryInterfaceRequest(
objRef.Ipid,
NmxProcedureMetadata.INmxService2,
Guid.NewGuid());
var response = client.CallBoundObject(resolved.RemUnknownIpid, RemUnknownMessages.RemQueryInterfaceOpnum, request);
var parsed = RemUnknownMessages.ParseRemQueryInterfaceResponse(response.StubData.Span);
if (parsed.Result is null || parsed.Result.HResult != 0 || parsed.ErrorCode != 0)
{
throw new InvalidOperationException($"RemQueryInterface failed: hresult=0x{parsed.Result?.HResult ?? -1:X8}, error=0x{parsed.ErrorCode:X8}.");
}
return (host, port, parsed.Result.StandardObjectReference.Ipid);
}
private static void BindWithAuthentication(
DceRpcTcpClient client,
Guid interfaceId,
DceRpcClientAuthentication authentication,
string targetName)
{
if (authentication == DceRpcClientAuthentication.ManagedNtlm)
{
client.BindWithManagedNtlmPacketIntegrity(interfaceId, versionMajor: 0, versionMinor: 0);
return;
}
client.BindWithNtlmPacketIntegrity(interfaceId, versionMajor: 0, versionMinor: 0, targetName);
}
private static string ParseBracketedHost(string binding)
{
int open = binding.LastIndexOf('[');
if (open <= 0)
{
throw new FormatException($"Binding does not contain a bracketed host: {binding}");
}
return binding[..open];
}
private static int ParseBracketedPort(string binding)
{
int open = binding.LastIndexOf('[');
int close = binding.LastIndexOf(']');
if (open < 0 || close <= open)
{
throw new FormatException($"Binding does not contain a bracketed port: {binding}");
}
return int.Parse(binding.AsSpan(open + 1, close - open - 1), CultureInfo.InvariantCulture);
}
private static void ThrowIfFailed(int hresult, string operation)
{
if (hresult < 0)
{
Marshal.ThrowExceptionForHR(hresult);
}
if (hresult != 0)
{
throw new InvalidOperationException($"{operation} returned application status 0x{hresult:X8}.");
}
}
}
@@ -0,0 +1,389 @@
using System.Buffers.Binary;
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
namespace MxNativeClient;
public sealed class ManagedNtlmClientContext
{
private const uint NegotiateUnicode = 0x00000001;
private const uint RequestTarget = 0x00000004;
private const uint NegotiateSign = 0x00000010;
private const uint NegotiateSeal = 0x00000020;
private const uint NegotiateNtlm = 0x00000200;
private const uint NegotiateAlwaysSign = 0x00008000;
private const uint NegotiateExtendedSessionSecurity = 0x00080000;
private const uint NegotiateTargetInfo = 0x00800000;
private const uint NegotiateVersion = 0x02000000;
private const uint Negotiate128 = 0x20000000;
private const uint NegotiateKeyExchange = 0x40000000;
private const uint Negotiate56 = 0x80000000;
private readonly string _user;
private readonly string _password;
private readonly string _domain;
private readonly string _workstation;
private uint _flags;
private byte[] _exportedSessionKey = [];
private byte[] _clientSigningKey = [];
private Rc4? _clientSealingHandle;
private uint _sequence;
public ManagedNtlmClientContext(string user, string password, string domain, string? workstation = null)
{
_user = user;
_password = password;
_domain = domain;
_workstation = string.IsNullOrWhiteSpace(workstation) ? Environment.MachineName : workstation;
}
public static ManagedNtlmClientContext FromEnvironment()
{
string user = Environment.GetEnvironmentVariable("MX_RPC_USER")
?? throw new InvalidOperationException("MX_RPC_USER is required for managed NTLM.");
string password = Environment.GetEnvironmentVariable("MX_RPC_PASSWORD")
?? throw new InvalidOperationException("MX_RPC_PASSWORD is required for managed NTLM.");
string domain = Environment.GetEnvironmentVariable("MX_RPC_DOMAIN") ?? string.Empty;
return new ManagedNtlmClientContext(user, password, domain);
}
public byte[] CreateType1()
{
_flags = NegotiateKeyExchange
| NegotiateSign
| NegotiateAlwaysSign
| NegotiateSeal
| NegotiateTargetInfo
| NegotiateNtlm
| NegotiateExtendedSessionSecurity
| NegotiateUnicode
| RequestTarget
| Negotiate128
| Negotiate56;
byte[] message = new byte[32];
Encoding.ASCII.GetBytes("NTLMSSP\0").CopyTo(message, 0);
BinaryPrimitives.WriteUInt32LittleEndian(message.AsSpan(8, 4), 1);
BinaryPrimitives.WriteUInt32LittleEndian(message.AsSpan(12, 4), _flags);
return message;
}
public byte[] CreateType3(ReadOnlySpan<byte> type2)
{
var challenge = NtlmChallenge.Parse(type2);
_flags &= challenge.Flags;
byte[] clientChallenge = RandomNumberGenerator.GetBytes(8);
byte[] targetInfo = BuildTargetInfo(challenge.TargetInfo);
byte[] responseKeyNt = HmacMd5(NtHash(_password), Encoding.Unicode.GetBytes(_user.ToUpperInvariant() + _domain));
byte[] temp = BuildNtlmV2Temp(clientChallenge, targetInfo);
byte[] ntProof = HmacMd5(responseKeyNt, Combine(challenge.ServerChallenge, temp));
byte[] ntResponse = Combine(ntProof, temp);
byte[] lmResponse = Combine(HmacMd5(responseKeyNt, Combine(challenge.ServerChallenge, clientChallenge)), clientChallenge);
byte[] sessionBaseKey = HmacMd5(responseKeyNt, ntProof);
_exportedSessionKey = RandomNumberGenerator.GetBytes(16);
byte[] encryptedSessionKey = new Rc4(sessionBaseKey).Transform(_exportedSessionKey);
_clientSigningKey = SignKey(_exportedSessionKey, clientMode: true);
_clientSealingHandle = new Rc4(SealKey(_exportedSessionKey, clientMode: true));
_sequence = 0;
byte[] domain = Encoding.Unicode.GetBytes(_domain);
byte[] user = Encoding.Unicode.GetBytes(_user);
byte[] workstation = Encoding.Unicode.GetBytes(_workstation);
const int headerLength = 64;
int payloadLength = lmResponse.Length + ntResponse.Length + domain.Length + user.Length + workstation.Length + encryptedSessionKey.Length;
byte[] message = new byte[headerLength + payloadLength];
Encoding.ASCII.GetBytes("NTLMSSP\0").CopyTo(message, 0);
BinaryPrimitives.WriteUInt32LittleEndian(message.AsSpan(8, 4), 3);
int offset = headerLength;
WriteSecurityBuffer(message, 12, lmResponse, ref offset);
WriteSecurityBuffer(message, 20, ntResponse, ref offset);
WriteSecurityBuffer(message, 28, domain, ref offset);
WriteSecurityBuffer(message, 36, user, ref offset);
WriteSecurityBuffer(message, 44, workstation, ref offset);
WriteSecurityBuffer(message, 52, encryptedSessionKey, ref offset);
BinaryPrimitives.WriteUInt32LittleEndian(message.AsSpan(60, 4), _flags);
return message;
}
public byte[] Sign(ReadOnlySpan<byte> message)
{
if (_clientSealingHandle is null || _clientSigningKey.Length == 0)
{
throw new InvalidOperationException("NTLM context has not completed Type3 negotiation.");
}
byte[] sequenceBytes = new byte[4];
BinaryPrimitives.WriteUInt32LittleEndian(sequenceBytes, _sequence);
byte[] digest = HmacMd5(_clientSigningKey, Combine(sequenceBytes, message.ToArray()));
byte[] checksum = _clientSealingHandle.Transform(digest[..8]);
byte[] signature = new byte[16];
BinaryPrimitives.WriteUInt32LittleEndian(signature.AsSpan(0, 4), 1);
checksum.CopyTo(signature.AsSpan(4, 8));
BinaryPrimitives.WriteUInt32LittleEndian(signature.AsSpan(12, 4), _sequence);
_sequence++;
return signature;
}
private static byte[] BuildNtlmV2Temp(byte[] clientChallenge, byte[] targetInfo)
{
byte[] temp = new byte[28 + targetInfo.Length];
temp[0] = 1;
temp[1] = 1;
BinaryPrimitives.WriteInt64LittleEndian(temp.AsSpan(8, 8), DateTimeOffset.UtcNow.ToFileTime());
clientChallenge.CopyTo(temp.AsSpan(16, 8));
targetInfo.CopyTo(temp.AsSpan(28));
return temp;
}
private static byte[] BuildTargetInfo(ReadOnlySpan<byte> original)
{
var pairs = AvPair.ParseAll(original);
byte[]? dnsHost = pairs.FirstOrDefault(static pair => pair.Id == 3)?.Value;
if (dnsHost is not null)
{
byte[] prefix = Encoding.Unicode.GetBytes("cifs/");
pairs.RemoveAll(static pair => pair.Id == 9);
pairs.Add(new AvPair(9, Combine(prefix, dnsHost)));
}
if (!pairs.Any(static pair => pair.Id == 7))
{
byte[] timestamp = new byte[8];
BinaryPrimitives.WriteInt64LittleEndian(timestamp, DateTimeOffset.UtcNow.ToFileTime());
pairs.Add(new AvPair(7, timestamp));
}
using var stream = new MemoryStream();
Span<byte> header = stackalloc byte[4];
foreach (var pair in pairs.Where(static pair => pair.Id != 0))
{
BinaryPrimitives.WriteUInt16LittleEndian(header[..2], pair.Id);
BinaryPrimitives.WriteUInt16LittleEndian(header[2..], (ushort)pair.Value.Length);
stream.Write(header);
stream.Write(pair.Value);
}
stream.Write(new byte[4]);
return stream.ToArray();
}
private static byte[] SignKey(byte[] sessionKey, bool clientMode)
{
string magic = clientMode
? "session key to client-to-server signing key magic constant\0"
: "session key to server-to-client signing key magic constant\0";
return MD5.HashData(Combine(sessionKey, Encoding.ASCII.GetBytes(magic)));
}
private static byte[] SealKey(byte[] sessionKey, bool clientMode)
{
string magic = clientMode
? "session key to client-to-server sealing key magic constant\0"
: "session key to server-to-client sealing key magic constant\0";
return MD5.HashData(Combine(sessionKey, Encoding.ASCII.GetBytes(magic)));
}
private static byte[] NtHash(string password)
{
return Md4.Hash(Encoding.Unicode.GetBytes(password));
}
private static byte[] HmacMd5(byte[] key, byte[] data)
{
using var hmac = new HMACMD5(key);
return hmac.ComputeHash(data);
}
private static byte[] Combine(params byte[][] parts)
{
byte[] combined = new byte[parts.Sum(static part => part.Length)];
int offset = 0;
foreach (byte[] part in parts)
{
part.CopyTo(combined.AsSpan(offset));
offset += part.Length;
}
return combined;
}
private static void WriteSecurityBuffer(byte[] message, int descriptorOffset, byte[] value, ref int payloadOffset)
{
BinaryPrimitives.WriteUInt16LittleEndian(message.AsSpan(descriptorOffset, 2), (ushort)value.Length);
BinaryPrimitives.WriteUInt16LittleEndian(message.AsSpan(descriptorOffset + 2, 2), (ushort)value.Length);
BinaryPrimitives.WriteUInt32LittleEndian(message.AsSpan(descriptorOffset + 4, 4), (uint)payloadOffset);
value.CopyTo(message.AsSpan(payloadOffset));
payloadOffset += value.Length;
}
private sealed record NtlmChallenge(uint Flags, byte[] ServerChallenge, byte[] TargetInfo)
{
public static NtlmChallenge Parse(ReadOnlySpan<byte> message)
{
if (message.Length < 48 || !message[..8].SequenceEqual(Encoding.ASCII.GetBytes("NTLMSSP\0")))
{
throw new ArgumentException("NTLM challenge is truncated or invalid.", nameof(message));
}
int targetInfoLength = BinaryPrimitives.ReadUInt16LittleEndian(message.Slice(40, 2));
int targetInfoOffset = (int)BinaryPrimitives.ReadUInt32LittleEndian(message.Slice(44, 4));
if (targetInfoOffset < 0 || targetInfoOffset + targetInfoLength > message.Length)
{
throw new ArgumentException("NTLM challenge target-info buffer is invalid.", nameof(message));
}
return new NtlmChallenge(
BinaryPrimitives.ReadUInt32LittleEndian(message.Slice(20, 4)),
message.Slice(24, 8).ToArray(),
message.Slice(targetInfoOffset, targetInfoLength).ToArray());
}
}
private sealed record AvPair(ushort Id, byte[] Value)
{
public static List<AvPair> ParseAll(ReadOnlySpan<byte> buffer)
{
var pairs = new List<AvPair>();
int offset = 0;
while (offset + 4 <= buffer.Length)
{
ushort id = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(offset, 2));
ushort length = BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(offset + 2, 2));
offset += 4;
if (id == 0)
{
break;
}
if (offset + length > buffer.Length)
{
throw new ArgumentException("NTLM AV pair buffer is truncated.", nameof(buffer));
}
pairs.Add(new AvPair(id, buffer.Slice(offset, length).ToArray()));
offset += length;
}
return pairs;
}
}
private sealed class Rc4
{
private readonly byte[] _s = new byte[256];
private int _i;
private int _j;
public Rc4(byte[] key)
{
for (int i = 0; i < 256; i++)
{
_s[i] = (byte)i;
}
int j = 0;
for (int i = 0; i < 256; i++)
{
j = (j + _s[i] + key[i % key.Length]) & 0xff;
(_s[i], _s[j]) = (_s[j], _s[i]);
}
}
public byte[] Transform(ReadOnlySpan<byte> input)
{
byte[] output = new byte[input.Length];
for (int k = 0; k < input.Length; k++)
{
_i = (_i + 1) & 0xff;
_j = (_j + _s[_i]) & 0xff;
(_s[_i], _s[_j]) = (_s[_j], _s[_i]);
byte keyByte = _s[(_s[_i] + _s[_j]) & 0xff];
output[k] = (byte)(input[k] ^ keyByte);
}
return output;
}
}
private static class Md4
{
public static byte[] Hash(byte[] input)
{
uint a = 0x67452301;
uint b = 0xefcdab89;
uint c = 0x98badcfe;
uint d = 0x10325476;
byte[] padded = Pad(input);
Span<uint> x = stackalloc uint[16];
for (int offset = 0; offset < padded.Length; offset += 64)
{
uint aa = a;
uint bb = b;
uint cc = c;
uint dd = d;
for (int i = 0; i < 16; i++)
{
x[i] = BinaryPrimitives.ReadUInt32LittleEndian(padded.AsSpan(offset + i * 4, 4));
}
Round1(ref a, b, c, d, x[0], 3); Round1(ref d, a, b, c, x[1], 7); Round1(ref c, d, a, b, x[2], 11); Round1(ref b, c, d, a, x[3], 19);
Round1(ref a, b, c, d, x[4], 3); Round1(ref d, a, b, c, x[5], 7); Round1(ref c, d, a, b, x[6], 11); Round1(ref b, c, d, a, x[7], 19);
Round1(ref a, b, c, d, x[8], 3); Round1(ref d, a, b, c, x[9], 7); Round1(ref c, d, a, b, x[10], 11); Round1(ref b, c, d, a, x[11], 19);
Round1(ref a, b, c, d, x[12], 3); Round1(ref d, a, b, c, x[13], 7); Round1(ref c, d, a, b, x[14], 11); Round1(ref b, c, d, a, x[15], 19);
Round2(ref a, b, c, d, x[0], 3); Round2(ref d, a, b, c, x[4], 5); Round2(ref c, d, a, b, x[8], 9); Round2(ref b, c, d, a, x[12], 13);
Round2(ref a, b, c, d, x[1], 3); Round2(ref d, a, b, c, x[5], 5); Round2(ref c, d, a, b, x[9], 9); Round2(ref b, c, d, a, x[13], 13);
Round2(ref a, b, c, d, x[2], 3); Round2(ref d, a, b, c, x[6], 5); Round2(ref c, d, a, b, x[10], 9); Round2(ref b, c, d, a, x[14], 13);
Round2(ref a, b, c, d, x[3], 3); Round2(ref d, a, b, c, x[7], 5); Round2(ref c, d, a, b, x[11], 9); Round2(ref b, c, d, a, x[15], 13);
Round3(ref a, b, c, d, x[0], 3); Round3(ref d, a, b, c, x[8], 9); Round3(ref c, d, a, b, x[4], 11); Round3(ref b, c, d, a, x[12], 15);
Round3(ref a, b, c, d, x[2], 3); Round3(ref d, a, b, c, x[10], 9); Round3(ref c, d, a, b, x[6], 11); Round3(ref b, c, d, a, x[14], 15);
Round3(ref a, b, c, d, x[1], 3); Round3(ref d, a, b, c, x[9], 9); Round3(ref c, d, a, b, x[5], 11); Round3(ref b, c, d, a, x[13], 15);
Round3(ref a, b, c, d, x[3], 3); Round3(ref d, a, b, c, x[11], 9); Round3(ref c, d, a, b, x[7], 11); Round3(ref b, c, d, a, x[15], 15);
a += aa;
b += bb;
c += cc;
d += dd;
}
byte[] hash = new byte[16];
BinaryPrimitives.WriteUInt32LittleEndian(hash.AsSpan(0, 4), a);
BinaryPrimitives.WriteUInt32LittleEndian(hash.AsSpan(4, 4), b);
BinaryPrimitives.WriteUInt32LittleEndian(hash.AsSpan(8, 4), c);
BinaryPrimitives.WriteUInt32LittleEndian(hash.AsSpan(12, 4), d);
return hash;
}
private static byte[] Pad(byte[] input)
{
ulong bitLength = (ulong)input.Length * 8;
int paddedLength = input.Length + 1;
while (paddedLength % 64 != 56)
{
paddedLength++;
}
byte[] padded = new byte[paddedLength + 8];
input.CopyTo(padded.AsSpan());
padded[input.Length] = 0x80;
BinaryPrimitives.WriteUInt64LittleEndian(padded.AsSpan(paddedLength, 8), bitLength);
return padded;
}
private static uint F(uint x, uint y, uint z) => (x & y) | (~x & z);
private static uint G(uint x, uint y, uint z) => (x & y) | (x & z) | (y & z);
private static uint H(uint x, uint y, uint z) => x ^ y ^ z;
private static void Round1(ref uint a, uint b, uint c, uint d, uint x, int s) => a = BitOperations.RotateLeft(a + F(b, c, d) + x, s);
private static void Round2(ref uint a, uint b, uint c, uint d, uint x, int s) => a = BitOperations.RotateLeft(a + G(b, c, d) + x + 0x5a827999, s);
private static void Round3(ref uint a, uint b, uint c, uint d, uint x, int s) => a = BitOperations.RotateLeft(a + H(b, c, d) + x + 0x6ed9eba1, s);
}
}
+15
View File
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MxNativeCodec\MxNativeCodec.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.1" />
</ItemGroup>
</Project>
@@ -0,0 +1,910 @@
using System.Globalization;
using MxNativeCodec;
namespace MxNativeClient;
public sealed record MxNativeDataChangeEvent(
int ServerHandle,
int ItemHandle,
object? Value,
ushort Quality,
DateTime TimestampUtc,
IReadOnlyList<MxStatus> Statuses,
bool IsDuringRecovery = false);
public sealed record MxNativeWriteCompleteEvent(
int ServerHandle,
int ItemHandle,
IReadOnlyList<MxStatus> Statuses,
bool IsDuringRecovery = false);
public sealed record MxNativeOperationCompleteEvent(
int ServerHandle,
int ItemHandle,
IReadOnlyList<MxStatus> Statuses,
bool IsDuringRecovery = false);
public sealed record MxNativeBufferedDataChangeEvent(
int ServerHandle,
int ItemHandle,
short MxDataType,
IReadOnlyList<object?> Values,
IReadOnlyList<ushort> Qualities,
IReadOnlyList<DateTime> TimestampsUtc,
IReadOnlyList<MxStatus> Statuses,
bool IsDuringRecovery = false);
public sealed record MxNativeCompatibilityRecoveryAttemptEvent(
int ServerHandle,
int Attempt,
int MaxAttempts);
public sealed record MxNativeCompatibilityRecoveryFailureEvent(
int ServerHandle,
int Attempt,
int MaxAttempts,
Exception Exception,
bool WillRetry);
public sealed record MxNativeCompatibilityRecoveryCompletedEvent(
int ServerHandle,
int Attempt,
int MaxAttempts);
public sealed class MxNativeCompatibilityServer : IDisposable
{
private readonly object _gate = new();
private readonly Dictionary<int, MxNativeSession> _sessions = [];
private readonly Dictionary<int, CompatibilityItem> _items = [];
private readonly Dictionary<int, int> _bufferedUpdateIntervals = [];
private readonly Dictionary<int, int> _nextUserHandles = [];
private readonly Dictionary<int, Queue<int>> _pendingWriteItems = [];
private int _nextServerHandle = 1;
private int _nextItemHandle = 1;
private bool _disposed;
public event EventHandler<MxNativeDataChangeEvent>? DataChanged;
public event EventHandler<MxNativeBufferedDataChangeEvent>? BufferedDataChanged;
public event EventHandler<MxNativeWriteCompleteEvent>? WriteCompleted;
#pragma warning disable CS0067 // OperationComplete has the MXAccess event shape, but no firing path is modeled until captures define its trigger.
public event EventHandler<MxNativeOperationCompleteEvent>? OperationCompleted;
#pragma warning restore CS0067
public event EventHandler<MxNativeCompatibilityRecoveryAttemptEvent>? RecoveryAttemptStarted;
public event EventHandler<MxNativeCompatibilityRecoveryFailureEvent>? RecoveryAttemptFailed;
public event EventHandler<MxNativeCompatibilityRecoveryCompletedEvent>? RecoveryCompleted;
public int Register(string clientName)
{
ObjectDisposedException.ThrowIf(_disposed, this);
var session = MxNativeSession.Open(new MxNativeClientOptions
{
EngineName = string.IsNullOrWhiteSpace(clientName) ? $"MxNativeClient.{Environment.ProcessId}" : clientName,
});
int serverHandle;
lock (_gate)
{
serverHandle = _nextServerHandle++;
_sessions.Add(serverHandle, session);
_nextUserHandles.Add(serverHandle, 1);
_pendingWriteItems.Add(serverHandle, new Queue<int>());
}
session.CallbackReceived += (_, evt) => OnCallbackReceived(serverHandle, evt);
session.OperationStatusReceived += (_, evt) => OnOperationStatusReceived(serverHandle, evt);
session.RecoveryAttemptStarted += (_, evt) => RecoveryAttemptStarted?.Invoke(
this,
new MxNativeCompatibilityRecoveryAttemptEvent(serverHandle, evt.Attempt, evt.MaxAttempts));
session.RecoveryAttemptFailed += (_, evt) => RecoveryAttemptFailed?.Invoke(
this,
new MxNativeCompatibilityRecoveryFailureEvent(
serverHandle,
evt.Attempt,
evt.MaxAttempts,
evt.Exception,
evt.WillRetry));
session.RecoveryCompleted += (_, evt) => RecoveryCompleted?.Invoke(
this,
new MxNativeCompatibilityRecoveryCompletedEvent(serverHandle, evt.Attempt, evt.MaxAttempts));
return serverHandle;
}
public void Unregister(int serverHandle)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
foreach (int itemHandle in _items
.Where(pair => pair.Value.ServerHandle == serverHandle)
.Select(pair => pair.Key)
.ToArray())
{
_items.Remove(itemHandle);
}
_sessions.Remove(serverHandle);
_bufferedUpdateIntervals.Remove(serverHandle);
_nextUserHandles.Remove(serverHandle);
_pendingWriteItems.Remove(serverHandle);
}
session.Dispose();
}
public void RecoverConnection(int serverHandle)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
}
session.RecoverConnection();
}
public async Task RecoverConnectionAsync(
int serverHandle,
MxNativeRecoveryPolicy? policy = null,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
}
await session.RecoverConnectionAsync(policy, cancellationToken).ConfigureAwait(false);
}
public int AddItem(int serverHandle, string itemDefinition)
{
return AddItemAsync(serverHandle, itemDefinition).GetAwaiter().GetResult();
}
public async Task<int> AddItemAsync(
int serverHandle,
string itemDefinition,
CancellationToken cancellationToken = default)
{
return await AddItemCoreAsync(serverHandle, itemDefinition, itemContext: null, cancellationToken).ConfigureAwait(false);
}
private async Task<int> AddItemCoreAsync(
int serverHandle,
string itemDefinition,
string? itemContext,
CancellationToken cancellationToken)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
}
GalaxyTagMetadata? metadata;
string resolvedReference;
try
{
(metadata, resolvedReference) = await ResolveWithOptionalContextAsync(
session,
itemDefinition,
itemContext,
cancellationToken).ConfigureAwait(false);
}
catch (InvalidOperationException ex) when (IsMissingReferenceResolution(ex))
{
metadata = null;
resolvedReference = string.IsNullOrWhiteSpace(itemContext)
? itemDefinition
: CombineItemContext(itemDefinition, itemContext);
}
lock (_gate)
{
int itemHandle = _nextItemHandle++;
_items.Add(
itemHandle,
metadata is null
? new CompatibilityItem(
serverHandle,
resolvedReference,
metadata: null,
isInvalidReference: true)
: new CompatibilityItem(serverHandle, resolvedReference, metadata));
return itemHandle;
}
}
public int AddItem2(int serverHandle, string itemDefinition, string itemContext)
{
return AddItemCoreAsync(serverHandle, itemDefinition, itemContext, CancellationToken.None).GetAwaiter().GetResult();
}
public void RemoveItem(int serverHandle, int itemHandle)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
CompatibilityItem item;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
item = GetItemLocked(serverHandle, itemHandle);
_items.Remove(itemHandle);
}
if (item.Subscription is not null)
{
session.Unsubscribe(item.Subscription.CorrelationId);
}
}
public void Advise(int serverHandle, int itemHandle)
{
AdviseAsync(serverHandle, itemHandle).GetAwaiter().GetResult();
}
public Task AdviseSupervisoryAsync(int serverHandle, int itemHandle, CancellationToken cancellationToken = default)
{
return AdviseAsync(serverHandle, itemHandle, cancellationToken);
}
public void AdviseSupervisory(int serverHandle, int itemHandle)
{
Advise(serverHandle, itemHandle);
}
public async Task AdviseAsync(
int serverHandle,
int itemHandle,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
CompatibilityItem item;
bool fireInvalidReference = false;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
item = GetItemLocked(serverHandle, itemHandle);
if (item.Subscription is not null)
{
return;
}
if (item.IsInvalidReference)
{
item.IsAdvisedInvalidReference = true;
fireInvalidReference = true;
}
}
if (fireInvalidReference)
{
ThreadPool.QueueUserWorkItem(_ => FireInvalidReferenceDataChange(serverHandle, itemHandle));
return;
}
if (item.Metadata is null)
{
throw new InvalidOperationException("Valid compatibility items must carry Galaxy metadata.");
}
MxNativeSubscription subscription = item.IsBuffered
? await session.RegisterBufferedItemAsync(
item.ItemDefinition,
item.ItemContext,
itemHandle,
cancellationToken).ConfigureAwait(false)
: await session.SubscribeAsync(item.TagReference, cancellationToken).ConfigureAwait(false);
lock (_gate)
{
if (_items.TryGetValue(itemHandle, out CompatibilityItem? current))
{
current.Subscription = subscription;
}
}
}
public void UnAdvise(int serverHandle, int itemHandle)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
CompatibilityItem item;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
item = GetItemLocked(serverHandle, itemHandle);
if (item.IsInvalidReference)
{
item.IsAdvisedInvalidReference = false;
return;
}
if (item.Subscription is null)
{
return;
}
}
session.Unsubscribe(item.Subscription.CorrelationId);
lock (_gate)
{
if (_items.TryGetValue(itemHandle, out CompatibilityItem? current))
{
current.Subscription = null;
}
}
}
public void Write(int serverHandle, int itemHandle, object value, int userId = 0)
{
WriteAsync(serverHandle, itemHandle, value).GetAwaiter().GetResult();
}
public async Task WriteAsync(
int serverHandle,
int itemHandle,
object value,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
CompatibilityItem item;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
item = GetItemLocked(serverHandle, itemHandle);
if (item.IsInvalidReference)
{
return;
}
if (item.IsBuffered)
{
throw new ArgumentException("Normal Write is not valid for buffered item handles.", nameof(itemHandle));
}
if (item.Subscription is null)
{
throw new ArgumentException("Write requires an advised item handle.", nameof(itemHandle));
}
if (item.Metadata?.IsBufferProperty == true)
{
return;
}
GetPendingWriteItemsLocked(serverHandle).Enqueue(itemHandle);
}
try
{
await session.WriteAsync(item.TagReference, value, cancellationToken: cancellationToken).ConfigureAwait(false);
}
catch
{
lock (_gate)
{
RemovePendingWriteItemLocked(serverHandle, itemHandle);
}
throw;
}
}
public void Write2(int serverHandle, int itemHandle, object value, DateTime timestamp, int userId = 0)
{
Write2Async(serverHandle, itemHandle, value, timestamp).GetAwaiter().GetResult();
}
public void Write2(int serverHandle, int itemHandle, object value, object timestamp, int userId = 0)
{
Write2(serverHandle, itemHandle, value, CoerceWriteTimestamp(timestamp), userId);
}
public async Task Write2Async(
int serverHandle,
int itemHandle,
object value,
DateTime timestamp,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
CompatibilityItem item;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
item = GetItemLocked(serverHandle, itemHandle);
if (item.IsInvalidReference)
{
return;
}
if (item.IsBuffered)
{
throw new ArgumentException("Normal Write2 is not valid for buffered item handles.", nameof(itemHandle));
}
if (item.Subscription is null)
{
throw new ArgumentException("Write2 requires an advised item handle.", nameof(itemHandle));
}
if (item.Metadata?.IsBufferProperty == true)
{
return;
}
GetPendingWriteItemsLocked(serverHandle).Enqueue(itemHandle);
}
try
{
await session.Write2Async(item.TagReference, value, timestamp, cancellationToken: cancellationToken).ConfigureAwait(false);
}
catch
{
lock (_gate)
{
RemovePendingWriteItemLocked(serverHandle, itemHandle);
}
throw;
}
}
public Task Write2Async(
int serverHandle,
int itemHandle,
object value,
object timestamp,
CancellationToken cancellationToken = default)
{
return Write2Async(serverHandle, itemHandle, value, CoerceWriteTimestamp(timestamp), cancellationToken);
}
public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object value)
{
throw new NotSupportedException("Dedicated WriteSecured parity is not implemented because current MXAccess captures do not emit a successful NMX secured-write body.");
}
public void WriteSecured2(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object value, DateTime timestamp)
{
WriteSecured2Async(serverHandle, itemHandle, value, timestamp, currentUserId, verifierUserId).GetAwaiter().GetResult();
}
public void WriteSecured2(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object value, object timestamp)
{
WriteSecured2(serverHandle, itemHandle, currentUserId, verifierUserId, value, CoerceWriteTimestamp(timestamp));
}
public async Task WriteSecured2Async(int serverHandle, int itemHandle, object value, DateTime timestamp, int currentUserId, int verifierUserId = 0)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
CompatibilityItem item;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
item = GetItemLocked(serverHandle, itemHandle);
}
await session.WriteSecured2Async(
item.TagReference,
value,
timestamp,
currentUserId,
verifierUserId).ConfigureAwait(false);
}
public Task WriteSecured2Async(
int serverHandle,
int itemHandle,
object value,
object timestamp,
int currentUserId,
int verifierUserId = 0)
{
return WriteSecured2Async(
serverHandle,
itemHandle,
value,
CoerceWriteTimestamp(timestamp),
currentUserId,
verifierUserId);
}
public int AuthenticateUser(int serverHandle, string verifyUser, string verifyUserPassword)
{
ObjectDisposedException.ThrowIf(_disposed, this);
ArgumentNullException.ThrowIfNull(verifyUser);
_ = verifyUserPassword;
lock (_gate)
{
_ = GetSessionLocked(serverHandle);
return AllocateUserHandleLocked(serverHandle);
}
}
public int ArchestrAUserToId(int serverHandle, string userIdGuid)
{
ObjectDisposedException.ThrowIf(_disposed, this);
lock (_gate)
{
_ = GetSessionLocked(serverHandle);
}
if (!Guid.TryParse(userIdGuid, out Guid parsedGuid))
{
throw new ArgumentException("ArchestrA user ID must be a GUID string.", nameof(userIdGuid));
}
lock (_gate)
{
return AllocateUserHandleLocked(serverHandle);
}
}
public void Suspend(int serverHandle, int itemHandle, out MxStatus status)
{
ObjectDisposedException.ThrowIf(_disposed, this);
lock (_gate)
{
_ = GetSessionLocked(serverHandle);
CompatibilityItem item = GetItemLocked(serverHandle, itemHandle);
if (item.Subscription is null)
{
status = new MxStatus(0, MxStatusCategory.Unknown, MxStatusSource.Unknown, 0);
throw new ArgumentException("Suspend requires an advised item handle.", nameof(itemHandle));
}
}
status = MxStatus.SuspendPending;
}
public void Activate(int serverHandle, int itemHandle, out MxStatus status)
{
ObjectDisposedException.ThrowIf(_disposed, this);
lock (_gate)
{
_ = GetSessionLocked(serverHandle);
CompatibilityItem item = GetItemLocked(serverHandle, itemHandle);
if (item.Subscription is null)
{
status = new MxStatus(0, MxStatusCategory.Unknown, MxStatusSource.Unknown, 0);
throw new ArgumentException("Activate requires an advised item handle.", nameof(itemHandle));
}
}
status = MxStatus.ActivateOk;
}
public int AddBufferedItem(int serverHandle, string itemDefinition, string itemContext)
{
return AddBufferedItemAsync(serverHandle, itemDefinition, itemContext).GetAwaiter().GetResult();
}
public async Task<int> AddBufferedItemAsync(
int serverHandle,
string itemDefinition,
string itemContext,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
MxNativeSession session;
lock (_gate)
{
session = GetSessionLocked(serverHandle);
}
(GalaxyTagMetadata metadata, string routeReference) = await ResolveWithOptionalContextAsync(
session,
itemDefinition,
itemContext,
cancellationToken).ConfigureAwait(false);
lock (_gate)
{
int itemHandle = _nextItemHandle++;
_items.Add(
itemHandle,
new CompatibilityItem(
serverHandle,
routeReference,
metadata,
isBuffered: true,
itemDefinition: itemDefinition,
itemContext: itemContext));
return itemHandle;
}
}
public void SetBufferedUpdateInterval(int serverHandle, int updateInterval)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (updateInterval <= 0)
{
throw new ArgumentOutOfRangeException(nameof(updateInterval), updateInterval, "Buffered update interval must be positive.");
}
lock (_gate)
{
_ = GetSessionLocked(serverHandle);
_bufferedUpdateIntervals[serverHandle] = ((updateInterval + 99) / 100) * 100;
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
MxNativeSession[] sessions;
lock (_gate)
{
sessions = _sessions.Values.ToArray();
_sessions.Clear();
_items.Clear();
_bufferedUpdateIntervals.Clear();
_nextUserHandles.Clear();
_pendingWriteItems.Clear();
}
foreach (MxNativeSession session in sessions)
{
session.Dispose();
}
_disposed = true;
}
private void OnCallbackReceived(int serverHandle, MxNativeCallbackEvent evt)
{
int itemHandle;
CompatibilityItem? item;
lock (_gate)
{
var matched = _items.FirstOrDefault(pair =>
pair.Value.ServerHandle == serverHandle &&
pair.Value.Subscription?.CorrelationId == evt.Message.ItemCorrelationId);
itemHandle = matched.Key;
item = matched.Value;
}
if (itemHandle == 0 || item is null)
{
return;
}
if (evt.Record.Value is null)
{
return;
}
if (item.IsBuffered)
{
BufferedDataChanged?.Invoke(
this,
new MxNativeBufferedDataChangeEvent(
serverHandle,
itemHandle,
item.Metadata?.MxDataType ?? 0,
[evt.Record.Value],
[evt.Record.Quality],
[evt.Record.TimestampUtc],
[evt.Status],
evt.IsDuringRecovery));
return;
}
if (item.Metadata is not null && ShouldSuppressCompatibilityDataChange(item.Metadata, evt.Record.Value))
{
return;
}
DataChanged?.Invoke(
this,
new MxNativeDataChangeEvent(
serverHandle,
itemHandle,
evt.Record.Value,
evt.Record.Quality,
evt.Record.TimestampUtc,
[evt.Status],
evt.IsDuringRecovery));
}
internal static bool ShouldSuppressCompatibilityDataChange(GalaxyTagMetadata metadata, object value)
{
return metadata.MxDataType == (short)MxDataType.InternationalizedString
&& value is string text
&& text.Length == 0;
}
private void FireInvalidReferenceDataChange(int serverHandle, int itemHandle)
{
DataChanged?.Invoke(
this,
new MxNativeDataChangeEvent(
serverHandle,
itemHandle,
Value: null,
Quality: 0,
TimestampUtc: DateTime.UtcNow,
[MxStatus.InvalidReferenceConfiguration]));
}
private void OnOperationStatusReceived(int serverHandle, MxNativeOperationStatusEvent evt)
{
int itemHandle;
lock (_gate)
{
if (!_pendingWriteItems.TryGetValue(serverHandle, out Queue<int>? pendingItems)
|| !pendingItems.TryDequeue(out itemHandle))
{
return;
}
}
if (!evt.Message.IsMxAccessWriteComplete)
{
return;
}
WriteCompleted?.Invoke(
this,
new MxNativeWriteCompleteEvent(serverHandle, itemHandle, [evt.Message.Status], evt.IsDuringRecovery));
}
private MxNativeSession GetSessionLocked(int serverHandle)
{
if (!_sessions.TryGetValue(serverHandle, out MxNativeSession? session))
{
throw new ArgumentException("Unknown MX native server handle.", nameof(serverHandle));
}
return session;
}
private CompatibilityItem GetItemLocked(int serverHandle, int itemHandle)
{
if (!_items.TryGetValue(itemHandle, out CompatibilityItem? item) || item.ServerHandle != serverHandle)
{
throw new ArgumentException("Unknown MX native item handle.", nameof(itemHandle));
}
return item;
}
private int AllocateUserHandleLocked(int serverHandle)
{
int next = _nextUserHandles.TryGetValue(serverHandle, out int value) ? value : 1;
_nextUserHandles[serverHandle] = next + 1;
return next;
}
private Queue<int> GetPendingWriteItemsLocked(int serverHandle)
{
if (!_pendingWriteItems.TryGetValue(serverHandle, out Queue<int>? pendingItems))
{
pendingItems = new Queue<int>();
_pendingWriteItems.Add(serverHandle, pendingItems);
}
return pendingItems;
}
private static bool IsMissingReferenceResolution(InvalidOperationException ex)
{
return ex.Message.Contains("was not found in the deployed repository metadata", StringComparison.Ordinal);
}
private static string CombineItemContext(string itemDefinition, string itemContext)
{
if (string.IsNullOrWhiteSpace(itemContext))
{
return itemDefinition;
}
string trimmedContext = itemContext.TrimEnd('.');
string trimmedDefinition = itemDefinition.TrimStart('.');
return trimmedDefinition.StartsWith(trimmedContext + ".", StringComparison.OrdinalIgnoreCase)
? trimmedDefinition
: $"{trimmedContext}.{trimmedDefinition}";
}
private static async Task<(GalaxyTagMetadata Metadata, string ResolvedReference)> ResolveWithOptionalContextAsync(
MxNativeSession session,
string itemDefinition,
string? itemContext,
CancellationToken cancellationToken)
{
try
{
GalaxyTagMetadata metadata = await session.ResolveTagAsync(itemDefinition, cancellationToken).ConfigureAwait(false);
return (metadata, itemDefinition);
}
catch (Exception ex) when (!string.IsNullOrWhiteSpace(itemContext) && CanRetryWithItemContext(ex))
{
string combinedReference = CombineItemContext(itemDefinition, itemContext);
GalaxyTagMetadata metadata = await session.ResolveTagAsync(combinedReference, cancellationToken).ConfigureAwait(false);
return (metadata, combinedReference);
}
}
private static bool CanRetryWithItemContext(Exception ex)
{
return ex is ArgumentException
|| (ex is InvalidOperationException invalidOperation && IsMissingReferenceResolution(invalidOperation));
}
private static DateTime CoerceWriteTimestamp(object timestamp)
{
return timestamp switch
{
DateTime dateTime => dateTime,
DateTimeOffset dateTimeOffset => dateTimeOffset.UtcDateTime,
double oaDate => DateTime.FromOADate(oaDate),
float oaDate => DateTime.FromOADate(oaDate),
decimal oaDate => DateTime.FromOADate((double)oaDate),
string text when DateTime.TryParse(
text,
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeLocal,
out DateTime parsed) => parsed,
_ => throw new ArgumentException(
"Timestamp must be a DateTime, DateTimeOffset, OLE Automation date, or invariant parseable date/time string.",
nameof(timestamp)),
};
}
private void RemovePendingWriteItemLocked(int serverHandle, int itemHandle)
{
if (!_pendingWriteItems.TryGetValue(serverHandle, out Queue<int>? pendingItems)
|| pendingItems.Count == 0)
{
return;
}
int pendingCount = pendingItems.Count;
bool removed = false;
for (int i = 0; i < pendingCount; i++)
{
int pendingItem = pendingItems.Dequeue();
if (!removed && pendingItem == itemHandle)
{
removed = true;
continue;
}
pendingItems.Enqueue(pendingItem);
}
}
private sealed class CompatibilityItem(
int serverHandle,
string tagReference,
GalaxyTagMetadata? metadata,
bool isBuffered = false,
bool isInvalidReference = false,
string itemDefinition = "",
string itemContext = "")
{
public int ServerHandle { get; } = serverHandle;
public string TagReference { get; } = tagReference;
public GalaxyTagMetadata? Metadata { get; } = metadata;
public bool IsBuffered { get; } = isBuffered;
public bool IsInvalidReference { get; } = isInvalidReference;
public string ItemDefinition { get; } = itemDefinition;
public string ItemContext { get; } = itemContext;
public MxNativeSubscription? Subscription { get; set; }
public bool IsAdvisedInvalidReference { get; set; }
}
}
+669
View File
@@ -0,0 +1,669 @@
using System.Runtime.InteropServices;
using System.Threading;
using MxNativeCodec;
namespace MxNativeClient;
public sealed record MxNativeClientOptions
{
public int LocalEngineId { get; init; } = GenerateDefaultLocalEngineId();
public string EngineName { get; init; } = $"MxNativeClient.{Environment.ProcessId}";
public int PartnerVersion { get; init; } = 6;
public int GalaxyId { get; init; } = 1;
public int SourcePlatformId { get; init; } = 1;
public DceRpcClientAuthentication Authentication { get; init; } = DceRpcClientAuthentication.ManagedNtlm;
public int? HeartbeatTicksPerBeat { get; init; }
public int HeartbeatMaxMissedTicks { get; init; } = 3;
private static int GenerateDefaultLocalEngineId()
{
return 0x7000 + (Environment.ProcessId & 0x0fff);
}
}
public sealed record MxNativeRecoveryPolicy
{
public static MxNativeRecoveryPolicy SingleAttempt { get; } = new();
public int MaxAttempts { get; init; } = 1;
public TimeSpan Delay { get; init; } = TimeSpan.Zero;
public void Validate()
{
if (MaxAttempts < 1)
{
throw new ArgumentOutOfRangeException(nameof(MaxAttempts), "Recovery attempts must be at least one.");
}
if (Delay < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(Delay), "Recovery delay cannot be negative.");
}
}
}
public sealed record MxNativeRecoveryAttemptEvent(int Attempt, int MaxAttempts);
public sealed record MxNativeRecoveryFailureEvent(
int Attempt,
int MaxAttempts,
Exception Exception,
bool WillRetry);
public sealed record MxNativeRecoveryCompletedEvent(int Attempt, int MaxAttempts);
public sealed record MxNativeSubscription(
Guid CorrelationId,
string TagReference,
GalaxyTagMetadata Metadata,
bool IsBuffered = false,
string BufferedItemDefinition = "",
string BufferedItemContext = "",
int BufferedItemHandle = 0);
public sealed record MxNativeCallbackEvent(
NmxSubscriptionMessage Message,
NmxSubscriptionRecord Record,
byte[] RawBody,
bool IsDuringRecovery = false)
{
public MxStatus Status => Record.ToDataChangeStatus();
}
public sealed record MxNativeOperationStatusEvent(
NmxOperationStatusMessage Message,
byte[] RawBody,
bool IsDuringRecovery = false);
public sealed record MxNativeReferenceRegistrationEvent(
NmxReferenceRegistrationResultMessage Message,
byte[] RawBody,
bool IsDuringRecovery = false);
public sealed record MxNativeUnparsedCallbackEvent(
byte[] RawBody,
string ParseError,
bool IsDuringRecovery = false);
public sealed class MxNativeSession : IDisposable
{
private readonly MxNativeClientOptions _options;
private readonly GalaxyRepositoryTagResolver _resolver;
private readonly NmxCallbackSink _callback;
private readonly GCHandle _callbackHandle;
private readonly Dictionary<Guid, MxNativeSubscription> _subscriptions = [];
private readonly HashSet<PublisherEndpoint> _publisherEndpoints = [];
private ManagedNmxService2Client _service;
private int _recoveryActive;
private bool _disposed;
private MxNativeSession(
MxNativeClientOptions options,
ManagedNmxService2Client service,
GalaxyRepositoryTagResolver resolver,
NmxCallbackSink callback,
GCHandle callbackHandle)
{
_options = options;
_service = service;
_resolver = resolver;
_callback = callback;
_callbackHandle = callbackHandle;
_callback.DataReceived += OnCallbackReceived;
_callback.StatusReceived += OnCallbackReceived;
}
public event EventHandler<MxNativeCallbackEvent>? CallbackReceived;
public event EventHandler<MxNativeOperationStatusEvent>? OperationStatusReceived;
public event EventHandler<MxNativeReferenceRegistrationEvent>? ReferenceRegistrationReceived;
public event EventHandler<MxNativeUnparsedCallbackEvent>? UnparsedCallbackReceived;
public event EventHandler<MxNativeRecoveryAttemptEvent>? RecoveryAttemptStarted;
public event EventHandler<MxNativeRecoveryFailureEvent>? RecoveryAttemptFailed;
public event EventHandler<MxNativeRecoveryCompletedEvent>? RecoveryCompleted;
public IReadOnlyCollection<MxNativeSubscription> Subscriptions => _subscriptions.Values;
public static MxNativeSession Open(MxNativeClientOptions? options = null)
{
options ??= new MxNativeClientOptions();
var callback = new NmxCallbackSink();
var callbackHandle = GCHandle.Alloc(callback);
try
{
return new MxNativeSession(
options,
CreateRegisteredService(options, callback),
new GalaxyRepositoryTagResolver(),
callback,
callbackHandle);
}
catch
{
callbackHandle.Free();
throw;
}
}
public Task<GalaxyTagMetadata> ResolveTagAsync(string tagReference, CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
return _resolver.ResolveAsync(tagReference, cancellationToken);
}
public Task<IReadOnlyList<GalaxyTagMetadata>> BrowseAsync(
string objectTagLike = "%",
string attributeLike = "%",
int maxRows = 100,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
return _resolver.BrowseAsync(objectTagLike, attributeLike, maxRows, cancellationToken);
}
public async Task WriteAsync(
string tagReference,
object value,
int writeIndex = 1,
uint clientToken = 0,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
GalaxyTagMetadata tag = await ResolveTagAsync(tagReference, cancellationToken).ConfigureAwait(false);
EnsureSucceeded(
_service.Write(
_options.LocalEngineId,
tag,
value,
writeIndex,
clientToken,
_options.GalaxyId,
_options.GalaxyId,
_options.SourcePlatformId),
nameof(ManagedNmxService2Client.Write));
}
public async Task Write2Async(
string tagReference,
object value,
DateTime timestamp,
int writeIndex = 1,
uint clientToken = 0,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
GalaxyTagMetadata tag = await ResolveTagAsync(tagReference, cancellationToken).ConfigureAwait(false);
EnsureSucceeded(
_service.Write2(
_options.LocalEngineId,
tag,
value,
timestamp,
writeIndex,
clientToken,
_options.GalaxyId,
_options.GalaxyId,
_options.SourcePlatformId),
nameof(ManagedNmxService2Client.Write2));
}
public Task WriteSecuredAsync(
string tagReference,
object value,
int currentUserId,
int verifierUserId = 0,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
throw new NotSupportedException(
"Dedicated MXAccess WriteSecured parity is not implemented. Captures 036, 038, and 039 show the installed MXAccess WriteSecured method returning 0x80004021 before emitting an NMX write body; normal Write to secured-classified public tags is supported through WriteAsync.");
}
public async Task WriteSecured2Async(
string tagReference,
object value,
DateTime timestamp,
int currentUserId,
int verifierUserId = 0,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
GalaxyTagMetadata tag = await ResolveTagAsync(tagReference, cancellationToken).ConfigureAwait(false);
EnsureSucceeded(
_service.WriteSecured2(
_options.LocalEngineId,
tag,
value,
timestamp,
_options.EngineName,
currentUserId,
verifierUserId,
writeIndex: 1,
clientToken: 0,
_options.GalaxyId,
_options.GalaxyId,
_options.SourcePlatformId),
nameof(ManagedNmxService2Client.WriteSecured2));
}
public async Task<MxNativeSubscription> SubscribeAsync(
string tagReference,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
GalaxyTagMetadata tag = await ResolveTagAsync(tagReference, cancellationToken).ConfigureAwait(false);
EnsurePublisherConnected(tag);
var subscription = new MxNativeSubscription(Guid.NewGuid(), tagReference, tag);
EnsureSucceeded(
_service.AdviseSupervisory(
_options.LocalEngineId,
tag,
subscription.CorrelationId,
_options.GalaxyId,
_options.GalaxyId,
_options.SourcePlatformId),
nameof(ManagedNmxService2Client.AdviseSupervisory));
_subscriptions.Add(subscription.CorrelationId, subscription);
return subscription;
}
public async Task<MxNativeSubscription> RegisterBufferedItemAsync(
string itemDefinition,
string itemContext,
int itemHandle,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
string routeReference = string.IsNullOrWhiteSpace(itemContext)
? itemDefinition
: $"{itemContext.TrimEnd('.')}.{itemDefinition.TrimStart('.')}";
GalaxyTagMetadata routeTag = await ResolveTagAsync(routeReference, cancellationToken).ConfigureAwait(false);
EnsurePublisherConnected(routeTag);
var subscription = new MxNativeSubscription(
Guid.NewGuid(),
routeReference,
routeTag,
IsBuffered: true,
BufferedItemDefinition: itemDefinition,
BufferedItemContext: itemContext,
BufferedItemHandle: itemHandle);
var message = new NmxReferenceRegistrationMessage(
itemHandle,
subscription.CorrelationId,
NmxReferenceRegistrationMessage.ToBufferedItemDefinition(itemDefinition),
itemContext,
Subscribe: true);
EnsureSucceeded(
_service.RegisterReference(
_options.LocalEngineId,
routeTag,
message,
_options.GalaxyId,
_options.GalaxyId,
_options.SourcePlatformId),
nameof(ManagedNmxService2Client.RegisterReference));
_subscriptions.Add(subscription.CorrelationId, subscription);
return subscription;
}
public async Task<object?> ReadAsync(
string tagReference,
TimeSpan timeout,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (timeout <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(timeout), "Read timeout must be positive.");
}
using var timeoutSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutSource.CancelAfter(timeout);
var completion = new TaskCompletionSource<object?>(
TaskCreationOptions.RunContinuationsAsynchronously);
MxNativeSubscription? subscription = null;
using CancellationTokenRegistration cancellation = timeoutSource.Token.Register(
static state => ((TaskCompletionSource<object?>)state!).TrySetCanceled(),
completion);
EventHandler<MxNativeCallbackEvent> handler = (_, evt) =>
{
if (subscription is null ||
evt.Message.ItemCorrelationId != subscription.CorrelationId ||
evt.Record.Value is null)
{
return;
}
completion.TrySetResult(evt.Record.Value);
};
CallbackReceived += handler;
try
{
subscription = await SubscribeAsync(tagReference, timeoutSource.Token).ConfigureAwait(false);
return await completion.Task.ConfigureAwait(false);
}
finally
{
CallbackReceived -= handler;
if (subscription is not null && _subscriptions.ContainsKey(subscription.CorrelationId))
{
Unsubscribe(subscription.CorrelationId);
}
}
}
public void Unsubscribe(Guid correlationId)
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (!_subscriptions.Remove(correlationId, out MxNativeSubscription? subscription))
{
return;
}
if (!subscription.IsBuffered)
{
EnsureSucceeded(
_service.UnAdvise(
_options.LocalEngineId,
subscription.Metadata,
subscription.CorrelationId,
_options.GalaxyId,
_options.GalaxyId,
_options.SourcePlatformId),
nameof(ManagedNmxService2Client.UnAdvise));
}
}
public void RecoverConnection()
{
ObjectDisposedException.ThrowIf(_disposed, this);
try
{
RecoveryAttemptStarted?.Invoke(this, new MxNativeRecoveryAttemptEvent(1, 1));
RecoverConnectionCore();
RecoveryCompleted?.Invoke(this, new MxNativeRecoveryCompletedEvent(1, 1));
}
catch (Exception ex) when (IsRecoverableRecoveryException(ex))
{
RecoveryAttemptFailed?.Invoke(this, new MxNativeRecoveryFailureEvent(1, 1, ex, WillRetry: false));
throw;
}
}
public async Task RecoverConnectionAsync(
MxNativeRecoveryPolicy? policy = null,
CancellationToken cancellationToken = default)
{
ObjectDisposedException.ThrowIf(_disposed, this);
policy ??= MxNativeRecoveryPolicy.SingleAttempt;
policy.Validate();
Exception? lastError = null;
for (int attempt = 1; attempt <= policy.MaxAttempts; attempt++)
{
cancellationToken.ThrowIfCancellationRequested();
RecoveryAttemptStarted?.Invoke(this, new MxNativeRecoveryAttemptEvent(attempt, policy.MaxAttempts));
try
{
RecoverConnectionCore();
RecoveryCompleted?.Invoke(this, new MxNativeRecoveryCompletedEvent(attempt, policy.MaxAttempts));
return;
}
catch (Exception ex) when (IsRecoverableRecoveryException(ex))
{
lastError = ex;
bool willRetry = attempt < policy.MaxAttempts;
RecoveryAttemptFailed?.Invoke(
this,
new MxNativeRecoveryFailureEvent(attempt, policy.MaxAttempts, ex, willRetry));
if (!willRetry)
{
break;
}
if (policy.Delay > TimeSpan.Zero)
{
await Task.Delay(policy.Delay, cancellationToken).ConfigureAwait(false);
}
}
}
throw new InvalidOperationException(
$"MX native recovery failed after {policy.MaxAttempts} attempt(s).",
lastError);
}
private void RecoverConnectionCore()
{
Interlocked.Increment(ref _recoveryActive);
try
{
ManagedNmxService2Client replacement = CreateRegisteredService(_options, _callback);
try
{
foreach (PublisherEndpoint endpoint in _publisherEndpoints)
{
ConnectPublisher(replacement, endpoint);
}
foreach (MxNativeSubscription subscription in _subscriptions.Values)
{
ReAdviseSubscription(replacement, subscription);
}
}
catch
{
replacement.Dispose();
throw;
}
ManagedNmxService2Client oldService = _service;
_service = replacement;
oldService.Dispose();
}
finally
{
Interlocked.Decrement(ref _recoveryActive);
}
}
public void Dispose()
{
if (_disposed)
{
return;
}
foreach (MxNativeSubscription subscription in _subscriptions.Values.ToArray())
{
if (!subscription.IsBuffered)
{
TryCleanup(() => _service.UnAdvise(
_options.LocalEngineId,
subscription.Metadata,
subscription.CorrelationId,
_options.GalaxyId,
_options.GalaxyId,
_options.SourcePlatformId));
}
}
foreach (PublisherEndpoint publisherEndpoint in _publisherEndpoints)
{
TryCleanup(() => _service.RemoveSubscriberEngine(
publisherEndpoint.EngineId,
_options.GalaxyId,
_options.SourcePlatformId,
_options.LocalEngineId));
}
TryCleanup(() => _service.UnregisterEngine(_options.LocalEngineId));
_service.Dispose();
if (_callbackHandle.IsAllocated)
{
_callbackHandle.Free();
}
_disposed = true;
}
private void EnsurePublisherConnected(GalaxyTagMetadata tag)
{
var endpoint = new PublisherEndpoint(tag.PlatformId, tag.EngineId);
if (_publisherEndpoints.Contains(endpoint))
{
return;
}
ConnectPublisher(_service, endpoint);
_publisherEndpoints.Add(endpoint);
}
private void ConnectPublisher(ManagedNmxService2Client service, PublisherEndpoint endpoint)
{
EnsureSucceeded(
service.Connect(_options.LocalEngineId, _options.GalaxyId, endpoint.PlatformId, endpoint.EngineId),
nameof(ManagedNmxService2Client.Connect));
EnsureSucceeded(
service.AddSubscriberEngine(endpoint.EngineId, _options.GalaxyId, _options.SourcePlatformId, _options.LocalEngineId),
nameof(ManagedNmxService2Client.AddSubscriberEngine));
}
private void ReAdviseSubscription(ManagedNmxService2Client service, MxNativeSubscription subscription)
{
if (subscription.IsBuffered)
{
var message = new NmxReferenceRegistrationMessage(
subscription.BufferedItemHandle,
subscription.CorrelationId,
NmxReferenceRegistrationMessage.ToBufferedItemDefinition(subscription.BufferedItemDefinition),
subscription.BufferedItemContext,
Subscribe: true);
EnsureSucceeded(
service.RegisterReference(
_options.LocalEngineId,
subscription.Metadata,
message,
_options.GalaxyId,
_options.GalaxyId,
_options.SourcePlatformId),
nameof(ManagedNmxService2Client.RegisterReference));
return;
}
EnsureSucceeded(
service.AdviseSupervisory(
_options.LocalEngineId,
subscription.Metadata,
subscription.CorrelationId,
_options.GalaxyId,
_options.GalaxyId,
_options.SourcePlatformId),
nameof(ManagedNmxService2Client.AdviseSupervisory));
}
private void OnCallbackReceived(object? sender, NmxServiceMessage message)
{
bool isDuringRecovery = Volatile.Read(ref _recoveryActive) > 0;
if (NmxOperationStatusMessage.TryParseProcessDataReceivedBody(message.Body, out var operationStatus))
{
OperationStatusReceived?.Invoke(
this,
new MxNativeOperationStatusEvent(operationStatus, message.Body, isDuringRecovery));
return;
}
if (NmxReferenceRegistrationResultMessage.TryParseProcessDataReceivedBody(message.Body, out var registrationResult))
{
ReferenceRegistrationReceived?.Invoke(
this,
new MxNativeReferenceRegistrationEvent(registrationResult!, message.Body, isDuringRecovery));
return;
}
NmxSubscriptionMessage parsed;
try
{
parsed = NmxSubscriptionMessage.ParseProcessDataReceivedBody(message.Body);
}
catch (ArgumentException ex)
{
UnparsedCallbackReceived?.Invoke(
this,
new MxNativeUnparsedCallbackEvent(message.Body, ex.Message, isDuringRecovery));
return;
}
foreach (NmxSubscriptionRecord record in parsed.Records)
{
CallbackReceived?.Invoke(this, new MxNativeCallbackEvent(parsed, record, message.Body, isDuringRecovery));
}
}
private static void EnsureSucceeded(int hresult, string operation)
{
if (hresult == 0)
{
return;
}
if (hresult < 0)
{
Marshal.ThrowExceptionForHR(hresult);
}
throw new InvalidOperationException($"{operation} returned application status 0x{hresult:X8}.");
}
private static ManagedNmxService2Client CreateRegisteredService(MxNativeClientOptions options, NmxCallbackSink callback)
{
byte[] callbackObjRef = ComObjRefProvider.MarshalInterfaceObjRef(
callback,
NmxProcedureMetadata.INmxSvcCallback,
ComObjRefProvider.MarshalContextDifferentMachine);
ManagedNmxService2Client service = ManagedNmxService2Client.Create(options.Authentication);
try
{
EnsureSucceeded(
service.RegisterEngine2(options.LocalEngineId, options.EngineName, options.PartnerVersion, callbackObjRef),
nameof(ManagedNmxService2Client.RegisterEngine2));
if (options.HeartbeatTicksPerBeat is { } heartbeatTicks)
{
EnsureSucceeded(
service.SetHeartbeatSendInterval(heartbeatTicks, options.HeartbeatMaxMissedTicks),
nameof(ManagedNmxService2Client.SetHeartbeatSendInterval));
}
return service;
}
catch
{
service.Dispose();
throw;
}
}
private static void TryCleanup(Func<int> operation)
{
try
{
_ = operation();
}
catch (Exception ex) when (ex is InvalidOperationException or COMException or ObjectDisposedException)
{
}
}
private static bool IsRecoverableRecoveryException(Exception ex)
{
return ex is InvalidOperationException or COMException or IOException or ObjectDisposedException;
}
private readonly record struct PublisherEndpoint(int PlatformId, int EngineId);
}
+39
View File
@@ -0,0 +1,39 @@
using System.Runtime.InteropServices;
namespace MxNativeClient;
public sealed record NmxServiceMessage(byte[] Body);
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public sealed unsafe class NmxCallbackSink : INmxSvcCallback
{
public event EventHandler<NmxServiceMessage>? DataReceived;
public event EventHandler<NmxServiceMessage>? StatusReceived;
public void DataReceivedRaw(int bufferSize, ref sbyte dataBuffer)
{
DataReceived?.Invoke(this, new NmxServiceMessage(CopyBuffer(bufferSize, ref dataBuffer)));
}
public void StatusReceivedRaw(int bufferSize, ref sbyte statusBuffer)
{
StatusReceived?.Invoke(this, new NmxServiceMessage(CopyBuffer(bufferSize, ref statusBuffer)));
}
private static byte[] CopyBuffer(int bufferSize, ref sbyte firstByte)
{
if (bufferSize <= 0)
{
return [];
}
var output = new byte[bufferSize];
fixed (sbyte* source = &firstByte)
{
new ReadOnlySpan<byte>(source, bufferSize).CopyTo(output);
}
return output;
}
}
+92
View File
@@ -0,0 +1,92 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace MxNativeClient;
[ComImport]
[Guid("AE24BD51-2E80-44CC-905B-E5446C942BEB")]
[ClassInterface(ClassInterfaceType.None)]
internal sealed class NmxServiceClass : INmxService2
{
public extern void RegisterEngine(int engineId, string engineName, INmxSvcCallback callback);
public extern void UnRegisterEngine(int engineId);
public extern void Connect(int localEngineId, int remoteGalaxyId, int remotePlatformId, int remoteEngineId);
public extern void TransferData(int remoteGalaxyId, int remotePlatformId, int remoteEngineId, int size, ref byte messageBody);
public extern void AddSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId);
public extern void RemoveSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId);
public extern void SetHeartbeatSendInterval(int ticksPerBeat, int maxMissedTicks);
public extern void RegisterEngine2(int engineId, string engineName, int version, INmxSvcCallback callback);
public extern void GetPartnerVersion(int galaxyId, int platformId, int engineId, out int version);
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("575008DB-845D-46C6-A906-F6F8CA86F315")]
internal interface INmxService
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void RegisterEngine(int engineId, [MarshalAs(UnmanagedType.BStr)] string engineName, [MarshalAs(UnmanagedType.Interface)] INmxSvcCallback callback);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void UnRegisterEngine(int engineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void Connect(int localEngineId, int remoteGalaxyId, int remotePlatformId, int remoteEngineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void TransferData(int remoteGalaxyId, int remotePlatformId, int remoteEngineId, int size, ref byte messageBody);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void AddSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void RemoveSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void SetHeartbeatSendInterval(int ticksPerBeat, int maxMissedTicks);
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("2630A513-A974-4B1A-8025-457A9A7C56B8")]
internal interface INmxService2 : INmxService
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void RegisterEngine(int engineId, [MarshalAs(UnmanagedType.BStr)] string engineName, [MarshalAs(UnmanagedType.Interface)] INmxSvcCallback callback);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void UnRegisterEngine(int engineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void Connect(int localEngineId, int remoteGalaxyId, int remotePlatformId, int remoteEngineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void TransferData(int remoteGalaxyId, int remotePlatformId, int remoteEngineId, int size, ref byte messageBody);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void AddSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void RemoveSubscriberEngine(int localEngineId, int subscriberGalaxyId, int subscriberPlatformId, int subscriberEngineId);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
new void SetHeartbeatSendInterval(int ticksPerBeat, int maxMissedTicks);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void RegisterEngine2(int engineId, [MarshalAs(UnmanagedType.BStr)] string engineName, int version, [MarshalAs(UnmanagedType.Interface)] INmxSvcCallback callback);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetPartnerVersion(int galaxyId, int platformId, int engineId, out int version);
}
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("B49F92F7-C748-4169-8ECA-A0670B012746")]
public interface INmxSvcCallback
{
[PreserveSig]
void DataReceivedRaw(int bufferSize, ref sbyte dataBuffer);
[PreserveSig]
void StatusReceivedRaw(int bufferSize, ref sbyte statusBuffer);
}
+115
View File
@@ -0,0 +1,115 @@
namespace MxNativeClient;
public static class NmxProcedureMetadata
{
public static readonly Guid INmxService2 = new("2630A513-A974-4B1A-8025-457A9A7C56B8");
public static readonly Guid INmxSvcCallback = new("B49F92F7-C748-4169-8ECA-A0670B012746");
public static readonly NdrProcedureDescriptor RegisterEngine = new(
InterfaceId: INmxService2,
Name: nameof(RegisterEngine),
Opnum: 3,
X86StackSize: 20,
ClientBufferSize: 8,
ServerBufferSize: 8,
ParameterCountIncludingReturn: 4);
public static readonly NdrProcedureDescriptor UnRegisterEngine = new(
InterfaceId: INmxService2,
Name: nameof(UnRegisterEngine),
Opnum: 4,
X86StackSize: 12,
ClientBufferSize: 8,
ServerBufferSize: 8,
ParameterCountIncludingReturn: 2);
public static readonly NdrProcedureDescriptor Connect = new(
InterfaceId: INmxService2,
Name: nameof(Connect),
Opnum: 5,
X86StackSize: 24,
ClientBufferSize: 32,
ServerBufferSize: 8,
ParameterCountIncludingReturn: 5);
public static readonly NdrProcedureDescriptor TransferData = new(
InterfaceId: INmxService2,
Name: nameof(TransferData),
Opnum: 6,
X86StackSize: 28,
ClientBufferSize: 32,
ServerBufferSize: 8,
ParameterCountIncludingReturn: 6);
public static readonly NdrProcedureDescriptor AddSubscriberEngine = new(
InterfaceId: INmxService2,
Name: nameof(AddSubscriberEngine),
Opnum: 7,
X86StackSize: 24,
ClientBufferSize: 32,
ServerBufferSize: 8,
ParameterCountIncludingReturn: 5);
public static readonly NdrProcedureDescriptor RemoveSubscriberEngine = new(
InterfaceId: INmxService2,
Name: nameof(RemoveSubscriberEngine),
Opnum: 8,
X86StackSize: 24,
ClientBufferSize: 32,
ServerBufferSize: 8,
ParameterCountIncludingReturn: 5);
public static readonly NdrProcedureDescriptor SetHeartbeatSendInterval = new(
InterfaceId: INmxService2,
Name: nameof(SetHeartbeatSendInterval),
Opnum: 9,
X86StackSize: 16,
ClientBufferSize: 16,
ServerBufferSize: 8,
ParameterCountIncludingReturn: 3);
public static readonly NdrProcedureDescriptor RegisterEngine2 = new(
InterfaceId: INmxService2,
Name: nameof(RegisterEngine2),
Opnum: 10,
X86StackSize: 24,
ClientBufferSize: 16,
ServerBufferSize: 8,
ParameterCountIncludingReturn: 5);
public static readonly NdrProcedureDescriptor GetPartnerVersion = new(
InterfaceId: INmxService2,
Name: nameof(GetPartnerVersion),
Opnum: 11,
X86StackSize: 24,
ClientBufferSize: 24,
ServerBufferSize: 36,
ParameterCountIncludingReturn: 5);
public static readonly NdrProcedureDescriptor DataReceived = new(
InterfaceId: INmxSvcCallback,
Name: nameof(DataReceived),
Opnum: 3,
X86StackSize: 16,
ClientBufferSize: 8,
ServerBufferSize: 8,
ParameterCountIncludingReturn: 3);
public static readonly NdrProcedureDescriptor StatusReceived = new(
InterfaceId: INmxSvcCallback,
Name: nameof(StatusReceived),
Opnum: 4,
X86StackSize: 16,
ClientBufferSize: 8,
ServerBufferSize: 8,
ParameterCountIncludingReturn: 3);
}
public sealed record NdrProcedureDescriptor(
Guid InterfaceId,
string Name,
int Opnum,
int X86StackSize,
int ClientBufferSize,
int ServerBufferSize,
int ParameterCountIncludingReturn);
+216
View File
@@ -0,0 +1,216 @@
using System.Buffers.Binary;
using System.Text;
namespace MxNativeClient;
public sealed record NmxGetPartnerVersionResult(OrpcThat OrpcThat, int PartnerVersion, int HResult);
public sealed record NmxHResultResponse(OrpcThat OrpcThat, int HResult);
public static class NmxService2Messages
{
public static Guid ClassIdNmxService { get; } = new("AE24BD51-2E80-44CC-905B-E5446C942BEB");
public static Guid InterfaceId { get; } = NmxProcedureMetadata.INmxService2;
public const ushort RegisterEngineOpnum = 3;
public const ushort UnregisterEngineOpnum = 4;
public const ushort ConnectOpnum = 5;
public const ushort TransferDataOpnum = 6;
public const ushort AddSubscriberEngineOpnum = 7;
public const ushort RemoveSubscriberEngineOpnum = 8;
public const ushort SetHeartbeatSendIntervalOpnum = 9;
public const ushort RegisterEngine2Opnum = 10;
public const ushort GetPartnerVersionOpnum = 11;
public static byte[] EncodeGetPartnerVersionRequest(
OrpcThis orpcThis,
int galaxyId,
int platformId,
int engineId)
{
byte[] buffer = new byte[OrpcThis.EncodedLengthWithoutExtensions + 12];
orpcThis.Encode().CopyTo(buffer.AsSpan());
WriteInt32(buffer.AsSpan(32, 4), galaxyId);
WriteInt32(buffer.AsSpan(36, 4), platformId);
WriteInt32(buffer.AsSpan(40, 4), engineId);
return buffer;
}
public static NmxGetPartnerVersionResult ParseGetPartnerVersionResponse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < OrpcThat.EncodedLengthWithoutExtensions + 8)
{
throw new ArgumentException("GetPartnerVersion response is too short.", nameof(buffer));
}
return new NmxGetPartnerVersionResult(
OrpcThat.Parse(buffer),
ReadInt32(buffer[8..12]),
ReadInt32(buffer[12..16]));
}
public static byte[] EncodeConnectRequest(
OrpcThis orpcThis,
int localEngineId,
int remoteGalaxyId,
int remotePlatformId,
int remoteEngineId)
{
byte[] buffer = new byte[OrpcThis.EncodedLengthWithoutExtensions + 16];
orpcThis.Encode().CopyTo(buffer.AsSpan());
WriteInt32(buffer.AsSpan(32, 4), localEngineId);
WriteInt32(buffer.AsSpan(36, 4), remoteGalaxyId);
WriteInt32(buffer.AsSpan(40, 4), remotePlatformId);
WriteInt32(buffer.AsSpan(44, 4), remoteEngineId);
return buffer;
}
public static byte[] EncodeSubscriberEngineRequest(
OrpcThis orpcThis,
int localEngineId,
int subscriberGalaxyId,
int subscriberPlatformId,
int subscriberEngineId)
{
byte[] buffer = new byte[OrpcThis.EncodedLengthWithoutExtensions + 16];
orpcThis.Encode().CopyTo(buffer.AsSpan());
WriteInt32(buffer.AsSpan(32, 4), localEngineId);
WriteInt32(buffer.AsSpan(36, 4), subscriberGalaxyId);
WriteInt32(buffer.AsSpan(40, 4), subscriberPlatformId);
WriteInt32(buffer.AsSpan(44, 4), subscriberEngineId);
return buffer;
}
public static byte[] EncodeUnregisterEngineRequest(
OrpcThis orpcThis,
int localEngineId)
{
byte[] buffer = new byte[OrpcThis.EncodedLengthWithoutExtensions + 4];
orpcThis.Encode().CopyTo(buffer.AsSpan());
WriteInt32(buffer.AsSpan(32, 4), localEngineId);
return buffer;
}
public static byte[] EncodeSetHeartbeatSendIntervalRequest(
OrpcThis orpcThis,
int ticksPerBeat,
int maxMissedTicks)
{
byte[] buffer = new byte[OrpcThis.EncodedLengthWithoutExtensions + 8];
orpcThis.Encode().CopyTo(buffer.AsSpan());
WriteInt32(buffer.AsSpan(32, 4), ticksPerBeat);
WriteInt32(buffer.AsSpan(36, 4), maxMissedTicks);
return buffer;
}
public static byte[] EncodeTransferDataRequest(
OrpcThis orpcThis,
int remoteGalaxyId,
int remotePlatformId,
int remoteEngineId,
ReadOnlySpan<byte> messageBody)
{
int bodyOffset = OrpcThis.EncodedLengthWithoutExtensions + 20;
int paddedLength = Align(bodyOffset + messageBody.Length, 4);
byte[] buffer = new byte[paddedLength];
orpcThis.Encode().CopyTo(buffer.AsSpan());
WriteInt32(buffer.AsSpan(32, 4), remoteGalaxyId);
WriteInt32(buffer.AsSpan(36, 4), remotePlatformId);
WriteInt32(buffer.AsSpan(40, 4), remoteEngineId);
WriteInt32(buffer.AsSpan(44, 4), messageBody.Length);
WriteInt32(buffer.AsSpan(48, 4), messageBody.Length);
messageBody.CopyTo(buffer.AsSpan(bodyOffset));
return buffer;
}
public static byte[] EncodeRegisterEngine2Request(
OrpcThis orpcThis,
int localEngineId,
string engineName,
int version,
byte[]? callbackObjRef = null)
{
byte[] bstr = EncodeBstrUserMarshal(engineName);
byte[] callback = callbackObjRef is null
? EncodeNullInterfacePointer()
: EncodeInterfacePointer(callbackObjRef);
int bstrOffset = OrpcThis.EncodedLengthWithoutExtensions + 8;
int versionOffset = Align(bstrOffset + bstr.Length, 4);
int length = Align(versionOffset + 4 + callback.Length, 4);
byte[] buffer = new byte[length];
orpcThis.Encode().CopyTo(buffer.AsSpan());
int offset = OrpcThis.EncodedLengthWithoutExtensions;
WriteInt32(buffer.AsSpan(offset, 4), localEngineId);
offset += 4;
WriteInt32(buffer.AsSpan(offset, 4), 0x72657355);
offset += 4;
bstr.CopyTo(buffer.AsSpan(offset));
offset = versionOffset;
WriteInt32(buffer.AsSpan(offset, 4), version);
offset += 4;
callback.CopyTo(buffer.AsSpan(offset));
return buffer;
}
public static byte[] EncodeBstrUserMarshal(string value)
{
byte[] utf16 = Encoding.Unicode.GetBytes(value);
if ((utf16.Length % 2) != 0)
{
throw new ArgumentException("BSTR payload must be UTF-16.", nameof(value));
}
int charCount = utf16.Length / 2;
byte[] buffer = new byte[12 + utf16.Length];
WriteInt32(buffer.AsSpan(0, 4), charCount);
WriteInt32(buffer.AsSpan(4, 4), utf16.Length);
WriteInt32(buffer.AsSpan(8, 4), charCount);
utf16.CopyTo(buffer.AsSpan(12));
return buffer;
}
public static NmxHResultResponse ParseHResultResponse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < OrpcThat.EncodedLengthWithoutExtensions + 4)
{
throw new ArgumentException("HRESULT response is too short.", nameof(buffer));
}
return new NmxHResultResponse(
OrpcThat.Parse(buffer),
ReadInt32(buffer[8..12]));
}
private static int ReadInt32(ReadOnlySpan<byte> buffer)
{
return BinaryPrimitives.ReadInt32LittleEndian(buffer);
}
private static void WriteInt32(Span<byte> buffer, int value)
{
BinaryPrimitives.WriteInt32LittleEndian(buffer, value);
}
private static int Align(int value, int alignment)
{
int remainder = value % alignment;
return remainder == 0 ? value : value + alignment - remainder;
}
private static byte[] EncodeNullInterfacePointer()
{
return [0, 0, 0, 0];
}
private static byte[] EncodeInterfacePointer(byte[] objRef)
{
int length = Align(12 + objRef.Length, 4);
byte[] buffer = new byte[length];
WriteInt32(buffer.AsSpan(0, 4), 0x00020000);
WriteInt32(buffer.AsSpan(4, 4), objRef.Length);
WriteInt32(buffer.AsSpan(8, 4), objRef.Length);
objRef.CopyTo(buffer.AsSpan(12));
return buffer;
}
}
+112
View File
@@ -0,0 +1,112 @@
using System.Runtime.InteropServices;
using MxNativeCodec;
namespace MxNativeClient;
public sealed class NmxServiceClient : IDisposable
{
public const int ObservedNmxVersion = 30000;
private readonly INmxService2 _service;
private readonly object _comObject;
private int? _registeredEngineId;
private bool _disposed;
private NmxServiceClient(INmxService2 service)
{
_service = service;
_comObject = service;
}
public static NmxServiceClient Create()
{
var service = (INmxService2)new NmxServiceClass();
return new NmxServiceClient(service);
}
public void RegisterEngine(int engineId, string engineName, NmxCallbackSink callback, int version = ObservedNmxVersion)
{
ObjectDisposedException.ThrowIf(_disposed, this);
_service.RegisterEngine2(engineId, engineName, version, callback);
_registeredEngineId = engineId;
}
public void UnregisterEngine()
{
ObjectDisposedException.ThrowIf(_disposed, this);
if (_registeredEngineId is not { } engineId)
{
return;
}
_service.UnRegisterEngine(engineId);
_registeredEngineId = null;
}
public int GetPartnerVersion(int galaxyId, int platformId, int engineId)
{
ObjectDisposedException.ThrowIf(_disposed, this);
_service.GetPartnerVersion(galaxyId, platformId, engineId, out var version);
return version;
}
public void Connect(int localEngineId, int remoteGalaxyId, int remotePlatformId, int remoteEngineId)
{
ObjectDisposedException.ThrowIf(_disposed, this);
_service.Connect(localEngineId, remoteGalaxyId, remotePlatformId, remoteEngineId);
}
public void SetHeartbeatSendInterval(int ticksPerBeat, int maxMissedTicks)
{
ObjectDisposedException.ThrowIf(_disposed, this);
_service.SetHeartbeatSendInterval(ticksPerBeat, maxMissedTicks);
}
public void TransferData(int remoteGalaxyId, int remotePlatformId, int remoteEngineId, ReadOnlySpan<byte> messageBody)
{
ObjectDisposedException.ThrowIf(_disposed, this);
ValidateTransferDataBody(messageBody, nameof(messageBody));
var copy = messageBody.ToArray();
_service.TransferData(remoteGalaxyId, remotePlatformId, remoteEngineId, copy.Length, ref copy[0]);
}
public void Dispose()
{
if (_disposed)
{
return;
}
try
{
if (_registeredEngineId is not null)
{
UnregisterEngine();
}
}
finally
{
if (Marshal.IsComObject(_comObject))
{
Marshal.ReleaseComObject(_comObject);
}
_disposed = true;
}
}
private static void ValidateTransferDataBody(ReadOnlySpan<byte> messageBody, string parameterName)
{
if (messageBody.IsEmpty)
{
throw new ArgumentException("TransferData body cannot be empty.", parameterName);
}
NmxTransferEnvelopeTemplate.FromObserved(messageBody);
if (messageBody.Length == NmxTransferEnvelopeTemplate.HeaderLength)
{
throw new ArgumentException("TransferData body must include an inner message after the 46-byte envelope.", parameterName);
}
}
}
@@ -0,0 +1,45 @@
using System.Buffers.Binary;
namespace MxNativeClient;
public sealed record NmxCallbackRequest(OrpcThis OrpcThis, byte[] Body);
public static class NmxSvcCallbackMessages
{
public static Guid InterfaceId { get; } = NmxProcedureMetadata.INmxSvcCallback;
public const ushort DataReceivedOpnum = 3;
public const ushort StatusReceivedOpnum = 4;
public static NmxCallbackRequest ParseCallbackRequest(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < OrpcThis.EncodedLengthWithoutExtensions + 8)
{
throw new ArgumentException("Callback request is too short.", nameof(buffer));
}
var orpcThis = OrpcThis.Parse(buffer);
int size = BinaryPrimitives.ReadInt32LittleEndian(buffer.Slice(OrpcThis.EncodedLengthWithoutExtensions, 4));
int maxCount = BinaryPrimitives.ReadInt32LittleEndian(buffer.Slice(OrpcThis.EncodedLengthWithoutExtensions + 4, 4));
if (size < 0 || maxCount < size)
{
throw new ArgumentException("Callback request has invalid array size metadata.", nameof(buffer));
}
int bodyOffset = OrpcThis.EncodedLengthWithoutExtensions + 8;
if (bodyOffset + size > buffer.Length)
{
throw new ArgumentException("Callback request byte array is truncated.", nameof(buffer));
}
return new NmxCallbackRequest(orpcThis, buffer.Slice(bodyOffset, size).ToArray());
}
public static byte[] EncodeCallbackResponse(int hresult)
{
byte[] buffer = new byte[OrpcThat.EncodedLengthWithoutExtensions + sizeof(int)];
new OrpcThat(0, 0).Encode().CopyTo(buffer.AsSpan());
BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(OrpcThat.EncodedLengthWithoutExtensions, sizeof(int)), hresult);
return buffer;
}
}
@@ -0,0 +1,82 @@
namespace MxNativeClient;
public sealed class ObjectExporterClient
{
private readonly string _host;
private readonly int _port;
public ObjectExporterClient(string host = "127.0.0.1", int port = 135)
{
_host = host;
_port = port;
}
public ResolveOxidFailure ResolveOxidUnauthenticated(ulong oxid, IReadOnlyList<ushort>? requestedProtseqs = null)
{
using var client = new DceRpcTcpClient(_host, _port);
client.Connect();
var bind = client.Bind(ObjectExporterMessages.IObjectExporter, versionMajor: 0, versionMinor: 0);
if (bind.PacketType != DceRpcPacketType.BindAck)
{
throw new InvalidOperationException($"Unexpected bind response packet type {bind.PacketType}.");
}
byte[] request = ObjectExporterMessages.EncodeResolveOxidRequest(
oxid,
requestedProtseqs ?? [ObjectExporterMessages.ProtseqNcacnIpTcp]);
var response = client.Call(contextId: 0, ObjectExporterMessages.ResolveOxidOpnum, request);
return ObjectExporterMessages.ParseResolveOxidFailure(response.StubData.Span);
}
public DceRpcResponsePdu ResolveOxidWithNtlmConnect(ulong oxid, IReadOnlyList<ushort>? requestedProtseqs = null)
{
using var client = new DceRpcTcpClient(_host, _port);
client.Connect();
var bind = client.BindWithNtlmConnect(ObjectExporterMessages.IObjectExporter, versionMajor: 0, versionMinor: 0);
if (bind.PacketType != DceRpcPacketType.BindAck)
{
throw new InvalidOperationException($"Unexpected bind response packet type {bind.PacketType}.");
}
byte[] request = ObjectExporterMessages.EncodeResolveOxidRequest(
oxid,
requestedProtseqs ?? [ObjectExporterMessages.ProtseqNcacnIpTcp]);
return client.CallBound(ObjectExporterMessages.ResolveOxidOpnum, request);
}
public DceRpcResponsePdu ResolveOxidWithNtlmPacketIntegrity(ulong oxid, IReadOnlyList<ushort>? requestedProtseqs = null)
{
using var client = new DceRpcTcpClient(_host, _port);
client.Connect();
var bind = client.BindWithNtlmPacketIntegrity(ObjectExporterMessages.IObjectExporter, versionMajor: 0, versionMinor: 0, targetName: _host);
if (bind.PacketType != DceRpcPacketType.BindAck)
{
throw new InvalidOperationException($"Unexpected bind response packet type {bind.PacketType}.");
}
byte[] request = ObjectExporterMessages.EncodeResolveOxidRequest(
oxid,
requestedProtseqs ?? [ObjectExporterMessages.ProtseqNcacnIpTcp]);
return client.CallBound(ObjectExporterMessages.ResolveOxidOpnum, request);
}
public DceRpcResponsePdu ResolveOxidWithManagedNtlmPacketIntegrity(ulong oxid, IReadOnlyList<ushort>? requestedProtseqs = null)
{
using var client = new DceRpcTcpClient(_host, _port);
client.Connect();
var bind = client.BindWithManagedNtlmPacketIntegrity(ObjectExporterMessages.IObjectExporter, versionMajor: 0, versionMinor: 0);
if (bind.PacketType != DceRpcPacketType.BindAck)
{
throw new InvalidOperationException($"Unexpected bind response packet type {bind.PacketType}.");
}
byte[] request = ObjectExporterMessages.EncodeResolveOxidRequest(
oxid,
requestedProtseqs ?? [ObjectExporterMessages.ProtseqNcacnIpTcp]);
return client.CallBound(ObjectExporterMessages.ResolveOxidOpnum, request);
}
}
@@ -0,0 +1,141 @@
using System.Buffers.Binary;
namespace MxNativeClient;
public static class ObjectExporterMessages
{
public static readonly Guid IObjectExporter = new("99FCFEC4-5260-101B-BBCB-00AA0021347A");
public const ushort ResolveOxidOpnum = 0;
public const ushort SimplePingOpnum = 1;
public const ushort ComplexPingOpnum = 2;
public const ushort ServerAliveOpnum = 3;
public const ushort ResolveOxid2Opnum = 4;
public const ushort ServerAlive2Opnum = 5;
public const ushort ProtseqNcacnIpTcp = 0x0007;
public const ushort ProtseqNcalRpc = 0x001f;
public static byte[] EncodeResolveOxidRequest(ulong oxid, IReadOnlyList<ushort> requestedProtseqs)
{
if (requestedProtseqs.Count == 0)
{
throw new ArgumentException("At least one protocol sequence is required.", nameof(requestedProtseqs));
}
int length = 8 + 2 + 2 + 4 + requestedProtseqs.Count * sizeof(ushort);
length = Align(length, 4);
byte[] buffer = new byte[length];
BinaryPrimitives.WriteUInt64LittleEndian(buffer.AsSpan(0, 8), oxid);
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(8, 2), (ushort)requestedProtseqs.Count);
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(12, 4), (uint)requestedProtseqs.Count);
for (int i = 0; i < requestedProtseqs.Count; i++)
{
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(16 + i * sizeof(ushort), sizeof(ushort)), requestedProtseqs[i]);
}
return buffer;
}
public static ResolveOxidFailure ParseResolveOxidFailure(ReadOnlySpan<byte> responseStub)
{
if (responseStub.Length < 4)
{
throw new ArgumentException("ResolveOxid response stub is too short.", nameof(responseStub));
}
return new ResolveOxidFailure(BinaryPrimitives.ReadUInt32LittleEndian(responseStub[^4..]));
}
public static ResolveOxidResult ParseResolveOxidResult(ReadOnlySpan<byte> responseStub)
{
if (responseStub.Length < 32)
{
throw new ArgumentException("ResolveOxid response stub is too short.", nameof(responseStub));
}
uint referentId = BinaryPrimitives.ReadUInt32LittleEndian(responseStub[0..4]);
if (referentId == 0)
{
uint nullStatus = BinaryPrimitives.ReadUInt32LittleEndian(responseStub[^4..]);
return new ResolveOxidResult([], Guid.Empty, 0, nullStatus);
}
uint maxCount = BinaryPrimitives.ReadUInt32LittleEndian(responseStub[4..8]);
ushort entries = BinaryPrimitives.ReadUInt16LittleEndian(responseStub[8..10]);
ushort securityOffset = BinaryPrimitives.ReadUInt16LittleEndian(responseStub[10..12]);
if (maxCount < entries)
{
throw new ArgumentException("ResolveOxid DUALSTRINGARRAY max count is smaller than entry count.", nameof(responseStub));
}
int arrayOffset = 12;
int arrayBytes = checked((int)maxCount * sizeof(ushort));
if (arrayOffset + arrayBytes > responseStub.Length)
{
throw new ArgumentException("ResolveOxid DUALSTRINGARRAY is truncated.", nameof(responseStub));
}
var decoded = DecodeDualStringArray(responseStub.Slice(arrayOffset, entries * sizeof(ushort)), entries, securityOffset);
int offset = Align(arrayOffset + arrayBytes, 4);
if (offset + 24 > responseStub.Length)
{
throw new ArgumentException("ResolveOxid trailing fields are truncated.", nameof(responseStub));
}
return new ResolveOxidResult(
decoded,
new Guid(responseStub.Slice(offset, 16)),
BinaryPrimitives.ReadUInt32LittleEndian(responseStub.Slice(offset + 16, 4)),
BinaryPrimitives.ReadUInt32LittleEndian(responseStub.Slice(offset + 20, 4)));
}
private static IReadOnlyList<ComDualStringEntry> DecodeDualStringArray(ReadOnlySpan<byte> data, ushort entries, ushort securityOffset)
{
var strings = new List<ComDualStringEntry>();
for (int i = 0; i < entries;)
{
int entryStart = i;
ushort towerId = BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(i * 2, 2));
i++;
if (towerId == 0)
{
continue;
}
var text = new List<char>();
while (i < entries)
{
ushort value = BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(i * 2, 2));
i++;
if (value == 0)
{
break;
}
text.Add(value >= 0x20 && value <= 0x7e ? (char)value : '?');
}
strings.Add(new ComDualStringEntry(
towerId,
towerId == ProtseqNcacnIpTcp ? "ncacn_ip_tcp" : $"protseq_0x{towerId:x4}",
new string(text.ToArray()),
IsSecurityBinding: entryStart >= securityOffset));
}
return strings;
}
private static int Align(int value, int alignment)
{
int remainder = value % alignment;
return remainder == 0 ? value : value + alignment - remainder;
}
}
public sealed record ResolveOxidFailure(uint ErrorStatus);
public sealed record ResolveOxidResult(
IReadOnlyList<ComDualStringEntry> Bindings,
Guid RemUnknownIpid,
uint AuthnHint,
uint ErrorStatus);
+140
View File
@@ -0,0 +1,140 @@
using System.Buffers.Binary;
namespace MxNativeClient;
public readonly record struct ComVersion(ushort Major, ushort Minor)
{
public static ComVersion Version57 { get; } = new(5, 7);
}
public sealed record OrpcThis(
ComVersion Version,
uint Flags,
uint Reserved1,
Guid Cid,
uint ExtensionsReferentId)
{
public const int EncodedLengthWithoutExtensions = 32;
public static OrpcThis Create(Guid cid, ComVersion? version = null)
{
return new OrpcThis(version ?? ComVersion.Version57, 0, 0, cid, 0);
}
public static OrpcThis Parse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < EncodedLengthWithoutExtensions)
{
throw new ArgumentException("ORPCTHIS buffer is too short.", nameof(buffer));
}
return new OrpcThis(
Version: new ComVersion(
BinaryPrimitives.ReadUInt16LittleEndian(buffer[0..2]),
BinaryPrimitives.ReadUInt16LittleEndian(buffer[2..4])),
Flags: BinaryPrimitives.ReadUInt32LittleEndian(buffer[4..8]),
Reserved1: BinaryPrimitives.ReadUInt32LittleEndian(buffer[8..12]),
Cid: new Guid(buffer.Slice(12, 16)),
ExtensionsReferentId: BinaryPrimitives.ReadUInt32LittleEndian(buffer[28..32]));
}
public byte[] Encode()
{
byte[] buffer = new byte[EncodedLengthWithoutExtensions];
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(0, 2), Version.Major);
BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(2, 2), Version.Minor);
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(4, 4), Flags);
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(8, 4), Reserved1);
Cid.TryWriteBytes(buffer.AsSpan(12, 16));
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(28, 4), ExtensionsReferentId);
return buffer;
}
}
public sealed record OrpcThat(uint Flags, uint ExtensionsReferentId)
{
public const int EncodedLengthWithoutExtensions = 8;
public static OrpcThat Parse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < EncodedLengthWithoutExtensions)
{
throw new ArgumentException("ORPCTHAT buffer is too short.", nameof(buffer));
}
return new OrpcThat(
Flags: BinaryPrimitives.ReadUInt32LittleEndian(buffer[0..4]),
ExtensionsReferentId: BinaryPrimitives.ReadUInt32LittleEndian(buffer[4..8]));
}
public byte[] Encode()
{
byte[] buffer = new byte[EncodedLengthWithoutExtensions];
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(0, 4), Flags);
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(4, 4), ExtensionsReferentId);
return buffer;
}
}
public sealed record MInterfacePointer(byte[] ObjRefBytes)
{
public byte[] Encode()
{
byte[] buffer = new byte[sizeof(uint) + ObjRefBytes.Length];
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(0, sizeof(uint)), (uint)ObjRefBytes.Length);
ObjRefBytes.CopyTo(buffer.AsSpan(sizeof(uint)));
return buffer;
}
public static MInterfacePointer Parse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < sizeof(uint))
{
throw new ArgumentException("MInterfacePointer buffer is too short.", nameof(buffer));
}
uint size = BinaryPrimitives.ReadUInt32LittleEndian(buffer[..sizeof(uint)]);
if (size > buffer.Length - sizeof(uint))
{
throw new ArgumentException("MInterfacePointer OBJREF payload is truncated.", nameof(buffer));
}
return new MInterfacePointer(buffer.Slice(sizeof(uint), (int)size).ToArray());
}
public ComObjRef ParseObjRef()
{
return ComObjRef.Parse(ObjRefBytes);
}
}
public sealed record StdObjRef(uint Flags, uint PublicRefs, ulong Oxid, ulong Oid, Guid Ipid)
{
public const int EncodedLength = 40;
public static StdObjRef Parse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < EncodedLength)
{
throw new ArgumentException("STDOBJREF buffer is too short.", nameof(buffer));
}
return new StdObjRef(
Flags: BinaryPrimitives.ReadUInt32LittleEndian(buffer[0..4]),
PublicRefs: BinaryPrimitives.ReadUInt32LittleEndian(buffer[4..8]),
Oxid: BinaryPrimitives.ReadUInt64LittleEndian(buffer[8..16]),
Oid: BinaryPrimitives.ReadUInt64LittleEndian(buffer[16..24]),
Ipid: new Guid(buffer.Slice(24, 16)));
}
public byte[] Encode()
{
byte[] buffer = new byte[EncodedLength];
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(0, 4), Flags);
BinaryPrimitives.WriteUInt32LittleEndian(buffer.AsSpan(4, 4), PublicRefs);
BinaryPrimitives.WriteUInt64LittleEndian(buffer.AsSpan(8, 8), Oxid);
BinaryPrimitives.WriteUInt64LittleEndian(buffer.AsSpan(16, 8), Oid);
Ipid.TryWriteBytes(buffer.AsSpan(24, 16));
return buffer;
}
}
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("MxNativeClient.Tests")]
+79
View File
@@ -0,0 +1,79 @@
using System.Buffers.Binary;
namespace MxNativeClient;
public static class RemUnknownMessages
{
public static readonly Guid IRemUnknown = new("00000131-0000-0000-C000-000000000046");
public const ushort RemQueryInterfaceOpnum = 3;
public const ushort RemAddRefOpnum = 4;
public const ushort RemReleaseOpnum = 5;
public static byte[] EncodeRemQueryInterfaceRequest(Guid sourceIpid, Guid requestedIid, Guid causalityId, uint publicRefs = 5)
{
var orpcThis = OrpcThis.Create(causalityId).Encode();
byte[] body = new byte[orpcThis.Length + 16 + 4 + 4 + 4 + 16];
int offset = 0;
orpcThis.CopyTo(body.AsSpan(offset));
offset += orpcThis.Length;
sourceIpid.TryWriteBytes(body.AsSpan(offset, 16));
offset += 16;
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(offset, 4), publicRefs);
offset += 4;
BinaryPrimitives.WriteUInt16LittleEndian(body.AsSpan(offset, 2), 1);
offset += 2;
body[offset++] = 0xce;
body[offset++] = 0xce; // NDR alignment before the conformant IID array max count.
BinaryPrimitives.WriteUInt32LittleEndian(body.AsSpan(offset, 4), 1);
offset += 4;
requestedIid.TryWriteBytes(body.AsSpan(offset, 16));
return body;
}
public static RemQueryInterfaceResponse ParseRemQueryInterfaceResponse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < OrpcThat.EncodedLengthWithoutExtensions + 4 + RemQiResult.EncodedLength + 4)
{
throw new ArgumentException("RemQueryInterface response is too short.", nameof(buffer));
}
var orpcThat = OrpcThat.Parse(buffer);
uint referentId = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(OrpcThat.EncodedLengthWithoutExtensions, 4));
int offset = OrpcThat.EncodedLengthWithoutExtensions + 4;
RemQiResult? result = null;
if (referentId != 0)
{
offset += 4; // Conformant array max count for the REMQIRESULT result array.
result = RemQiResult.Parse(buffer[offset..]);
}
if (result is not null)
{
offset += RemQiResult.EncodedLength;
}
uint errorCode = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(offset, 4));
return new RemQueryInterfaceResponse(orpcThat, result, errorCode);
}
}
public sealed record RemQueryInterfaceResponse(OrpcThat OrpcThat, RemQiResult? Result, uint ErrorCode);
public sealed record RemQiResult(int HResult, StdObjRef StandardObjectReference)
{
public const int EncodedLength = sizeof(int) + sizeof(int) + StdObjRef.EncodedLength;
public static RemQiResult Parse(ReadOnlySpan<byte> buffer)
{
if (buffer.Length < EncodedLength)
{
throw new ArgumentException("REMQIRESULT buffer is too short.", nameof(buffer));
}
return new RemQiResult(
HResult: BinaryPrimitives.ReadInt32LittleEndian(buffer[..4]),
StandardObjectReference: StdObjRef.Parse(buffer[8..]));
}
}