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");
}
}