using System.Reflection; namespace ZB.MOM.WW.OtOpcUa.Driver.S7; /// /// PR-S7-E2 / #303 — narrow seam covering the "send a password to a hardened CPU" /// wire path. calls /// after OpenAsync succeeds and before the /// pre-flight PUT/GET probe runs, so a hardened S7-1500 / ET 200SP CPU that gates /// reads behind a connection-level password unlocks before the probe drops it. /// /// /// /// The runtime implementation () discovers /// the underlying S7.Net.Plc.SendPassword / SendPasswordAsync /// methods reflectively because S7netplus 0.20 doesn't yet expose them in a /// strongly-typed surface — the seam keeps this driver compiling against the /// current pinned package version while still calling whatever the next minor /// release ships. When neither method exists, /// stays false and /// is a no-op so a misconfigured "Password /// set, library doesn't oblige" deployment surfaces as a one-line warning at /// Init rather than a hard failure (failure shifts to first per-tag read on the /// hardened CPU, which is the same shape as if the operator had forgotten to /// enable PUT/GET). /// /// /// Tests inject a fake to exercise both branches without touching the live /// S7netplus stack. /// /// internal interface IS7PlcAuthGate { /// /// true when the underlying S7netplus Plc exposes a public /// SendPassword(string) or SendPasswordAsync(string, CancellationToken) /// method. false on S7netplus 0.20 (which has no such surface). /// bool SupportsSendPassword { get; } /// /// Send to the connected PLC. No-op (and returns /// false) when is false; /// returns true after a successful send. Throws cleanly when the wire /// reports auth-failed — wraps the /// throw into a typed so the operator /// sees a "password authentication failed" message rather than a generic /// S7.Net.PlcException. /// Task TrySendPasswordAsync(string password, CancellationToken cancellationToken); } /// /// Production backed by reflection over the S7netplus /// S7.Net.Plc instance. S7netplus 0.20 does NOT expose a /// SendPassword; the reflection probe survives that gracefully and a future /// 0.21+ that adds the API gets called automatically without a code change here. /// internal sealed class ReflectionS7PlcAuthGate : IS7PlcAuthGate { private readonly object _plc; private readonly MethodInfo? _syncMethod; private readonly MethodInfo? _asyncMethod; public ReflectionS7PlcAuthGate(object plc) { _plc = plc ?? throw new ArgumentNullException(nameof(plc)); var type = plc.GetType(); // Probe both shapes: synchronous void SendPassword(string) and async // Task SendPasswordAsync(string, CancellationToken). Either is acceptable; // the async overload wins when both exist (no thread-block on init). _asyncMethod = type.GetMethod( "SendPasswordAsync", BindingFlags.Instance | BindingFlags.Public, binder: null, types: [typeof(string), typeof(CancellationToken)], modifiers: null); _syncMethod = type.GetMethod( "SendPassword", BindingFlags.Instance | BindingFlags.Public, binder: null, types: [typeof(string)], modifiers: null); } public bool SupportsSendPassword => _asyncMethod is not null || _syncMethod is not null; public async Task TrySendPasswordAsync(string password, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(password); if (_asyncMethod is not null) { // Unwrap TargetInvocationException so the caller sees the real S7.Net.PlcException // (or whatever the library threw) rather than the reflection wrapper. try { var result = _asyncMethod.Invoke(_plc, [password, cancellationToken]); if (result is Task task) { await task.ConfigureAwait(false); } return true; } catch (TargetInvocationException tie) when (tie.InnerException is not null) { throw tie.InnerException; } } if (_syncMethod is not null) { try { _syncMethod.Invoke(_plc, [password]); return true; } catch (TargetInvocationException tie) when (tie.InnerException is not null) { throw tie.InnerException; } } return false; } }