Files
Marc Schier dbc09b8e22 Adopt System.TimeProvider across the stack (fixes #2458) (#3829)
# Description

Adopt
[`System.TimeProvider`](https://learn.microsoft.com/dotnet/api/system.timeprovider)
across the stack so that every place that creates timers, runs duration
math, or schedules periodic work is mockable, deterministic in tests,
and immune to wall-clock changes.

The migration touches Core, Core.Types, Server, Client, PubSub,
Configuration, Gds (server + client), Lds, and the Quickstarts/Console
reference apps. Per-component changes follow the same pattern:

1. Add a `private readonly TimeProvider m_timeProvider;` field.
2. Accept a nullable `TimeProvider? timeProvider = null` as the **last**
constructor parameter (new overload; legacy constructor stays for binary
compatibility and delegates with `timeProvider: null`).
3. Initialise the field with `m_timeProvider = timeProvider ??=
TimeProvider.System;`.
4. Replace `new Timer(...)`, `Stopwatch`, `HiResClock.*`,
`DateTime.UtcNow`-based duration math, and `Task.Delay(delay, ct)` in
timing loops with the matching `m_timeProvider` API (`CreateTimer`,
`GetTimestamp` / `GetElapsedTime`, `GetUtcNow().UtcDateTime`,
`Task.Delay(delay, m_timeProvider, ct)`).
5. Wire the provider through DI: `AddOpcUaServerBuilder` /
`AddOpcUaClientBuilder` register `TimeProvider.System` via
`TryAddSingleton<TimeProvider>()` so tests can swap in
`Microsoft.Extensions.Time.Testing.FakeTimeProvider`.

`HiResClock` itself is kept (so the
`ApplicationConfiguration.DisableHiResClock` schema flag still
round-trips), but **every public member** is now `[Obsolete]` with a
single shared message pointing to the new migration-guide section. The
few internal call sites that still need to set/read `HiResClock` are
wrapped with `#pragma warning disable CS0618`.

### Highlights per component

- **Core / Stack** — `Opc.Ua.Core`, `Opc.Ua.Core.Types`, channel
pipeline (`UaSCBinaryClientChannel`, `ChannelToken`,
`UaSCBinaryChannel`), `ApplicationConfiguration`, certificate
validators, `JwksIssuerKeyResolver`, `TimeProviderExtensions` polyfills
(`GetTimestampMilliseconds`, `GetTickCount`,
`CancellationTokenSource(timeout, timeProvider)`, `Task.Delay(delay,
provider, ct)` polyfill for older TFMs).
- **Server** — session lockout, subscription pipeline
(publish/sample/keep-alive/lifetime), `MonitoredItemQueue`, alarm
sampling rate, `IntervalRunner`, server diagnostics, configuration state
machines, hosted services.
- **Client** — `Session`, managed/V2 subscription engines, reconnect
handlers, channel keep-alive, periodic publishing, complex-type
discovery polling.
- **PubSub** — `IntervalRunner` pipeline, message-receive timers,
MQTT/UDP keep-alive, publishers/readers.
- **Gds / Lds** — `LdsServer`, `LinqApplicationsDatabase` expiry
housekeeping, GDS clients (push, pull), `DirectoryCertificateStore`,
`ConnectionStateMachine`, classic subscription engine.
- **Quickstarts / Console samples** — `SampleNodeManager`,
`ReferenceNodeManager`, `AlarmNodeManager`, `BoilerState`,
`MemoryBufferState`, `PublishedValuesWrites`. Samples resolve via
`(Server as ITimeProviderProvider)?.TimeProvider ?? TimeProvider.System`
so existing ctor signatures stay binary-compatible.
- **Tests** — `Opc.Ua.PubSub.Tests` adds `IntervalRunnerTests`; existing
`SubscriptionTests`, `UaPublisherTests`, and `TestDataSystem` move off
`HiResClock`. Added `Microsoft.Extensions.TimeProvider.Testing` package
for `FakeTimeProvider` (project-load warning surfaces on net48/net472
only — runtime is .NET 8+).

### Migration guide

New section [**Time and Timer abstraction
(`TimeProvider`)**](Docs/MigrationGuide.md#time-and-timer-abstraction-timeprovider)
documents:

- The canonical replacement table (`HiResClock.UtcNow` →
`timeProvider.GetUtcNow().UtcDateTime`, `HiResClock.TickCount64` →
`timeProvider.GetTimestamp()`, `new Timer` → `timeProvider.CreateTimer`,
`Task.Delay(delay, ct)` → `Task.Delay(delay, timeProvider, ct)`, …).
- The `TimeProvider? timeProvider = null` + `??=` constructor pattern.
- DI wiring and how to swap in `FakeTimeProvider` in tests.
- Before/after snippets for `HiResClock.TickCount64`,
`HiResClock.UtcNow`, and `new Timer(...)`.

## Related Issues

- Fixes #2458

## Checklist

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-06-03 22:38:32 +02:00

5098 lines
205 KiB
C#

/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.Collections.Generic;
using System.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
{
/// <summary>
/// Manages a session with a server.
/// </summary>
/// <remarks>
/// <para>
/// Many properties on <see cref="ConfiguredEndpoint"/> (e.g. <c>EndpointUrl</c>,
/// <c>Configuration</c>) and <see cref="EndpointDescription"/> (<c>SecurityPolicyUri</c>)
/// are annotated nullable on the schema-generated DTOs but are required at runtime once
/// a session has been created. The same applies to <see cref="ApplicationConfiguration"/>
/// members such as <c>ClientConfiguration</c>, <c>CertificateValidator</c> and
/// <c>ApplicationName</c>: a <c>Session</c> cannot be opened without them - see
/// <see cref="ValidateClientConfiguration(ApplicationConfiguration)"/>.
/// Null-forgiving (<c>!</c>) suppressions in this file reflect those lifecycle
/// invariants which the C# compiler cannot infer from the type system.
/// </para>
/// <para>
/// The instance-level fields <c>m_serverCertificate</c>, <c>m_instanceCertificate</c>
/// and <c>m_userTokenSecurityPolicyUri</c> are nullable until <c>OpenAsync</c> /
/// <c>ActivateSessionAsync</c> populates them; uses inside post-open code paths bang
/// them to acknowledge the open-session invariant. A <c>!</c> on a return value of
/// <see cref="SecurityPolicies.GetInfo"/> (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 <see cref="NullReferenceException"/> if a
/// caller-supplied policy URI is unknown.
/// </para>
/// </remarks>
public partial class Session : SessionClientBatched, ISession,
ISnapshotRestore<SessionState>, ISnapshotRestore<SessionConfiguration>
{
private const int kReconnectTimeout = 15000;
/// <summary>
/// Constructs a new instance of the <see cref="Session"/> class.
/// </summary>
/// <param name="channel">The channel used to communicate with the server.</param>
/// <param name="configuration">The configuration for the client application.</param>
/// <param name="endpoint">The endpoint use to initialize the channel.</param>
/// <exception cref="ArgumentException">
/// <paramref name="channel"/> is not an <see cref="ITransportChannel"/>.
/// </exception>
[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)
{
}
/// <summary>
/// Constructs a new instance of the <see cref="ISession"/> class.
/// </summary>
/// <param name="channel">The channel used to communicate with the server.</param>
/// <param name="configuration">The configuration for the client application.</param>
/// <param name="endpoint">The endpoint used to initialize the channel.</param>
/// <param name="clientCertificate">The certificate to use for the client.</param>
/// <param name="clientCertificateChain">The certificate chain of the client
/// certificate.</param>
/// <param name="availableEndpoints">The list of available endpoints returned
/// by server in GetEndpoints() response.</param>
/// <param name="discoveryProfileUris">The value of profileUris used in
/// GetEndpoints() request.</param>
/// <param name="engineFactory">Optional subscription engine factory. When
/// <c>null</c> the session uses <see cref="ClassicSubscriptionEngineFactory"/>
/// (the classic engine) by default. Pass
/// <see cref="DefaultSubscriptionEngineFactory.Instance"/> explicitly
/// to opt into the V2 engine.</param>
/// <param name="timeProvider">Optional <see cref="TimeProvider"/> used by the
/// session for keep-alive timers and elapsed-time calculations. When
/// <see langword="null"/>, <see cref="TimeProvider.System"/> is used.</param>
/// <remarks>
/// 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 <i>availableEndpoints</i> and <i>discoveryProfileUris</i> parameters are
/// used to validate that the list of EndpointDescriptions returned at GetEndpoints
/// matches the list returned at CreateSession.
/// </remarks>
public Session(
ITransportChannel channel,
ApplicationConfiguration configuration,
ConfiguredEndpoint endpoint,
Certificate? clientCertificate = null,
CertificateCollection? clientCertificateChain = null,
ArrayOf<EndpointDescription> availableEndpoints = default,
ArrayOf<string> 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;
}
/// <summary>
/// Initializes a new instance of the <see cref="ISession"/> class.
/// </summary>
/// <param name="channel">The channel.</param>
/// <param name="template">The template session.</param>
/// <param name="copyEventHandlers">if set to <c>true</c> the event handlers are copied.</param>
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));
}
}
/// <summary>
/// Initializes the session.
/// </summary>
/// <exception cref="ArgumentNullException">
/// <paramref name="messageContext"/> is null.
/// </exception>
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<Session>();
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
};
}
/// <summary>
/// Check if all required configuration fields are populated.
/// </summary>
/// <remarks>
/// Subsequent code paths in this class dereference <see cref="ApplicationConfiguration.ClientConfiguration"/>,
/// <see cref="ApplicationConfiguration.SecurityConfiguration"/> and
/// <see cref="ApplicationConfiguration.CertificateManager"/> using the null-forgiving
/// (<c>!</c>) operator. This method enforces the matching invariant at session
/// construction time; the compiler cannot infer the narrowing across method boundaries.
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="configuration"/> is <c>null</c>.</exception>
/// <exception cref="ServiceResultException"></exception>
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);
}
/// <summary>
/// Validates the server nonce and security parameters of user identity.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
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.");
}
}
}
}
/// <summary>
/// Closes the session and the underlying channel.
/// </summary>
#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();
}
}
/// <inheritdoc/>
public async ValueTask DisposeAsync()
{
GC.SuppressFinalize(this);
await DisposeAsyncCore(true).ConfigureAwait(false);
}
/// <summary>
/// Core async dispose logic for the session.
/// </summary>
/// <param name="disposing">True if called from dispose, false otherwise.</param>
#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<Subscription>? 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);
}
}
/// <summary>
/// Raised when a keep alive arrives from the server or an error is detected.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public event KeepAliveEventHandler KeepAlive
{
add => m_KeepAlive += value;
remove => m_KeepAlive -= value;
}
/// <summary>
/// Raised when a notification message arrives in a publish response.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public event NotificationEventHandler Notification
{
add => m_Publish += value;
remove => m_Publish -= value;
}
/// <summary>
/// Raised when an exception occurs while processing a publish response.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public event PublishErrorEventHandler PublishError
{
add => m_PublishError += value;
remove => m_PublishError -= value;
}
/// <inheritdoc/>
public event PublishSequenceNumbersToAcknowledgeEventHandler PublishSequenceNumbersToAcknowledge
{
add => m_PublishSequenceNumbersToAcknowledge += value;
remove => m_PublishSequenceNumbersToAcknowledge -= value;
}
/// <summary>
/// Raised when a subscription is added or removed
/// </summary>
public event EventHandler SubscriptionsChanged
{
add => m_SubscriptionsChanged += value;
remove => m_SubscriptionsChanged -= value;
}
/// <summary>
/// Raised to indicate the session is closing.
/// </summary>
public event EventHandler SessionClosing
{
add => m_SessionClosing += value;
remove => m_SessionClosing -= value;
}
/// <inheritdoc/>
public event EventHandler SessionConfigurationChanged
{
add => m_SessionConfigurationChanged += value;
remove => m_SessionConfigurationChanged -= value;
}
/// <summary>
/// A session factory that was used to create the session.
/// </summary>
public ISessionFactory SessionFactory { get; set; }
/// <summary>
/// Gets the subscription engine factory used by this session.
/// </summary>
public ISubscriptionEngineFactory SubscriptionEngineFactory { get; }
/// <summary>
/// Gets the active subscription engine. Internal — exposes the
/// engine for adapter scenarios such as <see cref="ManagedSession"/>
/// surfacing the new V2 <c>ISubscriptionManager</c>.
/// </summary>
internal ISubscriptionEngine SubscriptionEngine => m_engine;
/// <summary>
/// Gets the endpoint used to connect to the server.
/// </summary>
public ConfiguredEndpoint ConfiguredEndpoint => m_endpoint;
/// <summary>
/// Gets the name assigned to the session.
/// </summary>
public string SessionName => m_sessionName;
/// <summary>
/// Whether the session is reconnecting
/// </summary>
public bool Reconnecting { get; private set; }
/// <summary>
/// Whether the session is closing
/// </summary>
public bool Closing { get; private set; }
/// <summary>
/// Gets the period for wich the server will maintain the session if
/// there is no communication from the client.
/// </summary>
public double SessionTimeout => m_sessionTimeout;
/// <summary>
/// Gets the local handle assigned to the session.
/// </summary>
public object? Handle { get; set; }
/// <summary>
/// Gets the user identity currently used for the session.
/// </summary>
public IUserIdentity Identity => m_identity;
/// <summary>
/// Gets a list of user identities that can be used to connect to the server.
/// </summary>
public IEnumerable<IUserIdentity> IdentityHistory => m_identityHistory;
/// <summary>
/// Gets the table of namespace uris known to the server.
/// </summary>
public NamespaceTable NamespaceUris { get; private set; }
/// <summary>
/// Gets the table of remote server uris known to the server.
/// </summary>
public StringTable ServerUris { get; private set; }
/// <summary>
/// Gets the system context for use with the session.
/// </summary>
public ISystemContext SystemContext => m_systemContext;
/// <summary>
/// Gets the factory used to create encodeable objects that the server understands.
/// </summary>
public IEncodeableFactory Factory { get; private set; }
/// <summary>
/// Gets the cache of the server's type tree.
/// </summary>
public ITypeTable TypeTree => m_nodeCache.TypeTree;
/// <summary>
/// Gets the cache of nodes fetched from the server.
/// </summary>
public INodeCache NodeCache => m_nodeCache;
/// <summary>
/// Gets the context to use for filter operations.
/// </summary>
public IFilterContext FilterContext
=> new FilterContext(NamespaceUris, m_nodeCache.TypeTree, m_preferredLocales, m_telemetry);
/// <summary>
/// Gets the locales that the server should use when returning localized text.
/// </summary>
public ArrayOf<string> PreferredLocales => m_preferredLocales;
/// <summary>
/// Gets the subscriptions owned by the session.
/// </summary>
public IEnumerable<Subscription> Subscriptions
{
get
{
lock (m_lock)
{
return [.. m_subscriptions];
}
}
}
/// <summary>
/// Gets the number of subscriptions owned by the session.
/// </summary>
public int SubscriptionCount
{
get
{
lock (m_lock)
{
return m_subscriptions.Count;
}
}
}
/// <summary>
/// If the subscriptions are deleted when a session is closed.
/// </summary>
/// <remarks>
/// Default <c>true</c>, set to <c>false</c> if subscriptions need to
/// be transferred or for durable subscriptions.
/// </remarks>
public bool DeleteSubscriptionsOnClose { get; set; }
/// <inheritdoc/>
public int PublishRequestCancelDelayOnCloseSession { get; set; }
/// <summary>
/// If the subscriptions are transferred when a session is reconnected.
/// </summary>
/// <remarks>
/// Default <c>false</c>, set to <c>true</c> if subscriptions should
/// be transferred after reconnect. Service must be supported by server.
/// </remarks>
public bool TransferSubscriptionsOnReconnect { get; set; }
/// <summary>
/// Whether the endpoint Url domain is checked in the certificate.
/// </summary>
public bool CheckDomain => m_checkDomain;
/// <summary>
/// Gets or Sets the default subscription for the session.
/// </summary>
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;
}
}
/// <summary>
/// Gets or Sets how frequently the server is pinged to see if communication is still working.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public int KeepAliveInterval
{
get => m_keepAliveInterval;
set
{
m_keepAliveInterval = value;
ResetKeepAliveTimer();
}
}
/// <summary>
/// Returns true if the session is not receiving keep alives.
/// </summary>
/// <remarks>
/// 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
/// <see cref="m_keepAliveIntervalFactor"/> / <see cref="m_keepAliveGuardBand"/> fields.
/// or if another error was reported.
/// Set to false is communication is ok or recovered.
/// </remarks>
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;
}
}
/// <summary>
/// Gets the time of the last keep alive.
/// </summary>
public DateTime LastKeepAliveTime
{
get
{
long ticks = Interlocked.Read(ref m_lastKeepAliveTime);
return new DateTime(ticks, DateTimeKind.Utc);
}
}
/// <summary>
/// Gets the monotonic timestamp (from <see cref="TimeProvider.GetTimestamp"/>) of the
/// last keep alive. Use together with <see cref="TimeProvider.GetElapsedTime(long)"/>
/// to compute the elapsed time since the last keep alive without DateTime drift or
/// 32-bit tick wrap.
/// </summary>
public long LastKeepAliveTimestamp => m_lastKeepAliveTimestamp;
/// <summary>
/// Gets the number of outstanding publish or keep alive requests.
/// </summary>
public int OutstandingRequestCount
{
get
{
lock (m_outstandingRequests)
{
return m_outstandingRequests.Count;
}
}
}
/// <summary>
/// Gets the number of outstanding publish or keep alive requests which appear to be missing.
/// </summary>
public int DefunctRequestCount
{
get
{
lock (m_outstandingRequests)
{
int count = 0;
for (LinkedListNode<AsyncRequestState>? ii = m_outstandingRequests.First;
ii != null;
ii = ii.Next)
{
if (ii.Value.Defunct)
{
count++;
}
}
return count;
}
}
}
/// <summary>
/// Gets the number of good outstanding publish requests.
/// </summary>
public int GoodPublishRequestCount
{
get
{
lock (m_outstandingRequests)
{
int count = 0;
for (LinkedListNode<AsyncRequestState>? ii = m_outstandingRequests.First;
ii != null;
ii = ii.Next)
{
if (!ii.Value.Defunct && ii.Value.RequestTypeId == DataTypes.PublishRequest)
{
count++;
}
}
return count;
}
}
}
/// <summary>
/// Gets and sets the minimum number of publish requests to be used in the session.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public int MinPublishRequestCount
{
get => m_engine.MinPublishRequestCount;
set => m_engine.MinPublishRequestCount = value;
}
/// <summary>
/// Gets and sets the maximum number of publish requests to be used in the session.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public int MaxPublishRequestCount
{
get => m_engine.MaxPublishRequestCount;
set => m_engine.MaxPublishRequestCount = value;
}
/// <inheritdoc/>
public ContinuationPointPolicy ContinuationPointPolicy { get; set; }
= ContinuationPointPolicy.Default;
/// <inheritdoc/>
public event RenewUserIdentityEventHandler RenewUserIdentity
{
add => m_RenewUserIdentity += value;
remove => m_RenewUserIdentity -= value;
}
private event RenewUserIdentityEventHandler? m_RenewUserIdentity;
/// <inheritdoc/>
public virtual void Snapshot(out SessionState state)
{
using Activity? activity = m_telemetry.StartActivity();
Snapshot(out SessionConfiguration configuration);
// Snapshot subscription state
var subscriptionStates = new List<SubscriptionState>(SubscriptionCount);
foreach (Subscription subscription in Subscriptions)
{
subscription.Snapshot(out SubscriptionState subscriptionState);
subscriptionStates.Add(subscriptionState);
}
state = new SessionState(configuration)
{
Subscriptions = subscriptionStates
};
}
/// <inheritdoc/>
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);
}
}
/// <inheritdoc/>
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
};
}
/// <inheritdoc/>
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);
}
}
/// <inheritdoc/>
public bool ApplySessionConfiguration(SessionConfiguration sessionConfiguration)
{
if (sessionConfiguration == null)
{
throw new ArgumentNullException(nameof(sessionConfiguration));
}
Restore(sessionConfiguration);
return true;
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public virtual void Save(
Stream stream,
IEnumerable<Subscription> subscriptions,
IEnumerable<Type>? knownTypes = null)
{
using Activity? activity = m_telemetry.StartActivity();
// Snapshot subscription state
var subscriptionStates = new List<SubscriptionState>();
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);
}
}
/// <inheritdoc/>
public virtual IEnumerable<Subscription> Load(
Stream stream,
bool transferSubscriptions = false,
IEnumerable<Type>? 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<string?> nsUris = decoder.ReadStringArray(null);
ArrayOf<string?> 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;
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(m_endpoint, m_sessionName, SessionId);
}
/// <summary>
/// An overrideable version of a session clone which is used
/// internally to create new subclassed clones from a Session class.
/// </summary>
public virtual Session CloneSession(ITransportChannel channel, bool copyEventHandlers)
{
return new Session(channel, this, copyEventHandlers);
}
/// <inheritdoc/>
public async Task OpenAsync(
string sessionName,
uint sessionTimeout,
IUserIdentity identity,
ArrayOf<string> 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<EndpointDescription> 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<StatusCode> certificateResults = activateResponse.Results;
ArrayOf<DiagnosticInfo> 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;
}
}
/// <inheritdoc/>
public Task ChangePreferredLocalesAsync(
ArrayOf<string> preferredLocales,
CancellationToken ct)
{
return UpdateSessionAsync(Identity, preferredLocales, ct);
}
/// <summary>
/// Refreshes the current user identity by asking a provider for a
/// fresh token and reactivating the existing session with the
/// current server nonce.
/// </summary>
/// <param name="provider">The provider that materializes the new identity.</param>
/// <param name="ct">A cancellation token.</param>
/// <exception cref="ArgumentNullException"><paramref name="provider"/> is <c>null</c>.</exception>
/// <exception cref="ServiceResultException">
/// <c>BadIdentityTokenRejected</c> when no offered policy can be
/// satisfied by the provider, or <c>BadIdentityChangeNotSupported</c>
/// when the server refuses user-identity changes on an active
/// session.
/// </exception>
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);
}
}
/// <inheritdoc/>
public async Task UpdateSessionAsync(
IUserIdentity? identity,
ArrayOf<string> 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();
}
/// <inheritdoc/>
public async Task<bool> 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;
}
/// <inheritdoc/>
public async Task<bool> RemoveSubscriptionsAsync(
IEnumerable<Subscription> subscriptions,
CancellationToken ct = default)
{
ThrowIfDisposed();
using Activity? activity = m_telemetry.StartActivity();
if (subscriptions == null)
{
throw new ArgumentNullException(nameof(subscriptions));
}
var subscriptionsToDelete = new List<Subscription>();
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;
}
/// <inheritdoc/>
public async Task<bool> ReactivateSubscriptionsAsync(
SubscriptionCollection subscriptions,
bool sendInitialValues,
CancellationToken ct = default)
{
ThrowIfDisposed();
using Activity? activity = m_telemetry.StartActivity();
ArrayOf<uint> 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<ServiceResult> 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;
}
/// <inheritdoc/>
public async Task<bool> TransferSubscriptionsAsync(
SubscriptionCollection subscriptions,
bool sendInitialValues,
CancellationToken ct)
{
using Activity? activity = m_telemetry.StartActivity();
ArrayOf<uint> 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<TransferResult> results = response.Results;
ArrayOf<DiagnosticInfo> 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;
}
/// <inheritdoc/>
public async Task FetchNamespaceTablesAsync(CancellationToken ct = default)
{
using Activity? activity = m_telemetry.StartActivity();
ArrayOf<ReadValueId> nodesToRead = PrepareNamespaceTableNodesToRead();
// read from server.
ReadResponse response = await ReadAsync(
null,
0,
TimestampsToReturn.Neither,
nodesToRead,
ct)
.ConfigureAwait(false);
ArrayOf<DataValue> values = response.Results;
ArrayOf<DiagnosticInfo> diagnosticInfos = response.DiagnosticInfos;
ResponseHeader responseHeader = response.ResponseHeader;
ValidateResponse(values, nodesToRead);
ValidateDiagnosticInfos(diagnosticInfos, nodesToRead);
UpdateNamespaceTable(values, diagnosticInfos, responseHeader);
}
/// <inheritdoc/>
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<ExpandedNodeId>();
foreach (IReference reference in node.Find(ReferenceTypeIds.HasSubtype, false))
{
subTypes.Add(reference.TargetId);
}
if (subTypes.Count > 0)
{
await FetchTypeTreeAsync(subTypes, ct).ConfigureAwait(false);
}
}
}
/// <inheritdoc/>
public async Task FetchTypeTreeAsync(
ArrayOf<ExpandedNodeId> typeIds,
CancellationToken ct = default)
{
using Activity? activity = m_telemetry.StartActivity();
ArrayOf<NodeId> referenceTypeIds = [ReferenceTypeIds.HasSubtype];
ArrayOf<INode> nodes = await NodeCache
.FindReferencesAsync(typeIds, referenceTypeIds, false, false, ct)
.ConfigureAwait(false);
var subTypes = new List<ExpandedNodeId>();
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);
}
}
/// <inheritdoc/>
public async Task FetchOperationLimitsAsync(CancellationToken ct)
{
using Activity? activity = m_telemetry.StartActivity();
// Helper extraction
static uint GetUInt32(ref int index, ArrayOf<DataValue> values, ArrayOf<ServiceResult> 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<DataValue> values, ArrayOf<ServiceResult> 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<DataValue> values, ArrayOf<ServiceResult> 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<NodeId> nodeIds =
[
VariableIds.Server_ServerCapabilities_OperationLimits_MaxNodesPerRead
];
(ArrayOf<DataValue> values, ArrayOf<ServiceResult> 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;
}
}
/// <summary>
/// Recreates a session based on a specified template.
/// </summary>
/// <param name="ct">Cancellation Token to cancel operation with</param>
/// <returns>The new session object.</returns>
protected internal async Task<Session> 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;
}
/// <summary>
/// Recreates a session based on a specified template.
/// </summary>
/// <param name="connection">The waiting reverse connection.</param>
/// <param name="ct">Cancellation token to cancel operation with</param>
/// <returns>The new session object.</returns>
protected internal async Task<Session> 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;
}
/// <summary>
/// Recreates a session based on a specified template using the provided channel.
/// </summary>
/// <param name="transportChannel">The waiting reverse connection.</param>
/// <param name="ct">Cancellation token to cancel the operation with</param>
/// <returns>The new session object.</returns>
protected internal async Task<Session> 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;
}
/// <summary>
/// Re-establishes the session in place on the same
/// <see cref="Session"/> instance instead of allocating a new one.
/// Tears down the current channel, optionally switches to a new
/// <paramref name="endpoint"/>, and re-runs the
/// <c>CreateSession</c>/<c>ActivateSession</c> sequence so the
/// server assigns a fresh session id. The
/// in-memory subscription engine (<see cref="m_engine"/>),
/// subscription manager, and per-subscription contexts continue
/// to reference <c>this</c>; 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 <see cref="Subscriptions"/>.
/// </summary>
/// <remarks>
/// Designed to be the production wiring used by
/// <c>ManagedSession.HandleFailoverAsync</c> so that the inner
/// <see cref="Session"/> reference stays stable across a
/// failover. Differs from <see cref="ReconnectAsync"/> by
/// performing a full <c>CreateSession</c> against the server
/// (new server-side session id) instead of just re-activating
/// the existing one. Differs from
/// <see cref="RecreateAsync(CancellationToken)"/> overloads in
/// that the receiver is reused rather than cloned into a new
/// <see cref="Session"/> instance.
/// </remarks>
/// <param name="endpoint">Optional replacement endpoint. When
/// non-null and not the same instance as the current
/// <see cref="ConfiguredEndpoint"/> the new endpoint is adopted
/// before the channel is rebuilt.</param>
/// <param name="connection">Optional waiting reverse
/// connection. Mutually exclusive with
/// <paramref name="channel"/>.</param>
/// <param name="channel">Optional pre-built transport channel.
/// Mutually exclusive with <paramref name="connection"/>. When
/// neither is supplied a new outbound channel is built against
/// <see cref="ConfiguredEndpoint"/>.</param>
/// <param name="ct">Cancellation token.</param>
/// <exception cref="ServiceResultException"></exception>
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();
}
}
}
/// <inheritdoc/>
public override Task<StatusCode> CloseAsync(CancellationToken ct = default)
{
return CloseAsync(m_keepAliveInterval, true, ct);
}
/// <inheritdoc/>
public virtual async Task<StatusCode> 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;
}
}
/// <inheritdoc/>
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();
}
}
/// <inheritdoc/>
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<StatusCode> certificateResults = activateResult.Results;
ArrayOf<DiagnosticInfo> 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
/// <inheritdoc/>
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
/// <summary>
/// Recreate the subscriptions in a recreated session.
/// </summary>
/// <param name="transferSubscriptionTemplates">Uses Transfer service
/// if set to <c>true</c>.</param>
/// <param name="subscriptionsTemplate">The template for the subscriptions.</param>
/// <param name="ct">Cancellation token to cancel operation with</param>
private async Task RecreateSubscriptionsAsync(
bool transferSubscriptionTemplates,
IEnumerable<Subscription> 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);
}
}
}
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
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;
}
/// <summary>
/// Handles an error when validating the application instance certificate provided by the server.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
protected virtual void OnApplicationCertificateError(
ByteString serverCertificate,
ServiceResult result)
{
throw new ServiceResultException(result);
}
/// <summary>
/// Starts a timer to check that the connection to the server is still available.
/// </summary>
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<ReadValueId> 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));
}
}
/// <inheritdoc/>
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);
}
/// <summary>
/// Reset the timer used to send keep alive messages.
/// </summary>
private void ResetKeepAliveTimer()
{
lock (m_lock)
{
if (m_keepAliveWorker != null)
{
m_keepAliveTimer.Change(TimeSpan.FromMilliseconds(m_keepAliveInterval),
TimeSpan.FromMilliseconds(m_keepAliveInterval));
}
}
}
/// <summary>
/// Stops the keep alive timer.
/// </summary>
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();
}
}
/// <summary>
/// Waits for outstanding publish requests to complete or cancels them.
/// </summary>
private async Task WaitForOrCancelOutstandingPublishRequestsAsync(CancellationToken ct)
{
// Get outstanding publish requests
List<uint> 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<uint> 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);
}
}
}
}
/// <summary>
/// Removes a completed async request.
/// </summary>
private AsyncRequestState? RemoveRequest(Task result, uint requestId, uint typeId)
{
lock (m_outstandingRequests)
{
for (LinkedListNode<AsyncRequestState>? 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;
}
}
/// <summary>
/// Adds a new async request.
/// </summary>
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();
}
}
}
/// <summary>
/// Removes a completed async request.
/// </summary>
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<AsyncRequestState>? 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);
}
}
}
/// <summary>
/// Sends a keep alive by reading from the server.
/// </summary>
private async Task OnSendKeepAliveAsync(
ArrayOf<ReadValueId> 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<DataValue> values = result.Results;
ArrayOf<DiagnosticInfo> 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);
}
}
}
/// <summary>
/// Called when the server returns a keep alive response.
/// </summary>
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<AsyncRequestState>? 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.");
}
}
}
/// <summary>
/// Called when a error occurs during a keep alive.
/// </summary>
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;
}
/// <summary>
/// Prepare a list of subscriptions to delete.
/// </summary>
private bool PrepareSubscriptionsToDelete(
IEnumerable<Subscription> subscriptions,
List<Subscription> 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;
}
/// <summary>
/// Prepares the list of node ids to read to fetch the namespace table.
/// </summary>
private static ArrayOf<ReadValueId> 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
}
];
}
/// <summary>
/// Updates the NamespaceTable with the result of the
/// <see cref="PrepareNamespaceTableNodesToRead"/> read operation.
/// Throws in case of types not matching or empty namespace
/// array returned.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
private void UpdateNamespaceTable(
ArrayOf<DataValue> values,
ArrayOf<DiagnosticInfo> 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<string> 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<string> serverArray))
{
ServerUris.Update(serverArray.ToArray()!);
}
}
#if OPCUA_V1_CLIENT
/// <summary>
/// Sends an additional publish request.
/// Only valid when using the classic subscription engine; the V2
/// engine drives publishing through its own worker pool.
/// </summary>
public bool BeginPublish(int timeout)
{
if (m_engine is ClassicSubscriptionEngine classicEngine)
{
return classicEngine.BeginPublish(timeout);
}
return false;
}
#endif
/// <summary>
/// Create the publish requests for the active subscriptions.
/// </summary>
public void StartPublishing(int timeout, bool fullQueue)
{
m_engine.StartPublishing(timeout, fullQueue);
}
/// <summary>
/// Helper to refresh the identity (reprompt for password, refresh token) in case of a Recreate of the Session.
/// </summary>
public virtual void RecreateRenewUserIdentity()
{
if (m_RenewUserIdentity != null)
{
IUserIdentity renewed = m_RenewUserIdentity(this, m_identity);
m_identity = renewed ?? new UserIdentity();
}
}
/// <summary>
/// Helper to throw a recreate session exception.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
private static void ThrowCouldNotRecreateSessionException(Exception e, string sessionName)
{
throw ServiceResultException.Create(
StatusCodes.BadCommunicationError,
e,
"Could not recreate session {0}:{1}",
sessionName,
e.Message);
}
/// <summary>
/// Validates the identity for an open call.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
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<byte>();
for (int i = 0; i < m_instanceCertificateChain.Count; i++)
{
clientCertificateChain.AddRange(m_instanceCertificateChain[i].RawData);
}
clientCertificateChainData = clientCertificateChain.ToByteString();
}
}
/// <summary>
/// Validates the server certificate returned.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
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.");
}
}
}
/// <summary>
/// Validates the server signature created with the client nonce.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
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.");
}
}
}
/// <summary>
/// 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 <see cref="ValidateServerEndpoints"/>
/// with the applicationUri of the server description before the validation.
/// </summary>
private void ValidateServerCertificateApplicationUri(
Certificate? serverCertificate,
ConfiguredEndpoint endpoint)
{
if (serverCertificate != null)
{
ICertificateValidatorEx validator = m_configuration.CertificateManager;
validator.ValidateApplicationUri(serverCertificate, endpoint);
}
}
/// <summary>
/// Validates the server endpoints returned.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
private void ValidateServerEndpoints(ArrayOf<EndpointDescription> serverEndpoints)
{
if (!m_discoveryServerEndpoints.IsEmpty)
{
// Compare EndpointDescriptions returned at GetEndpoints with values returned at CreateSession
ArrayOf<EndpointDescription> 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<EndpointDescription>();
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.");
}
}
/// <summary>
/// Find and return matching application description
/// </summary>
/// <param name="endpointDescriptions">The descriptions to search through</param>
/// <param name="match">The description to match</param>
/// <param name="matchPort">Match criteria includes port</param>
/// <returns>Matching description or null if no description is matching</returns>
private EndpointDescription? FindMatchingDescription(
ArrayOf<EndpointDescription> 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;
}
/// <summary>
/// Update the target description from the source description
/// </summary>
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;
}
/// <summary>
/// Invokes a DeleteSubscriptions call for the specified subscriptionId.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
private async ValueTask DeleteSubscriptionAsync(
uint subscriptionId,
CancellationToken ct = default)
{
try
{
m_logger.LogInformation(
"Deleting server subscription for SubscriptionId={SubscriptionId}",
subscriptionId);
// delete the subscription.
ArrayOf<uint> subscriptionIds = [subscriptionId];
DeleteSubscriptionsResponse response = await DeleteSubscriptionsAsync(
null,
subscriptionIds,
ct).ConfigureAwait(false);
ResponseHeader responseHeader = response.ResponseHeader;
ArrayOf<StatusCode> results = response.Results;
ArrayOf<DiagnosticInfo> 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);
}
}
/// <summary>
/// Asynchronously load instance certificate
/// </summary>
/// <exception cref="ServiceResultException"></exception>
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);
}
/// <summary>
/// Load certificate for connection.
/// </summary>
/// <exception cref="ServiceResultException"></exception>
internal static async Task<Certificate> 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);
}
/// <summary>
/// Load certificate chain for connection.
/// </summary>
internal static async Task<CertificateCollection?> LoadCertificateChainAsync(
ApplicationConfiguration configuration,
Certificate clientCertificate,
CancellationToken ct = default)
{
CertificateCollection? clientCertificateChain = null;
// load certificate chain.
if (configuration.SecurityConfiguration.SendCertificateChain)
{
clientCertificateChain = [clientCertificate];
var issuers = new List<CertificateIssuerReference>();
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;
}
/// <summary>
/// Creates and validates the subscription ids for a transfer.
/// </summary>
/// <param name="subscriptions">The subscriptions to transfer.</param>
/// <returns>The subscription ids for the transfer.</returns>
/// <exception cref="ServiceResultException">Thrown if a subscription is in invalid state.</exception>
private ArrayOf<uint> CreateSubscriptionIdsForTransfer(
SubscriptionCollection subscriptions)
{
var subscriptionIds = new List<uint>();
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();
}
/// <summary>
/// Indicates that the session configuration has changed.
/// </summary>
private void IndicateSessionConfigurationChanged()
{
try
{
m_SessionConfigurationChanged?.Invoke(this, EventArgs.Empty);
}
catch (Exception e)
{
m_logger.LogError(
e,
"Unexpected error calling SessionConfigurationChanged event handler.");
}
}
/// <summary>
/// Creates a request header with additional parameters
/// for the ecc user token security policy, if needed.
/// </summary>
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;
}
/// <summary>
/// Create a subscription the provided item options
/// </summary>
protected virtual Subscription CreateSubscription(SubscriptionOptions? options = null)
{
return new Subscription(m_telemetry, options);
}
/// <summary>
/// Process the AdditionalHeader field of a ResponseHeader
/// </summary>
/// <exception cref="ServiceResultException"></exception>
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<byte>(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);
}
}
}
}
/// <summary>
/// The period for which the server will maintain the session if there is no communication from the client.
/// </summary>
protected double m_sessionTimeout;
/// <summary>
/// The locales that the server should use when returning localized text.
/// </summary>
protected ArrayOf<string> m_preferredLocales;
/// <summary>
/// The Application Configuration.
/// </summary>
protected ApplicationConfiguration m_configuration;
/// <summary>
/// The endpoint configured for the session.
/// </summary>
protected ConfiguredEndpoint m_endpoint;
/// <summary>
/// The endpoint used while connected to the server.
/// </summary>
protected ConfiguredEndpoint m_effectiveEndpoint;
/// <summary>
/// The Instance Certificate.
/// </summary>
protected Certificate? m_instanceCertificate;
/// <summary>
/// The Instance Certificate Chain.
/// </summary>
protected CertificateCollection? m_instanceCertificateChain;
/// <summary>
/// The session telemetry context
/// </summary>
protected ITelemetryContext m_telemetry;
/// <summary>
/// If set to<c>true</c> then the domain in the certificate must match the endpoint used.
/// </summary>
protected bool m_checkDomain;
/// <summary>
/// The name assigned to the session.
/// </summary>
protected string m_sessionName;
/// <summary>
/// The user identity currently used for the session.
/// </summary>
protected IUserIdentity m_identity;
/// <summary>
/// Factor applied to the <see cref="m_keepAliveInterval"/> before <see cref="KeepAliveStopped"/> is set to true
/// </summary>
protected int m_keepAliveIntervalFactor = 1;
/// <summary>m
/// Time in milliseconds added to <see cref="m_keepAliveInterval"/> before <see cref="KeepAliveStopped"/> is set to true
/// </summary>
protected int m_keepAliveGuardBand = 1000;
private readonly Lock m_lock = new();
private readonly List<Subscription> 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<IUserIdentity> 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<AsyncRequestState> 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<EndpointDescription> m_discoveryServerEndpoints;
private readonly ArrayOf<string> 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;
}
/// <summary>
/// The event arguments provided when a keep alive response arrives.
/// </summary>
public class KeepAliveEventArgs : EventArgs
{
/// <summary>
/// Creates a new instance.
/// </summary>
public KeepAliveEventArgs(
ServiceResult? status,
ServerState currentState,
DateTime currentTime)
{
Status = status;
CurrentState = currentState;
CurrentTime = currentTime;
}
/// <summary>
/// Gets the status associated with the keep alive operation.
/// </summary>
public ServiceResult? Status { get; }
/// <summary>
/// Gets the current server state.
/// </summary>
public ServerState CurrentState { get; }
/// <summary>
/// Gets the current server time.
/// </summary>
public DateTime CurrentTime { get; }
/// <summary>
/// Gets or sets a flag indicating whether the session should send another keep alive.
/// </summary>
public bool CancelKeepAlive { get; set; }
}
/// <summary>
/// Represents the event arguments provided when a new notification message arrives.
/// </summary>
public class NotificationEventArgs : EventArgs
{
/// <summary>
/// Creates a new instance.
/// </summary>
public NotificationEventArgs(
Subscription subscription,
NotificationMessage notificationMessage,
ArrayOf<string> stringTable)
{
Subscription = subscription;
NotificationMessage = notificationMessage;
StringTable = stringTable;
}
/// <summary>
/// Gets the subscription that the notification applies to.
/// </summary>
public Subscription Subscription { get; }
/// <summary>
/// Gets the notification message.
/// </summary>
public NotificationMessage NotificationMessage { get; }
/// <summary>
/// Gets the string table returned with the notification message.
/// </summary>
public ArrayOf<string> StringTable { get; }
}
/// <summary>
/// Represents the event arguments provided when a publish error occurs.
/// </summary>
public class PublishErrorEventArgs : EventArgs
{
/// <summary>
/// Creates a new instance.
/// </summary>
public PublishErrorEventArgs(ServiceResult status)
{
Status = status;
}
/// <summary>
/// Creates a new instance.
/// </summary>
internal PublishErrorEventArgs(
ServiceResult status,
uint subscriptionId,
uint sequenceNumber)
{
Status = status;
SubscriptionId = subscriptionId;
SequenceNumber = sequenceNumber;
}
/// <summary>
/// Gets the status associated with the keep alive operation.
/// </summary>
public ServiceResult Status { get; }
/// <summary>
/// Gets the subscription with the message that could not be republished.
/// </summary>
public uint SubscriptionId { get; }
/// <summary>
/// Gets the sequence number for the message that could not be republished.
/// </summary>
public uint SequenceNumber { get; }
}
/// <summary>
/// Represents the event arguments provided when publish response
/// sequence numbers are about to be ackknowledged with a publish request.
/// </summary>
/// <remarks>
/// A callee can defer an acknowledge to the next publish request by
/// moving the <see cref="SubscriptionAcknowledgement"/> 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.
/// </remarks>
public class PublishSequenceNumbersToAcknowledgeEventArgs : EventArgs
{
/// <summary>
/// Creates a new instance.
/// </summary>
public PublishSequenceNumbersToAcknowledgeEventArgs(
List<SubscriptionAcknowledgement> acknowledgementsToSend,
List<SubscriptionAcknowledgement> deferredAcknowledgementsToSend)
{
AcknowledgementsToSend = acknowledgementsToSend;
DeferredAcknowledgementsToSend = deferredAcknowledgementsToSend;
}
/// <summary>
/// The acknowledgements which are sent with the next publish request.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public List<SubscriptionAcknowledgement> AcknowledgementsToSend { get; }
/// <summary>
/// The deferred list of acknowledgements.
/// </summary>
/// <remarks>
/// The callee can transfer an outstanding <see cref="SubscriptionAcknowledgement"/>
/// to this list to defer the acknowledge of a sequence number to the next publish request.
/// </remarks>
public List<SubscriptionAcknowledgement> DeferredAcknowledgementsToSend { get; }
}
}