/* ======================================================================== * 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.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Opc.Ua.Identity; using Opc.Ua.Security.Certificates; namespace Opc.Ua.Client { /// /// Manages a session with a server. /// /// /// /// Many properties on (e.g. EndpointUrl, /// Configuration) and (SecurityPolicyUri) /// are annotated nullable on the schema-generated DTOs but are required at runtime once /// a session has been created. The same applies to /// members such as ClientConfiguration, CertificateValidator and /// ApplicationName: a Session cannot be opened without them - see /// . /// Null-forgiving (!) suppressions in this file reflect those lifecycle /// invariants which the C# compiler cannot infer from the type system. /// /// /// The instance-level fields m_serverCertificate, m_instanceCertificate /// and m_userTokenSecurityPolicyUri are nullable until OpenAsync / /// ActivateSessionAsync populates them; uses inside post-open code paths bang /// them to acknowledge the open-session invariant. A ! on a return value of /// (which returns nullable) is reused immediately /// thereafter and the implicit dereference would NRE on null, so the bang preserves the /// pre-nullable behavior of throwing a if a /// caller-supplied policy URI is unknown. /// /// public partial class Session : SessionClientBatched, ISession, ISnapshotRestore, ISnapshotRestore { private const int kReconnectTimeout = 15000; /// /// Constructs a new instance of the class. /// /// The channel used to communicate with the server. /// The configuration for the client application. /// The endpoint use to initialize the channel. /// /// is not an . /// [Obsolete("Use constructor with ITransportChannel instead of ISessionChannel.")] public Session( ISessionChannel channel, ApplicationConfiguration configuration, ConfiguredEndpoint endpoint) : this( channel is ITransportChannel transportChannel ? transportChannel : throw new ArgumentException("not a transport channel"), configuration, endpoint, engineFactory: null) { } /// /// Constructs a new instance of the class. /// /// The channel used to communicate with the server. /// The configuration for the client application. /// The endpoint used to initialize the channel. /// The certificate to use for the client. /// The certificate chain of the client /// certificate. /// The list of available endpoints returned /// by server in GetEndpoints() response. /// The value of profileUris used in /// GetEndpoints() request. /// Optional subscription engine factory. When /// null the session uses /// (the classic engine) by default. Pass /// explicitly /// to opt into the V2 engine. /// Optional used by the /// session for keep-alive timers and elapsed-time calculations. When /// , is used. /// /// The application configuration is used to look up the certificate if none /// is provided. The clientCertificate must have the private key. This will /// require that the certificate be loaded from a certicate store. Converting /// a DER encoded blob to a X509Certificate2 will not include a private key. /// The availableEndpoints and discoveryProfileUris parameters are /// used to validate that the list of EndpointDescriptions returned at GetEndpoints /// matches the list returned at CreateSession. /// public Session( ITransportChannel channel, ApplicationConfiguration configuration, ConfiguredEndpoint endpoint, Certificate? clientCertificate = null, CertificateCollection? clientCertificateChain = null, ArrayOf availableEndpoints = default, ArrayOf discoveryProfileUris = default, ISubscriptionEngineFactory? engineFactory = null, TimeProvider? timeProvider = null) : this( channel, configuration, endpoint, channel.MessageContext ?? configuration.CreateMessageContext(), engineFactory, timeProvider) { m_instanceCertificate = clientCertificate; m_instanceCertificateChain = clientCertificateChain; m_discoveryServerEndpoints = availableEndpoints; m_discoveryProfileUris = discoveryProfileUris; } /// /// Initializes a new instance of the class. /// /// The channel. /// The template session. /// if set to true the event handlers are copied. public Session(ITransportChannel channel, Session template, bool copyEventHandlers) : this( channel, template.m_configuration, template.ConfiguredEndpoint, channel.MessageContext ?? template.m_configuration.CreateMessageContext(), template.SubscriptionEngineFactory, template.m_timeProvider) { // AddRef so the clone has its own reference; the template // retains its existing one. Both Sessions independently // dispose their respective references in Dispose. m_instanceCertificate = template.m_instanceCertificate?.AddRef(); m_instanceCertificateChain = template.m_instanceCertificateChain?.AddRef(); m_effectiveEndpoint = template.m_effectiveEndpoint; SessionFactory = template.SessionFactory; m_defaultSubscription = template.m_defaultSubscription; DeleteSubscriptionsOnClose = template.DeleteSubscriptionsOnClose; TransferSubscriptionsOnReconnect = template.TransferSubscriptionsOnReconnect; PublishRequestCancelDelayOnCloseSession = template.PublishRequestCancelDelayOnCloseSession; m_sessionTimeout = template.m_sessionTimeout; m_maxRequestMessageSize = template.m_maxRequestMessageSize; m_engine.MinPublishRequestCount = template.m_engine.MinPublishRequestCount; m_engine.MaxPublishRequestCount = template.m_engine.MaxPublishRequestCount; m_preferredLocales = template.PreferredLocales; m_sessionName = template.SessionName; Handle = template.Handle; m_identity = template.Identity; m_keepAliveInterval = template.KeepAliveInterval; // Create timer for keep alive event triggering but in off state m_keepAliveTimer = m_timeProvider.CreateTimer( _ => m_keepAliveEvent.Set(), this, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); m_checkDomain = template.m_checkDomain; ContinuationPointPolicy = template.ContinuationPointPolicy; ReturnDiagnostics = template.ReturnDiagnostics; if (template.OperationTimeout > 0) { OperationTimeout = template.OperationTimeout; } if (copyEventHandlers) { m_KeepAlive = template.m_KeepAlive; m_Publish = template.m_Publish; m_PublishError = template.m_PublishError; m_PublishSequenceNumbersToAcknowledge = template .m_PublishSequenceNumbersToAcknowledge; m_SubscriptionsChanged = template.m_SubscriptionsChanged; m_SessionClosing = template.m_SessionClosing; m_SessionConfigurationChanged = template.m_SessionConfigurationChanged; m_RenewUserIdentity = template.m_RenewUserIdentity; } foreach (Subscription subscription in template.Subscriptions) { AddSubscription(subscription.CloneSubscription(copyEventHandlers)); } } /// /// Initializes the session. /// /// /// is null. /// private Session( ITransportChannel channel, ApplicationConfiguration configuration, ConfiguredEndpoint endpoint, IServiceMessageContext messageContext, ISubscriptionEngineFactory? engineFactory = null, TimeProvider? timeProvider = null) : base(channel, messageContext.Telemetry) { if (messageContext == null) { throw new ArgumentNullException(nameof(messageContext)); } m_timeProvider = timeProvider ?? TimeProvider.System; m_telemetry = messageContext.Telemetry; m_logger = m_telemetry.CreateLogger(); SessionFactory ??= new DefaultSessionFactory(m_telemetry) { ReturnDiagnostics = ReturnDiagnostics }; NamespaceUris = new NamespaceTable(); ServerUris = new StringTable(); Factory = EncodeableFactory.Create(); m_keepAliveInterval = 5000; m_sessionName = string.Empty; DeleteSubscriptionsOnClose = true; PublishRequestCancelDelayOnCloseSession = 5000; // 5 seconds default ValidateClientConfiguration(configuration); // save configuration information. m_configuration = configuration; m_effectiveEndpoint = m_endpoint = endpoint; m_identity = new UserIdentity(); // update the default subscription. DefaultSubscription.MinLifetimeInterval = (uint)m_configuration.ClientConfiguration! .MinSubscriptionLifetime; // initialize operation limits from client configuration. if (m_configuration.ClientConfiguration.OperationLimits != null) { OperationLimits clientLimits = m_configuration.ClientConfiguration.OperationLimits; OperationLimits.MaxNodesPerRead = clientLimits.MaxNodesPerRead; OperationLimits.MaxNodesPerHistoryReadData = clientLimits.MaxNodesPerHistoryReadData; OperationLimits.MaxNodesPerHistoryReadEvents = clientLimits.MaxNodesPerHistoryReadEvents; OperationLimits.MaxNodesPerWrite = clientLimits.MaxNodesPerWrite; OperationLimits.MaxNodesPerHistoryUpdateData = clientLimits.MaxNodesPerHistoryUpdateData; OperationLimits.MaxNodesPerHistoryUpdateEvents = clientLimits.MaxNodesPerHistoryUpdateEvents; OperationLimits.MaxNodesPerMethodCall = clientLimits.MaxNodesPerMethodCall; OperationLimits.MaxNodesPerBrowse = clientLimits.MaxNodesPerBrowse; OperationLimits.MaxNodesPerRegisterNodes = clientLimits.MaxNodesPerRegisterNodes; OperationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds = clientLimits.MaxNodesPerTranslateBrowsePathsToNodeIds; OperationLimits.MaxNodesPerNodeManagement = clientLimits.MaxNodesPerNodeManagement; OperationLimits.MaxMonitoredItemsPerCall = clientLimits.MaxMonitoredItemsPerCall; } NamespaceUris = messageContext.NamespaceUris; ServerUris = messageContext.ServerUris; Factory = messageContext.Factory; // initialize the NodeCache late, it needs references to the namespaceUris m_nodeCache = new NodeCache(new NodeCacheContext(this), m_telemetry); // Create timer for keep alive event triggering but in off state m_keepAliveTimer = m_timeProvider.CreateTimer( _ => m_keepAliveEvent.Set(), this, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); // Create the subscription engine. Session defaults to the // classic engine (legacy applications + classic Subscription // API). ManagedSession explicitly opts in to the V2 engine // via its builder. SubscriptionEngineFactory = engineFactory ?? (ReferenceEquals(m_timeProvider, TimeProvider.System) ? ClassicSubscriptionEngineFactory.Instance : new ClassicSubscriptionEngineFactory(m_timeProvider)); m_engine = SubscriptionEngineFactory.Create(new SessionEngineContext(this)); // set the default preferred locales. m_preferredLocales = [CultureInfo.CurrentCulture.Name]; // create a context to use. m_systemContext = new SessionSystemContext(m_telemetry) { SystemHandle = this, EncodeableFactory = Factory, NamespaceUris = NamespaceUris, ServerUris = ServerUris, TypeTable = TypeTree, PreferredLocales = default, SessionId = default, UserIdentity = null }; } /// /// Check if all required configuration fields are populated. /// /// /// Subsequent code paths in this class dereference , /// and /// using the null-forgiving /// (!) operator. This method enforces the matching invariant at session /// construction time; the compiler cannot infer the narrowing across method boundaries. /// /// is null. /// private static void ValidateClientConfiguration(ApplicationConfiguration configuration) { string configurationField; if (configuration == null) { throw new ArgumentNullException(nameof(configuration)); } if (configuration.ClientConfiguration == null) { configurationField = "ClientConfiguration"; } else if (configuration.SecurityConfiguration == null) { configurationField = "SecurityConfiguration"; } else { return; } throw ServiceResultException.ConfigurationError( "The client configuration does not specify the configuration field {0}.", configurationField); } /// /// Validates the server nonce and security parameters of user identity. /// /// private void ValidateServerNonce( IUserIdentity identity, ByteString serverNonce, string? securityPolicyUri, ByteString previousServerNonce, MessageSecurityMode channelSecurityMode = MessageSecurityMode.None) { // skip validation if server nonce is not used for encryption. if (string.IsNullOrEmpty(securityPolicyUri) || securityPolicyUri == SecurityPolicies.None) { return; } if (identity != null && identity.TokenType != UserTokenType.Anonymous) { // the server nonce should be validated if the token includes a secret. if (!Nonce.ValidateNonce( serverNonce.ToArray(), MessageSecurityMode.SignAndEncrypt, m_configuration.SecurityConfiguration.NonceLength)) { if (channelSecurityMode == MessageSecurityMode.SignAndEncrypt || m_configuration.SecurityConfiguration.SuppressNonceValidationErrors) { m_logger.LogWarning( Utils.TraceMasks.Security, "Warning: The server nonce has not the correct length or is not random enough. " + "The error is suppressed by user setting or because the channel is encrypted."); } else { throw ServiceResultException.Create( StatusCodes.BadNonceInvalid, "The server nonce has not the correct length or is not random enough."); } } // check that new nonce is different from the previously returned server nonce. if (!previousServerNonce.IsEmpty && serverNonce == previousServerNonce) { if (channelSecurityMode == MessageSecurityMode.SignAndEncrypt || m_configuration.SecurityConfiguration.SuppressNonceValidationErrors) { m_logger.LogWarning( Utils.TraceMasks.Security, "Warning: The Server nonce is equal with previously returned nonce. " + "The error is suppressed by user setting or because the channel is encrypted."); } else { throw ServiceResultException.Create( StatusCodes.BadNonceInvalid, "Server nonce is equal with previously returned nonce."); } } } } /// /// Closes the session and the underlying channel. /// #pragma warning disable CA2215 // Dispose methods should call base class dispose protected override void Dispose(bool disposing) #pragma warning restore CA2215 // Dispose methods should call base class dispose { if (!m_disposeAsyncCalled) { DisposeAsyncCore(disposing).AsTask().GetAwaiter().GetResult(); } } /// public async ValueTask DisposeAsync() { GC.SuppressFinalize(this); await DisposeAsyncCore(true).ConfigureAwait(false); } /// /// Core async dispose logic for the session. /// /// True if called from dispose, false otherwise. #pragma warning disable IDE1006 // Standard IAsyncDisposable pattern name protected virtual async ValueTask DisposeAsyncCore(bool disposing) #pragma warning restore IDE1006 { if (m_disposeAsyncCalled) { return; } m_disposeAsyncCalled = true; if (disposing) { if (Disposed) { return; } try { await StopKeepAliveTimerAsync().ConfigureAwait(false); } catch (Exception ex) when (ex is not OutOfMemoryException) { m_logger.LogDebug(ex, "Error stopping keep alive timer during dispose."); } try { if (Connected) { await WaitForOrCancelOutstandingPublishRequestsAsync(default).ConfigureAwait(false); await base.CloseSessionAsync(null, DeleteSubscriptionsOnClose, default).ConfigureAwait(false); } } catch (Exception ex) when (ex is not OutOfMemoryException) { m_logger.LogDebug(ex, "Error closing session during dispose."); } m_defaultSubscription?.Dispose(); m_nodeCache?.Dispose(); List? subscriptions; lock (m_lock) { subscriptions = [.. m_subscriptions]; m_subscriptions.Clear(); } foreach (Subscription subscription in subscriptions) { subscription?.Dispose(); } subscriptions.Clear(); m_reconnectLock.Dispose(); m_eccServerEphemeralKey?.Dispose(); m_eccServerEphemeralKey = null; m_instanceCertificate?.Dispose(); m_instanceCertificate = null; m_instanceCertificateChain?.Dispose(); m_instanceCertificateChain = null; m_serverCertificate?.Dispose(); m_serverCertificate = null; m_keepAliveCancellation?.Dispose(); m_keepAliveCancellation = null; m_engine?.Dispose(); } base.Dispose(disposing); if (disposing) { m_keepAliveTimer.Dispose(); // suppress spurious events m_KeepAlive = null; m_Publish = null; m_PublishError = null; m_PublishSequenceNumbersToAcknowledge = null; m_SubscriptionsChanged = null; m_SessionClosing = null; m_SessionConfigurationChanged = null; Debug.Assert(Disposed); } } /// /// Raised when a keep alive arrives from the server or an error is detected. /// /// /// Once a session is created a timer will periodically read the server state and current time. /// If this read operation succeeds this event will be raised each time the keep alive period elapses. /// If an error is detected (KeepAliveStopped == true) then this event will be raised as well. /// public event KeepAliveEventHandler KeepAlive { add => m_KeepAlive += value; remove => m_KeepAlive -= value; } /// /// Raised when a notification message arrives in a publish response. /// /// /// All publish requests are managed by the Session object. When a response arrives it is /// validated and passed to the appropriate Subscription object and this event is raised. /// public event NotificationEventHandler Notification { add => m_Publish += value; remove => m_Publish -= value; } /// /// Raised when an exception occurs while processing a publish response. /// /// /// Exceptions in a publish response are not necessarily fatal and the Session will /// attempt to recover by issuing Republish requests if missing messages are detected. /// That said, timeout errors may be a symptom of a OperationTimeout that is too short /// when compared to the shortest PublishingInterval/KeepAliveCount amount the current /// Subscriptions. The OperationTimeout should be twice the minimum value for /// PublishingInterval*KeepAliveCount. /// public event PublishErrorEventHandler PublishError { add => m_PublishError += value; remove => m_PublishError -= value; } /// public event PublishSequenceNumbersToAcknowledgeEventHandler PublishSequenceNumbersToAcknowledge { add => m_PublishSequenceNumbersToAcknowledge += value; remove => m_PublishSequenceNumbersToAcknowledge -= value; } /// /// Raised when a subscription is added or removed /// public event EventHandler SubscriptionsChanged { add => m_SubscriptionsChanged += value; remove => m_SubscriptionsChanged -= value; } /// /// Raised to indicate the session is closing. /// public event EventHandler SessionClosing { add => m_SessionClosing += value; remove => m_SessionClosing -= value; } /// public event EventHandler SessionConfigurationChanged { add => m_SessionConfigurationChanged += value; remove => m_SessionConfigurationChanged -= value; } /// /// A session factory that was used to create the session. /// public ISessionFactory SessionFactory { get; set; } /// /// Gets the subscription engine factory used by this session. /// public ISubscriptionEngineFactory SubscriptionEngineFactory { get; } /// /// Gets the active subscription engine. Internal — exposes the /// engine for adapter scenarios such as /// surfacing the new V2 ISubscriptionManager. /// internal ISubscriptionEngine SubscriptionEngine => m_engine; /// /// Gets the endpoint used to connect to the server. /// public ConfiguredEndpoint ConfiguredEndpoint => m_endpoint; /// /// Gets the name assigned to the session. /// public string SessionName => m_sessionName; /// /// Whether the session is reconnecting /// public bool Reconnecting { get; private set; } /// /// Whether the session is closing /// public bool Closing { get; private set; } /// /// Gets the period for wich the server will maintain the session if /// there is no communication from the client. /// public double SessionTimeout => m_sessionTimeout; /// /// Gets the local handle assigned to the session. /// public object? Handle { get; set; } /// /// Gets the user identity currently used for the session. /// public IUserIdentity Identity => m_identity; /// /// Gets a list of user identities that can be used to connect to the server. /// public IEnumerable IdentityHistory => m_identityHistory; /// /// Gets the table of namespace uris known to the server. /// public NamespaceTable NamespaceUris { get; private set; } /// /// Gets the table of remote server uris known to the server. /// public StringTable ServerUris { get; private set; } /// /// Gets the system context for use with the session. /// public ISystemContext SystemContext => m_systemContext; /// /// Gets the factory used to create encodeable objects that the server understands. /// public IEncodeableFactory Factory { get; private set; } /// /// Gets the cache of the server's type tree. /// public ITypeTable TypeTree => m_nodeCache.TypeTree; /// /// Gets the cache of nodes fetched from the server. /// public INodeCache NodeCache => m_nodeCache; /// /// Gets the context to use for filter operations. /// public IFilterContext FilterContext => new FilterContext(NamespaceUris, m_nodeCache.TypeTree, m_preferredLocales, m_telemetry); /// /// Gets the locales that the server should use when returning localized text. /// public ArrayOf PreferredLocales => m_preferredLocales; /// /// Gets the subscriptions owned by the session. /// public IEnumerable Subscriptions { get { lock (m_lock) { return [.. m_subscriptions]; } } } /// /// Gets the number of subscriptions owned by the session. /// public int SubscriptionCount { get { lock (m_lock) { return m_subscriptions.Count; } } } /// /// If the subscriptions are deleted when a session is closed. /// /// /// Default true, set to false if subscriptions need to /// be transferred or for durable subscriptions. /// public bool DeleteSubscriptionsOnClose { get; set; } /// public int PublishRequestCancelDelayOnCloseSession { get; set; } /// /// If the subscriptions are transferred when a session is reconnected. /// /// /// Default false, set to true if subscriptions should /// be transferred after reconnect. Service must be supported by server. /// public bool TransferSubscriptionsOnReconnect { get; set; } /// /// Whether the endpoint Url domain is checked in the certificate. /// public bool CheckDomain => m_checkDomain; /// /// Gets or Sets the default subscription for the session. /// public Subscription DefaultSubscription { get => m_defaultSubscription ??= CreateSubscription(new SubscriptionOptions { DisplayName = "Subscription", PublishingInterval = 1000, KeepAliveCount = 10, LifetimeCount = 1000, Priority = 255, PublishingEnabled = true, MinLifetimeInterval = (uint)m_configuration.ClientConfiguration! .MinSubscriptionLifetime }); set { m_defaultSubscription?.Dispose(); m_defaultSubscription = value; } } /// /// Gets or Sets how frequently the server is pinged to see if communication is still working. /// /// /// This interval controls how much time elaspes before a communication error is detected. /// If everything is ok the KeepAlive event will be raised each time this period elapses. /// public int KeepAliveInterval { get => m_keepAliveInterval; set { m_keepAliveInterval = value; ResetKeepAliveTimer(); } } /// /// Returns true if the session is not receiving keep alives. /// /// /// Set to true if the server does not respond for the /// KeepAliveInterval * 1 (KeepAliveIntervalFactor) + 1 Second (KeepAliveGuardBand) * /// To change the sensitivity of the keep alive check, set the /// / fields. /// or if another error was reported. /// Set to false is communication is ok or recovered. /// public bool KeepAliveStopped { get { StatusCode lastKeepAliveErrorStatusCode = m_lastKeepAliveErrorStatusCode; if (StatusCode.IsGood(lastKeepAliveErrorStatusCode) || lastKeepAliveErrorStatusCode == StatusCodes.BadNoCommunication) { TimeSpan elapsed = m_timeProvider.GetElapsedTime(m_lastKeepAliveTimestamp); // add a guard band to allow for network lag. return TimeSpan.FromMilliseconds( (m_keepAliveInterval * m_keepAliveIntervalFactor) + m_keepAliveGuardBand) <= elapsed; } // another error was reported which caused keep alive to stop. return true; } } /// /// Gets the time of the last keep alive. /// public DateTime LastKeepAliveTime { get { long ticks = Interlocked.Read(ref m_lastKeepAliveTime); return new DateTime(ticks, DateTimeKind.Utc); } } /// /// Gets the monotonic timestamp (from ) of the /// last keep alive. Use together with /// to compute the elapsed time since the last keep alive without DateTime drift or /// 32-bit tick wrap. /// public long LastKeepAliveTimestamp => m_lastKeepAliveTimestamp; /// /// Gets the number of outstanding publish or keep alive requests. /// public int OutstandingRequestCount { get { lock (m_outstandingRequests) { return m_outstandingRequests.Count; } } } /// /// Gets the number of outstanding publish or keep alive requests which appear to be missing. /// public int DefunctRequestCount { get { lock (m_outstandingRequests) { int count = 0; for (LinkedListNode? ii = m_outstandingRequests.First; ii != null; ii = ii.Next) { if (ii.Value.Defunct) { count++; } } return count; } } } /// /// Gets the number of good outstanding publish requests. /// public int GoodPublishRequestCount { get { lock (m_outstandingRequests) { int count = 0; for (LinkedListNode? ii = m_outstandingRequests.First; ii != null; ii = ii.Next) { if (!ii.Value.Defunct && ii.Value.RequestTypeId == DataTypes.PublishRequest) { count++; } } return count; } } } /// /// Gets and sets the minimum number of publish requests to be used in the session. /// /// public int MinPublishRequestCount { get => m_engine.MinPublishRequestCount; set => m_engine.MinPublishRequestCount = value; } /// /// Gets and sets the maximum number of publish requests to be used in the session. /// /// public int MaxPublishRequestCount { get => m_engine.MaxPublishRequestCount; set => m_engine.MaxPublishRequestCount = value; } /// public ContinuationPointPolicy ContinuationPointPolicy { get; set; } = ContinuationPointPolicy.Default; /// public event RenewUserIdentityEventHandler RenewUserIdentity { add => m_RenewUserIdentity += value; remove => m_RenewUserIdentity -= value; } private event RenewUserIdentityEventHandler? m_RenewUserIdentity; /// public virtual void Snapshot(out SessionState state) { using Activity? activity = m_telemetry.StartActivity(); Snapshot(out SessionConfiguration configuration); // Snapshot subscription state var subscriptionStates = new List(SubscriptionCount); foreach (Subscription subscription in Subscriptions) { subscription.Snapshot(out SubscriptionState subscriptionState); subscriptionStates.Add(subscriptionState); } state = new SessionState(configuration) { Subscriptions = subscriptionStates }; } /// public virtual void Restore(SessionState state) { using Activity? activity = m_telemetry.StartActivity(); ThrowIfDisposed(); Restore((SessionConfiguration)state); if (state.Subscriptions.IsEmpty) { return; } foreach (SubscriptionState subscriptionState in state.Subscriptions) { // Restore subscription from state Subscription subscription = CreateSubscription(subscriptionState); subscription.Restore(subscriptionState); AddSubscription(subscription); } } /// public void Snapshot(out SessionConfiguration sessionConfiguration) { ByteString serverNonce = !m_serverNonce.IsEmpty ? m_serverNonce : default; ByteString clientNonce = m_clientNonce != null ? ByteString.From(m_clientNonce) : default; ByteString serverEccEphemeralKey = m_eccServerEphemeralKey?.Data != null ? ByteString.From([.. m_eccServerEphemeralKey.Data]) : default; sessionConfiguration = new SessionConfiguration { SessionName = SessionName, SessionId = SessionId, AuthenticationToken = AuthenticationToken, Identity = Identity, ConfiguredEndpoint = ConfiguredEndpoint, CheckDomain = CheckDomain, ServerNonce = serverNonce, ClientNonce = clientNonce, ServerEccEphemeralKey = serverEccEphemeralKey, UserIdentityTokenPolicy = m_userTokenSecurityPolicyUri }; } /// public void Restore(SessionConfiguration sessionConfiguration) { ThrowIfDisposed(); ByteString serverCertificate = m_endpoint.Description?.ServerCertificate ?? default; m_sessionName = sessionConfiguration.SessionName ?? "SessionName"; m_serverCertificate?.Dispose(); m_serverCertificate = !serverCertificate.IsEmpty ? Certificate.FromRawData(serverCertificate) : null; m_identity = sessionConfiguration.Identity ?? new UserIdentity(); m_checkDomain = sessionConfiguration.CheckDomain; m_serverNonce = sessionConfiguration.ServerNonce; m_clientNonce = !sessionConfiguration.ClientNonce.IsNull ? sessionConfiguration.ClientNonce.ToArray() : null; m_userTokenSecurityPolicyUri = sessionConfiguration.UserIdentityTokenPolicy; if (sessionConfiguration.ServerEccEphemeralKey.Length > 0) { string? ephemeralKeyPolicyUri = !string.IsNullOrEmpty(m_userTokenSecurityPolicyUri) ? m_userTokenSecurityPolicyUri : m_endpoint.Description?.SecurityPolicyUri ?? SecurityPolicies.None; SecurityPolicyInfo? ephemeralKeyPolicy = SecurityPolicies.GetInfo(ephemeralKeyPolicyUri!); m_eccServerEphemeralKey = Nonce.CreateNonce( ephemeralKeyPolicy!, sessionConfiguration.ServerEccEphemeralKey.ToArray()); } else { m_eccServerEphemeralKey = null; } lock (m_lock) { SessionCreated( sessionConfiguration.SessionId, sessionConfiguration.AuthenticationToken); } } /// public bool ApplySessionConfiguration(SessionConfiguration sessionConfiguration) { if (sessionConfiguration == null) { throw new ArgumentNullException(nameof(sessionConfiguration)); } Restore(sessionConfiguration); return true; } /// public SessionConfiguration SaveSessionConfiguration(Stream? stream = null) { Snapshot(out SessionConfiguration sessionConfiguration); if (stream != null) { IServiceMessageContext context = MessageContext ?? throw new InvalidOperationException("Missing service message context"); using var encoder = new BinaryEncoder( stream, context, true); encoder.WriteStringArray( null, context.NamespaceUris.ToArrayOf()); encoder.WriteStringArray( null, context.ServerUris.ToArrayOf()); sessionConfiguration.Encode(encoder); } return sessionConfiguration; } /// public virtual void Save( Stream stream, IEnumerable subscriptions, IEnumerable? knownTypes = null) { using Activity? activity = m_telemetry.StartActivity(); // Snapshot subscription state var subscriptionStates = new List(); foreach (Subscription subscription in subscriptions) { subscription.Snapshot(out SubscriptionState state); subscriptionStates.Add(state); } IServiceMessageContext context = MessageContext ?? throw new InvalidOperationException("Missing service message context"); using var encoder = new BinaryEncoder( stream, context, true); encoder.WriteStringArray( null, context.NamespaceUris.ToArrayOf()); encoder.WriteStringArray( null, context.ServerUris.ToArrayOf()); encoder.WriteInt32(null, subscriptionStates.Count); foreach (SubscriptionState state in subscriptionStates) { state.Encode(encoder); } } /// public virtual IEnumerable Load( Stream stream, bool transferSubscriptions = false, IEnumerable? knownTypes = null) { using Activity? activity = m_telemetry.StartActivity(); IServiceMessageContext context = MessageContext ?? throw new InvalidOperationException("Missing service message context"); using var decoder = new BinaryDecoder( stream, context, true); ArrayOf nsUris = decoder.ReadStringArray(null); ArrayOf serverUris = decoder.ReadStringArray(null); decoder.SetMappingTables( new NamespaceTable(nsUris.Memory.ToArray()!), new StringTable(serverUris.Memory.ToArray()!)); int count = decoder.ReadInt32(null); if (count <= 0) { return []; } var subscriptions = new SubscriptionCollection(count); for (int i = 0; i < count; i++) { var state = new SubscriptionState(); state.Decode(decoder); // Restore subscription from state Subscription subscription = CreateSubscription(state); subscription.Restore(state); if (!transferSubscriptions) { // ServerId must be reset if the saved list of subscriptions // is not used to transfer a subscription foreach (MonitoredItem monitoredItem in subscription.MonitoredItems) { monitoredItem.ServerId = 0; } } AddSubscription(subscription); subscriptions.Add(subscription); } return subscriptions; } /// public override bool Equals(object? obj) { if (ReferenceEquals(this, obj)) { return true; } if (obj is ISession session) { if (!m_endpoint.Equals(session.Endpoint)) { return false; } if (!m_sessionName.Equals(session.SessionName, StringComparison.Ordinal)) { return false; } if (!SessionId.Equals(session.SessionId)) { return false; } return true; } return false; } /// public override int GetHashCode() { return HashCode.Combine(m_endpoint, m_sessionName, SessionId); } /// /// An overrideable version of a session clone which is used /// internally to create new subclassed clones from a Session class. /// public virtual Session CloneSession(ITransportChannel channel, bool copyEventHandlers) { return new Session(channel, this, copyEventHandlers); } /// public async Task OpenAsync( string sessionName, uint sessionTimeout, IUserIdentity identity, ArrayOf preferredLocales, bool checkDomain, bool closeChannel, CancellationToken ct) { ThrowIfDisposed(); using Activity? activity = m_telemetry.StartActivity(); uint maxMessageSize = (uint?)MessageContext?.MaxMessageSize ?? throw ServiceResultException.Unexpected( "Transport channel is null or does not have a message context"); // Load certificate and chain if not already loaded. await LoadInstanceCertificateAsync(false, ct).ConfigureAwait(false); OpenValidateIdentity( ref identity, out UserTokenPolicy identityPolicy, out string securityPolicyUri, out bool requireEncryption); // validate the server certificate /certificate chain. IUserIdentityTokenHandler identityToken = identity.TokenHandler.Copy(); Certificate? serverCertificate = null; ByteString certificateData = m_endpoint.Description.ServerCertificate; if (certificateData.Length > 0) { using CertificateCollection serverCertificateChain = Utils.ParseCertificateChainBlob( certificateData, m_telemetry); if (serverCertificateChain.Count > 0) { serverCertificate = serverCertificateChain[0].AddRef(); } if (requireEncryption) { ICertificateValidatorEx validator = m_configuration.CertificateManager; CertificateValidationResult result = await validator .ValidateAsync(serverCertificateChain, ct: ct) .ConfigureAwait(false); if (!result.IsValid) { throw new ServiceResultException(result.StatusCode); } if (checkDomain && serverCertificateChain.Count > 0) { validator.ValidateDomains(serverCertificateChain[0], m_endpoint); } // save for reconnect m_checkDomain = checkDomain; } } // create a nonce. int length = m_configuration.SecurityConfiguration.NonceLength; m_clientNonce = Nonce.CreateRandomNonceData(length); var clientNonce = ByteString.From(m_clientNonce); // send the application instance certificate for the client. BuildCertificateData( out ByteString clientCertificateData, out ByteString clientCertificateChainData); var clientDescription = new ApplicationDescription { ApplicationUri = m_configuration.ApplicationUri, ApplicationName = LocalizedText.From(m_configuration.ApplicationName!), ApplicationType = ApplicationType.Client, ProductUri = m_configuration.ProductUri }; if (sessionTimeout == 0) { sessionTimeout = (uint)m_configuration.ClientConfiguration!.DefaultSessionTimeout; } // select the security policy for the user token. RequestHeader requestHeader = CreateRequestHeaderPerUserTokenPolicy( identityPolicy.SecurityPolicyUri); bool successCreateSession = false; CreateSessionResponse? response = null; // if security none, first try to connect without certificate if (m_endpoint.Description.SecurityPolicyUri == SecurityPolicies.None) { // first try to connect with client certificate NULL try { response = await base.CreateSessionAsync( null, clientDescription, m_endpoint.Description.Server.ApplicationUri, m_endpoint.EndpointUrl!.ToString(), sessionName, clientNonce, default, sessionTimeout, maxMessageSize, ct).ConfigureAwait(false); successCreateSession = true; } catch (Exception ex) when (ex is not OperationCanceledException) { m_logger.LogWarning(ex, "Create session failed with client certificate NULL."); successCreateSession = false; } } if (!successCreateSession) { response = await base.CreateSessionAsync( requestHeader, clientDescription, m_endpoint.Description.Server.ApplicationUri, m_endpoint.EndpointUrl!.ToString(), sessionName, clientNonce, clientCertificateChainData.IsEmpty ? clientCertificateData : clientCertificateChainData, sessionTimeout, maxMessageSize, ct).ConfigureAwait(false); } if (response is null || response.SessionId.IsNull) { throw ServiceResultException.Unexpected( "Create response returned null session id"); } NodeId sessionId = response.SessionId; NodeId sessionCookie = response.AuthenticationToken; ByteString serverNonce = response.ServerNonce; ByteString serverCertificateData = response.ServerCertificate; SignatureData serverSignature = response.ServerSignature; ArrayOf serverEndpoints = response.ServerEndpoints; m_sessionTimeout = response.RevisedSessionTimeout; m_maxRequestMessageSize = response.MaxRequestMessageSize; // save session id. lock (m_lock) { // save session id and cookie in base base.SessionCreated(sessionId, sessionCookie); } m_logger.LogInformation("Revised session timeout value: {SessionTimeout}.", m_sessionTimeout); m_logger.LogInformation( "Max response message size value: {MaxMessageSize}. Max request message size: {MaxRequestSize}", maxMessageSize, m_maxRequestMessageSize); // we need to call CloseSession if CreateSession was successful but some other exception is thrown try { // verify that the server returned the same instance certificate. ValidateServerCertificateData(serverCertificateData); ValidateServerEndpoints(serverEndpoints); ValidateServerCertificateApplicationUri(serverCertificate, m_endpoint); ValidateServerSignature( serverCertificate, serverSignature, clientCertificateData, clientCertificateChainData, m_clientNonce, serverNonce); // process additional header ProcessResponseAdditionalHeader(response.ResponseHeader, serverCertificate); // create the client signature. SecurityPolicyInfo? securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); // create the client signature. byte[] dataToSign = securityPolicy!.GetClientSignatureData( TransportChannel.ChannelThumbprint, serverNonce.ToArray(), serverCertificate?.RawData, TransportChannel.ServerChannelCertificate, TransportChannel.ClientChannelCertificate, m_clientNonce ?? []); SignatureData clientSignature = SecurityPolicies.CreateSignatureData( securityPolicyUri, m_instanceCertificate!, dataToSign); // select the security policy for the user token. string tokenSecurityPolicyUri = string.IsNullOrEmpty(identityPolicy.SecurityPolicyUri) ? m_endpoint.Description.SecurityPolicyUri ?? SecurityPolicies.None // identityPolicy.SecurityPolicyUri is non-empty here; legacy BCL targets // lack [NotNullWhen(false)] on string.IsNullOrEmpty. : identityPolicy.SecurityPolicyUri!; // validate server nonce and security parameters for user identity. ValidateServerNonce( identity, serverNonce, tokenSecurityPolicyUri, m_previousServerNonce, m_endpoint.Description.SecurityMode); // remember the policy actually used to encrypt the user token m_userTokenSecurityPolicyUri = tokenSecurityPolicyUri; SignatureData? userTokenSignature = null; if (identityToken.Token is X509IdentityToken) { // sign data with user token. dataToSign = securityPolicy.GetUserTokenSignatureData( TransportChannel.ChannelThumbprint, serverNonce.ToArray(), serverCertificate?.RawData, TransportChannel.ServerChannelCertificate, m_instanceCertificate?.RawData, TransportChannel.ClientChannelCertificate, m_clientNonce ?? []); userTokenSignature = await identityToken.SignAsync( dataToSign, tokenSecurityPolicyUri, ct).ConfigureAwait(false); } else { // encrypt token. await identityToken.EncryptAsync( serverCertificate!, serverNonce.ToArray(), tokenSecurityPolicyUri, MessageContext, m_eccServerEphemeralKey, m_instanceCertificate, m_instanceCertificateChain, m_endpoint.Description.SecurityMode != MessageSecurityMode.None, ct).ConfigureAwait(false); } // copy the preferred locales if provided. if (preferredLocales.Count > 0) { m_preferredLocales = preferredLocales; } RequestHeader? header = CreateRequestHeaderForActivateSession(tokenSecurityPolicyUri!); // activate session. ActivateSessionResponse activateResponse = await ActivateSessionAsync( header, clientSignature, [], m_preferredLocales, new ExtensionObject(identityToken.Token), userTokenSignature, ct) .ConfigureAwait(false); // process additional header ProcessResponseAdditionalHeader(activateResponse.ResponseHeader, serverCertificate); serverNonce = activateResponse.ServerNonce; ArrayOf certificateResults = activateResponse.Results; ArrayOf certificateDiagnosticInfos = activateResponse.DiagnosticInfos; for (int i = 0; i < certificateResults.Count; i++) { m_logger.LogInformation( "ActivateSession result[{Index}] = {Result}", i, certificateResults[i]); } // fetch namespaces. await FetchNamespaceTablesAsync(ct).ConfigureAwait(false); lock (m_lock) { // save nonces. m_sessionName = sessionName; m_identity = identity; m_previousServerNonce = m_serverNonce; m_serverNonce = serverNonce; m_serverCertificate?.Dispose(); m_serverCertificate = serverCertificate; // update system context. m_systemContext.PreferredLocales = m_preferredLocales; m_systemContext.SessionId = SessionId; m_systemContext.UserIdentity = identity; } // fetch operation limits await FetchOperationLimitsAsync(ct).ConfigureAwait(false); // start keep alive thread. await StartKeepAliveTimerAsync().ConfigureAwait(false); // raise event that session configuration changed. IndicateSessionConfigurationChanged(); } catch (Exception ex) { m_logger.LogError(ex, "Failed to activate session - closing."); try { await base.CloseSessionAsync(null, false, CancellationToken.None) .ConfigureAwait(false); } catch (Exception e) { m_logger.LogError( e, "Cleanup: CloseSessionAsync() or CloseChannelAsync() raised exception."); } finally { lock (m_lock) { SessionCreated(default, default); } } if (closeChannel) { await CloseChannelAsync(CancellationToken.None).ConfigureAwait(false); } serverCertificate?.Dispose(); throw; } } /// public Task ChangePreferredLocalesAsync( ArrayOf preferredLocales, CancellationToken ct) { return UpdateSessionAsync(Identity, preferredLocales, ct); } /// /// Refreshes the current user identity by asking a provider for a /// fresh token and reactivating the existing session with the /// current server nonce. /// /// The provider that materializes the new identity. /// A cancellation token. /// is null. /// /// BadIdentityTokenRejected when no offered policy can be /// satisfied by the provider, or BadIdentityChangeNotSupported /// when the server refuses user-identity changes on an active /// session. /// public async ValueTask UpdateIdentityAsync( IClientIdentityProvider provider, CancellationToken ct = default) { if (provider == null) { throw new ArgumentNullException(nameof(provider)); } ThrowIfDisposed(); using Activity? activity = m_telemetry.StartActivity(); await AcquireIdentityUpdateLockAsync(ct).ConfigureAwait(false); try { IdentitySelectionContext context; lock (m_lock) { if (!Connected) { throw new ServiceResultException( StatusCodes.BadInvalidState, "Not connected to server."); } context = new IdentitySelectionContext( m_endpoint.Description, m_endpoint.Description.UserIdentityTokens.ToArray() ?? [], MessageContext); } IUserIdentity identity = await provider.AcquireIdentityAsync(context, ct) .ConfigureAwait(false); await UpdateSessionAsync(identity, default, ct).ConfigureAwait(false); } catch (ServiceResultException ex) when (ex.StatusCode == StatusCodes.BadIdentityChangeNotSupported) { m_logger.LogInformation( ex, "Server rejected an in-session identity change for {SessionId}.", SessionId); throw; } finally { Reconnecting = false; m_reconnectLock.Release(); } } private async Task AcquireIdentityUpdateLockAsync(CancellationToken ct) { while (true) { await m_reconnectLock.WaitAsync(ct).ConfigureAwait(false); if (!Reconnecting) { Reconnecting = true; return; } m_reconnectLock.Release(); await m_timeProvider.Delay(TimeSpan.FromMilliseconds(100), ct).ConfigureAwait(false); } } /// public async Task UpdateSessionAsync( IUserIdentity? identity, ArrayOf preferredLocales, CancellationToken ct = default) { ThrowIfDisposed(); using Activity? activity = m_telemetry.StartActivity(); ByteString serverNonce = default; lock (m_lock) { // check connection state. if (!Connected) { throw new ServiceResultException( StatusCodes.BadInvalidState, "Not connected to server."); } // get current nonce. serverNonce = m_serverNonce; preferredLocales = preferredLocales.IsEmpty ? m_preferredLocales : preferredLocales; } // get the identity token. string securityPolicyUri = m_endpoint.Description.SecurityPolicyUri ?? SecurityPolicies.None; SecurityPolicyInfo? securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); // create the client signature. byte[] dataToSign = securityPolicy!.GetClientSignatureData( TransportChannel.ChannelThumbprint, serverNonce.ToArray(), m_serverCertificate?.RawData, TransportChannel.ServerChannelCertificate, TransportChannel.ClientChannelCertificate, m_clientNonce ?? []); SignatureData clientSignature = SecurityPolicies.CreateSignatureData( securityPolicyUri, m_instanceCertificate!, dataToSign); // choose a default token. identity ??= new UserIdentity(); // check that the user identity is supported by the endpoint. UserTokenPolicy? identityPolicy = m_endpoint.Description.FindUserTokenPolicy( identity.TokenHandler.Token.PolicyId!, securityPolicyUri) ?? m_endpoint.Description.FindUserTokenPolicy( identity.TokenType, identity.IssuedTokenType, securityPolicyUri) ?? throw ServiceResultException.Create( StatusCodes.BadIdentityTokenInvalid, "Endpoint does not support the user identity type provided."); // select the security policy for the user token. string tokenSecurityPolicyUri = string.IsNullOrEmpty(identityPolicy.SecurityPolicyUri) ? securityPolicyUri // identityPolicy.SecurityPolicyUri is non-empty here; legacy BCL targets // lack [NotNullWhen(false)] on string.IsNullOrEmpty. : identityPolicy.SecurityPolicyUri!; bool requireEncryption = tokenSecurityPolicyUri != SecurityPolicies.None; // validate the server certificate before encrypting tokens. if (m_serverCertificate != null && requireEncryption && identity.TokenType != UserTokenType.Anonymous) { ICertificateValidatorEx validator = m_configuration.CertificateManager; CertificateValidationResult result = await validator .ValidateAsync(m_serverCertificate, ct: ct) .ConfigureAwait(false); if (!result.IsValid) { throw new ServiceResultException(result.StatusCode); } } // validate server nonce and security parameters for user identity. ValidateServerNonce( identity, serverNonce, tokenSecurityPolicyUri, m_previousServerNonce, m_endpoint.Description.SecurityMode); // sign/encrypt with a disposable token handler copy to avoid mutating stored credentials. IUserIdentityTokenHandler identityToken = identity.TokenHandler.Copy(); identityToken.UpdatePolicy(identityPolicy); SignatureData? userTokenSignature = null; if (identityToken.Token is X509IdentityToken) { dataToSign = securityPolicy.GetUserTokenSignatureData( TransportChannel.ChannelThumbprint, serverNonce.ToArray(), m_serverCertificate?.RawData, TransportChannel.ServerChannelCertificate, m_instanceCertificate?.RawData, TransportChannel.ClientChannelCertificate, m_clientNonce ?? []); userTokenSignature = await identityToken.SignAsync( dataToSign, tokenSecurityPolicyUri, ct).ConfigureAwait(false); } else { // encrypt token. await identityToken.EncryptAsync( m_serverCertificate!, serverNonce.ToArray(), tokenSecurityPolicyUri, MessageContext, m_eccServerEphemeralKey, m_instanceCertificate, m_instanceCertificateChain, m_endpoint.Description.SecurityMode != MessageSecurityMode.None, ct).ConfigureAwait(false); } m_userTokenSecurityPolicyUri = tokenSecurityPolicyUri; RequestHeader? requestHeader = CreateRequestHeaderForActivateSession( tokenSecurityPolicyUri!); ActivateSessionResponse response = await ActivateSessionAsync( requestHeader, clientSignature, [], preferredLocales, new ExtensionObject(identityToken.Token), userTokenSignature, ct).ConfigureAwait(false); serverNonce = response.ServerNonce; ProcessResponseAdditionalHeader(response.ResponseHeader, m_serverCertificate); // save nonce and new values. lock (m_lock) { if (identity != null) { m_identity = identity; } m_previousServerNonce = m_serverNonce; m_serverNonce = serverNonce; m_preferredLocales = preferredLocales; // update system context. m_systemContext.PreferredLocales = m_preferredLocales; m_systemContext.SessionId = SessionId; m_systemContext.UserIdentity = identity; } IndicateSessionConfigurationChanged(); } /// public async Task RemoveSubscriptionAsync( Subscription subscription, CancellationToken ct = default) { ThrowIfDisposed(); using Activity? activity = m_telemetry.StartActivity(); if (subscription == null) { throw new ArgumentNullException(nameof(subscription)); } if (subscription.Created) { await subscription.DeleteAsync(false, ct).ConfigureAwait(false); } lock (m_lock) { if (!m_subscriptions.Remove(subscription)) { return false; } subscription.Session = null; } m_SubscriptionsChanged?.Invoke(this, EventArgs.Empty); return true; } /// public async Task RemoveSubscriptionsAsync( IEnumerable subscriptions, CancellationToken ct = default) { ThrowIfDisposed(); using Activity? activity = m_telemetry.StartActivity(); if (subscriptions == null) { throw new ArgumentNullException(nameof(subscriptions)); } var subscriptionsToDelete = new List(); bool removed = PrepareSubscriptionsToDelete(subscriptions, subscriptionsToDelete); foreach (Subscription subscription in subscriptionsToDelete) { await subscription.DeleteAsync(true, ct).ConfigureAwait(false); } if (removed) { m_SubscriptionsChanged?.Invoke(this, EventArgs.Empty); } return removed; } /// public async Task ReactivateSubscriptionsAsync( SubscriptionCollection subscriptions, bool sendInitialValues, CancellationToken ct = default) { ThrowIfDisposed(); using Activity? activity = m_telemetry.StartActivity(); ArrayOf subscriptionIds = CreateSubscriptionIdsForTransfer(subscriptions); int failedSubscriptions = 0; if (subscriptionIds.Count > 0) { bool reconnecting = false; await m_reconnectLock.WaitAsync(ct).ConfigureAwait(false); try { reconnecting = Reconnecting; Reconnecting = true; for (int ii = 0; ii < subscriptions.Count; ii++) { if (!await subscriptions[ii] .TransferAsync(this, subscriptionIds[ii], [], ct) .ConfigureAwait(false)) { m_logger.LogError( "SubscriptionId {SubscriptionId} failed to reactivate.", subscriptionIds[ii]); failedSubscriptions++; } } if (sendInitialValues) { try { ArrayOf resendResults = await this.ResendDataAsync( subscriptions.Select(s => s.Id).ToArrayOf(), ct).ConfigureAwait(false); for (int ii = 0; ii < resendResults.Count; ii++) { // no need to try for subscriptions which do not exist if (StatusCode.IsNotGood(resendResults[ii].StatusCode)) { m_logger.LogError( "SubscriptionId {SubscriptionId} failed to resend data.", subscriptionIds[ii]); } } } catch (ServiceResultException sre) { m_logger.LogError(sre, "Failed to call resend data for subscriptions."); } } m_logger.LogInformation( "Session REACTIVATE of {Count} subscriptions completed. {FailCount} failed.", subscriptions.Count, failedSubscriptions); } finally { Reconnecting = reconnecting; m_reconnectLock.Release(); } StartPublishing(OperationTimeout, true); } else { m_logger.LogInformation("No subscriptions. TransferSubscription skipped."); } return failedSubscriptions == 0; } /// public async Task TransferSubscriptionsAsync( SubscriptionCollection subscriptions, bool sendInitialValues, CancellationToken ct) { using Activity? activity = m_telemetry.StartActivity(); ArrayOf subscriptionIds = CreateSubscriptionIdsForTransfer(subscriptions); int failedSubscriptions = 0; if (subscriptionIds.Count > 0) { bool reconnecting = false; await m_reconnectLock.WaitAsync(ct).ConfigureAwait(false); try { reconnecting = Reconnecting; Reconnecting = true; TransferSubscriptionsResponse response = await base.TransferSubscriptionsAsync( null, subscriptionIds, sendInitialValues, ct) .ConfigureAwait(false); ArrayOf results = response.Results; ArrayOf diagnosticInfos = response.DiagnosticInfos; ResponseHeader responseHeader = response.ResponseHeader; if (!StatusCode.IsGood(responseHeader.ServiceResult)) { m_logger.LogError( "TransferSubscription failed: {ServiceResult}", responseHeader.ServiceResult); return false; } ValidateResponse(results, subscriptionIds); ValidateDiagnosticInfos(diagnosticInfos, subscriptionIds); for (int ii = 0; ii < subscriptions.Count; ii++) { if (StatusCode.IsGood(results[ii].StatusCode)) { if (await subscriptions[ii].TransferAsync( this, subscriptionIds[ii], results[ii].AvailableSequenceNumbers, ct) .ConfigureAwait(false)) { // create ack for available sequence numbers foreach (uint sequenceNumber in results[ii] .AvailableSequenceNumbers) { if (m_engine is ClassicSubscriptionEngine classicEngine) { classicEngine.AddPendingAcknowledgement( subscriptionIds[ii], sequenceNumber); } } } else { m_logger.LogInformation( "SubscriptionId {SubscriptionId} could not be moved to session.", subscriptionIds[ii]); failedSubscriptions++; } } else if (results[ii].StatusCode == StatusCodes.BadNothingToDo) { m_logger.LogInformation( "SubscriptionId {SubscriptionId} is already member of the session.", subscriptionIds[ii]); failedSubscriptions++; } else { m_logger.LogError( "SubscriptionId {SubscriptionId} failed to transfer, StatusCode={StatusCode}", subscriptionIds[ii], results[ii].StatusCode); failedSubscriptions++; } } } catch (Exception ex) { m_logger.LogError(ex, "Session TRANSFER ASYNC of {Count} subscriptions Failed due to unexpected Exception", subscriptions.Count); failedSubscriptions++; } finally { Reconnecting = reconnecting; m_reconnectLock.Release(); } StartPublishing(OperationTimeout, false); } else { m_logger.LogInformation("No subscriptions. TransferSubscription skipped."); } return failedSubscriptions == 0; } /// public async Task FetchNamespaceTablesAsync(CancellationToken ct = default) { using Activity? activity = m_telemetry.StartActivity(); ArrayOf nodesToRead = PrepareNamespaceTableNodesToRead(); // read from server. ReadResponse response = await ReadAsync( null, 0, TimestampsToReturn.Neither, nodesToRead, ct) .ConfigureAwait(false); ArrayOf values = response.Results; ArrayOf diagnosticInfos = response.DiagnosticInfos; ResponseHeader responseHeader = response.ResponseHeader; ValidateResponse(values, nodesToRead); ValidateDiagnosticInfos(diagnosticInfos, nodesToRead); UpdateNamespaceTable(values, diagnosticInfos, responseHeader); } /// public async Task FetchTypeTreeAsync(ExpandedNodeId typeId, CancellationToken ct = default) { using Activity? activity = m_telemetry.StartActivity(); if (await NodeCache.FindAsync(typeId, ct).ConfigureAwait(false) is Node node) { var subTypes = new List(); foreach (IReference reference in node.Find(ReferenceTypeIds.HasSubtype, false)) { subTypes.Add(reference.TargetId); } if (subTypes.Count > 0) { await FetchTypeTreeAsync(subTypes, ct).ConfigureAwait(false); } } } /// public async Task FetchTypeTreeAsync( ArrayOf typeIds, CancellationToken ct = default) { using Activity? activity = m_telemetry.StartActivity(); ArrayOf referenceTypeIds = [ReferenceTypeIds.HasSubtype]; ArrayOf nodes = await NodeCache .FindReferencesAsync(typeIds, referenceTypeIds, false, false, ct) .ConfigureAwait(false); var subTypes = new List(); foreach (INode inode in nodes) { if (inode is Node node) { foreach (IReference reference in node.Find(ReferenceTypeIds.HasSubtype, false)) { if (!typeIds.Contains(reference.TargetId)) { subTypes.Add(reference.TargetId); } } } } if (subTypes.Count > 0) { await FetchTypeTreeAsync(subTypes.ToArrayOf(), ct).ConfigureAwait(false); } } /// public async Task FetchOperationLimitsAsync(CancellationToken ct) { using Activity? activity = m_telemetry.StartActivity(); // Helper extraction static uint GetUInt32(ref int index, ArrayOf values, ArrayOf errors) { DataValue value = values[index]; ServiceResult error = errors.Count > 0 ? errors[index] : ServiceResult.Good; index++; return ServiceResult.IsNotBad(error) ? value.WrappedValue.GetUInt32() : default; } static ushort GetUInt16(ref int index, ArrayOf values, ArrayOf errors) { DataValue value = values[index]; ServiceResult error = errors.Count > 0 ? errors[index] : ServiceResult.Good; index++; return ServiceResult.IsNotBad(error) ? value.WrappedValue.GetUInt16() : default; } static double GetDouble(ref int index, ArrayOf values, ArrayOf errors) { DataValue value = values[index]; ServiceResult error = errors.Count > 0 ? errors[index] : ServiceResult.Good; index++; return ServiceResult.IsNotBad(error) ? value.WrappedValue.GetDouble() : default; } // Apply operation limit logic: if client value is 0, use server value; // otherwise use min(client, server) static uint ApplyOperationLimit(uint clientLimit, uint serverLimit) { if (clientLimit == 0) { return serverLimit; } if (serverLimit == 0) { return clientLimit; } return Math.Min(clientLimit, serverLimit); } // First we read the node read max to optimize the second read. ArrayOf nodeIds = [ VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerRead ]; (ArrayOf values, ArrayOf errors) = await this.ReadValuesAsync(nodeIds, ct).ConfigureAwait(false); int index = 0; OperationLimits.MaxNodesPerRead = ApplyOperationLimit( OperationLimits.MaxNodesPerRead, GetUInt32(ref index, values, errors)); nodeIds = [ VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerHistoryReadData, VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerHistoryReadEvents, VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerWrite, VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerRead, VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerHistoryUpdateData, VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerHistoryUpdateEvents, VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerMethodCall, VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerBrowse, VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerRegisterNodes, VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerNodeManagement, VariableIds.Server_ServerCapabilities_OperationLimits_MaxMonitoredItemsPerCall, VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerTranslateBrowsePathsToNodeIds, VariableIds.Server_ServerCapabilities_MaxBrowseContinuationPoints, VariableIds.Server_ServerCapabilities_MaxHistoryContinuationPoints, VariableIds.Server_ServerCapabilities_MaxQueryContinuationPoints, VariableIds.Server_ServerCapabilities_MaxStringLength, VariableIds.Server_ServerCapabilities_MaxArrayLength, VariableIds.Server_ServerCapabilities_MaxByteStringLength, VariableIds.Server_ServerCapabilities_MinSupportedSampleRate, VariableIds.Server_ServerCapabilities_MaxSessions, VariableIds.Server_ServerCapabilities_MaxSubscriptions, VariableIds.Server_ServerCapabilities_MaxMonitoredItems, VariableIds.Server_ServerCapabilities_MaxMonitoredItemsPerSubscription, VariableIds.Server_ServerCapabilities_MaxMonitoredItemsQueueSize, VariableIds.Server_ServerCapabilities_MaxSubscriptionsPerSession, VariableIds.Server_ServerCapabilities_MaxWhereClauseParameters, VariableIds.Server_ServerCapabilities_MaxSelectClauseParameters ]; (values, errors) = await this.ReadValuesAsync(nodeIds, ct).ConfigureAwait(false); index = 0; OperationLimits.MaxNodesPerHistoryReadData = ApplyOperationLimit( OperationLimits.MaxNodesPerHistoryReadData, GetUInt32(ref index, values, errors)); OperationLimits.MaxNodesPerHistoryReadEvents = ApplyOperationLimit( OperationLimits.MaxNodesPerHistoryReadEvents, GetUInt32(ref index, values, errors)); OperationLimits.MaxNodesPerWrite = ApplyOperationLimit( OperationLimits.MaxNodesPerWrite, GetUInt32(ref index, values, errors)); OperationLimits.MaxNodesPerRead = ApplyOperationLimit( OperationLimits.MaxNodesPerRead, GetUInt32(ref index, values, errors)); OperationLimits.MaxNodesPerHistoryUpdateData = ApplyOperationLimit( OperationLimits.MaxNodesPerHistoryUpdateData, GetUInt32(ref index, values, errors)); OperationLimits.MaxNodesPerHistoryUpdateEvents = ApplyOperationLimit( OperationLimits.MaxNodesPerHistoryUpdateEvents, GetUInt32(ref index, values, errors)); OperationLimits.MaxNodesPerMethodCall = ApplyOperationLimit( OperationLimits.MaxNodesPerMethodCall, GetUInt32(ref index, values, errors)); OperationLimits.MaxNodesPerBrowse = ApplyOperationLimit( OperationLimits.MaxNodesPerBrowse, GetUInt32(ref index, values, errors)); OperationLimits.MaxNodesPerRegisterNodes = ApplyOperationLimit( OperationLimits.MaxNodesPerRegisterNodes, GetUInt32(ref index, values, errors)); OperationLimits.MaxNodesPerNodeManagement = ApplyOperationLimit( OperationLimits.MaxNodesPerNodeManagement, GetUInt32(ref index, values, errors)); OperationLimits.MaxMonitoredItemsPerCall = ApplyOperationLimit( OperationLimits.MaxMonitoredItemsPerCall, GetUInt32(ref index, values, errors)); OperationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds = ApplyOperationLimit( OperationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds, GetUInt32(ref index, values, errors)); ServerCapabilities.MaxBrowseContinuationPoints = GetUInt16(ref index, values, errors); ServerCapabilities.MaxHistoryContinuationPoints = GetUInt16(ref index, values, errors); ServerCapabilities.MaxQueryContinuationPoints = GetUInt16(ref index, values, errors); ServerCapabilities.MaxStringLength = GetUInt32(ref index, values, errors); ServerCapabilities.MaxArrayLength = GetUInt32(ref index, values, errors); ServerCapabilities.MaxByteStringLength = GetUInt32(ref index, values, errors); ServerCapabilities.MinSupportedSampleRate = GetDouble(ref index, values, errors); ServerCapabilities.MaxSessions = GetUInt32(ref index, values, errors); ServerCapabilities.MaxSubscriptions = GetUInt32(ref index, values, errors); ServerCapabilities.MaxMonitoredItems = GetUInt32(ref index, values, errors); ServerCapabilities.MaxMonitoredItemsPerSubscription = GetUInt32(ref index, values, errors); ServerCapabilities.MaxMonitoredItemsQueueSize = GetUInt32(ref index, values, errors); ServerCapabilities.MaxSubscriptionsPerSession = GetUInt32(ref index, values, errors); ServerCapabilities.MaxWhereClauseParameters = GetUInt32(ref index, values, errors); ServerCapabilities.MaxSelectClauseParameters = GetUInt32(ref index, values, errors); uint maxByteStringLength = (uint?)m_configuration.TransportQuotas?.MaxByteStringLength ?? 0u; if (maxByteStringLength != 0 && (ServerCapabilities.MaxByteStringLength == 0 || ServerCapabilities.MaxByteStringLength > maxByteStringLength)) { ServerCapabilities.MaxByteStringLength = maxByteStringLength; } } /// /// Recreates a session based on a specified template. /// /// Cancellation Token to cancel operation with /// The new session object. protected internal async Task RecreateAsync( CancellationToken ct = default) { ServiceMessageContext messageContext = m_configuration .CreateMessageContext(Factory); // The channel takes ownership of the cert and chain. AddRef so the // original Session retains its references for its own lifetime. CertificateCollection? channelChain = m_configuration.SecurityConfiguration.SendCertificateChain ? m_instanceCertificateChain?.AddRef() : null; // create the channel object used to connect to the server. ITransportChannel channel = await UaChannelBase.CreateUaBinaryChannelAsync( m_configuration, ConfiguredEndpoint.Description, ConfiguredEndpoint.Configuration!, m_instanceCertificate?.AddRef(), channelChain, messageContext, ct).ConfigureAwait(false); // create the session object. Session session = CloneSession(channel, true); try { session.RecreateRenewUserIdentity(); UserIdentity? tempIdentity = session.Identity == null ? new UserIdentity() : null; // open the session. await session .OpenAsync( SessionName, (uint)SessionTimeout, session.Identity ?? tempIdentity!, PreferredLocales, m_checkDomain, ct) .ConfigureAwait(false); await session.RecreateSubscriptionsAsync( TransferSubscriptionsOnReconnect, Subscriptions, ct).ConfigureAwait(false); } catch (Exception e) { session.Dispose(); ThrowCouldNotRecreateSessionException(e, SessionName); } return session; } /// /// Recreates a session based on a specified template. /// /// The waiting reverse connection. /// Cancellation token to cancel operation with /// The new session object. protected internal async Task RecreateAsync( ITransportWaitingConnection connection, CancellationToken ct = default) { ServiceMessageContext messageContext = m_configuration .CreateMessageContext(Factory); // The channel takes ownership of the cert and chain. AddRef so the // original Session retains its references for its own lifetime. CertificateCollection? channelChain = m_configuration.SecurityConfiguration.SendCertificateChain ? m_instanceCertificateChain?.AddRef() : null; // create the channel object used to connect to the server. ITransportChannel channel = await UaChannelBase.CreateUaBinaryChannelAsync( m_configuration, connection, ConfiguredEndpoint.Description, ConfiguredEndpoint.Configuration!, m_instanceCertificate?.AddRef(), channelChain, messageContext, ct).ConfigureAwait(false); // create the session object. Session session = CloneSession(channel, true); try { session.RecreateRenewUserIdentity(); UserIdentity? tempIdentity = session.Identity == null ? new UserIdentity() : null; // open the session. await session .OpenAsync( SessionName, (uint)SessionTimeout, session.Identity ?? tempIdentity!, PreferredLocales, CheckDomain, ct) .ConfigureAwait(false); await session.RecreateSubscriptionsAsync( TransferSubscriptionsOnReconnect, Subscriptions, ct).ConfigureAwait(false); } catch (Exception e) { session.Dispose(); ThrowCouldNotRecreateSessionException(e, SessionName); } return session; } /// /// Recreates a session based on a specified template using the provided channel. /// /// The waiting reverse connection. /// Cancellation token to cancel the operation with /// The new session object. protected internal async Task RecreateAsync( ITransportChannel transportChannel, CancellationToken ct = default) { if (transportChannel == null) { return await RecreateAsync(ct).ConfigureAwait(false); } // create the session object. Session session = CloneSession(transportChannel, true); try { session.RecreateRenewUserIdentity(); // open the session. await session .OpenAsync( SessionName, (uint)SessionTimeout, session.Identity ?? new UserIdentity(), PreferredLocales, CheckDomain, false, ct) .ConfigureAwait(false); // create the subscriptions. await session.RecreateSubscriptionsAsync( false, Subscriptions, ct).ConfigureAwait(false); } catch (Exception e) { session.Dispose(); ThrowCouldNotRecreateSessionException(e, SessionName); } return session; } /// /// Re-establishes the session in place on the same /// instance instead of allocating a new one. /// Tears down the current channel, optionally switches to a new /// , and re-runs the /// CreateSession/ActivateSession sequence so the /// server assigns a fresh session id. The /// in-memory subscription engine (), /// subscription manager, and per-subscription contexts continue /// to reference this; once the new session is activated /// the V2 engine is given a chance to drive transfer-or-recreate /// against the new session id, and the /// classic template-based recreate runs for V1 subscriptions /// owned by . /// /// /// Designed to be the production wiring used by /// ManagedSession.HandleFailoverAsync so that the inner /// reference stays stable across a /// failover. Differs from by /// performing a full CreateSession against the server /// (new server-side session id) instead of just re-activating /// the existing one. Differs from /// overloads in /// that the receiver is reused rather than cloned into a new /// instance. /// /// Optional replacement endpoint. When /// non-null and not the same instance as the current /// the new endpoint is adopted /// before the channel is rebuilt. /// Optional waiting reverse /// connection. Mutually exclusive with /// . /// Optional pre-built transport channel. /// Mutually exclusive with . When /// neither is supplied a new outbound channel is built against /// . /// Cancellation token. /// protected internal async Task RecreateInPlaceAsync( ConfiguredEndpoint? endpoint = null, ITransportWaitingConnection? connection = null, ITransportChannel? channel = null, CancellationToken ct = default) { ThrowIfDisposed(); using Activity? activity = m_telemetry.StartActivity(); NodeId previousSessionId = SessionId; // Quiesce the V2 engine outside the reconnect lock so // workers can complete their current cycle without // contending with the reset path. The classic engine has // different publish semantics and does not need a drain. #if OPCUA_V1_CLIENT if (m_engine is not ClassicSubscriptionEngine) #endif { m_engine.PausePublishing(); if (m_engine is DefaultSubscriptionEngine v2Engine && v2Engine.SubscriptionManager is Subscriptions.SubscriptionManager v2Manager) { await v2Manager.DrainAsync(ct).ConfigureAwait(false); } } bool resetReconnect = false; await m_reconnectLock.WaitAsync(ct).ConfigureAwait(false); try { if (Reconnecting) { m_reconnectLock.Release(); throw ServiceResultException.Create( StatusCodes.BadInvalidState, "Session is already attempting to reconnect."); } Reconnecting = true; resetReconnect = true; m_reconnectLock.Release(); m_logger.LogInformation( "Session RECREATE-IN-PLACE {SessionId} starting...", previousSessionId); await StopKeepAliveTimerAsync().ConfigureAwait(false); // need to refresh the identity (reprompt for password, // refresh token). RecreateRenewUserIdentity(); // Switch endpoint before the channel is built. if (endpoint != null && !ReferenceEquals(endpoint, m_endpoint)) { m_endpoint = endpoint; m_effectiveEndpoint = endpoint; } // Build / install the new transport channel. ITransportChannel newChannel; if (channel != null) { newChannel = channel; } else { ServiceMessageContext messageContext = m_configuration .CreateMessageContext(Factory); if (connection != null) { newChannel = await UaChannelBase .CreateUaBinaryChannelAsync( m_configuration, connection, m_endpoint.Description, m_endpoint.Configuration!, m_instanceCertificate?.AddRef(), m_configuration.SecurityConfiguration .SendCertificateChain ? m_instanceCertificateChain?.AddRef() : null, messageContext, ct) .ConfigureAwait(false); } else { newChannel = await UaChannelBase .CreateUaBinaryChannelAsync( m_configuration, m_endpoint.Description, m_endpoint.Configuration!, m_instanceCertificate?.AddRef(), m_configuration.SecurityConfiguration .SendCertificateChain ? m_instanceCertificateChain?.AddRef() : null, messageContext, ct) .ConfigureAwait(false); } } TransportChannel = newChannel; // Clear server-assigned identity so OpenAsync below // performs a full CreateSession rather than a re- // activation of the (now invalid) prior session id. lock (m_lock) { SessionCreated(default, default); } // Reset the reconnecting flag so OpenAsync (which // checks/raises its own state) can proceed. await m_reconnectLock.WaitAsync(ct).ConfigureAwait(false); Reconnecting = false; resetReconnect = false; m_reconnectLock.Release(); UserIdentity? tempIdentity = m_identity == null ? new UserIdentity() : null; await OpenAsync( m_sessionName, (uint)m_sessionTimeout, m_identity ?? tempIdentity!, m_preferredLocales, m_checkDomain, true, ct) .ConfigureAwait(false); #if OPCUA_V1_CLIENT // V1: drive the classic template-based recreate using // the subscriptions still attached to this Session. await RecreateSubscriptionsAsync( TransferSubscriptionsOnReconnect, Subscriptions, ct) .ConfigureAwait(false); #endif // V2: hand the previous session id to the engine so // configured subscriptions can attempt transfer or // fall back to recreate against the new session id. await m_engine.RecreateSubscriptionsAsync( previousSessionId, ct) .ConfigureAwait(false); m_logger.LogInformation( "Session RECREATE-IN-PLACE completed: " + "{OldSessionId} -> {NewSessionId}", previousSessionId, SessionId); } catch (Exception ex) { m_logger.LogError( ex, "Session RECREATE-IN-PLACE {SessionId} failed.", previousSessionId); throw; } finally { if (resetReconnect) { await m_reconnectLock .WaitAsync(CancellationToken.None) .ConfigureAwait(false); Reconnecting = false; m_reconnectLock.Release(); } #if OPCUA_V1_CLIENT if (m_engine is not ClassicSubscriptionEngine) #endif { m_engine.ResumePublishing(); } } } /// public override Task CloseAsync(CancellationToken ct = default) { return CloseAsync(m_keepAliveInterval, true, ct); } /// public virtual async Task CloseAsync( int timeout, bool closeChannel, CancellationToken ct = default) { // check if already called. if (Disposed) { return StatusCodes.Good; } if (Closing) { return StatusCodes.Bad; } StatusCode result = StatusCodes.Good; Closing = true; using Activity? activity = m_telemetry.StartActivity(); try { // stop the keep alive timer. await StopKeepAliveTimerAsync().ConfigureAwait(false); // check if correctly connected. bool connected = Connected; // halt all background threads. if (connected && m_SessionClosing != null) { try { m_SessionClosing(this, EventArgs.Empty); } catch (Exception e) { m_logger.LogError(e, "Session: Unexpected error raising SessionClosing event."); } } // close the session with the server. if (connected) { try { // Wait for or cancel outstanding publish requests before closing session. await WaitForOrCancelOutstandingPublishRequestsAsync(ct).ConfigureAwait(false); // close the session and delete all subscriptions if specified. var requestHeader = new RequestHeader { TimeoutHint = timeout > 0 ? (uint)timeout : (uint)(OperationTimeout > 0 ? OperationTimeout : 0) }; CloseSessionResponse response = await base.CloseSessionAsync( requestHeader, DeleteSubscriptionsOnClose, ct).ConfigureAwait(false); } // don't throw errors on disconnect, but return them // so the caller can log the error. catch (ServiceResultException sre) { m_logger.LogDebug(sre, "Error closing session during Close."); result = sre.StatusCode; } catch (Exception e1) { m_logger.LogDebug(e1, "Error closing session during Close."); result = StatusCodes.Bad; } finally { if (closeChannel) { try { await CloseChannelAsync(ct).ConfigureAwait(false); } catch (Exception e2) { m_logger.LogDebug(e2, "Error closing channel during Close"); } } // raised notification indicating the session is closed. lock (m_lock) { SessionCreated(default, default); } } } // clean up. if (closeChannel) { Dispose(); } return result; } finally { Closing = false; } } /// public async Task ReloadInstanceCertificateAsync(CancellationToken ct = default) { ThrowIfDisposed(); using Activity? activity = m_telemetry.StartActivity(); await m_reconnectLock.WaitAsync(ct).ConfigureAwait(false); try { // Force reload m_instanceCertificate?.Dispose(); m_instanceCertificate = null; await LoadInstanceCertificateAsync(false, ct).ConfigureAwait(false); } finally { m_reconnectLock.Release(); } } /// public async Task ReconnectAsync( ITransportWaitingConnection? connection, ITransportChannel? channel, CancellationToken ct) { ThrowIfDisposed(); using Activity? activity = m_telemetry.StartActivity(); bool resetReconnect = false; await m_reconnectLock.WaitAsync(ct).ConfigureAwait(false); try { bool reconnecting = Reconnecting; Reconnecting = true; resetReconnect = true; m_reconnectLock.Release(); // check if already connecting. if (reconnecting) { m_logger.LogWarning("Session is already attempting to reconnect."); throw ServiceResultException.Create( StatusCodes.BadInvalidState, "Session is already attempting to reconnect."); } m_logger.LogInformation("Session RECONNECT {SessionId} starting...", SessionId); await StopKeepAliveTimerAsync().ConfigureAwait(false); // need to refresh the identity (reprompt for password, refresh token). RecreateRenewUserIdentity(); // // It is possible the session was created and a previous configuration was // applied, then reconnect can be called even though we are not connected // But while valid we also want to check that otherwise the endpoint was not // changed. // await LoadInstanceCertificateAsync(true, ct).ConfigureAwait(false); string securityPolicyUri = m_endpoint.Description.SecurityPolicyUri ?? SecurityPolicies.None; SecurityPolicyInfo? securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri); EndpointDescription endpoint = m_endpoint.Description; // check that the user identity is supported by the endpoint. UserTokenPolicy? identityPolicy = endpoint.FindUserTokenPolicy( m_identity.TokenType, m_identity.IssuedTokenType, m_userTokenSecurityPolicyUri ?? endpoint.SecurityPolicyUri!); if (identityPolicy == null) { m_logger.LogError( "Reconnect: Endpoint does not support the user identity type provided."); throw ServiceResultException.Create( StatusCodes.BadIdentityTokenInvalid, "Endpoint does not support the user identity type provided."); } // select the security policy for the user token. string tokenSecurityPolicyUri = string.IsNullOrEmpty(identityPolicy.SecurityPolicyUri) ? endpoint.SecurityPolicyUri ?? SecurityPolicies.None // identityPolicy.SecurityPolicyUri is non-empty here; legacy BCL targets // lack [NotNullWhen(false)] on string.IsNullOrEmpty. : identityPolicy.SecurityPolicyUri!; if (m_userTokenSecurityPolicyUri == null) { m_userTokenSecurityPolicyUri = tokenSecurityPolicyUri; } else { tokenSecurityPolicyUri = m_userTokenSecurityPolicyUri; } // validate server nonce and security parameters for user identity. ValidateServerNonce( m_identity, m_serverNonce, m_userTokenSecurityPolicyUri, m_previousServerNonce, m_endpoint.Description.SecurityMode); // sign/encrypt with a disposable token handler copy to avoid mutating stored credentials. IUserIdentityTokenHandler identityToken = m_identity.TokenHandler.Copy(); identityToken.UpdatePolicy(identityPolicy); m_logger.LogInformation("Session REPLACING channel for {SessionId}.", SessionId); if (connection != null) { ITransportChannel? transportChannel = NullableTransportChannel; // check if the channel supports reconnect. if (transportChannel != null && (transportChannel.SupportedFeatures & TransportChannelFeatures.Reconnect) != 0) { await transportChannel.ReconnectAsync(connection, ct).ConfigureAwait(false); } else { // initialize the channel which will be created with the server. transportChannel = await UaChannelBase.CreateUaBinaryChannelAsync( m_configuration, connection, m_endpoint.Description, m_endpoint.Configuration!, m_instanceCertificate?.AddRef(), m_configuration.SecurityConfiguration.SendCertificateChain ? m_instanceCertificateChain?.AddRef() : null, MessageContext, ct).ConfigureAwait(false); // disposes the existing channel. TransportChannel = transportChannel; } } else if (channel != null) { TransportChannel = channel; } else { ITransportChannel? transportChannel = NullableTransportChannel; // check if the channel supports reconnect. if (transportChannel != null && (transportChannel.SupportedFeatures & TransportChannelFeatures.Reconnect) != 0) { await transportChannel.ReconnectAsync(ct: ct).ConfigureAwait(false); } else { // initialize the channel which will be created with the server. transportChannel = await UaChannelBase.CreateUaBinaryChannelAsync( m_configuration, m_endpoint.Description, m_endpoint.Configuration!, m_instanceCertificate?.AddRef(), m_configuration.SecurityConfiguration.SendCertificateChain ? m_instanceCertificateChain?.AddRef() : null, MessageContext, ct).ConfigureAwait(false); // disposes the existing channel. TransportChannel = transportChannel; } } ITransportChannel activeChannel = TransportChannel; byte[] channelThumbprint = activeChannel.ChannelThumbprint; byte[] serverChannelCertificate = activeChannel.ServerChannelCertificate; byte[] clientChannelCertificate = activeChannel.ClientChannelCertificate; // HTTPS channels populate the server TLS certificate lazily after the first request. // Reconnect signs before that, so use the server certificate from the session state. if ((serverChannelCertificate == null || serverChannelCertificate.Length == 0) && m_serverCertificate != null) { serverChannelCertificate = m_serverCertificate.RawData; } if ((clientChannelCertificate == null || clientChannelCertificate.Length == 0) && m_instanceCertificate != null) { clientChannelCertificate = m_instanceCertificate.RawData; } // create the client signature. byte[] dataToSign = securityPolicy!.GetClientSignatureData( channelThumbprint, m_serverNonce.ToArray(), m_serverCertificate?.RawData, serverChannelCertificate, clientChannelCertificate, m_clientNonce ?? []); SignatureData clientSignature = SecurityPolicies.CreateSignatureData( endpoint.SecurityPolicyUri!, m_instanceCertificate!, dataToSign); dataToSign = securityPolicy.GetUserTokenSignatureData( channelThumbprint, m_serverNonce.ToArray(), m_serverCertificate?.RawData, serverChannelCertificate, m_instanceCertificate?.RawData, clientChannelCertificate, m_clientNonce ?? []); SignatureData? userTokenSignature = null; if (identityToken.Token is X509IdentityToken) { userTokenSignature = await identityToken.SignAsync( dataToSign, tokenSecurityPolicyUri, ct).ConfigureAwait(false); } else { // encrypt token. await identityToken.EncryptAsync( m_serverCertificate!, m_serverNonce.ToArray(), tokenSecurityPolicyUri, MessageContext, m_eccServerEphemeralKey, m_instanceCertificate, m_instanceCertificateChain, m_endpoint.Description.SecurityMode != MessageSecurityMode.None, ct).ConfigureAwait(false); } m_logger.LogInformation("Session RE-ACTIVATING {SessionId}.", SessionId); RequestHeader header = CreateRequestHeaderForActivateSession( tokenSecurityPolicyUri!) ?? new RequestHeader(); header.TimeoutHint = kReconnectTimeout; using CancellationTokenSource timeoutCts = m_timeProvider .CreateCancellationTokenSource(TimeSpan.FromMilliseconds(kReconnectTimeout / 2)); using var timeout = CancellationTokenSource.CreateLinkedTokenSource( ct, timeoutCts.Token); try { // reactivate session. ActivateSessionResponse activateResult = await ActivateSessionAsync( header, clientSignature, [], m_preferredLocales, new ExtensionObject(identityToken.Token), userTokenSignature, timeout.Token).ConfigureAwait(false); ByteString serverNonce = activateResult.ServerNonce; ArrayOf certificateResults = activateResult.Results; ArrayOf certificateDiagnosticInfos = activateResult.DiagnosticInfos; m_logger.LogInformation("Session RECONNECT {SessionId} completed successfully.", SessionId); lock (m_lock) { m_previousServerNonce = m_serverNonce; m_serverNonce = serverNonce; } await m_reconnectLock.WaitAsync(ct).ConfigureAwait(false); Reconnecting = false; resetReconnect = false; m_reconnectLock.Release(); StartPublishing(OperationTimeout, true); await StartKeepAliveTimerAsync().ConfigureAwait(false); IndicateSessionConfigurationChanged(); } catch (OperationCanceledException) when (timeout.IsCancellationRequested && !ct.IsCancellationRequested) { var error = ServiceResult.Create( StatusCodes.BadRequestTimeout, "ACTIVATE SESSION timed out. {0}/{1}", GoodPublishRequestCount, OutstandingRequestCount); m_logger.LogWarning( "ACTIVATE SESSION ASYNC timed out. {GoodRequestCount}/{OutstandingRequestCount}", GoodPublishRequestCount, OutstandingRequestCount); throw new ServiceResultException(error); } } finally { if (resetReconnect) { await m_reconnectLock.WaitAsync(ct).ConfigureAwait(false); Reconnecting = false; m_reconnectLock.Release(); } } } #if OPCUA_V1_CLIENT /// public async Task<(bool, ServiceResult)> RepublishAsync( uint subscriptionId, uint sequenceNumber, CancellationToken ct) { using Activity? activity = m_telemetry.StartActivity(); // Republish is part of the classic publish flow. With the V2 // subscription engine, the SubscriptionManager handles republish // internally and this method is not used. if (m_engine is not ClassicSubscriptionEngine classicEngine) { throw new InvalidOperationException( "RepublishAsync is only supported when the session uses the classic " + "subscription engine. The V2 engine handles republish internally."); } // send republish request. var requestHeader = new RequestHeader { TimeoutHint = (uint)OperationTimeout, ReturnDiagnostics = (uint)(int)ReturnDiagnostics, RequestHandle = Utils.IncrementIdentifier(ref classicEngine.PublishCounter) }; try { m_logger.LogInformation( "Requesting RepublishAsync for {SubscriptionId}-{SequenceNumber}", subscriptionId, sequenceNumber); // request republish. RepublishResponse response = await RepublishAsync( requestHeader, subscriptionId, sequenceNumber, ct) .ConfigureAwait(false); ResponseHeader responseHeader = response.ResponseHeader; NotificationMessage notificationMessage = response.NotificationMessage; m_logger.LogInformation( "Received RepublishAsync for {SubscriptionId}-{SequenceNumber}-{ServiceResult}", subscriptionId, sequenceNumber, responseHeader.ServiceResult); // process response. classicEngine.ProcessPublishResponse( responseHeader, subscriptionId, default, false, notificationMessage); return (true, ServiceResult.Good); } catch (Exception e) { return classicEngine.ProcessRepublishResponseError(e, subscriptionId, sequenceNumber); } } #endif /// /// Recreate the subscriptions in a recreated session. /// /// Uses Transfer service /// if set to true. /// The template for the subscriptions. /// Cancellation token to cancel operation with private async Task RecreateSubscriptionsAsync( bool transferSubscriptionTemplates, IEnumerable subscriptionsTemplate, CancellationToken ct) { using Activity? activity = m_telemetry.StartActivity(); bool transferred = false; if (transferSubscriptionTemplates) { try { transferred = await TransferSubscriptionsAsync( [.. subscriptionsTemplate], false, ct) .ConfigureAwait(false); } catch (ServiceResultException sre) { if (sre.StatusCode == StatusCodes.BadServiceUnsupported) { TransferSubscriptionsOnReconnect = false; m_logger.LogWarning( "Transfer subscription unsupported, TransferSubscriptionsOnReconnect set to false."); } else { m_logger.LogError(sre, "Transfer subscriptions failed."); } } catch (Exception ex) { m_logger.LogError(ex, "Unexpected Transfer subscriptions error."); } } if (!transferred) { // Create the subscriptions which were not transferred. foreach (Subscription subscription in Subscriptions) { if (!subscription.Created) { await subscription.CreateAsync(ct).ConfigureAwait(false); } } } } /// public bool AddSubscription(Subscription subscription) { ThrowIfDisposed(); if (subscription == null) { throw new ArgumentNullException(nameof(subscription)); } lock (m_lock) { if (m_subscriptions.Contains(subscription)) { return false; } subscription.Session = this; subscription.Telemetry = m_telemetry; m_subscriptions.Add(subscription); } m_SubscriptionsChanged?.Invoke(this, EventArgs.Empty); return true; } /// public bool RemoveTransferredSubscription(Subscription subscription) { ThrowIfDisposed(); if (subscription == null) { throw new ArgumentNullException(nameof(subscription)); } if (subscription.Session != this) { return false; } lock (m_lock) { if (!m_subscriptions.Remove(subscription)) { return false; } subscription.Session = null; } m_SubscriptionsChanged?.Invoke(this, EventArgs.Empty); return true; } /// /// Handles an error when validating the application instance certificate provided by the server. /// /// protected virtual void OnApplicationCertificateError( ByteString serverCertificate, ServiceResult result) { throw new ServiceResultException(result); } /// /// Starts a timer to check that the connection to the server is still available. /// private async ValueTask StartKeepAliveTimerAsync() { int keepAliveInterval = m_keepAliveInterval; m_lastKeepAliveErrorStatusCode = StatusCodes.Good; Interlocked.Exchange( ref m_lastKeepAliveTime, m_timeProvider.GetUtcNow().UtcDateTime.Ticks); m_lastKeepAliveTimestamp = m_timeProvider.GetTimestamp(); m_serverState = ServerState.Unknown; ArrayOf nodesToRead = [ // read the server state. new ReadValueId { NodeId = VariableIds.Server_ServerStatus_State, AttributeId = Attributes.Value, DataEncoding = QualifiedName.Null, IndexRange = null } ]; await StopKeepAliveTimerAsync().ConfigureAwait(false); lock (m_lock) { ThrowIfDisposed(); if (m_keepAliveWorker == null) { var keepAliveCancellation = new CancellationTokenSource(); m_keepAliveWorker = Task.Run( () => OnSendKeepAliveAsync(nodesToRead, keepAliveCancellation.Token), keepAliveCancellation.Token); m_keepAliveCancellation?.Dispose(); m_keepAliveCancellation = keepAliveCancellation; } // send initial keep alive. m_keepAliveTimer.Change(TimeSpan.Zero, TimeSpan.FromMilliseconds(m_keepAliveInterval)); } } /// protected override void RequestCompleted( IServiceRequest request, IServiceResponse response, string serviceName) { StatusCode? sr = response?.ResponseHeader?.ServiceResult; if (sr != null && StatusCode.IsGood(sr.Value)) { // Any successful response proves the server is alive — // reset keep-alive so we don't send a redundant read. ResetKeepAliveTimer(); } base.RequestCompleted(request, response!, serviceName); } /// /// Reset the timer used to send keep alive messages. /// private void ResetKeepAliveTimer() { lock (m_lock) { if (m_keepAliveWorker != null) { m_keepAliveTimer.Change(TimeSpan.FromMilliseconds(m_keepAliveInterval), TimeSpan.FromMilliseconds(m_keepAliveInterval)); } } } /// /// Stops the keep alive timer. /// private async ValueTask StopKeepAliveTimerAsync() { Task? keepAliveWorker; CancellationTokenSource? keepAliveCancellation; lock (m_lock) { ThrowIfDisposed(); keepAliveWorker = m_keepAliveWorker; keepAliveCancellation = m_keepAliveCancellation; m_keepAliveWorker = null; m_keepAliveCancellation = null; m_keepAliveTimer.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); } if (keepAliveWorker == null) { Debug.Assert(keepAliveCancellation == null); return; } Debug.Assert(keepAliveCancellation != null); try { await keepAliveCancellation!.CancelAsync().ConfigureAwait(false); if (!m_inKeepAliveCallback) { // Make sure no circular loops await keepAliveWorker.ConfigureAwait(false); } } catch (OperationCanceledException) { } catch (Exception ex) { m_logger.LogDebug(ex, "Keep alive task did not stop cleanly."); } finally { keepAliveCancellation!.Dispose(); } } /// /// Waits for outstanding publish requests to complete or cancels them. /// private async Task WaitForOrCancelOutstandingPublishRequestsAsync(CancellationToken ct) { // Get outstanding publish requests List publishRequestHandles = []; lock (m_outstandingRequests) { foreach (AsyncRequestState state in m_outstandingRequests) { if (state.RequestTypeId == DataTypes.PublishRequest && !state.Defunct) { publishRequestHandles.Add(state.RequestId); } } } if (publishRequestHandles.Count == 0) { m_logger.LogDebug("No outstanding publish requests to cancel."); return; } m_logger.LogInformation( "Waiting for {Count} outstanding publish requests to complete before closing session.", publishRequestHandles.Count); // Wait for outstanding requests with timeout if (PublishRequestCancelDelayOnCloseSession != 0) { int waitTimeout = PublishRequestCancelDelayOnCloseSession < 0 ? int.MaxValue : PublishRequestCancelDelayOnCloseSession; long startTimestamp = m_timeProvider.GetTimestamp(); while (true) { // Check if all publish requests completed int remainingCount = 0; lock (m_outstandingRequests) { foreach (AsyncRequestState state in m_outstandingRequests) { if (state.RequestTypeId == DataTypes.PublishRequest && !state.Defunct) { remainingCount++; } } } if (remainingCount == 0) { m_logger.LogDebug("All outstanding publish requests completed."); return; } // Check timeout int elapsed = (int)m_timeProvider.GetElapsedTime(startTimestamp).TotalMilliseconds; if (elapsed >= waitTimeout) { m_logger.LogWarning( "Timeout waiting for {Count} publish requests to complete. Cancelling them.", remainingCount); break; } // Check cancellation if (ct.IsCancellationRequested) { m_logger.LogWarning("Cancellation requested while waiting for publish requests."); break; } // Wait a bit before checking again try { await m_timeProvider.Delay(TimeSpan.FromMilliseconds(100), ct).ConfigureAwait(false); } catch (OperationCanceledException) { m_logger.LogWarning("Cancellation requested while waiting for publish requests."); break; } } } // Cancel remaining outstanding publish requests List requestsToCancel = []; lock (m_outstandingRequests) { foreach (AsyncRequestState state in m_outstandingRequests) { if (state.RequestTypeId == DataTypes.PublishRequest && !state.Defunct) { requestsToCancel.Add(state.RequestId); } } } if (requestsToCancel.Count > 0) { m_logger.LogInformation( "Cancelling {Count} outstanding publish requests.", requestsToCancel.Count); // Cancel each outstanding publish request foreach (uint requestHandle in requestsToCancel) { try { var requestHeader = new RequestHeader { TimeoutHint = (uint)OperationTimeout }; await CancelAsync(requestHeader, requestHandle, ct).ConfigureAwait(false); m_logger.LogDebug("Cancelled publish request with handle {Handle}.", requestHandle); } catch (Exception ex) { // Log but don't throw - we're closing anyway m_logger.LogWarning( ex, "Error cancelling publish request with handle {Handle}.", requestHandle); } } } } /// /// Removes a completed async request. /// private AsyncRequestState? RemoveRequest(Task result, uint requestId, uint typeId) { lock (m_outstandingRequests) { for (LinkedListNode? ii = m_outstandingRequests.First; ii != null; ii = ii.Next) { if (ReferenceEquals(result, ii.Value.Result) || (requestId == ii.Value.RequestId && typeId == ii.Value.RequestTypeId)) { AsyncRequestState state = ii.Value; m_outstandingRequests.Remove(ii); return state; } } return null; } } /// /// Adds a new async request. /// private void AsyncRequestStarted(Task result, Activity? activity, uint requestId, uint typeId) { lock (m_outstandingRequests) { // check if the request completed asynchronously. AsyncRequestState? state = RemoveRequest(result, requestId, typeId); // add a new request. if (state == null) { state = new AsyncRequestState { Activity = activity, Defunct = false, RequestId = requestId, RequestTypeId = typeId, Result = result, Timestamp = m_timeProvider.GetTimestamp() }; m_outstandingRequests.AddLast(state); } else { state.Dispose(); } } } /// /// Removes a completed async request. /// private void AsyncRequestCompleted(Task result, uint requestId, uint typeId) { lock (m_outstandingRequests) { // remove the request. AsyncRequestState? state = RemoveRequest(result, requestId, typeId); if (state != null) { // mark any old requests as defunct (i.e. the should have returned before this request). TimeSpan maxAge = TimeSpan.FromMilliseconds(1000); for (LinkedListNode? ii = m_outstandingRequests.First; ii != null; ii = ii.Next) { if (ii.Value.RequestTypeId == typeId && m_timeProvider.GetElapsedTime( ii.Value.Timestamp, state.Timestamp) > maxAge) { ii.Value.Defunct = true; } } state.Dispose(); } // add a dummy placeholder since the begin request has not completed yet. if (state == null) { state = new AsyncRequestState { Defunct = true, RequestId = requestId, RequestTypeId = typeId, Result = result, Timestamp = m_timeProvider.GetTimestamp(), Activity = null }; m_outstandingRequests.AddLast(state); } } } /// /// Sends a keep alive by reading from the server. /// private async Task OnSendKeepAliveAsync( ArrayOf nodesToRead, CancellationToken ct) { while (!ct.IsCancellationRequested && !Disposed) { await m_keepAliveEvent.WaitAsync(ct).ConfigureAwait(false); try { // check if session has been closed. if (!Connected || Disposed) { continue; } // check if session has been closed. if (Reconnecting) { m_logger.LogWarning( "Session {SessionId}: KeepAlive ignored while reconnecting.", SessionId); continue; } // raise error if keep alives are not coming back. if (KeepAliveStopped && !OnKeepAliveError( ServiceResult.Create( StatusCodes.BadNoCommunication, "Server not responding to keep alive requests.") )) { continue; } var requestHeader = new RequestHeader { RequestHandle = Utils.IncrementIdentifier(ref m_keepAliveCounter), TimeoutHint = (uint)(KeepAliveInterval * 2), ReturnDiagnostics = 0 }; ReadResponse result = await ReadAsync( requestHeader, 0, TimestampsToReturn.Neither, nodesToRead, ct).ConfigureAwait(false); // read the server status. ArrayOf values = result.Results; ArrayOf diagnosticInfos = result.DiagnosticInfos; ResponseHeader responseHeader = result.ResponseHeader; ValidateResponse(values, nodesToRead); ValidateDiagnosticInfos(diagnosticInfos, nodesToRead); // validate value returned. ServiceResult error = ValidateDataValue( values[0], TypeInfo.Scalars.Int32, 0, diagnosticInfos, responseHeader); if (ServiceResult.IsBad(error)) { m_logger.LogError("Keep alive read failed: {ServiceResult}, EndpointUrl={EndpointUrl}, RequestCount={Good}/{Outstanding}", error, Endpoint?.EndpointUrl, GoodPublishRequestCount, OutstandingRequestCount); throw new ServiceResultException(error); } // send notification that keep alive completed. if (!values[0].WrappedValue.TryGetValue(out ServerState serverState)) { serverState = ServerState.Unknown; } OnKeepAlive(serverState, (DateTime)responseHeader.Timestamp); } catch (ServiceResultException sre) { // recover from error condition when secure channel is still alive OnKeepAliveError(sre.Result); } catch (ObjectDisposedException) when (Disposed) { // This should not happen, but we fail gracefully anyway } catch (TaskCanceledException) { //expected exception type } catch (Exception e) { m_logger.LogError( "Could not send keep alive request: {RequestType} {Message}", e.GetType().FullName, e.Message); } } } /// /// Called when the server returns a keep alive response. /// protected virtual void OnKeepAlive(ServerState currentState, DateTime currentTime) { // restart publishing if keep alives recovered. if (KeepAliveStopped) { // ignore if already reconnecting. if (Reconnecting) { return; } m_lastKeepAliveErrorStatusCode = StatusCodes.Good; Interlocked.Exchange( ref m_lastKeepAliveTime, m_timeProvider.GetUtcNow().UtcDateTime.Ticks); m_lastKeepAliveTimestamp = m_timeProvider.GetTimestamp(); lock (m_outstandingRequests) { for (LinkedListNode? ii = m_outstandingRequests.First; ii != null; ii = ii.Next) { if (ii.Value.RequestTypeId == DataTypes.PublishRequest) { ii.Value.Defunct = true; } } } StartPublishing(OperationTimeout, false); } else { m_lastKeepAliveErrorStatusCode = StatusCodes.Good; Interlocked.Exchange( ref m_lastKeepAliveTime, m_timeProvider.GetUtcNow().UtcDateTime.Ticks); m_lastKeepAliveTimestamp = m_timeProvider.GetTimestamp(); } // save server state. m_serverState = currentState; KeepAliveEventHandler? callback = m_KeepAlive; if (callback != null) { try { callback(this, new KeepAliveEventArgs(null, currentState, currentTime)); } catch (Exception e) { m_logger.LogError(e, "Session: Unexpected error invoking KeepAliveCallback."); } } } /// /// Called when a error occurs during a keep alive. /// protected virtual bool OnKeepAliveError(ServiceResult result) { m_lastKeepAliveErrorStatusCode = result.StatusCode; if (result.StatusCode == StatusCodes.BadNoCommunication) { //keep alive read timed out TimeSpan elapsed = m_timeProvider.GetElapsedTime(m_lastKeepAliveTimestamp); m_logger.LogInformation( "KEEP ALIVE LATE: {Duration}ms, EndpointUrl={EndpointUrl}, RequestCount={Good}/{Outstanding}", elapsed.TotalMilliseconds, Endpoint?.EndpointUrl, GoodPublishRequestCount, OutstandingRequestCount); } KeepAliveEventHandler? callback = m_KeepAlive; if (callback != null) { try { m_inKeepAliveCallback = true; var args = new KeepAliveEventArgs( result, ServerState.Unknown, m_timeProvider.GetUtcNow().UtcDateTime); callback(this, args); m_inKeepAliveCallback = false; return !args.CancelKeepAlive; } catch (Exception e) { m_logger.LogError(e, "Session: Unexpected error invoking KeepAliveCallback."); } } return true; } /// /// Prepare a list of subscriptions to delete. /// private bool PrepareSubscriptionsToDelete( IEnumerable subscriptions, List subscriptionsToDelete) { bool removed = false; lock (m_lock) { foreach (Subscription subscription in subscriptions) { if (m_subscriptions.Remove(subscription)) { if (subscription.Created) { subscriptionsToDelete.Add(subscription); } removed = true; } } } return removed; } /// /// Prepares the list of node ids to read to fetch the namespace table. /// private static ArrayOf PrepareNamespaceTableNodesToRead() { return [ // request namespace array. new ReadValueId { NodeId = VariableIds.Server_NamespaceArray, AttributeId = Attributes.Value }, // request server array. new ReadValueId { NodeId = VariableIds.Server_ServerArray, AttributeId = Attributes.Value } ]; } /// /// Updates the NamespaceTable with the result of the /// read operation. /// Throws in case of types not matching or empty namespace /// array returned. /// /// private void UpdateNamespaceTable( ArrayOf values, ArrayOf diagnosticInfos, ResponseHeader responseHeader) { // validate namespace array. ServiceResult result = ValidateDataValue( values[0], TypeInfo.Arrays.String, 0, diagnosticInfos, responseHeader); if (ServiceResult.IsBad(result)) { throw ServiceResultException.Create(result.StatusCode.Code, "Cannot read NamespaceArray node. Validation of returned value failed."); } if (!values[0].WrappedValue.TryGetValue(out ArrayOf namespaceArray) || namespaceArray.IsEmpty) { throw ServiceResultException.Unexpected( "Retrieved namespace list contain no entries."); } if (namespaceArray[0] != Namespaces.OpcUa) { throw ServiceResultException.Unexpected( "Retrieved namespaces are missing OPC UA namespace at index 0."); } NamespaceUris.Update(namespaceArray.ToArray()!); if (StatusCode.IsBad(values[1].StatusCode)) { // Gracefully handle not loading server array. m_logger.LogError("Cannot read ServerArray node: {StatusCode} - skipping.", values[1].StatusCode); return; } // validate server array. result = ValidateDataValue( values[1], TypeInfo.Arrays.String, 1, diagnosticInfos, responseHeader); if (ServiceResult.IsBad(result)) { throw ServiceResultException.Create(result.StatusCode.Code, "Cannot read ServerArray node. Validation of returned value failed."); } if (values[1].WrappedValue.TryGetValue(out ArrayOf serverArray)) { ServerUris.Update(serverArray.ToArray()!); } } #if OPCUA_V1_CLIENT /// /// Sends an additional publish request. /// Only valid when using the classic subscription engine; the V2 /// engine drives publishing through its own worker pool. /// public bool BeginPublish(int timeout) { if (m_engine is ClassicSubscriptionEngine classicEngine) { return classicEngine.BeginPublish(timeout); } return false; } #endif /// /// Create the publish requests for the active subscriptions. /// public void StartPublishing(int timeout, bool fullQueue) { m_engine.StartPublishing(timeout, fullQueue); } /// /// Helper to refresh the identity (reprompt for password, refresh token) in case of a Recreate of the Session. /// public virtual void RecreateRenewUserIdentity() { if (m_RenewUserIdentity != null) { IUserIdentity renewed = m_RenewUserIdentity(this, m_identity); m_identity = renewed ?? new UserIdentity(); } } /// /// Helper to throw a recreate session exception. /// /// private static void ThrowCouldNotRecreateSessionException(Exception e, string sessionName) { throw ServiceResultException.Create( StatusCodes.BadCommunicationError, e, "Could not recreate session {0}:{1}", sessionName, e.Message); } /// /// Validates the identity for an open call. /// /// private void OpenValidateIdentity( ref IUserIdentity identity, out UserTokenPolicy identityPolicy, out string securityPolicyUri, out bool requireEncryption) { // check connection state. lock (m_lock) { if (Connected) { throw new ServiceResultException( StatusCodes.BadInvalidState, "Already connected to server."); } } securityPolicyUri = m_endpoint.Description.SecurityPolicyUri ?? SecurityPolicies.None; // catch security policies which are not supported by core if (SecurityPolicies.GetDisplayName(securityPolicyUri) == null) { throw ServiceResultException.Create( StatusCodes.BadSecurityChecksFailed, "The chosen security policy is not supported by the client to connect to the server."); } // get the identity token. identity ??= new UserIdentity(); // check that the user identity is supported by the endpoint. identityPolicy = m_endpoint.Description.FindUserTokenPolicy( identity.TokenHandler.Token.PolicyId!, securityPolicyUri!)!; if (identityPolicy == null) { // try looking up by TokenType if the policy id was not found. identityPolicy = m_endpoint.Description.FindUserTokenPolicy( identity.TokenType, identity.IssuedTokenType, securityPolicyUri) ?? throw ServiceResultException.Create( StatusCodes.BadIdentityTokenInvalid, "Endpoint does not support the user identity type provided."); identity.TokenHandler.UpdatePolicy(identityPolicy); } requireEncryption = securityPolicyUri != SecurityPolicies.None; if (!requireEncryption) { requireEncryption = identityPolicy.SecurityPolicyUri != SecurityPolicies.None && !string.IsNullOrEmpty(identityPolicy.SecurityPolicyUri); } } private void BuildCertificateData( out ByteString clientCertificateData, out ByteString clientCertificateChainData) { // send the application instance certificate for the client. clientCertificateData = ByteString.From(m_instanceCertificate?.RawData); clientCertificateChainData = default; if (m_instanceCertificateChain != null && m_instanceCertificateChain.Count > 0 && m_configuration.SecurityConfiguration.SendCertificateChain) { var clientCertificateChain = new List(); for (int i = 0; i < m_instanceCertificateChain.Count; i++) { clientCertificateChain.AddRange(m_instanceCertificateChain[i].RawData); } clientCertificateChainData = clientCertificateChain.ToByteString(); } } /// /// Validates the server certificate returned. /// /// private void ValidateServerCertificateData(ByteString serverCertificateData) { if (!serverCertificateData.IsEmpty && !m_endpoint.Description.ServerCertificate.IsEmpty && serverCertificateData != m_endpoint.Description.ServerCertificate) { try { // verify for certificate chain in endpoint. using CertificateCollection serverCertificateChain = Utils.ParseCertificateChainBlob( m_endpoint.Description.ServerCertificate, m_telemetry); if (serverCertificateChain.Count > 0 && !Utils.IsEqual(serverCertificateData, serverCertificateChain[0].RawData)) { throw ServiceResultException.Create( StatusCodes.BadCertificateInvalid, "Server did not return the certificate used to create the secure channel."); } } catch (Exception) { throw ServiceResultException.Create( StatusCodes.BadCertificateInvalid, "Server did not return the certificate used to create the secure channel."); } } } /// /// Validates the server signature created with the client nonce. /// /// private void ValidateServerSignature( Certificate? serverCertificate, SignatureData serverSignature, ByteString clientCertificateData, ByteString clientCertificateChainData, byte[]? clientNonce, ByteString serverNonce) { if (serverSignature == null || serverSignature.Signature.IsEmpty) { m_logger.LogInformation("Server signature is null or empty."); return; } // validate the server's signature. SecurityPolicyInfo? securityPolicy = SecurityPolicies.GetInfo(m_endpoint.Description.SecurityPolicyUri!); byte[] dataToSign = securityPolicy!.GetServerSignatureData( TransportChannel.ChannelThumbprint, clientNonce, TransportChannel.ServerChannelCertificate, clientCertificateData.ToArray(), TransportChannel.ClientChannelCertificate, serverNonce.ToArray()); if (!SecurityPolicies.VerifySignatureData( serverSignature, securityPolicy, serverCertificate!, dataToSign)) { // validate the signature with complete chain if the check with leaf certificate failed. if (!clientCertificateChainData.IsEmpty) { dataToSign = securityPolicy.GetServerSignatureData( TransportChannel.ChannelThumbprint, clientNonce, TransportChannel.ServerChannelCertificate, clientCertificateChainData.ToArray(), TransportChannel.ClientChannelCertificate, serverNonce.ToArray()); if (!SecurityPolicies.VerifySignatureData( serverSignature, securityPolicy, serverCertificate!, dataToSign)) { throw ServiceResultException.Create( StatusCodes.BadApplicationSignatureInvalid, "Server did not provide a correct signature for the nonce data provided by the client."); } } else { throw ServiceResultException.Create( StatusCodes.BadApplicationSignatureInvalid, "Server did not provide a correct signature for the nonce data provided by the client."); } } } /// /// Validates the ServerCertificate ApplicationUri to match the ApplicationUri /// of the Endpoint (Spec Part 4 5.4.1) returned by the CreateSessionResponse. /// Ensure the endpoint was matched in /// with the applicationUri of the server description before the validation. /// private void ValidateServerCertificateApplicationUri( Certificate? serverCertificate, ConfiguredEndpoint endpoint) { if (serverCertificate != null) { ICertificateValidatorEx validator = m_configuration.CertificateManager; validator.ValidateApplicationUri(serverCertificate, endpoint); } } /// /// Validates the server endpoints returned. /// /// private void ValidateServerEndpoints(ArrayOf serverEndpoints) { if (!m_discoveryServerEndpoints.IsEmpty) { // Compare EndpointDescriptions returned at GetEndpoints with values returned at CreateSession ArrayOf expectedServerEndpoints = default; if (!serverEndpoints.IsNull && !m_discoveryProfileUris.IsEmpty) { // Select EndpointDescriptions with a transportProfileUri that matches the // profileUris specified in the original GetEndpoints() request. var expectedServerEndpointsList = new List(); foreach (EndpointDescription serverEndpoint in serverEndpoints) { if (m_discoveryProfileUris.Contains(uri => uri == serverEndpoint.TransportProfileUri)) { expectedServerEndpointsList.Add(serverEndpoint); } } expectedServerEndpoints = expectedServerEndpointsList; } else { expectedServerEndpoints = serverEndpoints; } if (m_discoveryServerEndpoints.Count != expectedServerEndpoints.Count) { throw ServiceResultException.Create( StatusCodes.BadSecurityChecksFailed, "Server did not return a number of ServerEndpoints that matches the one from GetEndpoints."); } for (int ii = 0; ii < expectedServerEndpoints.Count; ii++) { EndpointDescription serverEndpoint = expectedServerEndpoints[ii]; EndpointDescription expectedServerEndpoint = m_discoveryServerEndpoints[ii]; if (serverEndpoint.SecurityMode != expectedServerEndpoint.SecurityMode || serverEndpoint.SecurityPolicyUri != expectedServerEndpoint .SecurityPolicyUri || serverEndpoint.TransportProfileUri != expectedServerEndpoint .TransportProfileUri || serverEndpoint.SecurityLevel != expectedServerEndpoint.SecurityLevel) { throw ServiceResultException.Create( StatusCodes.BadSecurityChecksFailed, "The list of ServerEndpoints returned at CreateSession does not match the list from GetEndpoints."); } if (serverEndpoint.UserIdentityTokens.Count != expectedServerEndpoint .UserIdentityTokens .Count) { throw ServiceResultException.Create( StatusCodes.BadSecurityChecksFailed, "The list of ServerEndpoints returned at CreateSession does not match the one from GetEndpoints."); } for (int jj = 0; jj < serverEndpoint.UserIdentityTokens.Count; jj++) { if (!serverEndpoint .UserIdentityTokens[jj] .IsEqual(expectedServerEndpoint.UserIdentityTokens[jj])) { throw ServiceResultException.Create( StatusCodes.BadSecurityChecksFailed, "The list of ServerEndpoints returned at CreateSession does not match the one from GetEndpoints."); } } } } // find the matching description (TBD - check domains against certificate). bool found = false; EndpointDescription? foundDescription = FindMatchingDescription( serverEndpoints, m_endpoint.Description, true); if (foundDescription != null) { found = true; // ensure endpoint has up to date information. UpdateDescription(m_endpoint.Description, foundDescription); } else { foundDescription = FindMatchingDescription( serverEndpoints, m_endpoint.Description, false); if (foundDescription != null) { found = true; // ensure endpoint has up to date information. UpdateDescription(m_endpoint.Description, foundDescription); } } // could be a security risk. if (!found) { throw ServiceResultException.Create( StatusCodes.BadSecurityChecksFailed, "Server did not return an EndpointDescription that matched the one used to create the secure channel."); } } /// /// Find and return matching application description /// /// The descriptions to search through /// The description to match /// Match criteria includes port /// Matching description or null if no description is matching private EndpointDescription? FindMatchingDescription( ArrayOf endpointDescriptions, EndpointDescription match, bool matchPort) { Uri? expectedUrl = Utils.ParseUri(match.EndpointUrl); for (int ii = 0; ii < endpointDescriptions.Count; ii++) { EndpointDescription serverEndpoint = endpointDescriptions[ii]; Uri? actualUrl = Utils.ParseUri(serverEndpoint.EndpointUrl); if (actualUrl != null && expectedUrl != null && actualUrl.Scheme == expectedUrl.Scheme && (!matchPort || actualUrl.Port == expectedUrl.Port) && serverEndpoint.SecurityPolicyUri == m_endpoint.Description.SecurityPolicyUri && serverEndpoint.SecurityMode == m_endpoint.Description.SecurityMode) { return serverEndpoint; } } return null; } /// /// Update the target description from the source description /// private static void UpdateDescription( EndpointDescription target, EndpointDescription source) { target.Server.ApplicationName = source.Server.ApplicationName; target.Server.ApplicationUri = source.Server.ApplicationUri; target.Server.ApplicationType = source.Server.ApplicationType; target.Server.ProductUri = source.Server.ProductUri; target.TransportProfileUri = source.TransportProfileUri; target.UserIdentityTokens = source.UserIdentityTokens; } /// /// Invokes a DeleteSubscriptions call for the specified subscriptionId. /// /// private async ValueTask DeleteSubscriptionAsync( uint subscriptionId, CancellationToken ct = default) { try { m_logger.LogInformation( "Deleting server subscription for SubscriptionId={SubscriptionId}", subscriptionId); // delete the subscription. ArrayOf subscriptionIds = [subscriptionId]; DeleteSubscriptionsResponse response = await DeleteSubscriptionsAsync( null, subscriptionIds, ct).ConfigureAwait(false); ResponseHeader responseHeader = response.ResponseHeader; ArrayOf results = response.Results; ArrayOf diagnosticInfos = response.DiagnosticInfos; // validate response. ValidateResponse(results, subscriptionIds); ValidateDiagnosticInfos(diagnosticInfos, subscriptionIds); if (StatusCode.IsBad(results[0])) { throw new ServiceResultException( GetResult(results[0], 0, diagnosticInfos, responseHeader)); } } catch (Exception e) { m_logger.LogError( e, "Session: Unexpected error while deleting subscription for SubscriptionId={SubscriptionId}.", subscriptionId); } } /// /// Asynchronously load instance certificate /// /// private async Task LoadInstanceCertificateAsync( bool throwIfConfigurationChangedFromLastLoad, CancellationToken ct = default) { m_endpoint.Description.SecurityPolicyUri ??= SecurityPolicies.None; if (m_endpoint.Description.SecurityPolicyUri == SecurityPolicies.None) { // No need to load instance certificates return; } if (m_instanceCertificate != null && m_instanceCertificate.HasPrivateKey && !m_endpoint.Equals(m_effectiveEndpoint)) { if (throwIfConfigurationChangedFromLastLoad) { // Updating a live session must be prevented unless the session was // closed. Therefore we need to throw here to catch this case during any // reconnect or other activation operation throw ServiceResultException.ConfigurationError( "Configuration was changed for an active session."); } // If the configured endpoint was updated while we are closed we reload. m_instanceCertificate.Dispose(); m_instanceCertificate = null; } if (m_instanceCertificate == null || !m_instanceCertificate.HasPrivateKey) { // Dispose any previously loaded certificate that lacked a // private key before overwriting it with the newly loaded one. m_instanceCertificate?.Dispose(); m_instanceCertificate = await LoadInstanceCertificateAsync( m_configuration, m_endpoint.Description.SecurityPolicyUri, m_telemetry, ct) .ConfigureAwait(false) ?? throw ServiceResultException.ConfigurationError( "The client configuration does not specify an application instance certificate."); m_effectiveEndpoint = m_endpoint; m_instanceCertificateChain?.Dispose(); m_instanceCertificateChain = null; // Reload the chain too } // check for private key. if (!m_instanceCertificate.HasPrivateKey) { throw ServiceResultException.ConfigurationError( "Client certificate configured for security policy {0} is missing a private key.", m_endpoint.Description.SecurityPolicyUri); } // load certificate chain. m_instanceCertificateChain ??= await LoadCertificateChainAsync( m_configuration, m_instanceCertificate, ct).ConfigureAwait(false); } /// /// Load certificate for connection. /// /// internal static async Task LoadInstanceCertificateAsync( ApplicationConfiguration configuration, string securityProfile, ITelemetryContext telemetry, CancellationToken ct = default) { return await configuration.SecurityConfiguration.FindApplicationCertificateAsync( securityProfile, privateKey: true, telemetry, ct).ConfigureAwait(false) ?? throw ServiceResultException.ConfigurationError( "ApplicationCertificate for the security profile {0} cannot be found.", securityProfile); } /// /// Load certificate chain for connection. /// internal static async Task LoadCertificateChainAsync( ApplicationConfiguration configuration, Certificate clientCertificate, CancellationToken ct = default) { CertificateCollection? clientCertificateChain = null; // load certificate chain. if (configuration.SecurityConfiguration.SendCertificateChain) { clientCertificateChain = [clientCertificate]; var issuers = new List(); try { if (configuration.CertificateManager != null) { await configuration.CertificateManager .GetIssuersAsync(clientCertificate, issuers, ct) .ConfigureAwait(false); } for (int i = 0; i < issuers.Count; i++) { clientCertificateChain.Add(issuers[i].Certificate); } } catch { clientCertificateChain.Dispose(); throw; } finally { // GetIssuersAsync returns caller-owned references; the // chain AddRefs each one above, so we must dispose ours. for (int i = 0; i < issuers.Count; i++) { issuers[i].Certificate?.Dispose(); } } } return clientCertificateChain; } /// /// Creates and validates the subscription ids for a transfer. /// /// The subscriptions to transfer. /// The subscription ids for the transfer. /// Thrown if a subscription is in invalid state. private ArrayOf CreateSubscriptionIdsForTransfer( SubscriptionCollection subscriptions) { var subscriptionIds = new List(); lock (m_lock) { foreach (Subscription subscription in subscriptions) { if (subscription.Created && SessionId.Equals(subscription.Session?.SessionId)) { throw new ServiceResultException( StatusCodes.BadInvalidState, Utils.Format( "The subscriptionId {0} is already created.", subscription.Id)); } if (subscription.TransferId == 0) { throw new ServiceResultException( StatusCodes.BadInvalidState, Utils.Format( "A subscription can not be transferred due to missing transfer Id.")); } subscriptionIds.Add(subscription.TransferId); } } return subscriptionIds.ToArrayOf(); } /// /// Indicates that the session configuration has changed. /// private void IndicateSessionConfigurationChanged() { try { m_SessionConfigurationChanged?.Invoke(this, EventArgs.Empty); } catch (Exception e) { m_logger.LogError( e, "Unexpected error calling SessionConfigurationChanged event handler."); } } /// /// Creates a request header with additional parameters /// for the ecc user token security policy, if needed. /// private RequestHeader CreateRequestHeaderPerUserTokenPolicy( string? identityTokenSecurityPolicyUri) { var requestHeader = new RequestHeader(); string userTokenSecurityPolicyUri = identityTokenSecurityPolicyUri ?? string.Empty; if (string.IsNullOrEmpty(userTokenSecurityPolicyUri)) { userTokenSecurityPolicyUri = m_endpoint.Description.SecurityPolicyUri ?? SecurityPolicies.None; } m_userTokenSecurityPolicyUri = userTokenSecurityPolicyUri; SecurityPolicyInfo? securityPolicy = SecurityPolicies.GetInfo(userTokenSecurityPolicyUri); if (securityPolicy!.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None) { var parameters = new AdditionalParametersType { Parameters = [ new KeyValuePair { Key = QualifiedName.From(AdditionalParameterNames.ECDHPolicyUri), Value = userTokenSecurityPolicyUri } ] }; requestHeader.AdditionalHeader = new ExtensionObject(parameters); m_logger.LogWarning("Request EphemeralKey for {Policy}.", userTokenSecurityPolicyUri); } return requestHeader; } private RequestHeader? CreateRequestHeaderForActivateSession( string userTokenSecurityPolicyUri) { var requestHeader = new RequestHeader(); var parameters = new AdditionalParametersType(); if (!string.IsNullOrEmpty(userTokenSecurityPolicyUri)) { SecurityPolicyInfo? userTokenSecurityPolicy = SecurityPolicies.GetInfo(userTokenSecurityPolicyUri); if (userTokenSecurityPolicy!.EphemeralKeyAlgorithm != CertificateKeyAlgorithm.None) { parameters.Parameters = [ new KeyValuePair { Key = QualifiedName.From(AdditionalParameterNames.ECDHPolicyUri), Value = userTokenSecurityPolicyUri } ]; m_logger.LogWarning("Requesting new EphmeralKey using {SecurityPolicyUri}.", userTokenSecurityPolicyUri); } } if (parameters.Parameters.Count == 0) { return null; } requestHeader.AdditionalHeader = new ExtensionObject(parameters); return requestHeader; } /// /// Create a subscription the provided item options /// protected virtual Subscription CreateSubscription(SubscriptionOptions? options = null) { return new Subscription(m_telemetry, options); } /// /// Process the AdditionalHeader field of a ResponseHeader /// /// protected virtual void ProcessResponseAdditionalHeader( ResponseHeader responseHeader, Certificate? serverCertificate) { if (responseHeader != null && responseHeader.AdditionalHeader.TryGetValue(out IEncodeable? e) && e is AdditionalParametersType parameters) { foreach (KeyValuePair ii in parameters.Parameters) { if (ii.Key == AdditionalParameterNames.Padding) { ByteString padding = ii.Value.GetByteString(default); if (ii.Value.TypeInfo != TypeInfo.Scalars.ByteString || padding.IsEmpty) { m_logger.LogWarning( "Server returned invalid message padding. Ignored."); } else if (padding.Length > 128) { m_logger.LogWarning( "Server returned a {Size}byte message padding that is too long. Ignored.", padding.Length); } else { m_logger.LogWarning("Ignoring Padding with {Length} Bytes", padding.Length); } continue; } if (ii.Key == AdditionalParameterNames.ECDHKey) { if (ii.Value.TypeInfo == TypeInfo.Scalars.StatusCode) { throw new ServiceResultException( ii.Value.GetStatusCode(StatusCodes.Bad), "Server could not provide an ECDHKey. User authentication not possible."); } if (ExtensionObject.ToEncodeable(ii.Value.GetExtensionObject()) is not EphemeralKeyType key) { throw new ServiceResultException( StatusCodes.BadDecodingError, "Server did not provide a valid ECDHKey. User authentication not possible."); } if (key.PublicKey.IsEmpty || key.Signature.IsEmpty) { throw new ServiceResultException( StatusCodes.BadDecodingError, "Server did not provide a valid ECDHKey. User authentication not possible."); } if (serverCertificate == null || m_userTokenSecurityPolicyUri == null) { throw new ServiceResultException( StatusCodes.BadDecodingError, "Server certificate or security policy URI is not available. User authentication not possible."); } if (!CryptoUtils.Verify( new ArraySegment(key.PublicKey.ToArray()), key.Signature.ToArray(), serverCertificate!, m_userTokenSecurityPolicyUri!)) { throw new ServiceResultException( StatusCodes.BadDecodingError, "Could not verify signature on ECDHKey. User authentication not possible."); } m_eccServerEphemeralKey = Nonce.CreateNonce( SecurityPolicies.GetInfo(m_userTokenSecurityPolicyUri!)!, key.PublicKey.ToArray()); m_logger.LogWarning("Updating ServerEphemeralKey: {Key} bytes", m_eccServerEphemeralKey.Data?.Length ?? 0); } } } } /// /// The period for which the server will maintain the session if there is no communication from the client. /// protected double m_sessionTimeout; /// /// The locales that the server should use when returning localized text. /// protected ArrayOf m_preferredLocales; /// /// The Application Configuration. /// protected ApplicationConfiguration m_configuration; /// /// The endpoint configured for the session. /// protected ConfiguredEndpoint m_endpoint; /// /// The endpoint used while connected to the server. /// protected ConfiguredEndpoint m_effectiveEndpoint; /// /// The Instance Certificate. /// protected Certificate? m_instanceCertificate; /// /// The Instance Certificate Chain. /// protected CertificateCollection? m_instanceCertificateChain; /// /// The session telemetry context /// protected ITelemetryContext m_telemetry; /// /// If set totrue then the domain in the certificate must match the endpoint used. /// protected bool m_checkDomain; /// /// The name assigned to the session. /// protected string m_sessionName; /// /// The user identity currently used for the session. /// protected IUserIdentity m_identity; /// /// Factor applied to the before is set to true /// protected int m_keepAliveIntervalFactor = 1; /// m /// Time in milliseconds added to before is set to true /// protected int m_keepAliveGuardBand = 1000; private readonly Lock m_lock = new(); private readonly List m_subscriptions = []; private uint m_maxRequestMessageSize; private readonly SessionSystemContext m_systemContext; #pragma warning disable CA2213 // Disposed in DisposeAsyncCore/Dispose private readonly NodeCache m_nodeCache; #pragma warning restore CA2213 private readonly List m_identityHistory = []; private byte[]? m_clientNonce; private ByteString m_serverNonce; private ByteString m_previousServerNonce; #pragma warning disable CA2213 // Disposed in Dispose method (m_serverCertificate?.Dispose() in cleanup path) private Certificate? m_serverCertificate; #pragma warning restore CA2213 private long m_lastKeepAliveTime; private long m_lastKeepAliveTimestamp; private StatusCode m_lastKeepAliveErrorStatusCode; private ServerState m_serverState; private int m_keepAliveInterval; #pragma warning disable CA2213 // Disposed in DisposeAsyncCore/Dispose private readonly ITimer m_keepAliveTimer; #pragma warning restore CA2213 private readonly AsyncAutoResetEvent m_keepAliveEvent = new(); private uint m_keepAliveCounter; private Task? m_keepAliveWorker; private bool m_inKeepAliveCallback; #pragma warning disable CA2213 // Disposed in DisposeAsyncCore/Dispose private CancellationTokenSource? m_keepAliveCancellation; private readonly SemaphoreSlim m_reconnectLock = new(1, 1); #pragma warning restore CA2213 private readonly LinkedList m_outstandingRequests = []; private string? m_userTokenSecurityPolicyUri; #pragma warning disable CA2213 // Disposed in DisposeAsyncCore/Dispose private Nonce? m_eccServerEphemeralKey; #pragma warning restore CA2213 private Subscription? m_defaultSubscription; private bool m_disposeAsyncCalled; private readonly ArrayOf m_discoveryServerEndpoints; private readonly ArrayOf m_discoveryProfileUris; private new readonly ILogger m_logger; private readonly TimeProvider m_timeProvider; #pragma warning disable CA2213 // Disposed in DisposeAsyncCore/Dispose private readonly ISubscriptionEngine m_engine; #pragma warning restore CA2213 private sealed class AsyncRequestState : IDisposable { public uint RequestTypeId { get; init; } public uint RequestId { get; init; } public long Timestamp { get; init; } public required Task Result { get; set; } public bool Defunct { get; set; } public required Activity? Activity { get; init; } public void Dispose() { Activity?.Dispose(); Debug.Assert(Result.IsCompleted); } } private event KeepAliveEventHandler? m_KeepAlive; private event NotificationEventHandler? m_Publish; private event PublishErrorEventHandler? m_PublishError; private event PublishSequenceNumbersToAcknowledgeEventHandler? m_PublishSequenceNumbersToAcknowledge; private event EventHandler? m_SubscriptionsChanged; private event EventHandler? m_SessionClosing; private event EventHandler? m_SessionConfigurationChanged; } /// /// The event arguments provided when a keep alive response arrives. /// public class KeepAliveEventArgs : EventArgs { /// /// Creates a new instance. /// public KeepAliveEventArgs( ServiceResult? status, ServerState currentState, DateTime currentTime) { Status = status; CurrentState = currentState; CurrentTime = currentTime; } /// /// Gets the status associated with the keep alive operation. /// public ServiceResult? Status { get; } /// /// Gets the current server state. /// public ServerState CurrentState { get; } /// /// Gets the current server time. /// public DateTime CurrentTime { get; } /// /// Gets or sets a flag indicating whether the session should send another keep alive. /// public bool CancelKeepAlive { get; set; } } /// /// Represents the event arguments provided when a new notification message arrives. /// public class NotificationEventArgs : EventArgs { /// /// Creates a new instance. /// public NotificationEventArgs( Subscription subscription, NotificationMessage notificationMessage, ArrayOf stringTable) { Subscription = subscription; NotificationMessage = notificationMessage; StringTable = stringTable; } /// /// Gets the subscription that the notification applies to. /// public Subscription Subscription { get; } /// /// Gets the notification message. /// public NotificationMessage NotificationMessage { get; } /// /// Gets the string table returned with the notification message. /// public ArrayOf StringTable { get; } } /// /// Represents the event arguments provided when a publish error occurs. /// public class PublishErrorEventArgs : EventArgs { /// /// Creates a new instance. /// public PublishErrorEventArgs(ServiceResult status) { Status = status; } /// /// Creates a new instance. /// internal PublishErrorEventArgs( ServiceResult status, uint subscriptionId, uint sequenceNumber) { Status = status; SubscriptionId = subscriptionId; SequenceNumber = sequenceNumber; } /// /// Gets the status associated with the keep alive operation. /// public ServiceResult Status { get; } /// /// Gets the subscription with the message that could not be republished. /// public uint SubscriptionId { get; } /// /// Gets the sequence number for the message that could not be republished. /// public uint SequenceNumber { get; } } /// /// Represents the event arguments provided when publish response /// sequence numbers are about to be ackknowledged with a publish request. /// /// /// A callee can defer an acknowledge to the next publish request by /// moving the to the deferred list. /// The callee can modify the list of acknowledgements to send, it is the /// responsibility of the caller to protect the lists for modifications. /// public class PublishSequenceNumbersToAcknowledgeEventArgs : EventArgs { /// /// Creates a new instance. /// public PublishSequenceNumbersToAcknowledgeEventArgs( List acknowledgementsToSend, List deferredAcknowledgementsToSend) { AcknowledgementsToSend = acknowledgementsToSend; DeferredAcknowledgementsToSend = deferredAcknowledgementsToSend; } /// /// The acknowledgements which are sent with the next publish request. /// /// /// A client may also choose to remove an acknowledgement from this list to add it back /// to the list in a subsequent callback when the request is fully processed. /// public List AcknowledgementsToSend { get; } /// /// The deferred list of acknowledgements. /// /// /// The callee can transfer an outstanding /// to this list to defer the acknowledge of a sequence number to the next publish request. /// public List DeferredAcknowledgementsToSend { get; } } }