@@ -0,0 +1,124 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user