mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-21 00:56:28 +08:00
dbc09b8e22
# 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.
5098 lines
205 KiB
C#
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; }
|
|
}
|
|
}
|