64e3fbe035
v2-ci / build (push) Failing after 1m43s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>, <param>, <typeparam>, and <inheritdoc/> tags to public members surfaced by commentchecker — resolves 5,847 of 5,869 issues (99.6%) across three /fixdocs passes.
109 lines
4.6 KiB
C#
109 lines
4.6 KiB
C#
using Opc.Ua;
|
|
using Opc.Ua.Client;
|
|
using Serilog;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Adapters;
|
|
|
|
/// <summary>
|
|
/// Production endpoint discovery that queries the real server.
|
|
/// </summary>
|
|
internal sealed class DefaultEndpointDiscovery : IEndpointDiscovery
|
|
{
|
|
private static readonly ILogger Logger = Log.ForContext<DefaultEndpointDiscovery>();
|
|
|
|
/// <summary>Selects an OPC UA endpoint matching the requested security mode.</summary>
|
|
/// <param name="config">The application configuration.</param>
|
|
/// <param name="endpointUrl">The endpoint URL to query.</param>
|
|
/// <param name="requestedMode">The requested message security mode.</param>
|
|
public EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl,
|
|
MessageSecurityMode requestedMode)
|
|
{
|
|
if (requestedMode == MessageSecurityMode.None)
|
|
{
|
|
#pragma warning disable CS0618 // Acceptable for endpoint selection
|
|
return CoreClientUtils.SelectEndpoint(config, endpointUrl, false);
|
|
#pragma warning restore CS0618
|
|
}
|
|
|
|
using var client = DiscoveryClient.Create(new Uri(endpointUrl));
|
|
var allEndpoints = client.GetEndpoints(null);
|
|
|
|
return EndpointSelector.SelectBest(allEndpoints, endpointUrl, requestedMode);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pure best-endpoint selection logic, extracted from <see cref="DefaultEndpointDiscovery"/>
|
|
/// so it can be unit tested without standing up a real <see cref="DiscoveryClient"/>.
|
|
/// </summary>
|
|
internal static class EndpointSelector
|
|
{
|
|
private static readonly ILogger Logger = Log.ForContext(typeof(EndpointSelector));
|
|
|
|
/// <summary>
|
|
/// Picks the best endpoint from the discovery response that matches the requested
|
|
/// security mode, preferring <c>Basic256Sha256</c>, and rewrites the endpoint URL
|
|
/// host to match the user-supplied URL when the discovery response advertises a
|
|
/// different hostname.
|
|
/// </summary>
|
|
/// <param name="allEndpoints">Endpoints returned by the discovery query, in any order.</param>
|
|
/// <param name="endpointUrl">The endpoint URL the operator supplied; supplies the hostname rewrite target.</param>
|
|
/// <param name="requestedMode">The requested OPC UA message security mode.</param>
|
|
/// <exception cref="InvalidOperationException">
|
|
/// Thrown when no endpoint matches <paramref name="requestedMode"/>; the message lists the
|
|
/// security mode + policy combinations the server returned so operators can diagnose mismatches.
|
|
/// </exception>
|
|
public static EndpointDescription SelectBest(
|
|
IEnumerable<EndpointDescription> allEndpoints,
|
|
string endpointUrl,
|
|
MessageSecurityMode requestedMode)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(allEndpoints);
|
|
if (string.IsNullOrWhiteSpace(endpointUrl))
|
|
throw new ArgumentException("Endpoint URL must not be null or empty.", nameof(endpointUrl));
|
|
|
|
// Materialise once so we can both iterate and produce a diagnostic message
|
|
// without re-running the underlying discovery enumeration.
|
|
var endpoints = allEndpoints.ToList();
|
|
|
|
EndpointDescription? best = null;
|
|
|
|
foreach (var ep in endpoints)
|
|
{
|
|
if (ep.SecurityMode != requestedMode)
|
|
continue;
|
|
|
|
if (best == null)
|
|
{
|
|
best = ep;
|
|
continue;
|
|
}
|
|
|
|
// Prefer Basic256Sha256 when multiple endpoints match the requested mode.
|
|
if (ep.SecurityPolicyUri == SecurityPolicies.Basic256Sha256)
|
|
best = ep;
|
|
}
|
|
|
|
if (best == null)
|
|
{
|
|
var available = string.Join(", ", endpoints.Select(e => $"{e.SecurityMode}/{e.SecurityPolicyUri}"));
|
|
throw new InvalidOperationException(
|
|
$"No endpoint found with security mode '{requestedMode}'. Available endpoints: {available}");
|
|
}
|
|
|
|
// Rewrite endpoint URL hostname to match user-supplied hostname. Necessary
|
|
// when the OPC UA server returns a discovery URL using a different hostname
|
|
// (e.g. internal DNS name) than the one the operator routed to.
|
|
var serverUri = new Uri(best.EndpointUrl);
|
|
var requestedUri = new Uri(endpointUrl);
|
|
if (serverUri.Host != requestedUri.Host)
|
|
{
|
|
var builder = new UriBuilder(best.EndpointUrl) { Host = requestedUri.Host };
|
|
best.EndpointUrl = builder.ToString();
|
|
Logger.Debug("Rewrote endpoint host from {ServerHost} to {RequestedHost}", serverUri.Host,
|
|
requestedUri.Host);
|
|
}
|
|
|
|
return best;
|
|
}
|
|
} |