/* ======================================================================== * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved. * * OPC Foundation MIT License 1.00 * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * The complete license agreement can be found here: * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Opc.Ua.Security; namespace Opc.Ua.Client { /// /// Defines numerous re-useable utility functions for clients. /// public static partial class CoreClientUtils { /// /// The default discover operation timeout. /// public static readonly int DefaultDiscoverTimeout = 15000; /// /// Discovers the servers on the local machine. /// /// The configuration. /// The telemetry context to use to create obvservability instruments /// Cancellation token to cancel operation with /// A list of server urls. public static ValueTask> DiscoverServersAsync( ApplicationConfiguration configuration, ITelemetryContext telemetry, CancellationToken ct = default) { return DiscoverServersAsync(configuration, DefaultDiscoverTimeout, telemetry, ct); } /// /// Discovers the servers on the local machine. /// /// The configuration. /// Operation timeout in milliseconds. /// The telemetry context to use to create obvservability instruments /// Cancellation token to cancel operation with /// A list of server urls. public static async ValueTask> DiscoverServersAsync( ApplicationConfiguration configuration, int discoverTimeout, ITelemetryContext telemetry, CancellationToken ct = default) { var serverUrls = new List(); // set a short timeout because this is happening in the drop down event. var endpointConfiguration = EndpointConfiguration.Create(configuration); endpointConfiguration.OperationTimeout = discoverTimeout; // Connect to the local discovery server and find the available servers. using DiscoveryClient client = await DiscoveryClient.CreateAsync( new Uri(Utils.Format(Utils.DiscoveryUrls[0], "localhost")), endpointConfiguration, telemetry, ct: ct).ConfigureAwait(false); ArrayOf servers = await client.FindServersAsync(default, ct).ConfigureAwait(false); // populate the drop down list with the discovery URLs for the available servers. for (int ii = 0; ii < servers.Count; ii++) { if (servers[ii].ApplicationType == ApplicationType.DiscoveryServer) { continue; } for (int jj = 0; jj < servers[ii].DiscoveryUrls.Count; jj++) { string discoveryUrl = servers[ii].DiscoveryUrls[jj]; // Many servers will use the '/discovery' suffix for the discovery endpoint. // The URL without this prefix should be the base URL for the server. if (discoveryUrl.EndsWith( ConfiguredEndpoint.DiscoverySuffix, StringComparison.OrdinalIgnoreCase)) { discoveryUrl = discoveryUrl[..^ConfiguredEndpoint.DiscoverySuffix.Length]; } // ensure duplicates do not get added. if (!serverUrls.Exists(serverUrl => serverUrl.Equals(discoveryUrl, StringComparison.OrdinalIgnoreCase))) { serverUrls.Add(discoveryUrl); } } } return serverUrls; } /// /// Finds the endpoint that best matches the current settings. /// public static ValueTask SelectEndpointAsync( ApplicationConfiguration application, ITransportWaitingConnection connection, bool useSecurity, ITelemetryContext telemetry, CancellationToken ct = default) { return SelectEndpointAsync( application, connection, useSecurity, DefaultDiscoverTimeout, telemetry, ct); } /// /// Finds the endpoint that best matches the current settings. /// /// public static async ValueTask SelectEndpointAsync( ApplicationConfiguration application, ITransportWaitingConnection connection, bool useSecurity, int discoverTimeout, ITelemetryContext telemetry, CancellationToken ct = default) { var endpointConfiguration = EndpointConfiguration.Create(application); endpointConfiguration.OperationTimeout = discoverTimeout > 0 ? discoverTimeout : DefaultDiscoverTimeout; using DiscoveryClient client = await DiscoveryClient.CreateAsync( application, connection, endpointConfiguration, ct: ct).ConfigureAwait(false); var url = new Uri(client.Endpoint?.EndpointUrl ?? throw ServiceResultException.Unexpected("Endpoint missing")); ArrayOf endpoints = await client.GetEndpointsAsync(default, ct).ConfigureAwait(false); return SelectEndpoint( application, url, endpoints, useSecurity, telemetry); } /// /// Finds the endpoint that best matches the current settings. /// /// The application configuration. /// The discovery URL. /// if set to true select an endpoint /// that uses security. /// The telemetry context to use to create obvservability instruments /// Cancellation token to cancel operation with /// The best available endpoint. public static ValueTask SelectEndpointAsync( ApplicationConfiguration application, string discoveryUrl, bool useSecurity, ITelemetryContext telemetry, CancellationToken ct = default) { return SelectEndpointAsync( application, discoveryUrl, useSecurity, DefaultDiscoverTimeout, telemetry, ct); } /// /// Finds the endpoint that best matches the current settings. /// /// The application configuration. /// The discovery URL. /// if set to true select an endpoint that uses security. /// The timeout for the discover operation. /// The telemetry context to use to create obvservability instruments /// Cancellation token to cancel operation with /// The best available endpoint. /// public static async ValueTask SelectEndpointAsync( ApplicationConfiguration application, string discoveryUrl, bool useSecurity, int discoverTimeout, ITelemetryContext telemetry, CancellationToken ct = default) { Uri uri = GetDiscoveryUrl(discoveryUrl); var endpointConfiguration = EndpointConfiguration.Create(application); endpointConfiguration.OperationTimeout = discoverTimeout; using DiscoveryClient client = await DiscoveryClient.CreateAsync( application, uri, endpointConfiguration, ct: ct).ConfigureAwait(false); // Connect to the server's discovery endpoint and find the available configuration. var url = new Uri(client.Endpoint?.EndpointUrl ?? throw ServiceResultException.Unexpected("Endpoint missing")); ArrayOf endpoints = await client.GetEndpointsAsync(default, ct).ConfigureAwait(false); EndpointDescription? selectedEndpoint = SelectEndpoint( application, url, endpoints, useSecurity, telemetry); if (selectedEndpoint != null) { Uri? endpointUrl = Utils.ParseUri(selectedEndpoint.EndpointUrl); if (endpointUrl != null && endpointUrl.Scheme == uri.Scheme) { var builder = new UriBuilder(endpointUrl) { Host = uri.IdnHost, Port = uri.Port }; selectedEndpoint.EndpointUrl = builder.ToString(); } } return selectedEndpoint; } /// /// Select the best supported endpoint from the /// EndpointDescriptions, with or without security. /// public static EndpointDescription? SelectEndpoint( ApplicationConfiguration configuration, Uri url, ArrayOf endpoints, bool useSecurity, ITelemetryContext telemetry) { EndpointDescription? selectedEndpoint = null; // select the best endpoint to use based on the selected URL and the UseSecurity checkbox. for (int ii = 0; ii < endpoints.Count; ii++) { EndpointDescription endpoint = endpoints[ii]; // check for a match on the URL scheme. if (endpoint.EndpointUrl != null && endpoint.EndpointUrl.StartsWith(url.Scheme, StringComparison.Ordinal)) { // check if security was requested. if (useSecurity) { if (endpoint.SecurityMode == MessageSecurityMode.None) { continue; } if (configuration != null) { // skip unsupported security policies if (!configuration.SecurityConfiguration.SupportedSecurityPolicies .Contains(p => p == endpoint.SecurityPolicyUri)) { continue; } } else { // skip unsupported security policies, for backward compatibility only // may contain policies for which no certificate is available // SecurityPolicyUri is annotated nullable on EndpointDescription but is // populated for any endpoint advertising security; downstream API takes // a non-nullable string parameter. if (SecurityPolicies.GetDisplayName(endpoint.SecurityPolicyUri!) == null) { continue; } } } else if (endpoint.SecurityMode != MessageSecurityMode.None) { continue; } // pick the first available endpoint by default. selectedEndpoint ??= endpoint; ILogger logger = telemetry.CreateLogger(); //Select endpoint if it has a higher calculated security level, than the previously selected one // SecurityPolicyUri is populated for any endpoint reaching this code path // (security mode != None); CalculateSecurityLevel takes a non-nullable string. if (SecuredApplication.CalculateSecurityLevel( endpoint.SecurityMode, endpoint.SecurityPolicyUri!, logger) > SecuredApplication.CalculateSecurityLevel( selectedEndpoint.SecurityMode, selectedEndpoint.SecurityPolicyUri!, logger)) { selectedEndpoint = endpoint; } } } // pick the first available endpoint by default. if (selectedEndpoint == null && endpoints.Count > 0) { selectedEndpoint = endpoints.Find(e => e.EndpointUrl?.StartsWith(url.Scheme, StringComparison.Ordinal) == true); } // return the selected endpoint. return selectedEndpoint; } /// /// Convert the discoveryUrl to a Uri and modify endpoint as per connection scheme if required. /// public static Uri GetDiscoveryUrl(string discoveryUrl) { // needs to add the '/discovery' back onto non-UA TCP URLs. if (Utils.IsUriHttpRelatedScheme(discoveryUrl) && !discoveryUrl.EndsWith( ConfiguredEndpoint.DiscoverySuffix, StringComparison.OrdinalIgnoreCase)) { discoveryUrl += ConfiguredEndpoint.DiscoverySuffix; } // parse the selected URL. return new Uri(discoveryUrl); } } }