diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Program.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Program.cs index db77d55d..cd2088b3 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Program.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware/Program.cs @@ -1,5 +1,6 @@ using System; -using System.Security.Principal; +using System.Net; +using System.Security.Cryptography.X509Certificates; using System.Threading; using Serilog; using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Backend; @@ -8,10 +9,11 @@ using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Ipc; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware; /// -/// Entry point for the Wonderware Historian sidecar. Reads pipe name, allowed-SID, -/// shared secret, and historian connection config from environment (the supervisor -/// passes them at spawn time per driver-stability.md). Hosts a named-pipe server -/// dispatching the five sidecar contracts (PR 3.3) to the Wonderware Historian SDK. +/// Entry point for the Wonderware Historian sidecar. Reads the shared secret, TCP +/// bind/port, optional TLS settings, and historian connection config from environment +/// (the supervisor passes them at spawn time per driver-stability.md). Hosts a +/// TCP server (optionally over TLS) dispatching the five sidecar contracts (PR 3.3) to +/// the Wonderware Historian SDK. /// public static class Program { @@ -29,19 +31,19 @@ public static class Program try { - var pipeName = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_PIPE") - ?? throw new InvalidOperationException("OTOPCUA_HISTORIAN_PIPE not set — supervisor must pass the sidecar pipe name"); - var allowedSidValue = Environment.GetEnvironmentVariable("OTOPCUA_ALLOWED_SID") - ?? throw new InvalidOperationException("OTOPCUA_ALLOWED_SID not set — supervisor must pass the server principal SID"); var sharedSecret = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_SECRET") ?? throw new InvalidOperationException("OTOPCUA_HISTORIAN_SECRET not set — supervisor must pass the per-process secret at spawn time"); - var allowedSid = new SecurityIdentifier(allowedSidValue); + var tcpPort = TryParseInt("OTOPCUA_HISTORIAN_TCP_PORT", 32569); + var bindRaw = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_BIND"); + var bind = string.IsNullOrWhiteSpace(bindRaw) ? IPAddress.Any : IPAddress.Parse(bindRaw); + var tlsEnabled = string.Equals(Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_TLS_ENABLED"), "true", StringComparison.OrdinalIgnoreCase); + X509Certificate2? tlsCert = tlsEnabled ? LoadTlsCert() : null; using var cts = new CancellationTokenSource(); Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); }; - // Sidecar can boot in "pipe-only" mode (no real Wonderware Historian SDK + // Sidecar can boot in "tcp idle" mode (no real Wonderware Historian SDK // initialization) for smoke + IPC tests. Production sets ENABLED=true so the // SDK opens its connection up front. var historianEnabled = string.Equals( @@ -50,7 +52,7 @@ public static class Program if (!historianEnabled) { - Log.Information("Wonderware historian sidecar starting in pipe-only mode (OTOPCUA_HISTORIAN_ENABLED!=true) — pipe={Pipe} allowedSid={Sid}", pipeName, allowedSidValue); + Log.Information("Wonderware historian sidecar starting in tcp idle mode (SDK disabled) (OTOPCUA_HISTORIAN_ENABLED!=true) — bind={Bind} port={Port} tls={Tls}", bind, tcpPort, tlsCert is not null); cts.Token.WaitHandle.WaitOne(); Log.Information("Wonderware historian sidecar stopping cleanly"); return 0; @@ -59,9 +61,9 @@ public static class Program using var historian = BuildHistorian(); var alarmWriter = BuildAlarmWriter(); var handler = new HistorianFrameHandler(historian, Log.Logger, alarmWriter); - using var server = new PipeServer(pipeName, allowedSid, sharedSecret, Log.Logger); + using var server = new TcpFrameServer(bind, tcpPort, sharedSecret, tlsCert, Log.Logger); - Log.Information("Wonderware historian sidecar serving — pipe={Pipe} allowedSid={Sid}", pipeName, allowedSidValue); + Log.Information("Wonderware historian sidecar serving — bind={Bind} port={Port} tls={Tls}", bind, tcpPort, tlsCert is not null); try { server.RunAsync(handler, cts.Token).GetAwaiter().GetResult(); } catch (OperationCanceledException) { /* clean shutdown via Ctrl-C */ } @@ -112,6 +114,26 @@ public static class Program return int.TryParse(raw, out var parsed) ? parsed : defaultValue; } + /// + /// Loads the TLS server certificate when TLS is enabled. The reference is either a + /// .pfx file path (decrypted with the optional password env var) or, if not a + /// file, a thumbprint resolved from the LocalMachine\My store. + /// + private static X509Certificate2 LoadTlsCert() + { + var certRef = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_TLS_CERT") + ?? throw new InvalidOperationException("OTOPCUA_HISTORIAN_TLS_CERT not set but TLS enabled — supply a .pfx path or a LocalMachine\\My store thumbprint"); + var pwd = Environment.GetEnvironmentVariable("OTOPCUA_HISTORIAN_TLS_CERT_PASSWORD"); + if (System.IO.File.Exists(certRef)) + return new X509Certificate2(certRef, pwd, X509KeyStorageFlags.MachineKeySet); + // else treat as a thumbprint in LocalMachine\My + using var store = new X509Store(StoreName.My, StoreLocation.LocalMachine); + store.Open(OpenFlags.ReadOnly); + var found = store.Certificates.Find(X509FindType.FindByThumbprint, certRef.Replace(" ", ""), validOnly: false); + if (found.Count == 0) throw new InvalidOperationException($"OTOPCUA_HISTORIAN_TLS_CERT thumbprint '{certRef}' not found in LocalMachine\\My and is not a file path"); + return found[0]; + } + /// /// Constructs the alarm-event writer when the alarm-write toggle is on, otherwise /// returns null so falls back to the diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/ProgramSmokeTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/ProgramSmokeTests.cs index e6647b0f..0ba6059d 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/ProgramSmokeTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests/ProgramSmokeTests.cs @@ -6,8 +6,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Tests; /// /// Smoke test confirming the sidecar project links and the test project resolves a -/// ProjectReference to it. Real behavioural tests arrive in PR 3.2 (backend lift) and -/// PR 3.3 (pipe server). For PR 3.1 we just verify the assembly identity is what the +/// ProjectReference to it. Real behavioural tests live with the TCP frame server +/// (TcpFrameServer); here we just verify the assembly identity is what the /// csproj declares. /// public class ProgramSmokeTests