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; } }