mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 22:55:08 +08:00
372 lines
16 KiB
C#
372 lines
16 KiB
C#
/* ========================================================================
|
|
* 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
|
|
{
|
|
/// <summary>
|
|
/// Defines numerous re-useable utility functions for clients.
|
|
/// </summary>
|
|
public static partial class CoreClientUtils
|
|
{
|
|
/// <summary>
|
|
/// The default discover operation timeout.
|
|
/// </summary>
|
|
public static readonly int DefaultDiscoverTimeout = 15000;
|
|
|
|
/// <summary>
|
|
/// Discovers the servers on the local machine.
|
|
/// </summary>
|
|
/// <param name="configuration">The configuration.</param>
|
|
/// <param name="telemetry">The telemetry context to use to create obvservability instruments</param>
|
|
/// <param name="ct">Cancellation token to cancel operation with</param>
|
|
/// <returns>A list of server urls.</returns>
|
|
public static ValueTask<IList<string>> DiscoverServersAsync(
|
|
ApplicationConfiguration configuration,
|
|
ITelemetryContext telemetry,
|
|
CancellationToken ct = default)
|
|
{
|
|
return DiscoverServersAsync(configuration, DefaultDiscoverTimeout, telemetry, ct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Discovers the servers on the local machine.
|
|
/// </summary>
|
|
/// <param name="configuration">The configuration.</param>
|
|
/// <param name="discoverTimeout">Operation timeout in milliseconds.</param>
|
|
/// <param name="telemetry">The telemetry context to use to create obvservability instruments</param>
|
|
/// <param name="ct">Cancellation token to cancel operation with</param>
|
|
/// <returns>A list of server urls.</returns>
|
|
public static async ValueTask<IList<string>> DiscoverServersAsync(
|
|
ApplicationConfiguration configuration,
|
|
int discoverTimeout,
|
|
ITelemetryContext telemetry,
|
|
CancellationToken ct = default)
|
|
{
|
|
var serverUrls = new List<string>();
|
|
|
|
// 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<ApplicationDescription> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds the endpoint that best matches the current settings.
|
|
/// </summary>
|
|
public static ValueTask<EndpointDescription?> SelectEndpointAsync(
|
|
ApplicationConfiguration application,
|
|
ITransportWaitingConnection connection,
|
|
bool useSecurity,
|
|
ITelemetryContext telemetry,
|
|
CancellationToken ct = default)
|
|
{
|
|
return SelectEndpointAsync(
|
|
application,
|
|
connection,
|
|
useSecurity,
|
|
DefaultDiscoverTimeout,
|
|
telemetry,
|
|
ct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds the endpoint that best matches the current settings.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
public static async ValueTask<EndpointDescription?> 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<EndpointDescription> endpoints =
|
|
await client.GetEndpointsAsync(default, ct).ConfigureAwait(false);
|
|
return SelectEndpoint(
|
|
application,
|
|
url,
|
|
endpoints,
|
|
useSecurity,
|
|
telemetry);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds the endpoint that best matches the current settings.
|
|
/// </summary>
|
|
/// <param name="application">The application configuration.</param>
|
|
/// <param name="discoveryUrl">The discovery URL.</param>
|
|
/// <param name="useSecurity">if set to <c>true</c> select an endpoint
|
|
/// that uses security.</param>
|
|
/// <param name="telemetry">The telemetry context to use to create obvservability instruments</param>
|
|
/// <param name="ct">Cancellation token to cancel operation with</param>
|
|
/// <returns>The best available endpoint.</returns>
|
|
public static ValueTask<EndpointDescription?> SelectEndpointAsync(
|
|
ApplicationConfiguration application,
|
|
string discoveryUrl,
|
|
bool useSecurity,
|
|
ITelemetryContext telemetry,
|
|
CancellationToken ct = default)
|
|
{
|
|
return SelectEndpointAsync(
|
|
application,
|
|
discoveryUrl,
|
|
useSecurity,
|
|
DefaultDiscoverTimeout,
|
|
telemetry,
|
|
ct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds the endpoint that best matches the current settings.
|
|
/// </summary>
|
|
/// <param name="application">The application configuration.</param>
|
|
/// <param name="discoveryUrl">The discovery URL.</param>
|
|
/// <param name="useSecurity">if set to <c>true</c> select an endpoint that uses security.</param>
|
|
/// <param name="discoverTimeout">The timeout for the discover operation.</param>
|
|
/// <param name="telemetry">The telemetry context to use to create obvservability instruments</param>
|
|
/// <param name="ct">Cancellation token to cancel operation with</param>
|
|
/// <returns>The best available endpoint.</returns>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
public static async ValueTask<EndpointDescription?> 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<EndpointDescription> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Select the best supported endpoint from the
|
|
/// EndpointDescriptions, with or without security.
|
|
/// </summary>
|
|
public static EndpointDescription? SelectEndpoint(
|
|
ApplicationConfiguration configuration,
|
|
Uri url,
|
|
ArrayOf<EndpointDescription> 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<DiscoveryClient>();
|
|
|
|
//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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert the discoveryUrl to a Uri and modify endpoint as per connection scheme if required.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|