mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 22:55:08 +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.
1423 lines
56 KiB
C#
1423 lines
56 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.CodeAnalysis;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using Opc.Ua.Security.Certificates;
|
|
|
|
namespace Opc.Ua.Server
|
|
{
|
|
/// <summary>
|
|
/// A generic session manager object for a server.
|
|
/// </summary>
|
|
public class Session : ISession
|
|
{
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Session"/> class.
|
|
/// </summary>
|
|
/// <param name="context">The context.</param>
|
|
/// <param name="server">The Server object.</param>
|
|
/// <param name="serverCertificate">The server certificate.</param>
|
|
/// <param name="authenticationToken">The unique private identifier assigned to the Session.</param>
|
|
/// <param name="clientNonce">The client nonce.</param>
|
|
/// <param name="serverNonce">The server nonce.</param>
|
|
/// <param name="sessionName">The name assigned to the Session.</param>
|
|
/// <param name="clientDescription">Application description for the client application.</param>
|
|
/// <param name="endpointUrl">The endpoint URL.</param>
|
|
/// <param name="clientCertificate">The client certificate.</param>
|
|
/// <param name="clientCertificateChain">The client certifiate chain</param>
|
|
/// <param name="sessionTimeout">The session timeout.</param>
|
|
/// <param name="maxBrowseContinuationPoints">The maximum number of browse continuation points.</param>
|
|
/// <param name="maxHistoryContinuationPoints">The maximum number of history continuation points.</param>
|
|
public Session(
|
|
OperationContext context,
|
|
IServerInternal server,
|
|
Certificate serverCertificate,
|
|
NodeId authenticationToken,
|
|
ByteString clientNonce,
|
|
Nonce serverNonce,
|
|
string sessionName,
|
|
ApplicationDescription clientDescription,
|
|
string endpointUrl,
|
|
Certificate clientCertificate,
|
|
CertificateCollection clientCertificateChain,
|
|
double sessionTimeout,
|
|
int maxBrowseContinuationPoints,
|
|
int maxHistoryContinuationPoints)
|
|
: this(
|
|
context,
|
|
server,
|
|
serverCertificate,
|
|
authenticationToken,
|
|
clientNonce,
|
|
serverNonce,
|
|
sessionName,
|
|
clientDescription,
|
|
endpointUrl,
|
|
clientCertificate,
|
|
clientCertificateChain,
|
|
sessionTimeout,
|
|
maxBrowseContinuationPoints,
|
|
maxHistoryContinuationPoints,
|
|
timeProvider: null)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="Session"/> class with an
|
|
/// explicit <see cref="TimeProvider"/>.
|
|
/// </summary>
|
|
/// <param name="context">The context.</param>
|
|
/// <param name="server">The Server object.</param>
|
|
/// <param name="serverCertificate">The server certificate.</param>
|
|
/// <param name="authenticationToken">The unique private identifier assigned to the Session.</param>
|
|
/// <param name="clientNonce">The client nonce.</param>
|
|
/// <param name="serverNonce">The server nonce.</param>
|
|
/// <param name="sessionName">The name assigned to the Session.</param>
|
|
/// <param name="clientDescription">Application description for the client application.</param>
|
|
/// <param name="endpointUrl">The endpoint URL.</param>
|
|
/// <param name="clientCertificate">The client certificate.</param>
|
|
/// <param name="clientCertificateChain">The client certifiate chain</param>
|
|
/// <param name="sessionTimeout">The session timeout.</param>
|
|
/// <param name="maxBrowseContinuationPoints">The maximum number of browse continuation points.</param>
|
|
/// <param name="maxHistoryContinuationPoints">The maximum number of history continuation points.</param>
|
|
/// <param name="timeProvider">
|
|
/// Optional <see cref="TimeProvider"/> used for monotonic timeout
|
|
/// calculations and last-contact diagnostics. When <c>null</c>, the
|
|
/// time provider exposed by the server (via
|
|
/// <see cref="ITimeProviderProvider"/>) is used, falling back to
|
|
/// <see cref="TimeProvider.System"/>.
|
|
/// </param>
|
|
public Session(
|
|
OperationContext context,
|
|
IServerInternal server,
|
|
Certificate serverCertificate,
|
|
NodeId authenticationToken,
|
|
ByteString clientNonce,
|
|
Nonce serverNonce,
|
|
string sessionName,
|
|
ApplicationDescription clientDescription,
|
|
string endpointUrl,
|
|
Certificate clientCertificate,
|
|
CertificateCollection clientCertificateChain,
|
|
double sessionTimeout,
|
|
int maxBrowseContinuationPoints,
|
|
int maxHistoryContinuationPoints,
|
|
TimeProvider? timeProvider)
|
|
{
|
|
if (context == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(context));
|
|
}
|
|
|
|
// verify that a secure channel was specified.
|
|
if (context.ChannelContext == null)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadSecureChannelIdInvalid);
|
|
}
|
|
|
|
m_server = server ?? throw new ArgumentNullException(nameof(server));
|
|
m_timeProvider = timeProvider
|
|
?? (server as ITimeProviderProvider)?.TimeProvider
|
|
?? TimeProvider.System;
|
|
m_logger = server.Telemetry.CreateLogger<Session>();
|
|
ClientNonce = clientNonce;
|
|
m_serverNonce = serverNonce;
|
|
m_sessionName = sessionName;
|
|
m_serverCertificate = serverCertificate;
|
|
ClientCertificate = clientCertificate;
|
|
|
|
m_clientIssuerCertificates = clientCertificateChain;
|
|
|
|
SecureChannelId = context.ChannelContext.SecureChannelId;
|
|
MaxBrowseContinuationPoints = maxBrowseContinuationPoints;
|
|
m_maxHistoryContinuationPoints = maxHistoryContinuationPoints;
|
|
EndpointDescription = context.ChannelContext.EndpointDescription!;
|
|
|
|
// use anonymous the default identity.
|
|
Identity = new UserIdentity();
|
|
|
|
// initialize diagnostics.
|
|
DateTime now = m_timeProvider.GetUtcNow().UtcDateTime;
|
|
m_lastContactTickCount = m_timeProvider.GetTimestampMilliseconds();
|
|
SessionDiagnostics = new SessionDiagnosticsDataType
|
|
{
|
|
SessionId = default,
|
|
SessionName = sessionName,
|
|
ClientDescription = clientDescription,
|
|
ServerUri = null,
|
|
EndpointUrl = endpointUrl,
|
|
ActualSessionTimeout = sessionTimeout,
|
|
ClientConnectionTime = now,
|
|
ClientLastContactTime = now
|
|
};
|
|
|
|
// initialize security diagnostics.
|
|
m_securityDiagnostics = new SessionSecurityDiagnosticsDataType
|
|
{
|
|
SessionId = Id,
|
|
ClientUserIdOfSession = Identity.DisplayName,
|
|
AuthenticationMechanism = Identity.TokenType.ToString(),
|
|
Encoding = context.ChannelContext.MessageEncoding.ToString()
|
|
};
|
|
m_securityDiagnostics.ClientUserIdHistory =
|
|
m_securityDiagnostics.ClientUserIdHistory.AddItem(Identity.DisplayName);
|
|
|
|
EndpointDescription? description = context.ChannelContext.EndpointDescription;
|
|
|
|
if (description != null)
|
|
{
|
|
m_securityDiagnostics.TransportProtocol = new Uri(description.EndpointUrl!).Scheme;
|
|
m_securityDiagnostics.SecurityMode = EndpointDescription.SecurityMode;
|
|
m_securityDiagnostics.SecurityPolicyUri = EndpointDescription.SecurityPolicyUri;
|
|
}
|
|
|
|
if (clientCertificate != null)
|
|
{
|
|
m_securityDiagnostics.ClientCertificate = clientCertificate.RawData.ToByteString();
|
|
}
|
|
|
|
ServerSystemContext systemContext = m_server.DefaultSystemContext.Copy(context);
|
|
|
|
// create diagnostics object.
|
|
Id = server.DiagnosticsNodeManager.CreateSessionDiagnosticsAsync(
|
|
systemContext,
|
|
SessionDiagnostics,
|
|
OnUpdateDiagnostics,
|
|
m_securityDiagnostics,
|
|
OnUpdateSecurityDiagnostics).AsTask().GetAwaiter().GetResult();
|
|
|
|
TraceState("CREATED");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Frees any unmanaged resources.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// An overrideable version of the Dispose.
|
|
/// </summary>
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
List<ContinuationPoint>? browseCPs;
|
|
lock (m_lock)
|
|
{
|
|
browseCPs = m_browseContinuationPoints;
|
|
m_browseContinuationPoints = null;
|
|
}
|
|
|
|
if (browseCPs != null)
|
|
{
|
|
for (int ii = 0; ii < browseCPs.Count; ii++)
|
|
{
|
|
browseCPs[ii]?.Dispose();
|
|
}
|
|
}
|
|
|
|
List<HistoryContinuationPoint>? historyCPs;
|
|
lock (m_lock)
|
|
{
|
|
historyCPs = m_historyContinuationPoints;
|
|
m_historyContinuationPoints = null;
|
|
}
|
|
|
|
if (historyCPs != null)
|
|
{
|
|
for (int ii = 0; ii < historyCPs.Count; ii++)
|
|
{
|
|
(historyCPs[ii].Value as IDisposable)?.Dispose();
|
|
}
|
|
}
|
|
m_userTokenNonce?.Dispose();
|
|
m_userTokenNonce = null;
|
|
|
|
IdentityToken = null!;
|
|
|
|
ClientCertificate?.Dispose();
|
|
m_clientIssuerCertificates?.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the identifier assigned to the session when it was created.
|
|
/// </summary>
|
|
public NodeId Id { get; }
|
|
|
|
/// <summary>
|
|
/// The user identity provided by the client.
|
|
/// </summary>
|
|
public IUserIdentity Identity { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The application defined mapping for user identity provided by the client.
|
|
/// </summary>
|
|
public IUserIdentity EffectiveIdentity { get; private set; } = null!;
|
|
|
|
/// <inheritdoc/>
|
|
public bool IsIdentityStale => Volatile.Read(ref m_identityStale) != 0;
|
|
|
|
/// <inheritdoc/>
|
|
public void MarkIdentityStale()
|
|
{
|
|
Volatile.Write(ref m_identityStale, 1);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void RefreshEffectiveIdentity(IUserIdentity effectiveIdentity)
|
|
{
|
|
if (effectiveIdentity == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(effectiveIdentity));
|
|
}
|
|
|
|
lock (m_lock)
|
|
{
|
|
EffectiveIdentity = effectiveIdentity;
|
|
// Clearing the stale flag while holding the session lock
|
|
// ensures any subsequent IsIdentityStale read observes a
|
|
// consistent (refreshed identity, cleared flag) pair.
|
|
Volatile.Write(ref m_identityStale, 0);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The user identity token provided by the client.
|
|
/// </summary>
|
|
public IUserIdentityTokenHandler IdentityToken { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// A lock which must be acquired before accessing the diagnostics.
|
|
/// </summary>
|
|
public object DiagnosticsLock => SessionDiagnostics;
|
|
|
|
/// <summary>
|
|
/// The diagnostics associated with the session.
|
|
/// </summary>
|
|
public SessionDiagnosticsDataType SessionDiagnostics { get; }
|
|
|
|
/// <summary>
|
|
/// The client Nonce associated with the session.
|
|
/// </summary>
|
|
public ByteString ClientNonce { get; }
|
|
|
|
/// <summary>
|
|
/// The server application instance certificate used by this session.
|
|
/// </summary>
|
|
public Certificate ServerCertificate => m_serverCertificate;
|
|
|
|
/// <summary>
|
|
/// The application instance certificate associated with the client.
|
|
/// </summary>
|
|
public Certificate ClientCertificate { get; }
|
|
|
|
/// <summary>
|
|
/// The locales requested when the session was created.
|
|
/// </summary>
|
|
public string[] PreferredLocales { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Whether the session timeout has elapsed since the last communication from the client.
|
|
/// </summary>
|
|
public bool HasExpired
|
|
{
|
|
get
|
|
{
|
|
lock (DiagnosticsLock)
|
|
{
|
|
return m_timeProvider.GetTimestampMilliseconds() - m_lastContactTickCount >
|
|
(long)SessionDiagnostics.ActualSessionTimeout;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The last time the session was contacted by the client.
|
|
/// </summary>
|
|
public DateTime ClientLastContactTime
|
|
{
|
|
get
|
|
{
|
|
lock (DiagnosticsLock)
|
|
{
|
|
return (DateTime)SessionDiagnostics.ClientLastContactTime;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The monotonic tick count (milliseconds) at the last client contact.
|
|
/// Used for timeout calculations that are immune to system time changes.
|
|
/// </summary>
|
|
public long LastContactTickCount
|
|
{
|
|
get
|
|
{
|
|
lock (DiagnosticsLock)
|
|
{
|
|
return m_lastContactTickCount;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether the session has been activated.
|
|
/// </summary>
|
|
public bool Activated { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Set the ECC security policy URI
|
|
/// </summary>
|
|
public virtual void SetUserTokenSecurityPolicy(string securityPolicyUri)
|
|
{
|
|
lock (m_lock)
|
|
{
|
|
m_userTokenSecurityPolicyUri = securityPolicyUri;
|
|
m_userTokenNonce = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create new ECC ephemeral key
|
|
/// </summary>
|
|
/// <returns>A new ephemeral key</returns>
|
|
public virtual EphemeralKeyType? GetNewEphemeralKey()
|
|
{
|
|
lock (m_lock)
|
|
{
|
|
if (m_userTokenSecurityPolicyUri == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
m_userTokenNonce = Nonce.CreateNonce(m_userTokenSecurityPolicyUri);
|
|
|
|
return new EphemeralKeyType
|
|
{
|
|
PublicKey = m_userTokenNonce.Data.ToByteString(),
|
|
Signature = CryptoUtils.Sign(
|
|
new ArraySegment<byte>(m_userTokenNonce.Data!),
|
|
m_serverCertificate,
|
|
m_userTokenSecurityPolicyUri).ToByteString()
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the session's endpoint
|
|
/// </summary>
|
|
public EndpointDescription EndpointDescription { get; } = null!;
|
|
|
|
/// <summary>
|
|
/// Returns the session's SecureChannelId
|
|
/// </summary>
|
|
public string SecureChannelId { get; private set; }
|
|
|
|
/// <summary>
|
|
/// allow derived classes access
|
|
/// </summary>
|
|
protected int MaxBrowseContinuationPoints { get; set; }
|
|
|
|
/// <summary>
|
|
/// Validates the request.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentNullException"><paramref name="requestHeader"/> is <c>null</c>.</exception>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
public virtual void ValidateRequest(RequestHeader requestHeader, SecureChannelContext secureChannelContext, RequestType requestType)
|
|
{
|
|
if (requestHeader == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(requestHeader));
|
|
}
|
|
|
|
lock (m_lock)
|
|
{
|
|
if (secureChannelContext == null || !IsSecureChannelValid(secureChannelContext.SecureChannelId))
|
|
{
|
|
UpdateDiagnosticCounters(requestType, true, true);
|
|
|
|
if (requestType == RequestType.CloseSession)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadSessionIdInvalid);
|
|
}
|
|
|
|
throw new ServiceResultException(StatusCodes.BadSecureChannelIdInvalid);
|
|
}
|
|
|
|
// verify that session has been activated.
|
|
if (!Activated && requestType != RequestType.CloseSession)
|
|
{
|
|
UpdateDiagnosticCounters(requestType, true, true);
|
|
throw new ServiceResultException(StatusCodes.BadSessionNotActivated);
|
|
}
|
|
|
|
// request accepted.
|
|
UpdateDiagnosticCounters(requestType, false, false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validate the diagnostic info.
|
|
/// </summary>
|
|
public virtual void ValidateDiagnosticInfo(RequestHeader requestHeader)
|
|
{
|
|
const uint additionalInfoDiagnosticsMask = (uint)(
|
|
DiagnosticsMasks.ServiceAdditionalInfo | DiagnosticsMasks.OperationAdditionalInfo);
|
|
if ((requestHeader.ReturnDiagnostics & additionalInfoDiagnosticsMask) != 0)
|
|
{
|
|
ArrayOf<NodeId> currentRoleIds = EffectiveIdentity?.GrantedRoleIds ?? default;
|
|
if (currentRoleIds.Contains(ObjectIds.WellKnownRole_SecurityAdmin) ||
|
|
currentRoleIds.Contains(ObjectIds.WellKnownRole_ConfigureAdmin))
|
|
{
|
|
requestHeader.ReturnDiagnostics
|
|
|= (uint)DiagnosticsMasks.UserPermissionAdditionalInfo;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the secure channel is currently valid.
|
|
/// </summary>
|
|
public virtual bool IsSecureChannelValid(string secureChannelId)
|
|
{
|
|
lock (m_lock)
|
|
{
|
|
return SecureChannelId == secureChannelId;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the requested locale ids.
|
|
/// </summary>
|
|
/// <returns>true if the new locale ids are different from the old locale ids.</returns>
|
|
/// <exception cref="ArgumentNullException"><paramref name="localeIds"/> is <c>null</c>.</exception>
|
|
public bool UpdateLocaleIds(ArrayOf<string> localeIds)
|
|
{
|
|
lock (m_lock)
|
|
{
|
|
string[] ids = [.. localeIds];
|
|
|
|
if (!Utils.IsEqual(ids, PreferredLocales))
|
|
{
|
|
PreferredLocales = ids;
|
|
|
|
// update diagnostics.
|
|
lock (DiagnosticsLock)
|
|
{
|
|
SessionDiagnostics.LocaleIds = [.. localeIds];
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Activates the session and binds it to the current secure channel.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
public void ValidateBeforeActivate(
|
|
OperationContext context,
|
|
SignatureData clientSignature,
|
|
ExtensionObject userIdentityToken,
|
|
SignatureData userTokenSignature,
|
|
out IUserIdentityTokenHandler? identityToken,
|
|
out UserTokenPolicy? userTokenPolicy)
|
|
{
|
|
lock (m_lock)
|
|
{
|
|
// verify that a secure channel was specified.
|
|
if (context.ChannelContext == null)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadSecureChannelIdInvalid);
|
|
}
|
|
|
|
// verify that the same security policy has been used.
|
|
EndpointDescription? endpoint = context.ChannelContext.EndpointDescription;
|
|
|
|
if (endpoint!.SecurityPolicyUri != EndpointDescription.SecurityPolicyUri ||
|
|
endpoint.SecurityMode != EndpointDescription.SecurityMode)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadSecurityPolicyRejected);
|
|
}
|
|
|
|
// verify the client signature.
|
|
if (ClientCertificate != null)
|
|
{
|
|
if (EndpointDescription.SecurityPolicyUri != SecurityPolicies.None &&
|
|
clientSignature != null &&
|
|
clientSignature.Signature.IsEmpty)
|
|
{
|
|
throw new ServiceResultException(
|
|
StatusCodes.BadApplicationSignatureInvalid);
|
|
}
|
|
|
|
SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(EndpointDescription.SecurityPolicyUri!)!;
|
|
|
|
byte[] dataToSign = securityPolicy!.GetClientSignatureData(
|
|
context.ChannelContext.ChannelThumbprint,
|
|
m_serverNonce.Data,
|
|
m_serverCertificate.RawData,
|
|
context.ChannelContext.ServerChannelCertificate,
|
|
context.ChannelContext.ClientChannelCertificate,
|
|
ClientNonce.ToArray());
|
|
|
|
if (!SecurityPolicies.VerifySignatureData(
|
|
clientSignature!,
|
|
EndpointDescription.SecurityPolicyUri!,
|
|
ClientCertificate,
|
|
dataToSign))
|
|
{
|
|
// verify for certificate chain in endpoint.
|
|
// validate the signature with complete chain if the check with leaf certificate failed.
|
|
using CertificateCollection serverCertificateChain =
|
|
Utils.ParseCertificateChainBlob(
|
|
EndpointDescription.ServerCertificate,
|
|
m_server.Telemetry);
|
|
if (serverCertificateChain.Count > 1)
|
|
{
|
|
var serverCertificateChainList = new List<byte>();
|
|
|
|
for (int i = 0; i < serverCertificateChain.Count; i++)
|
|
{
|
|
serverCertificateChainList.AddRange(
|
|
serverCertificateChain[i].RawData);
|
|
}
|
|
|
|
byte[] serverCertificateChainData = [.. serverCertificateChainList];
|
|
|
|
dataToSign = securityPolicy.GetClientSignatureData(
|
|
context.ChannelContext.ChannelThumbprint,
|
|
m_serverNonce.Data,
|
|
serverCertificateChainData,
|
|
context.ChannelContext.ServerChannelCertificate,
|
|
context.ChannelContext.ClientChannelCertificate,
|
|
ClientNonce.ToArray());
|
|
|
|
if (!SecurityPolicies.VerifySignatureData(
|
|
clientSignature!,
|
|
EndpointDescription.SecurityPolicyUri!,
|
|
ClientCertificate,
|
|
dataToSign))
|
|
{
|
|
throw new ServiceResultException(
|
|
StatusCodes.BadApplicationSignatureInvalid);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new ServiceResultException(
|
|
StatusCodes.BadApplicationSignatureInvalid);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!Activated)
|
|
{
|
|
// must active the session on the channel that was used to create it.
|
|
if (SecureChannelId != context.ChannelContext.SecureChannelId)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadSecureChannelIdInvalid);
|
|
}
|
|
}
|
|
|
|
// validate the user identity token.
|
|
identityToken = ValidateUserIdentityToken(
|
|
context,
|
|
userIdentityToken,
|
|
userTokenSignature,
|
|
out userTokenPolicy);
|
|
|
|
TraceState("VALIDATED");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Activates the session and binds it to the current secure channel.
|
|
/// </summary>
|
|
public bool Activate(
|
|
OperationContext context,
|
|
IUserIdentityTokenHandler identityToken,
|
|
IUserIdentity identity,
|
|
IUserIdentity effectiveIdentity,
|
|
ArrayOf<string> localeIds,
|
|
Nonce serverNonce)
|
|
{
|
|
lock (m_lock)
|
|
{
|
|
// update user identity.
|
|
bool changed = false;
|
|
|
|
if (identityToken != null &&
|
|
UpdateUserIdentity(identityToken, identity, effectiveIdentity))
|
|
{
|
|
changed = true;
|
|
}
|
|
|
|
// update local ids.
|
|
if (UpdateLocaleIds(localeIds))
|
|
{
|
|
changed = true;
|
|
}
|
|
|
|
if (!Activated)
|
|
{
|
|
// toggle the activated flag.
|
|
Activated = true;
|
|
|
|
TraceState("FIRST ACTIVATION");
|
|
}
|
|
else
|
|
{
|
|
// bind to the new secure channel. Activate is invoked from the
|
|
// session activation pipeline, which always supplies a channel context.
|
|
SecureChannelId = context.ChannelContext!.SecureChannelId;
|
|
|
|
TraceState("RE-ACTIVATION");
|
|
}
|
|
|
|
// update server nonce.
|
|
m_serverNonce = serverNonce;
|
|
|
|
// update the contact time.
|
|
lock (DiagnosticsLock)
|
|
{
|
|
SessionDiagnostics.ClientLastContactTime = m_timeProvider.GetUtcNow().UtcDateTime;
|
|
m_lastContactTickCount = m_timeProvider.GetTimestampMilliseconds();
|
|
}
|
|
|
|
// indicate whether the user context has changed.
|
|
return changed;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closes a session and removes itself from the address space.
|
|
/// </summary>
|
|
public async ValueTask CloseAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
TraceState("CLOSED");
|
|
|
|
await m_server.DiagnosticsNodeManager
|
|
.DeleteSessionDiagnosticsAsync(m_server.DefaultSystemContext, Id, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves a continuation point for a session.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If the session has too many continuation points the oldest one is dropped.
|
|
/// </remarks>
|
|
/// <exception cref="ArgumentNullException"><paramref name="continuationPoint"/> is <c>null</c>.</exception>
|
|
public void SaveContinuationPoint(ContinuationPoint continuationPoint)
|
|
{
|
|
if (continuationPoint == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(continuationPoint));
|
|
}
|
|
|
|
lock (m_lock)
|
|
{
|
|
m_browseContinuationPoints ??= [];
|
|
|
|
// remove the first continuation point if too many points.
|
|
while (m_browseContinuationPoints.Count > MaxBrowseContinuationPoints)
|
|
{
|
|
ContinuationPoint cp = m_browseContinuationPoints[0];
|
|
m_browseContinuationPoints.RemoveAt(0);
|
|
cp?.Dispose();
|
|
}
|
|
|
|
// add to end of list.
|
|
m_browseContinuationPoints.Add(continuationPoint);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Restores a continuation point for a session.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The caller is responsible for disposing the continuation point returned.
|
|
/// </remarks>
|
|
public ContinuationPoint? RestoreContinuationPoint(ByteString continuationPoint)
|
|
{
|
|
lock (m_lock)
|
|
{
|
|
if (m_browseContinuationPoints == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (continuationPoint.Length != 16)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var id = new Guid(continuationPoint.ToArray());
|
|
|
|
for (int ii = 0; ii < m_browseContinuationPoints.Count; ii++)
|
|
{
|
|
if (m_browseContinuationPoints[ii].Id == id)
|
|
{
|
|
ContinuationPoint cp = m_browseContinuationPoints[ii];
|
|
m_browseContinuationPoints.RemoveAt(ii);
|
|
return cp;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saves a continuation point used for historical reads.
|
|
/// </summary>
|
|
/// <param name="id">The identifier for the continuation point.</param>
|
|
/// <param name="continuationPoint">The continuation point.</param>
|
|
/// <remarks>
|
|
/// If the continuationPoint implements IDisposable it will be disposed when
|
|
/// the Session is closed or discarded.
|
|
/// </remarks>
|
|
/// <exception cref="ArgumentNullException"><paramref name="continuationPoint"/> is <c>null</c>.</exception>
|
|
public void SaveHistoryContinuationPoint(Guid id, object continuationPoint)
|
|
{
|
|
if (continuationPoint == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(continuationPoint));
|
|
}
|
|
|
|
lock (m_lock)
|
|
{
|
|
m_historyContinuationPoints ??= [];
|
|
|
|
// remove existing continuation point if space needed.
|
|
while (m_historyContinuationPoints.Count >= m_maxHistoryContinuationPoints)
|
|
{
|
|
HistoryContinuationPoint oldCP = m_historyContinuationPoints[0];
|
|
m_historyContinuationPoints.RemoveAt(0);
|
|
(oldCP.Value as IDisposable)?.Dispose();
|
|
}
|
|
|
|
// create the cp.
|
|
var cp = new HistoryContinuationPoint
|
|
{
|
|
Id = id,
|
|
Value = continuationPoint,
|
|
Timestamp = DateTime.UtcNow
|
|
};
|
|
|
|
m_historyContinuationPoints.Add(cp);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Restores a previously saves history continuation point.
|
|
/// </summary>
|
|
/// <param name="continuationPoint">The identifier for the continuation point.</param>
|
|
/// <returns>The save continuation point. null if not found.</returns>
|
|
public object? RestoreHistoryContinuationPoint(ByteString continuationPoint)
|
|
{
|
|
lock (m_lock)
|
|
{
|
|
if (m_historyContinuationPoints == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (continuationPoint.Length != 16)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var id = new Guid(continuationPoint.ToArray());
|
|
|
|
for (int ii = 0; ii < m_historyContinuationPoints.Count; ii++)
|
|
{
|
|
HistoryContinuationPoint cp = m_historyContinuationPoints[ii];
|
|
|
|
if (cp.Id == id)
|
|
{
|
|
m_historyContinuationPoints.RemoveAt(ii);
|
|
return cp.Value;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stores a continuation point used for historial reads.
|
|
/// </summary>
|
|
private class HistoryContinuationPoint
|
|
{
|
|
public Guid Id;
|
|
public object? Value;
|
|
public DateTime Timestamp;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Dumps the current state of the session queue.
|
|
/// </summary>
|
|
internal void TraceState(string context)
|
|
{
|
|
// Legacy event source logging
|
|
ServerUtils.EventLog.SessionState(
|
|
context,
|
|
Id.ToString(),
|
|
m_sessionName,
|
|
SecureChannelId,
|
|
Identity?.DisplayName ?? "(none)");
|
|
|
|
m_logger.LogInformation(
|
|
"Session {Context}, Id={SessionId}, Name={Name}, ChannelId={ChannelId}, User={User}",
|
|
context,
|
|
Id.ToString(),
|
|
m_sessionName,
|
|
SecureChannelId,
|
|
Identity?.DisplayName ?? "(none)");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a copy of the current diagnostics.
|
|
/// </summary>
|
|
private ServiceResult OnUpdateDiagnostics(
|
|
ISystemContext context,
|
|
NodeState node,
|
|
ref Variant value)
|
|
{
|
|
lock (DiagnosticsLock)
|
|
{
|
|
value = Variant.FromStructure(SessionDiagnostics);
|
|
}
|
|
|
|
return ServiceResult.Good;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a copy of the current security diagnostics.
|
|
/// </summary>
|
|
private ServiceResult OnUpdateSecurityDiagnostics(
|
|
ISystemContext context,
|
|
NodeState node,
|
|
ref Variant value)
|
|
{
|
|
lock (DiagnosticsLock)
|
|
{
|
|
value = Variant.FromStructure(m_securityDiagnostics);
|
|
}
|
|
|
|
return ServiceResult.Good;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates the identity token supplied by the client.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
private IUserIdentityTokenHandler ValidateUserIdentityToken(
|
|
OperationContext context,
|
|
ExtensionObject identityToken,
|
|
SignatureData userTokenSignature,
|
|
out UserTokenPolicy? policy)
|
|
{
|
|
policy = null!;
|
|
|
|
// check for anonymous (same as empty) token.
|
|
if (identityToken.IsNull ||
|
|
identityToken.TryGetValue(out AnonymousIdentityToken? _))
|
|
{
|
|
// check if an anonymous login is permitted.
|
|
if (!EndpointDescription.UserIdentityTokens.IsEmpty)
|
|
{
|
|
bool found = false;
|
|
|
|
for (int ii = 0; ii < EndpointDescription.UserIdentityTokens.Count; ii++)
|
|
{
|
|
if (EndpointDescription.UserIdentityTokens[ii]
|
|
.TokenType == UserTokenType.Anonymous)
|
|
{
|
|
found = true;
|
|
policy = EndpointDescription.UserIdentityTokens[ii];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadIdentityTokenRejected,
|
|
"Anonymous user token policy not supported.");
|
|
}
|
|
}
|
|
|
|
// create an anonymous token to use for subsequent validation.
|
|
return AnonymousIdentityTokenHandler.Create(policy!);
|
|
}
|
|
|
|
IUserIdentityTokenHandler token;
|
|
// check for unrecognized token.
|
|
if (identityToken.TryGetValue(out UserIdentityToken? decodedToken))
|
|
{
|
|
// get the token.
|
|
token = decodedToken.AsTokenHandler();
|
|
}
|
|
else
|
|
{
|
|
//handle the use case when the UserIdentityToken is binary encoded over xml message encoding
|
|
if (identityToken.Encoding != ExtensionObjectEncoding.Binary ||
|
|
!identityToken.TryGetAsBinary(out ByteString _))
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadUserAccessDenied,
|
|
"Invalid user identity token provided.");
|
|
}
|
|
if (BaseVariableState.DecodeExtensionObject(
|
|
null!,
|
|
typeof(UserIdentityToken),
|
|
identityToken,
|
|
false)
|
|
is not UserIdentityToken newToken)
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadUserAccessDenied,
|
|
"Invalid user identity token provided.");
|
|
}
|
|
|
|
policy = EndpointDescription.FindUserTokenPolicy(
|
|
newToken.PolicyId!,
|
|
EndpointDescription.SecurityPolicyUri!) ??
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadUserAccessDenied,
|
|
"User token policy not supported.",
|
|
"Opc.Ua.Server.Session.ValidateUserIdentityToken");
|
|
|
|
UserIdentityToken? userToken;
|
|
switch (policy.TokenType)
|
|
{
|
|
case UserTokenType.Anonymous:
|
|
userToken = (AnonymousIdentityToken)BaseVariableState.DecodeExtensionObject(
|
|
null!,
|
|
typeof(AnonymousIdentityToken),
|
|
identityToken,
|
|
true)!;
|
|
break;
|
|
case UserTokenType.UserName:
|
|
userToken = (UserNameIdentityToken)BaseVariableState.DecodeExtensionObject(
|
|
null!,
|
|
typeof(UserNameIdentityToken),
|
|
identityToken,
|
|
true)!;
|
|
break;
|
|
case UserTokenType.Certificate:
|
|
userToken = (X509IdentityToken)BaseVariableState.DecodeExtensionObject(
|
|
null!,
|
|
typeof(X509IdentityToken),
|
|
identityToken,
|
|
true)!;
|
|
break;
|
|
case UserTokenType.IssuedToken:
|
|
userToken = (IssuedIdentityToken)BaseVariableState.DecodeExtensionObject(
|
|
null!,
|
|
typeof(IssuedIdentityToken),
|
|
identityToken,
|
|
true)!;
|
|
break;
|
|
default:
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadUserAccessDenied,
|
|
"Invalid user identity token provided.");
|
|
}
|
|
|
|
token = userToken.AsTokenHandler()!;
|
|
}
|
|
|
|
// find the user token policy.
|
|
policy = EndpointDescription.FindUserTokenPolicy(
|
|
token.Token.PolicyId!,
|
|
EndpointDescription.SecurityPolicyUri!) ??
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadIdentityTokenInvalid,
|
|
"User token policy not supported.");
|
|
|
|
token.UpdatePolicy(policy);
|
|
|
|
// determine the security policy uri.
|
|
string? securityPolicyUri = policy.SecurityPolicyUri;
|
|
|
|
if (string.IsNullOrEmpty(securityPolicyUri))
|
|
{
|
|
securityPolicyUri = EndpointDescription.SecurityPolicyUri;
|
|
}
|
|
|
|
if (ServerBase.RequireEncryption(EndpointDescription))
|
|
{
|
|
// decrypt the token.
|
|
// check for valid certificate.
|
|
m_serverCertificate ??= Certificate.FromRawData(
|
|
EndpointDescription.ServerCertificate) ??
|
|
throw ServiceResultException.ConfigurationError(
|
|
"ApplicationCertificate cannot be found.");
|
|
|
|
try
|
|
{
|
|
// Sync-completing ValueTask in current implementations;
|
|
// safe to block. Future async stores will require
|
|
// hoisting decryption out of the lock.
|
|
ValueTask decryptTask = token.DecryptAsync(
|
|
m_serverCertificate,
|
|
m_serverNonce,
|
|
securityPolicyUri!,
|
|
m_server.MessageContext,
|
|
m_userTokenNonce,
|
|
ClientCertificate,
|
|
m_clientIssuerCertificates);
|
|
if (!decryptTask.IsCompletedSuccessfully)
|
|
{
|
|
decryptTask.AsTask().GetAwaiter().GetResult();
|
|
}
|
|
}
|
|
catch (Exception e) when (e is not ServiceResultException)
|
|
{
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadIdentityTokenInvalid,
|
|
e,
|
|
"Could not decrypt identity token.");
|
|
}
|
|
|
|
// verify the signature.
|
|
if (securityPolicyUri != SecurityPolicies.None)
|
|
{
|
|
SecurityPolicyInfo securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri!)!;
|
|
|
|
// ValidateUserIdentityToken runs inside session activation, which
|
|
// always carries a channel context.
|
|
SecureChannelContext channelContext = context.ChannelContext!;
|
|
|
|
byte[] dataToSign = securityPolicy!.GetUserTokenSignatureData(
|
|
channelContext.ChannelThumbprint,
|
|
m_serverNonce.Data,
|
|
m_serverCertificate.RawData,
|
|
channelContext.ServerChannelCertificate,
|
|
ClientCertificate?.RawData,
|
|
channelContext.ClientChannelCertificate,
|
|
ClientNonce.ToArray());
|
|
|
|
if (!VerifySync(token, dataToSign, userTokenSignature, securityPolicyUri!))
|
|
{
|
|
// verify for certificate chain in endpoint.
|
|
// validate the signature with complete chain if the check with leaf certificate failed.
|
|
using CertificateCollection serverCertificateChain =
|
|
Utils.ParseCertificateChainBlob(
|
|
EndpointDescription.ServerCertificate,
|
|
m_server.Telemetry);
|
|
if (serverCertificateChain.Count > 1)
|
|
{
|
|
var serverCertificateChainList = new List<byte>();
|
|
|
|
for (int i = 0; i < serverCertificateChain.Count; i++)
|
|
{
|
|
serverCertificateChainList.AddRange(
|
|
serverCertificateChain[i].RawData);
|
|
}
|
|
|
|
dataToSign = securityPolicy.GetUserTokenSignatureData(
|
|
channelContext.ChannelThumbprint,
|
|
m_serverNonce.Data,
|
|
[.. serverCertificateChainList],
|
|
channelContext.ServerChannelCertificate,
|
|
ClientCertificate?.RawData,
|
|
channelContext.ClientChannelCertificate,
|
|
ClientNonce.ToArray());
|
|
|
|
if (!VerifySync(token, dataToSign, userTokenSignature, securityPolicyUri!))
|
|
{
|
|
throw new ServiceResultException(
|
|
StatusCodes.BadIdentityTokenRejected,
|
|
"Invalid user signature!");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new ServiceResultException(
|
|
StatusCodes.BadIdentityTokenRejected,
|
|
"Invalid user signature!");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// validate user identity token.
|
|
return token;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Synchronously invokes <see cref="IUserIdentityTokenHandler.VerifyAsync"/>
|
|
/// on the assumption that the underlying implementation completes
|
|
/// synchronously (no real I/O). Used inside locked regions of
|
|
/// <see cref="ValidateBeforeActivate"/> where awaiting is not
|
|
/// possible. Future async-store implementations will require
|
|
/// hoisting verification out of the lock.
|
|
/// </summary>
|
|
private static bool VerifySync(
|
|
IUserIdentityTokenHandler token,
|
|
byte[] dataToSign,
|
|
SignatureData userTokenSignature,
|
|
string securityPolicyUri)
|
|
{
|
|
ValueTask<bool> task = token.VerifyAsync(
|
|
dataToSign,
|
|
userTokenSignature,
|
|
securityPolicyUri);
|
|
return task.IsCompletedSuccessfully
|
|
? task.Result
|
|
: task.AsTask().GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the user identity.
|
|
/// </summary>
|
|
/// <returns>true if the new identity is different from the old identity.</returns>
|
|
/// <exception cref="ArgumentNullException"><paramref name="identityToken"/> is <c>null</c>.</exception>
|
|
[MemberNotNull(nameof(EffectiveIdentity), nameof(IdentityToken))]
|
|
private bool UpdateUserIdentity(
|
|
IUserIdentityTokenHandler identityToken,
|
|
IUserIdentity identity,
|
|
IUserIdentity effectiveIdentity)
|
|
{
|
|
if (identityToken == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(identityToken));
|
|
}
|
|
|
|
lock (m_lock)
|
|
{
|
|
bool changed = EffectiveIdentity == null && effectiveIdentity != null;
|
|
|
|
if (EffectiveIdentity != null)
|
|
{
|
|
changed = !EffectiveIdentity.Equals(effectiveIdentity);
|
|
}
|
|
|
|
// always save the new identity since it may have additional information that does not affect equality.
|
|
IdentityToken = identityToken;
|
|
Identity = identity;
|
|
EffectiveIdentity = effectiveIdentity!;
|
|
|
|
// update diagnostics.
|
|
lock (DiagnosticsLock)
|
|
{
|
|
m_securityDiagnostics.ClientUserIdOfSession = identity.DisplayName;
|
|
m_securityDiagnostics.AuthenticationMechanism = identity.TokenType.ToString();
|
|
m_securityDiagnostics.ClientUserIdHistory =
|
|
m_securityDiagnostics.ClientUserIdHistory.AddItem(identity.DisplayName);
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the diagnostic counters associated with the request.
|
|
/// </summary>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
private void UpdateDiagnosticCounters(
|
|
RequestType requestType,
|
|
bool error,
|
|
bool authorizationError)
|
|
{
|
|
ServiceCounterDataType? counter = null;
|
|
|
|
lock (DiagnosticsLock)
|
|
{
|
|
if (!error)
|
|
{
|
|
SessionDiagnostics.ClientLastContactTime = m_timeProvider.GetUtcNow().UtcDateTime;
|
|
m_lastContactTickCount = m_timeProvider.GetTimestampMilliseconds();
|
|
}
|
|
|
|
SessionDiagnostics.TotalRequestCount.TotalCount++;
|
|
|
|
if (error)
|
|
{
|
|
SessionDiagnostics.TotalRequestCount.ErrorCount++;
|
|
|
|
if (authorizationError)
|
|
{
|
|
SessionDiagnostics.UnauthorizedRequestCount++;
|
|
}
|
|
}
|
|
|
|
switch (requestType)
|
|
{
|
|
case RequestType.Read:
|
|
counter = SessionDiagnostics.ReadCount;
|
|
break;
|
|
case RequestType.HistoryRead:
|
|
counter = SessionDiagnostics.HistoryReadCount;
|
|
break;
|
|
case RequestType.Write:
|
|
counter = SessionDiagnostics.WriteCount;
|
|
break;
|
|
case RequestType.HistoryUpdate:
|
|
counter = SessionDiagnostics.HistoryUpdateCount;
|
|
break;
|
|
case RequestType.Call:
|
|
counter = SessionDiagnostics.CallCount;
|
|
break;
|
|
case RequestType.CreateMonitoredItems:
|
|
counter = SessionDiagnostics.CreateMonitoredItemsCount;
|
|
break;
|
|
case RequestType.ModifyMonitoredItems:
|
|
counter = SessionDiagnostics.ModifyMonitoredItemsCount;
|
|
break;
|
|
case RequestType.SetMonitoringMode:
|
|
counter = SessionDiagnostics.SetMonitoringModeCount;
|
|
break;
|
|
case RequestType.SetTriggering:
|
|
counter = SessionDiagnostics.SetTriggeringCount;
|
|
break;
|
|
case RequestType.DeleteMonitoredItems:
|
|
counter = SessionDiagnostics.DeleteMonitoredItemsCount;
|
|
break;
|
|
case RequestType.CreateSubscription:
|
|
counter = SessionDiagnostics.CreateSubscriptionCount;
|
|
break;
|
|
case RequestType.ModifySubscription:
|
|
counter = SessionDiagnostics.ModifySubscriptionCount;
|
|
break;
|
|
case RequestType.SetPublishingMode:
|
|
counter = SessionDiagnostics.SetPublishingModeCount;
|
|
break;
|
|
case RequestType.Publish:
|
|
counter = SessionDiagnostics.PublishCount;
|
|
break;
|
|
case RequestType.Republish:
|
|
counter = SessionDiagnostics.RepublishCount;
|
|
break;
|
|
case RequestType.TransferSubscriptions:
|
|
counter = SessionDiagnostics.TransferSubscriptionsCount;
|
|
break;
|
|
case RequestType.DeleteSubscriptions:
|
|
counter = SessionDiagnostics.DeleteSubscriptionsCount;
|
|
break;
|
|
case RequestType.AddNodes:
|
|
counter = SessionDiagnostics.AddNodesCount;
|
|
break;
|
|
case RequestType.AddReferences:
|
|
counter = SessionDiagnostics.AddReferencesCount;
|
|
break;
|
|
case RequestType.DeleteNodes:
|
|
counter = SessionDiagnostics.DeleteNodesCount;
|
|
break;
|
|
case RequestType.DeleteReferences:
|
|
counter = SessionDiagnostics.DeleteReferencesCount;
|
|
break;
|
|
case RequestType.Browse:
|
|
counter = SessionDiagnostics.BrowseCount;
|
|
break;
|
|
case RequestType.BrowseNext:
|
|
counter = SessionDiagnostics.BrowseNextCount;
|
|
break;
|
|
case RequestType.TranslateBrowsePathsToNodeIds:
|
|
counter = SessionDiagnostics.TranslateBrowsePathsToNodeIdsCount;
|
|
break;
|
|
case RequestType.QueryFirst:
|
|
counter = SessionDiagnostics.QueryFirstCount;
|
|
break;
|
|
case RequestType.QueryNext:
|
|
counter = SessionDiagnostics.QueryNextCount;
|
|
break;
|
|
case RequestType.RegisterNodes:
|
|
counter = SessionDiagnostics.RegisterNodesCount;
|
|
break;
|
|
case RequestType.UnregisterNodes:
|
|
counter = SessionDiagnostics.UnregisterNodesCount;
|
|
break;
|
|
case RequestType.Unknown:
|
|
case RequestType.FindServers:
|
|
case RequestType.GetEndpoints:
|
|
case RequestType.CreateSession:
|
|
case RequestType.ActivateSession:
|
|
case RequestType.CloseSession:
|
|
case RequestType.Cancel:
|
|
break;
|
|
default:
|
|
throw ServiceResultException.Unexpected(
|
|
$"Unexpected RequestType {requestType}");
|
|
}
|
|
|
|
if (counter != null)
|
|
{
|
|
counter.TotalCount++;
|
|
|
|
if (error)
|
|
{
|
|
counter.ErrorCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (counter != null)
|
|
{
|
|
m_server.SessionManager.RaiseSessionDiagnosticsChangedEvent(this);
|
|
}
|
|
}
|
|
|
|
private readonly Lock m_lock = new();
|
|
private readonly ILogger m_logger;
|
|
private readonly IServerInternal m_server;
|
|
private readonly TimeProvider m_timeProvider;
|
|
private readonly string m_sessionName;
|
|
private Certificate m_serverCertificate;
|
|
private Nonce m_serverNonce;
|
|
private string? m_userTokenSecurityPolicyUri;
|
|
private Nonce? m_userTokenNonce;
|
|
private readonly CertificateCollection? m_clientIssuerCertificates;
|
|
private readonly int m_maxHistoryContinuationPoints;
|
|
private readonly SessionSecurityDiagnosticsDataType m_securityDiagnostics;
|
|
private List<ContinuationPoint>? m_browseContinuationPoints;
|
|
private List<HistoryContinuationPoint>? m_historyContinuationPoints;
|
|
private long m_lastContactTickCount;
|
|
private int m_identityStale;
|
|
}
|
|
}
|