/* ======================================================================== * 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.Threading; using System.Threading.Tasks; using Opc.Ua.Security.Certificates; namespace Opc.Ua.Client { /// /// Session factory that creates raw instances /// without automatic reconnection or failover. /// /// /// Use instead to get /// instances that handle reconnection /// and failover automatically. /// public class DefaultSessionFactory : ISessionFactory { /// /// The default instance of the factory. /// // Telemetry is required by the modern ctor; this obsolete singleton forwards null! // to preserve the pre-nullable static-instance pattern. [Obsolete("Use new DefaultSessionFactory(ITelemetryContext) instead.")] public static readonly DefaultSessionFactory Instance = new(null!); /// public ITelemetryContext Telemetry { get; init; } /// public DiagnosticsMasks ReturnDiagnostics { get; set; } /// /// Optional subscription engine factory to use when constructing /// a . When null, the session uses /// the classic engine (). /// New code paths default to the V2 /// engine () via /// the ManagedSessionBuilder. /// public ISubscriptionEngineFactory? SubscriptionEngineFactory { get; init; } /// /// Optional forwarded to every /// created by this factory. When /// , the session uses /// . /// public TimeProvider? TimeProvider { get; init; } /// /// Obsolete default constructor /// [Obsolete("Use DefaultSessionFactory(ITelemetryContext) instead.")] public DefaultSessionFactory() // Telemetry is required by the modern ctor; this obsolete bridge forwards null! // to preserve the parameterless instantiation pattern. : this(null!) { } /// /// Creates a new instance of the . /// public DefaultSessionFactory(ITelemetryContext telemetry) { Telemetry = telemetry; } /// public virtual Task CreateAsync( ApplicationConfiguration configuration, ConfiguredEndpoint endpoint, bool updateBeforeConnect, string sessionName, uint sessionTimeout, IUserIdentity? identity, ArrayOf preferredLocales, CancellationToken ct = default) { return CreateAsync( configuration, endpoint, updateBeforeConnect, false, sessionName, sessionTimeout, identity, preferredLocales, ct); } /// public virtual Task CreateAsync( ApplicationConfiguration configuration, ConfiguredEndpoint endpoint, bool updateBeforeConnect, bool checkDomain, string sessionName, uint sessionTimeout, IUserIdentity? identity, ArrayOf preferredLocales, CancellationToken ct = default) { return CreateAsync( configuration, null, endpoint, updateBeforeConnect, checkDomain, sessionName, sessionTimeout, identity, preferredLocales, ReturnDiagnostics, ct); } /// public virtual Task CreateAsync( ApplicationConfiguration configuration, ITransportWaitingConnection connection, ConfiguredEndpoint endpoint, bool updateBeforeConnect, bool checkDomain, string sessionName, uint sessionTimeout, IUserIdentity? identity, ArrayOf preferredLocales, CancellationToken ct = default) { return CreateAsync( configuration, connection, endpoint, updateBeforeConnect, checkDomain, sessionName, sessionTimeout, identity, preferredLocales, ReturnDiagnostics, ct); } /// public virtual async Task CreateAsync( ApplicationConfiguration configuration, ReverseConnectManager reverseConnectManager, ConfiguredEndpoint endpoint, bool updateBeforeConnect, bool checkDomain, string sessionName, uint sessionTimeout, IUserIdentity? userIdentity, ArrayOf preferredLocales, CancellationToken ct = default) { if (reverseConnectManager == null) { return await CreateAsync( configuration, endpoint, updateBeforeConnect, checkDomain, sessionName, sessionTimeout, userIdentity, preferredLocales, ct).ConfigureAwait(false); } ITransportWaitingConnection? connection; do { // Reverse-connect endpoints always carry an EndpointUrl and SecurityPolicyUri // populated when the configured endpoint was loaded; the bangs reflect that. connection = await reverseConnectManager .WaitForConnectionAsync( endpoint.EndpointUrl!, endpoint.ReverseConnect?.ServerUri, ct) .ConfigureAwait(false); if (updateBeforeConnect) { await endpoint.UpdateFromServerAsync( endpoint.EndpointUrl!, connection, endpoint.Description.SecurityMode, endpoint.Description.SecurityPolicyUri!, Telemetry, ct).ConfigureAwait(false); updateBeforeConnect = false; connection = null; } } while (connection == null); return await CreateAsync( configuration, connection, endpoint, false, checkDomain, sessionName, sessionTimeout, userIdentity, preferredLocales, ct).ConfigureAwait(false); } /// public virtual async Task CreateChannelAsync( ApplicationConfiguration configuration, ITransportWaitingConnection? connection, ConfiguredEndpoint endpoint, bool updateBeforeConnect, bool checkDomain, CancellationToken ct = default) { endpoint.UpdateBeforeConnect = updateBeforeConnect; EndpointDescription endpointDescription = endpoint.Description; // create the endpoint configuration (use the application configuration to provide default values). EndpointConfiguration? endpointConfiguration = endpoint.Configuration; if (endpointConfiguration == null) { endpoint.Configuration = endpointConfiguration = EndpointConfiguration.Create( configuration); } // create message context. ServiceMessageContext messageContext = configuration.CreateMessageContext(); // update endpoint description using the discovery endpoint. if (endpoint.UpdateBeforeConnect && connection == null) { await endpoint.UpdateFromServerAsync(messageContext.Telemetry, ct).ConfigureAwait(false); endpointDescription = endpoint.Description; // UpdateFromServerAsync re-reads Configuration from the discovery response; // it is set whenever the description was updated successfully. endpointConfiguration = endpoint.Configuration!; } // checks the domains in the certificate. if (checkDomain && endpoint.Description.ServerCertificate.Length > 0) { using var certificate = Certificate.FromRawData(endpoint.Description.ServerCertificate); ICertificateValidatorEx? validator = configuration.CertificateManager; validator?.ValidateDomains(certificate, endpoint); } Certificate? clientCertificate = null; CertificateCollection? clientCertificateChain = null; try { if (endpointDescription.SecurityPolicyUri is not null and not SecurityPolicies.None) { clientCertificate = await Session.LoadInstanceCertificateAsync( configuration, endpointDescription.SecurityPolicyUri, messageContext.Telemetry, ct).ConfigureAwait(false); clientCertificateChain = await Session.LoadCertificateChainAsync( configuration, clientCertificate, ct).ConfigureAwait(false); } // initialize the channel which will be created with the server. ITransportChannel channel; if (connection != null) { channel = await UaChannelBase.CreateUaBinaryChannelAsync( configuration, connection, endpointDescription, endpointConfiguration, clientCertificate, clientCertificateChain, messageContext, ct).ConfigureAwait(false); } else { channel = await UaChannelBase.CreateUaBinaryChannelAsync( configuration, endpointDescription, endpointConfiguration, clientCertificate, clientCertificateChain, messageContext, ct).ConfigureAwait(false); } // Ownership of the cert and chain has been transferred to the // channel's TransportChannelSettings; the channel disposes // them when it is disposed, so we must not dispose here. clientCertificate = null; clientCertificateChain = null; return channel; } finally { clientCertificateChain?.Dispose(); clientCertificate?.Dispose(); } } /// public virtual async Task RecreateAsync( ISession sessionTemplate, CancellationToken ct = default) { if (sessionTemplate is not Session template) { throw new ArgumentException( "The ISession provided is not of a supported type.", nameof(sessionTemplate)); } template.ReturnDiagnostics = ReturnDiagnostics; return await template.RecreateAsync(ct).ConfigureAwait(false); } /// public virtual async Task RecreateAsync( ISession sessionTemplate, ITransportWaitingConnection connection, CancellationToken ct = default) { if (sessionTemplate is not Session template) { throw new ArgumentException( "The ISession provided is not of a supported type.", nameof(sessionTemplate)); } template.ReturnDiagnostics = ReturnDiagnostics; return await template.RecreateAsync(connection, ct).ConfigureAwait(false); } /// public virtual async Task RecreateAsync( ISession sessionTemplate, ITransportChannel transportChannel, CancellationToken ct = default) { if (sessionTemplate is not Session template) { throw new ArgumentException( "The ISession provided is not of a supported type.", nameof(sessionTemplate)); } template.ReturnDiagnostics = ReturnDiagnostics; return await template.RecreateAsync(transportChannel, ct) .ConfigureAwait(false); } /// public virtual ISession Create( ITransportChannel channel, ApplicationConfiguration configuration, ConfiguredEndpoint endpoint, Certificate? clientCertificate = null, CertificateCollection? clientCertificateChain = null, ArrayOf availableEndpoints = default, ArrayOf discoveryProfileUris = default) { return new Session( channel, configuration, endpoint, clientCertificate, clientCertificateChain, availableEndpoints, discoveryProfileUris, SubscriptionEngineFactory, TimeProvider) { ReturnDiagnostics = ReturnDiagnostics }; } /// /// Creates a new communication session with a server using a reverse connection. /// /// The configuration for the client application. /// The client endpoint for the reverse connect. /// The endpoint for the server. /// If set to true the discovery endpoint is used to /// update the endpoint description before connecting. /// If set to true then the domain in the certificate must match /// the endpoint used. /// The name to assign to the session. /// The timeout period for the session. /// The user identity to associate with the session. /// The preferred locales. /// The return diagnostics to use on this session /// The cancellation token. /// The new session object. private async Task CreateAsync( ApplicationConfiguration configuration, ITransportWaitingConnection? connection, ConfiguredEndpoint endpoint, bool updateBeforeConnect, bool checkDomain, string sessionName, uint sessionTimeout, IUserIdentity? identity, ArrayOf preferredLocales, DiagnosticsMasks returnDiagnostics, CancellationToken ct = default) { // initialize the channel which will be created with the server. ITransportChannel channel = await CreateChannelAsync( configuration, connection, endpoint, updateBeforeConnect, checkDomain, ct) .ConfigureAwait(false); // create the session object. ISession session = Create(channel, configuration, endpoint, null); session.ReturnDiagnostics = returnDiagnostics; // create the session. UserIdentity? tempIdentity = identity == null ? new UserIdentity() : null; try { await session .OpenAsync( sessionName, sessionTimeout, // tempIdentity is set when identity is null (line above), so the // coalesce always yields a non-null IUserIdentity at runtime. identity ?? tempIdentity!, preferredLocales, checkDomain, ct) .ConfigureAwait(false); tempIdentity = null; // ownership transferred to session } catch (Exception) { session.Dispose(); throw; } return session; } } }