using Opc.Ua;
using Opc.Ua.Client;
using Serilog;
namespace ZB.MOM.WW.OtOpcUa.Client.Shared.Adapters;
///
/// Production endpoint discovery that queries the real server.
///
internal sealed class DefaultEndpointDiscovery : IEndpointDiscovery
{
private static readonly ILogger Logger = Log.ForContext();
/// Selects an OPC UA endpoint matching the requested security mode.
/// The application configuration.
/// The endpoint URL to query.
/// The requested message security mode.
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);
}
}
///
/// Pure best-endpoint selection logic, extracted from
/// so it can be unit tested without standing up a real .
///
internal static class EndpointSelector
{
private static readonly ILogger Logger = Log.ForContext(typeof(EndpointSelector));
///
/// Picks the best endpoint from the discovery response that matches the requested
/// security mode, preferring Basic256Sha256, and rewrites the endpoint URL
/// host to match the user-supplied URL when the discovery response advertises a
/// different hostname.
///
/// Endpoints returned by the discovery query, in any order.
/// The endpoint URL the operator supplied; supplies the hostname rewrite target.
/// The requested OPC UA message security mode.
///
/// Thrown when no endpoint matches ; the message lists the
/// security mode + policy combinations the server returned so operators can diagnose mismatches.
///
public static EndpointDescription SelectBest(
IEnumerable 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;
}
}