using System.IO.Pipes; using System.Security.Cryptography; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Scripting.OutOfProcess; using ZB.MOM.WW.OtOpcUa.Core.Scripting.Protocol; using ZB.MOM.WW.OtOpcUa.Core.Scripting.Protocol.V1; namespace ZB.MOM.WW.OtOpcUa.ScriptWorker.Tests; /// lmxopcua#780 design ยง6 "Pipe hardening": only the launched process, echoing its nonce, is accepted. public sealed class HandshakeTests { private static CancellationToken Ct => TestContext.Current.CancellationToken; private static async Task HandshakeWith(Func reply, string expectedNonce = "right", int? expectedPid = 1234) { var name = "otopcua-sw-test-" + Convert.ToHexString(RandomNumberGenerator.GetBytes(6)); await using var server = new NamedPipeServerStream(name, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly); await using var client = new NamedPipeClientStream(".", name, PipeDirection.InOut, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly); var accept = server.WaitForConnectionAsync(Ct); await client.ConnectAsync(TimeSpan.FromSeconds(5), Ct); await accept; var rogue = Task.Run(async () => { var hello = await ScriptWorkerFrameCodec.ReadAsync(client, ScriptWorkerFrameCodec.DefaultMaxFrameBytes, Ct); await ScriptWorkerFrameCodec.WriteAsync(client, reply(hello!), ScriptWorkerFrameCodec.DefaultMaxFrameBytes, Ct); await client.FlushAsync(Ct); }, Ct); try { using var timeout = CancellationTokenSource.CreateLinkedTokenSource(Ct); timeout.CancelAfter(TimeSpan.FromSeconds(5)); await WorkerConnection.HandshakeAsync(server, new Hello { Nonce = expectedNonce }, expectedNonce, expectedPid, timeout.Token); await rogue; return null; } catch (Exception ex) { return ex; } } private static Envelope Ready(string nonce, int pid) => new() { ProtocolVersion = ScriptWorkerFrameCodec.ProtocolVersion, Ready = new Ready { Nonce = nonce, Pid = pid }, }; [Fact] public async Task The_launched_process_echoing_its_nonce_is_accepted() => (await HandshakeWith(_ => Ready("right", 1234))).ShouldBeNull("positive control"); [Fact] public async Task A_wrong_nonce_is_refused() => (await HandshakeWith(_ => Ready("wrong", 1234))).ShouldBeOfType() .Message.ShouldContain("nonce"); [Fact] public async Task A_client_that_is_not_the_launched_process_is_refused() => (await HandshakeWith(_ => Ready("right", 999))).ShouldBeOfType() .Message.ShouldContain("pid"); [Fact] public async Task Anything_but_ready_first_is_refused() => (await HandshakeWith(_ => new Envelope { ProtocolVersion = ScriptWorkerFrameCodec.ProtocolVersion, Heartbeat = new Heartbeat(), })).ShouldBeOfType(); [Fact] public async Task The_hello_carries_the_nonce_to_echo() { string? seen = null; (await HandshakeWith(hello => { seen = hello.Hello.Nonce; return Ready(hello.Hello.Nonce, 1234); })).ShouldBeNull(); seen.ShouldBe("right"); } }