Files
lmxopcua/src/ZB.MOM.WW.OtOpcUa.Driver.S7/S7PlcAuthGate.cs
T
2026-04-26 10:51:07 -04:00

125 lines
5.3 KiB
C#

using System.Reflection;
namespace ZB.MOM.WW.OtOpcUa.Driver.S7;
/// <summary>
/// PR-S7-E2 / #303 — narrow seam covering the "send a password to a hardened CPU"
/// wire path. <see cref="S7Driver.InitializeAsync"/> calls
/// <see cref="TrySendPasswordAsync"/> after <c>OpenAsync</c> 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.
/// </summary>
/// <remarks>
/// <para>
/// The runtime implementation (<see cref="ReflectionS7PlcAuthGate"/>) discovers
/// the underlying <c>S7.Net.Plc.SendPassword</c> / <c>SendPasswordAsync</c>
/// 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,
/// <see cref="SupportsSendPassword"/> stays <c>false</c> and
/// <see cref="TrySendPasswordAsync"/> 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).
/// </para>
/// <para>
/// Tests inject a fake to exercise both branches without touching the live
/// S7netplus stack.
/// </para>
/// </remarks>
internal interface IS7PlcAuthGate
{
/// <summary>
/// <c>true</c> when the underlying S7netplus <c>Plc</c> exposes a public
/// <c>SendPassword(string)</c> or <c>SendPasswordAsync(string, CancellationToken)</c>
/// method. <c>false</c> on S7netplus 0.20 (which has no such surface).
/// </summary>
bool SupportsSendPassword { get; }
/// <summary>
/// Send <paramref name="password"/> to the connected PLC. No-op (and returns
/// <c>false</c>) when <see cref="SupportsSendPassword"/> is <c>false</c>;
/// returns <c>true</c> after a successful send. Throws cleanly when the wire
/// reports auth-failed — <see cref="S7Driver.InitializeAsync"/> wraps the
/// throw into a typed <see cref="InvalidOperationException"/> so the operator
/// sees a "password authentication failed" message rather than a generic
/// <c>S7.Net.PlcException</c>.
/// </summary>
Task<bool> TrySendPasswordAsync(string password, CancellationToken cancellationToken);
}
/// <summary>
/// Production <see cref="IS7PlcAuthGate"/> backed by reflection over the S7netplus
/// <c>S7.Net.Plc</c> instance. S7netplus 0.20 does NOT expose a
/// <c>SendPassword</c>; the reflection probe survives that gracefully and a future
/// 0.21+ that adds the API gets called automatically without a code change here.
/// </summary>
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<bool> 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;
}
}