120 lines
5.1 KiB
C#
120 lines
5.1 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.IO.Pipes;
|
|
using System.Security.Principal;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using MessagePack;
|
|
using Serilog;
|
|
using Serilog.Core;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Ipc;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Contracts;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
|
{
|
|
/// <summary>
|
|
/// Direct IPC handshake test — drives <see cref="PipeServer"/> with a hand-rolled client
|
|
/// built on <see cref="FrameReader"/>/<see cref="FrameWriter"/> from Shared. Stays in
|
|
/// net48 x86 alongside the Host (the Proxy's <c>GalaxyIpcClient</c> is net10 only and
|
|
/// cannot be loaded into this process). Functionally equivalent to going through
|
|
/// <c>GalaxyIpcClient</c> — proves the wire protocol + ACL + shared-secret enforcement.
|
|
/// Skipped on Administrator shells per the same PipeAcl-denies-Administrators guard.
|
|
/// </summary>
|
|
[Trait("Category", "Integration")]
|
|
public sealed class IpcHandshakeIntegrationTests
|
|
{
|
|
private static bool IsAdministrator()
|
|
{
|
|
using var identity = WindowsIdentity.GetCurrent();
|
|
return new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator);
|
|
}
|
|
|
|
private static async Task<(NamedPipeClientStream Stream, FrameReader Reader, FrameWriter Writer)>
|
|
ConnectAndHelloAsync(string pipeName, string secret, CancellationToken ct)
|
|
{
|
|
var stream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
|
|
await stream.ConnectAsync(5_000, ct);
|
|
|
|
var reader = new FrameReader(stream, leaveOpen: true);
|
|
var writer = new FrameWriter(stream, leaveOpen: true);
|
|
await writer.WriteAsync(MessageKind.Hello,
|
|
new Hello { PeerName = "test-client", SharedSecret = secret }, ct);
|
|
|
|
var ack = await reader.ReadFrameAsync(ct);
|
|
if (ack is null) throw new EndOfStreamException("no HelloAck");
|
|
if (ack.Value.Kind != MessageKind.HelloAck) throw new InvalidOperationException("unexpected first frame");
|
|
var ackMsg = MessagePackSerializer.Deserialize<HelloAck>(ack.Value.Body);
|
|
if (!ackMsg.Accepted) throw new UnauthorizedAccessException(ackMsg.RejectReason);
|
|
|
|
return (stream, reader, writer);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handshake_with_correct_secret_succeeds_and_heartbeat_round_trips()
|
|
{
|
|
if (IsAdministrator()) return;
|
|
|
|
using var identity = WindowsIdentity.GetCurrent();
|
|
var sid = identity.User!;
|
|
var pipe = $"OtOpcUaGalaxyTest-{Guid.NewGuid():N}";
|
|
const string secret = "test-secret-2026";
|
|
Logger log = new LoggerConfiguration().CreateLogger();
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
|
|
var server = new PipeServer(pipe, sid, secret, log);
|
|
var serverTask = Task.Run(() => server.RunOneConnectionAsync(
|
|
new GalaxyFrameHandler(new StubGalaxyBackend(), log), cts.Token));
|
|
|
|
var (stream, reader, writer) = await ConnectAndHelloAsync(pipe, secret, cts.Token);
|
|
using (stream)
|
|
using (reader)
|
|
using (writer)
|
|
{
|
|
await writer.WriteAsync(MessageKind.Heartbeat,
|
|
new Heartbeat { SequenceNumber = 42, UtcUnixMs = 1000 }, cts.Token);
|
|
|
|
var hbAckFrame = await reader.ReadFrameAsync(cts.Token);
|
|
hbAckFrame.HasValue.ShouldBeTrue();
|
|
hbAckFrame!.Value.Kind.ShouldBe(MessageKind.HeartbeatAck);
|
|
MessagePackSerializer.Deserialize<HeartbeatAck>(hbAckFrame.Value.Body).SequenceNumber.ShouldBe(42L);
|
|
}
|
|
|
|
cts.Cancel();
|
|
try { await serverTask; } catch { /* shutdown */ }
|
|
server.Dispose();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Handshake_with_wrong_secret_is_rejected()
|
|
{
|
|
if (IsAdministrator()) return;
|
|
|
|
using var identity = WindowsIdentity.GetCurrent();
|
|
var sid = identity.User!;
|
|
var pipe = $"OtOpcUaGalaxyTest-{Guid.NewGuid():N}";
|
|
Logger log = new LoggerConfiguration().CreateLogger();
|
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
|
|
|
var server = new PipeServer(pipe, sid, "real-secret", log);
|
|
var serverTask = Task.Run(() => server.RunOneConnectionAsync(
|
|
new GalaxyFrameHandler(new StubGalaxyBackend(), log), cts.Token));
|
|
|
|
await Should.ThrowAsync<UnauthorizedAccessException>(async () =>
|
|
{
|
|
var (s, r, w) = await ConnectAndHelloAsync(pipe, "wrong-secret", cts.Token);
|
|
s.Dispose();
|
|
r.Dispose();
|
|
w.Dispose();
|
|
});
|
|
|
|
cts.Cancel();
|
|
try { await serverTask; } catch { /* shutdown */ }
|
|
server.Dispose();
|
|
}
|
|
}
|
|
}
|