mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-19 15:22:15 +08:00
15eb55c46e
# Description Implements the OPC UA Part 12 §7.10.9 contract on the server (fixes #3562) AND the client-side counterpart for immediate detection of own-cert / trust-list / CRL changes (fixes #3160). ## Server side (#3562) Per Part 12 §7.10.9 the server _shall_ "force existing SecureChannels affected by the changes to renegotiate" once the `ApplyChanges` method response has been delivered. Previously the server reloaded the certificate but did not cut affected channels; clients kept using the old cert until natural rekey, and the code carried an explicit `TODO` next to a 1-second `Task.Delay` hack in `ConfigurationNodeManager.ApplyChanges`. Key server changes: * **New optional capability interface** `Opc.Ua.ITransportListenerCertificateRotation` (single method `CloseChannelsForCertificate(Certificate)`) — implemented by `TcpTransportListener` and `HttpsTransportListener`. Custom transport listeners can opt in without breaking `ITransportListener`. * **TCP path**: new internal `TcpListenerChannel.TryCloseForCertificateRotation(oldThumbprint, out globalChannelId)` matches the channel's negotiated `ServerCertificate` by thumbprint, sends `BadCertificateInvalid`, and calls `ChannelFaulted()` to actually close the socket. `TcpTransportListener.CloseChannelsForCertificate` snapshots `m_channels` outside `m_lock` to avoid lock inversion, invokes the per-channel helper, returns cut global channel ids. **The listener socket stays bound** — clients can reconnect immediately. * **HTTPS path**: HTTPS owns the cert at the Kestrel/HTTP.sys listener level, so a `Stop()`+`Start()` cycle is the only practical force-renegotiate. Moved out of `CertificateUpdate` (which now does only the soft fan-out) into `CloseChannelsForCertificate` so the staging event no longer disrupts in-flight requests prematurely. * **`ConfigurationNodeManager.ApplyChanges`** rewritten: - Captures the per-group pre-transaction certificate (preserved across multi-stage transactions per Part 12 §7.10.2) and the staged `UpdateCertificateData`. - Schedules a deferred apply: waits for the response to flush, re-syncs the registry via `CertificateManager.UpdateAsync`, then calls `CloseChannelsForCertificate` on every listener that implements the capability. - **Configurable grace period**: new `IConfigurationNodeManager.ApplyChangesGracePeriod` (default 250 ms, negative clamped to zero). Hosts running over higher-latency administrative links can raise it. - Exposes `IConfigurationNodeManager.DrainPendingApplyChangesAsync(CancellationToken)` for tests/hosts to deterministically await rotation. - Removes the explicit 1-second `Task.Delay` TODO hack. * **`Opc.Ua.Server.ITransportListenerRegistryProvider`** — narrow opt-in interface on `ServerInternalData` exposing the live transport listener list. Lets `ConfigurationNodeManager` drive the fan-out without expanding `IServerInternal` (binary compat for mocked test fixtures). * **Sessions are NOT closed**. Per spec, the cut is on the SecureChannel only; existing `Session.IsSecureChannelValid` + `Session.Activate` rebind handles transfer over the new channel. ## Client side (#3160) The client SDK previously had no automatic detection of changes to its own certificate, trust list, or CRL — users had to call `Session.ReloadInstanceCertificateAsync` manually and trigger a reconnect. The spec-mandated `ApplicationCertificateUpdated`, `TrustListUpdated`, and `CrlUpdated` enum values existed but only the first was being emitted by `CertificateManager`. Key client/shared changes: * **`CertificateManager` now emits `TrustListUpdated` and `CrlUpdated`** from `WriteTrustListAsync` (per mask) and from `TrustListTransaction.CommitAsync` (after the atomic apply). The new internal `NotifyTrustListChanged` also invalidates cached peer/user/HTTPS validation cores so subsequent validations pick up the fresh trust state. * **`ManagedSession.AutoReconnectOnCertificateChange`** opt-in property (default `false` for backwards compat). When `true`, `ManagedSession` subscribes to `CertificateManager.CertificateChanges` on construction and, on the three relevant kinds: - Calls `Session.ReloadInstanceCertificateAsync` on `ApplicationCertificateUpdated` so the next ActivateSession is signed with the rotated client cert. - Triggers a reconnect via the state machine so the new state is honoured within milliseconds rather than at the next `SecurityTokenLifetime` rekey. * **`ManagedSession.ApplicationCertificateChanged`** event surfaces every observed change regardless of the auto-reconnect policy for diagnostics or custom rotation logic. * Subscription is disposed by the existing `Dispose` / `DisposeAsync` paths. ```csharp using var managed = await ManagedSession.CreateAsync( configuration, endpoint, sessionFactory); managed.AutoReconnectOnCertificateChange = true; managed.ApplicationCertificateChanged += (sender, e) => { Console.WriteLine($"observed {e.Kind} on {e.TrustList}"); }; ``` ## Tests | Suite | net10.0 | net48 | |---|---|---| | `TransportListenerCertificateRotationTests` (new, 6 tests) | ✅ | ✅ | | `CertificateManagerTests` (4 new event tests) | ✅ 22/22 | ✅ 4/4 | | `PushCertManagement` | 50/53 ✅ | 50/53 ✅ | | `GDS PushTest` (focused subset) | 15/15 ✅ | 16/16 ✅ | | `Opc.Ua.Server.Tests` (focused subset) | 38/38 ✅ | 38/38 ✅ | | `GDS PushTest` (full) | 313/338 ✅ | — | Skips are pre-existing (cert-type mismatches, optional methods) — no regressions. New integration test `PushTest.ApplyChangesForcesChannelRenegotiateAsync` positively asserts the stale channel fails with a recognized channel-invalidation status after `ApplyChanges`, and that a fresh reconnect succeeds with the rotated cert. ## Documentation - `Docs/CertificateManager.md` — new sections: - "Server-Side Certificate Rotation via Push (OPC UA Part 12 §7.10.9)" — `ITransportListenerCertificateRotation` contract + custom listener example. - "Client-Side Auto-Detection of Certificate Changes (issue #3160)" — `AutoReconnectOnCertificateChange` + `ApplicationCertificateChanged` event + the three covered scenarios. ## Related Issues - Fixes #3562 - Fixes #3160 ## Checklist - [ ] 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. - [x] I ran focused tests locally using the **UA.slnx** projects against .net **framework 4.8** and .net **10**, and all passed. _(Full UA.slnx test run delegated to CI.)_ - [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings. - [ ] I have addressed **all** PR feedback received.
1141 lines
46 KiB
C#
1141 lines
46 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.Globalization;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Opc.Ua.Identity;
|
|
|
|
namespace Opc.Ua.Server
|
|
{
|
|
/// <summary>
|
|
/// A class that stores the globally accessible state of a server instance.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// This is a readonly class that is initialized when the server starts up. It provides
|
|
/// access to global objects and data that different parts of the server may require.
|
|
/// It also defines some global methods.
|
|
/// </para>
|
|
/// <para>
|
|
/// This object is constructed is three steps:
|
|
/// - the configuration is provided.
|
|
/// - the node managers et. al. are provided.
|
|
/// - the session/subscription managers are provided.
|
|
/// </para>
|
|
/// <para>The server is not running until all three steps are complete.</para>
|
|
/// <para>
|
|
/// The references returned from this object do not change after all three states are complete.
|
|
/// This ensures the object is thread safe even though it does not use a lock.
|
|
/// Objects returned from this object can be assumed to be threadsafe unless otherwise stated.
|
|
/// </para>
|
|
/// </remarks>
|
|
public class ServerInternalData :
|
|
IServerInternal,
|
|
AliasNames.IAliasNameStoreRegistryProvider,
|
|
Historian.IHistorianRegistryProvider,
|
|
ITransportListenerRegistryProvider,
|
|
ITimeProviderProvider
|
|
{
|
|
/// <summary>
|
|
/// Initializes the datastore with the server configuration.
|
|
/// </summary>
|
|
/// <param name="serverDescription">The server description.</param>
|
|
/// <param name="configuration">The configuration.</param>
|
|
/// <param name="messageContext">The message context.</param>
|
|
public ServerInternalData(
|
|
ServerProperties serverDescription,
|
|
ApplicationConfiguration configuration,
|
|
IServiceMessageContext messageContext)
|
|
: this(serverDescription, configuration, messageContext, null)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the datastore with the server configuration.
|
|
/// </summary>
|
|
/// <param name="serverDescription">The server description.</param>
|
|
/// <param name="configuration">The configuration.</param>
|
|
/// <param name="messageContext">The message context.</param>
|
|
/// <param name="timeProvider">The time provider used for all
|
|
/// time / duration calculations performed by the server, or
|
|
/// <c>null</c> to use <see cref="TimeProvider.System"/>.</param>
|
|
public ServerInternalData(
|
|
ServerProperties serverDescription,
|
|
ApplicationConfiguration configuration,
|
|
IServiceMessageContext messageContext,
|
|
TimeProvider? timeProvider)
|
|
{
|
|
m_timeProvider = timeProvider ?? TimeProvider.System;
|
|
m_serverDescription = serverDescription;
|
|
m_configuration = configuration;
|
|
MessageContext = messageContext;
|
|
|
|
m_endpointAddresses = [];
|
|
|
|
foreach (string baseAddresses in m_configuration.ServerConfiguration!.BaseAddresses)
|
|
{
|
|
Uri? url = Utils.ParseUri(baseAddresses);
|
|
|
|
if (url != null)
|
|
{
|
|
m_endpointAddresses.Add(url);
|
|
}
|
|
}
|
|
|
|
NamespaceUris = MessageContext.NamespaceUris;
|
|
Factory = MessageContext.Factory;
|
|
|
|
ServerUris = new StringTable();
|
|
TypeTree = new TypeTable(NamespaceUris);
|
|
HistorianRegistry = new Historian.HistorianProviderRegistry(NamespaceUris);
|
|
|
|
// add the server uri to the server table.
|
|
ServerUris.Append(m_configuration.ApplicationUri!);
|
|
|
|
// create the default system context.
|
|
DefaultSystemContext = new ServerSystemContext(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Frees any unmanaged resources.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// An overrideable version of the Dispose.
|
|
/// </summary>
|
|
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
m_roleStateBinding?.Dispose();
|
|
m_roleStateBinding = null;
|
|
(RoleManager as IDisposable)?.Dispose();
|
|
ResourceManager?.Dispose();
|
|
RequestManager?.Dispose();
|
|
AggregateManager?.Dispose();
|
|
ModellingRulesManager?.Dispose();
|
|
(NodeManager as IDisposable)?.Dispose();
|
|
SessionManager?.Dispose();
|
|
SubscriptionManager?.Dispose();
|
|
MonitoredItemQueueFactory?.Dispose();
|
|
(AliasNameStoreRegistry as IDisposable)?.Dispose();
|
|
(HistorianRegistry as IDisposable)?.Dispose();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The server-wide registry of OPC UA Part 17 alias-name stores.
|
|
/// Surfaces through the optional
|
|
/// <see cref="AliasNames.IAliasNameStoreRegistryProvider"/>
|
|
/// interface so consumers can discover it without any change to
|
|
/// <see cref="IServerInternal"/>; never <c>null</c>.
|
|
/// </summary>
|
|
public AliasNames.IAliasNameStoreRegistry AliasNameStoreRegistry { get; }
|
|
= new AliasNames.AliasNameStoreRegistry();
|
|
|
|
/// <summary>
|
|
/// A snapshot of the transport listeners currently bound to the
|
|
/// server. Populated by <see cref="StandardServer"/> once
|
|
/// listeners are opened so consumers such as
|
|
/// <see cref="ConfigurationNodeManager"/> can fan-out post-
|
|
/// response channel cuts per OPC UA Part 12 §7.10.9.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Returns an empty list before listeners are bound or after the
|
|
/// server has shut them down. Surfaced through the optional
|
|
/// <see cref="ITransportListenerRegistryProvider"/> interface so
|
|
/// external/mocked <see cref="IServerInternal"/> implementations
|
|
/// remain unaffected.
|
|
/// </remarks>
|
|
public IReadOnlyList<ITransportListener> TransportListeners
|
|
=> m_transportListeners ?? (IReadOnlyList<ITransportListener>)Array.Empty<ITransportListener>();
|
|
|
|
/// <summary>
|
|
/// Called by <see cref="StandardServer"/> after listeners are
|
|
/// bound (or torn down) to make them visible to downstream
|
|
/// consumers via <see cref="ITransportListenerRegistryProvider"/>.
|
|
/// </summary>
|
|
/// <param name="listeners">
|
|
/// The current listener registry. Passing <c>null</c> resets to
|
|
/// an empty snapshot.
|
|
/// </param>
|
|
public void SetTransportListenerRegistry(IReadOnlyList<ITransportListener>? listeners)
|
|
{
|
|
m_transportListeners = listeners;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// The server-wide registry of Part 11 historian providers.
|
|
/// Surfaces through the optional
|
|
/// <see cref="Historian.IHistorianRegistryProvider"/> interface so
|
|
/// consumers can discover it without any change to
|
|
/// <see cref="IServerInternal"/>; never <c>null</c>.
|
|
/// </summary>
|
|
public Historian.IHistorianProviderRegistry HistorianRegistry { get; }
|
|
|
|
/// <summary>
|
|
/// The time provider used for all time / duration calculations
|
|
/// performed by the server. Surfaces through the optional
|
|
/// <see cref="ITimeProviderProvider"/> interface so consumers can
|
|
/// discover it without any change to <see cref="IServerInternal"/>;
|
|
/// never <c>null</c>.
|
|
/// </summary>
|
|
public TimeProvider TimeProvider => m_timeProvider;
|
|
|
|
/// <summary>
|
|
/// The session manager to use with the server.
|
|
/// </summary>
|
|
/// <value>The session manager.</value>
|
|
public ISessionManager SessionManager { get; private set; } = null!;
|
|
|
|
/// <inheritdoc/>
|
|
public IRoleManager RoleManager { get; private set; } = new RoleManager();
|
|
|
|
/// <inheritdoc/>
|
|
public IServerIdentityRegistry IdentityRegistry { get; private set; } = new ServerIdentityRegistry();
|
|
|
|
/// <inheritdoc/>
|
|
public UserManagement.IUserManagement? UserManagement { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The subscription manager to use with the server.
|
|
/// </summary>
|
|
/// <value>The subscription manager.</value>
|
|
public ISubscriptionManager SubscriptionManager { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Stores the MasterNodeManager, the DiagnosticsNodeManager and the CoreNodeManager
|
|
/// </summary>
|
|
/// <param name="nodeManager">The node manager.</param>
|
|
[MemberNotNull(
|
|
nameof(NodeManager),
|
|
nameof(DiagnosticsNodeManager),
|
|
nameof(ConfigurationNodeManager),
|
|
nameof(CoreNodeManager))]
|
|
public void SetNodeManager(IMasterNodeManager nodeManager)
|
|
{
|
|
NodeManager = nodeManager;
|
|
DiagnosticsNodeManager = nodeManager.DiagnosticsNodeManager!;
|
|
ConfigurationNodeManager = nodeManager.ConfigurationNodeManager!;
|
|
CoreNodeManager = nodeManager.CoreNodeManager!;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stores the MainNodeManagerFactory
|
|
/// </summary>
|
|
/// <param name="mainNodeManagerFactory">The main node manager factory.</param>
|
|
[MemberNotNull(nameof(MainNodeManagerFactory))]
|
|
public void SetMainNodeManagerFactory(IMainNodeManagerFactory mainNodeManagerFactory)
|
|
{
|
|
MainNodeManagerFactory = mainNodeManagerFactory;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the EventManager, the ResourceManager, the RequestManager and the AggregateManager.
|
|
/// </summary>
|
|
/// <param name="eventManager">The event manager.</param>
|
|
/// <param name="resourceManager">The resource manager.</param>
|
|
/// <param name="requestManager">The request manager.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
[MemberNotNull(
|
|
nameof(EventManager),
|
|
nameof(ResourceManager),
|
|
nameof(RequestManager),
|
|
nameof(ServerObject),
|
|
nameof(NonThreadSafeStatus),
|
|
nameof(ServerDiagnostics))]
|
|
public async ValueTask CreateServerObjectAsync(
|
|
EventManager eventManager,
|
|
ResourceManager resourceManager,
|
|
RequestManager requestManager,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
EventManager = eventManager;
|
|
ResourceManager = resourceManager;
|
|
RequestManager = requestManager;
|
|
|
|
// create the server object.
|
|
await CreateServerObjectAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stores the SessionManager, the SubscriptionManager in the datastore.
|
|
/// </summary>
|
|
/// <param name="sessionManager">The session manager.</param>
|
|
/// <param name="subscriptionManager">The subscription manager.</param>
|
|
[MemberNotNull(nameof(SessionManager), nameof(SubscriptionManager))]
|
|
public void SetSessionManager(
|
|
ISessionManager sessionManager,
|
|
ISubscriptionManager subscriptionManager)
|
|
{
|
|
SessionManager = sessionManager;
|
|
SubscriptionManager = subscriptionManager;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stores the MonitoredItemQueueFactory in the datastore.
|
|
/// </summary>
|
|
/// <param name="monitoredItemQueueFactory">The MonitoredItemQueueFactory.</param>
|
|
[MemberNotNull(nameof(MonitoredItemQueueFactory))]
|
|
public void SetMonitoredItemQueueFactory(
|
|
IMonitoredItemQueueFactory monitoredItemQueueFactory)
|
|
{
|
|
MonitoredItemQueueFactory = monitoredItemQueueFactory;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stores the Subscriptionstore in the datastore.
|
|
/// </summary>
|
|
/// <param name="subscriptionStore">The subscriptionstore.</param>
|
|
[MemberNotNull(nameof(SubscriptionStore))]
|
|
public void SetSubscriptionStore(ISubscriptionStore subscriptionStore)
|
|
{
|
|
SubscriptionStore = subscriptionStore;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void SetRoleManager(IRoleManager roleManager)
|
|
{
|
|
RoleManager = roleManager ?? throw new ArgumentNullException(nameof(roleManager));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void SetIdentityRegistry(IServerIdentityRegistry registry)
|
|
{
|
|
IdentityRegistry = registry ?? throw new ArgumentNullException(nameof(registry));
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void SetUserManagement(UserManagement.IUserManagement userManagement)
|
|
{
|
|
UserManagement = userManagement ?? throw new ArgumentNullException(nameof(userManagement));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stores the AggregateManager in the datastore.
|
|
/// </summary>
|
|
/// <param name="aggregateManager">The AggregateManager.</param>
|
|
[MemberNotNull(nameof(AggregateManager))]
|
|
public void SetAggregateManager(AggregateManager aggregateManager)
|
|
{
|
|
AggregateManager = aggregateManager;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stores the ModellingRulesManager in the datastore.
|
|
/// </summary>
|
|
/// <param name="modellingRulesManager">The ModellingRulesManager.</param>
|
|
[MemberNotNull(nameof(ModellingRulesManager))]
|
|
public void SetModellingRulesManager(ModellingRulesManager modellingRulesManager)
|
|
{
|
|
ModellingRulesManager = modellingRulesManager;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The endpoint addresses used by the server.
|
|
/// </summary>
|
|
/// <value>The endpoint addresses.</value>
|
|
public IEnumerable<Uri> EndpointAddresses => m_endpointAddresses;
|
|
|
|
/// <summary>
|
|
/// The context to use when serializing/deserializing extension objects.
|
|
/// </summary>
|
|
/// <value>The message context.</value>
|
|
public IServiceMessageContext MessageContext { get; }
|
|
|
|
/// <summary>
|
|
/// The default system context for the server.
|
|
/// </summary>
|
|
/// <value>The default system context.</value>
|
|
public ServerSystemContext DefaultSystemContext { get; }
|
|
|
|
/// <summary>
|
|
/// The table of namespace uris known to the server.
|
|
/// </summary>
|
|
/// <value>The namespace URIs.</value>
|
|
public NamespaceTable NamespaceUris { get; }
|
|
|
|
/// <summary>
|
|
/// The table of remote server uris known to the server.
|
|
/// </summary>
|
|
/// <value>The server URIs.</value>
|
|
public StringTable ServerUris { get; }
|
|
|
|
/// <summary>
|
|
/// The factory used to create encodeable objects that the server understands.
|
|
/// </summary>
|
|
/// <value>The factory.</value>
|
|
public IEncodeableFactory Factory { get; }
|
|
|
|
/// <summary>
|
|
/// The datatypes, object types and variable types known to the server.
|
|
/// </summary>
|
|
/// <value>The type tree.</value>
|
|
/// <remarks>
|
|
/// The type tree table is a global object that all components of a server have access to.
|
|
/// Node managers must populate this table with all types that they define.
|
|
/// This object is thread safe.
|
|
/// </remarks>
|
|
public TypeTable TypeTree { get; }
|
|
|
|
/// <summary>
|
|
/// The master node manager for the server.
|
|
/// </summary>
|
|
/// <value>The node manager.</value>
|
|
public IMasterNodeManager NodeManager { get; private set; } = null!;
|
|
|
|
/// <inheritdoc/>
|
|
public IMainNodeManagerFactory MainNodeManagerFactory { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// The internal node manager for the servers.
|
|
/// </summary>
|
|
/// <value>The core node manager.</value>
|
|
public ICoreNodeManager CoreNodeManager { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Returns the node manager that managers the server diagnostics.
|
|
/// </summary>
|
|
/// <value>The diagnostics node manager.</value>
|
|
public IDiagnosticsNodeManager DiagnosticsNodeManager { get; private set; } = null!;
|
|
|
|
/// <inheritdoc/>
|
|
public IConfigurationNodeManager ConfigurationNodeManager { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// The manager for events that all components use to queue events that occur.
|
|
/// </summary>
|
|
/// <value>The event manager.</value>
|
|
public EventManager EventManager { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// A manager for localized resources that components can use to localize text.
|
|
/// </summary>
|
|
/// <value>The resource manager.</value>
|
|
public ResourceManager ResourceManager { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// A manager for outstanding requests that allows components to receive notifications if the timeout or are cancelled.
|
|
/// </summary>
|
|
/// <value>The request manager.</value>
|
|
public RequestManager RequestManager { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// A manager for aggregate calculators supported by the server.
|
|
/// </summary>
|
|
/// <value>The aggregate manager.</value>
|
|
public AggregateManager AggregateManager { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// A manager for modelling rules supported by the server.
|
|
/// </summary>
|
|
/// <value>The modelling rules manager.</value>
|
|
public ModellingRulesManager ModellingRulesManager { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// The manager for active sessions.
|
|
/// </summary>
|
|
/// <value>The session manager.</value>
|
|
ISessionManager IServerInternal.SessionManager => SessionManager;
|
|
|
|
/// <summary>
|
|
/// The manager for active subscriptions.
|
|
/// </summary>
|
|
ISubscriptionManager IServerInternal.SubscriptionManager => SubscriptionManager;
|
|
|
|
/// <summary>
|
|
/// The factory for durable monitored item queues
|
|
/// </summary>
|
|
public IMonitoredItemQueueFactory MonitoredItemQueueFactory { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// The store to persist and retrieve subscriptions
|
|
/// </summary>
|
|
public ISubscriptionStore SubscriptionStore { get; private set; } = null!;
|
|
|
|
/// <inheritdoc/>
|
|
public ITelemetryContext Telemetry => MessageContext.Telemetry;
|
|
|
|
/// <summary>
|
|
/// Returns the status object for the server.
|
|
/// </summary>
|
|
/// <value>The status.</value>
|
|
[Obsolete("No longer thread safe. To read the value use CurrentState, to write use UpdateServerStatus.")]
|
|
public ServerStatusValue Status => NonThreadSafeStatus;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the current state of the server.
|
|
/// </summary>
|
|
/// <value>The state of the current.</value>
|
|
public ServerState CurrentState
|
|
{
|
|
get
|
|
{
|
|
lock (NonThreadSafeStatus.Lock)
|
|
{
|
|
return NonThreadSafeStatus.Value.State;
|
|
}
|
|
}
|
|
set
|
|
{
|
|
lock (NonThreadSafeStatus.Lock)
|
|
{
|
|
NonThreadSafeStatus.Value.State = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the Server object node
|
|
/// </summary>
|
|
/// <value>The Server object node.</value>
|
|
public ServerObjectState ServerObject { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Used to synchronize access to the server diagnostics.
|
|
/// </summary>
|
|
/// <value>The diagnostics lock.</value>
|
|
public object DiagnosticsLock { get; } = new object();
|
|
|
|
/// <summary>
|
|
/// Used to synchronize write access to
|
|
/// the server diagnostics.
|
|
/// </summary>
|
|
/// <value>The diagnostics lock.</value>
|
|
public object DiagnosticsWriteLock
|
|
{
|
|
get
|
|
{
|
|
// implicitly force diagnostics update
|
|
DiagnosticsNodeManager?.ForceDiagnosticsScan();
|
|
return DiagnosticsLock;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the diagnostics structure for the server.
|
|
/// </summary>
|
|
/// <value>The server diagnostics.</value>
|
|
public ServerDiagnosticsSummaryDataType ServerDiagnostics { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Whether the server is currently running.
|
|
/// </summary>
|
|
/// <value>
|
|
/// <c>true</c> if this instance is running; otherwise, <c>false</c>.
|
|
/// </value>
|
|
/// <remarks>
|
|
/// This flag is set to false when the server shuts down. Threads running should check this flag whenever
|
|
/// they return from a blocking operation. If it is false the thread should clean up and terminate.
|
|
/// </remarks>
|
|
public bool IsRunning
|
|
{
|
|
get
|
|
{
|
|
if (NonThreadSafeStatus == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
lock (NonThreadSafeStatus.Lock)
|
|
{
|
|
if (NonThreadSafeStatus.Value.State == ServerState.Running)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (NonThreadSafeStatus.Value.State == ServerState.Shutdown &&
|
|
NonThreadSafeStatus.Value.SecondsTillShutdown > 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether the server is collecting diagnostics.
|
|
/// </summary>
|
|
/// <value><c>true</c> if diagnostics are enabled; otherwise, <c>false</c>.</value>
|
|
public bool DiagnosticsEnabled
|
|
{
|
|
get
|
|
{
|
|
if (DiagnosticsNodeManager == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return DiagnosticsNodeManager.DiagnosticsEnabled;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Status but non thread safe - internal so not part of public api
|
|
/// </summary>
|
|
internal ServerStatusValue NonThreadSafeStatus { get; private set; } = null!;
|
|
|
|
/// <summary>
|
|
/// Closes the specified session.
|
|
/// </summary>
|
|
/// <param name="context">The context.</param>
|
|
/// <param name="sessionId">The session identifier.</param>
|
|
/// <param name="deleteSubscriptions">if set to <c>true</c> subscriptions are to be deleted.</param>
|
|
[Obsolete("Use CloseSessionAsync instead.")]
|
|
public void CloseSession(
|
|
OperationContext context,
|
|
NodeId sessionId,
|
|
bool deleteSubscriptions)
|
|
{
|
|
CloseSessionAsync(context, sessionId, deleteSubscriptions)
|
|
.AsTask().GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Closes the specified session.
|
|
/// </summary>
|
|
/// <param name="context">The context.</param>
|
|
/// <param name="sessionId">The session identifier.</param>
|
|
/// <param name="deleteSubscriptions">if set to <c>true</c> subscriptions are to be deleted.</param>
|
|
/// <param name="cancellationToken">The cancellationToken</param>
|
|
public async ValueTask CloseSessionAsync(
|
|
OperationContext context,
|
|
NodeId sessionId,
|
|
bool deleteSubscriptions,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await NodeManager.SessionClosingAsync(context, sessionId, deleteSubscriptions, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
await SubscriptionManager.SessionClosingAsync(context, sessionId, deleteSubscriptions, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
await SessionManager.CloseSessionAsync(sessionId, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes the specified subscription.
|
|
/// </summary>
|
|
/// <param name="subscriptionId">The subscription identifier.</param>
|
|
/// <param name="cancellationToken">The cancellation token</param>
|
|
public async ValueTask DeleteSubscriptionAsync(uint subscriptionId, CancellationToken cancellationToken = default)
|
|
{
|
|
await SubscriptionManager.DeleteSubscriptionAsync(null!, subscriptionId, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by any component to report a global event.
|
|
/// </summary>
|
|
/// <param name="e">The event.</param>
|
|
public void ReportEvent(IFilterTarget e)
|
|
{
|
|
ReportEvent(DefaultSystemContext, e);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by any component to report a global event.
|
|
/// </summary>
|
|
/// <param name="context">The context.</param>
|
|
/// <param name="e">The event.</param>
|
|
public void ReportEvent(ISystemContext context, IFilterTarget e)
|
|
{
|
|
if ((!Auditing) && (e is AuditEventState))
|
|
{
|
|
// do not report auditing events if server Auditing flag is false
|
|
return;
|
|
}
|
|
|
|
ServerObject?.ReportEvent(context, e);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refreshes the conditions for the specified subscription.
|
|
/// </summary>
|
|
/// <param name="context">The context.</param>
|
|
/// <param name="subscriptionId">The subscription identifier.</param>
|
|
public void ConditionRefresh(OperationContext context, uint subscriptionId)
|
|
{
|
|
SubscriptionManager.ConditionRefresh(context, subscriptionId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refreshes the conditions for the specified subscription.
|
|
/// </summary>
|
|
/// <param name="context">The context.</param>
|
|
/// <param name="subscriptionId">The subscription identifier.</param>
|
|
/// <param name="monitoredItemId">The monitored item identifier.</param>
|
|
public void ConditionRefresh2(
|
|
OperationContext context,
|
|
uint subscriptionId,
|
|
uint monitoredItemId)
|
|
{
|
|
SubscriptionManager.ConditionRefresh2(context, subscriptionId, monitoredItemId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the server status safely.
|
|
/// </summary>
|
|
/// <param name="action">Action to perform on the server status object.</param>
|
|
/// <exception cref="ArgumentNullException"><paramref name="action"/> is <c>null</c>.</exception>
|
|
public void UpdateServerStatus(Action<ServerStatusValue> action)
|
|
{
|
|
if (action == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(action));
|
|
}
|
|
|
|
lock (NonThreadSafeStatus.Lock)
|
|
{
|
|
action.Invoke(NonThreadSafeStatus);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public bool Auditing { get; private set; }
|
|
|
|
/// <inheritdoc/>
|
|
public ISystemContext DefaultAuditContext => DefaultSystemContext.Copy();
|
|
|
|
/// <inheritdoc/>
|
|
public void ReportAuditEvent(ISystemContext context, AuditEventState e)
|
|
{
|
|
if (!Auditing)
|
|
{
|
|
// do not report auditing events if server Auditing flag is false
|
|
return;
|
|
}
|
|
|
|
ReportEvent(context, e);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the ServerObject and attaches it to the NodeManager.
|
|
/// </summary>
|
|
[MemberNotNull(
|
|
nameof(ServerObject),
|
|
nameof(NonThreadSafeStatus),
|
|
nameof(ServerDiagnostics))]
|
|
private async ValueTask CreateServerObjectAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
// get the server object.
|
|
ServerObjectState serverObject = ServerObject =
|
|
DiagnosticsNodeManager.FindPredefinedNode<ServerObjectState>(ObjectIds.Server);
|
|
|
|
// update server capabilities.
|
|
ServerCapabilitiesState serverCapabilities = serverObject.ServerCapabilities!;
|
|
serverObject.ServiceLevel!.Value = 255;
|
|
serverCapabilities.LocaleIdArray!.Value = ResourceManager
|
|
.GetAvailableLocales();
|
|
serverCapabilities.ServerProfileArray!.Value =
|
|
[
|
|
.. m_configuration.ServerConfiguration!.ServerProfileArray
|
|
];
|
|
|
|
BaseVariableState conformanceUnits = DiagnosticsNodeManager.FindPredefinedNode<BaseVariableState>(
|
|
VariableIds.Server_ServerCapabilities_ConformanceUnits);
|
|
if (conformanceUnits != null)
|
|
{
|
|
conformanceUnits.Value = Variant.From(Array.Empty<QualifiedName>().ToArrayOf());
|
|
}
|
|
|
|
serverCapabilities.MinSupportedSampleRate!.Value = 0;
|
|
serverCapabilities.MaxBrowseContinuationPoints!.Value = (ushort)
|
|
m_configuration.ServerConfiguration.MaxBrowseContinuationPoints;
|
|
serverCapabilities.MaxQueryContinuationPoints!.Value = (ushort)
|
|
m_configuration.ServerConfiguration.MaxQueryContinuationPoints;
|
|
serverCapabilities.MaxHistoryContinuationPoints!.Value = (ushort)
|
|
m_configuration.ServerConfiguration.MaxHistoryContinuationPoints;
|
|
serverCapabilities.MaxArrayLength!.Value = (uint)
|
|
m_configuration.TransportQuotas!.MaxArrayLength;
|
|
serverCapabilities.MaxStringLength!.Value = (uint)
|
|
m_configuration.TransportQuotas.MaxStringLength;
|
|
serverCapabilities.MaxByteStringLength!.Value = (uint)
|
|
m_configuration.TransportQuotas.MaxByteStringLength;
|
|
serverCapabilities.MaxSessions!.Value = (uint)
|
|
m_configuration.ServerConfiguration.MaxSessionCount;
|
|
serverCapabilities.MaxSubscriptions!.Value = (uint)
|
|
m_configuration.ServerConfiguration.MaxSubscriptionCount;
|
|
|
|
// Expose MaxSubscriptionsPerSession (optional property
|
|
// on ServerCapabilitiesType per Part 5 §6.3) so clients that
|
|
// enumerate per-session limits get a defined value instead of a
|
|
// missing-attribute response. Use the configured global
|
|
// MaxSubscriptionCount as the per-session ceiling — the SDK
|
|
// doesn't track per-session limits separately at this layer.
|
|
if (serverCapabilities.MaxSubscriptionsPerSession == null)
|
|
{
|
|
serverCapabilities.AddMaxSubscriptionsPerSession(DefaultSystemContext);
|
|
}
|
|
serverCapabilities.MaxSubscriptionsPerSession!.Value = (uint)Math.Max(1,
|
|
m_configuration.ServerConfiguration.MaxSubscriptionCount);
|
|
|
|
// Any operational limits Property that is provided shall have a non zero value.
|
|
OperationLimitsState? operationLimits = serverCapabilities.OperationLimits;
|
|
OperationLimits configOperationLimits = m_configuration.ServerConfiguration
|
|
.OperationLimits;
|
|
if (configOperationLimits != null)
|
|
{
|
|
operationLimits!.MaxNodesPerRead = SetPropertyValue(
|
|
operationLimits.MaxNodesPerRead!,
|
|
configOperationLimits.MaxNodesPerRead);
|
|
operationLimits.MaxNodesPerHistoryReadData = SetPropertyValue(
|
|
operationLimits.MaxNodesPerHistoryReadData!,
|
|
configOperationLimits.MaxNodesPerHistoryReadData);
|
|
operationLimits.MaxNodesPerHistoryReadEvents = SetPropertyValue(
|
|
operationLimits.MaxNodesPerHistoryReadEvents!,
|
|
configOperationLimits.MaxNodesPerHistoryReadEvents);
|
|
operationLimits.MaxNodesPerWrite = SetPropertyValue(
|
|
operationLimits.MaxNodesPerWrite!,
|
|
configOperationLimits.MaxNodesPerWrite);
|
|
operationLimits.MaxNodesPerHistoryUpdateData = SetPropertyValue(
|
|
operationLimits.MaxNodesPerHistoryUpdateData!,
|
|
configOperationLimits.MaxNodesPerHistoryUpdateData);
|
|
operationLimits.MaxNodesPerHistoryUpdateEvents = SetPropertyValue(
|
|
operationLimits.MaxNodesPerHistoryUpdateEvents!,
|
|
configOperationLimits.MaxNodesPerHistoryUpdateEvents);
|
|
operationLimits.MaxNodesPerMethodCall = SetPropertyValue(
|
|
operationLimits.MaxNodesPerMethodCall!,
|
|
configOperationLimits.MaxNodesPerMethodCall);
|
|
operationLimits.MaxNodesPerBrowse = SetPropertyValue(
|
|
operationLimits.MaxNodesPerBrowse!,
|
|
configOperationLimits.MaxNodesPerBrowse);
|
|
operationLimits.MaxNodesPerRegisterNodes = SetPropertyValue(
|
|
operationLimits.MaxNodesPerRegisterNodes!,
|
|
configOperationLimits.MaxNodesPerRegisterNodes);
|
|
operationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds = SetPropertyValue(
|
|
operationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds!,
|
|
configOperationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds);
|
|
operationLimits.MaxNodesPerNodeManagement = SetPropertyValue(
|
|
operationLimits.MaxNodesPerNodeManagement!,
|
|
configOperationLimits.MaxNodesPerNodeManagement);
|
|
operationLimits.MaxMonitoredItemsPerCall = SetPropertyValue(
|
|
operationLimits.MaxMonitoredItemsPerCall!,
|
|
configOperationLimits.MaxMonitoredItemsPerCall);
|
|
}
|
|
else
|
|
{
|
|
operationLimits!.MaxNodesPerRead =
|
|
operationLimits.MaxNodesPerHistoryReadData =
|
|
operationLimits.MaxNodesPerHistoryReadEvents =
|
|
operationLimits.MaxNodesPerWrite =
|
|
operationLimits.MaxNodesPerHistoryUpdateData =
|
|
operationLimits.MaxNodesPerHistoryUpdateEvents =
|
|
operationLimits.MaxNodesPerMethodCall =
|
|
operationLimits.MaxNodesPerBrowse =
|
|
operationLimits.MaxNodesPerRegisterNodes =
|
|
operationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds =
|
|
operationLimits.MaxNodesPerNodeManagement =
|
|
operationLimits.MaxMonitoredItemsPerCall =
|
|
null;
|
|
}
|
|
|
|
// setup PublishSubscribe Status State value
|
|
const PubSubState pubSubState = PubSubState.Disabled;
|
|
|
|
BaseVariableState default_PubSubState =
|
|
DiagnosticsNodeManager.FindPredefinedNode<BaseVariableState>(
|
|
VariableIds.PublishSubscribe_Status_State);
|
|
default_PubSubState.Value = Variant.From(pubSubState);
|
|
|
|
// setup value for SupportedTransportProfiles
|
|
BaseVariableState default_SupportedTransportProfiles =
|
|
DiagnosticsNodeManager.FindPredefinedNode<BaseVariableState>(
|
|
VariableIds.PublishSubscribe_SupportedTransportProfiles);
|
|
default_SupportedTransportProfiles.Value = "uadp";
|
|
|
|
// setup callbacks for dynamic values.
|
|
serverObject.NamespaceArray!.OnSimpleReadValue = OnReadNamespaceArray;
|
|
serverObject.NamespaceArray.MinimumSamplingInterval = 1000;
|
|
|
|
serverObject.ServerArray!.OnSimpleReadValue = OnReadServerArray;
|
|
serverObject.ServerArray.MinimumSamplingInterval = 1000;
|
|
|
|
// dynamic change of enabledFlag is disabled to pass CTT
|
|
serverObject.ServerDiagnostics!.EnabledFlag!.AccessLevel = AccessLevels.CurrentRead;
|
|
serverObject.ServerDiagnostics.EnabledFlag.UserAccessLevel = AccessLevels
|
|
.CurrentRead;
|
|
serverObject.ServerDiagnostics.EnabledFlag.OnSimpleReadValue
|
|
= OnReadDiagnosticsEnabledFlag;
|
|
serverObject.ServerDiagnostics.EnabledFlag.OnSimpleWriteValue
|
|
= OnWriteDiagnosticsEnabledFlag;
|
|
serverObject.ServerDiagnostics.EnabledFlag.MinimumSamplingInterval = 1000;
|
|
|
|
// initialize status.
|
|
DateTime nowUtc = m_timeProvider.GetUtcNow().UtcDateTime;
|
|
var serverStatus = new ServerStatusDataType
|
|
{
|
|
StartTime = nowUtc,
|
|
CurrentTime = nowUtc,
|
|
State = ServerState.Shutdown
|
|
};
|
|
|
|
var buildInfo = new BuildInfo
|
|
{
|
|
ProductName = m_serverDescription.ProductName,
|
|
ProductUri = m_serverDescription.ProductUri,
|
|
ManufacturerName = m_serverDescription.ManufacturerName,
|
|
SoftwareVersion = m_serverDescription.SoftwareVersion,
|
|
BuildNumber = m_serverDescription.BuildNumber,
|
|
BuildDate = m_serverDescription.BuildDate
|
|
};
|
|
BuildInfoVariableState buildInfoVariableState =
|
|
DiagnosticsNodeManager.FindPredefinedNode<BuildInfoVariableState>(
|
|
VariableIds.Server_ServerStatus_BuildInfo);
|
|
var buildInfoVariable = new BuildInfoVariableValue(
|
|
buildInfoVariableState,
|
|
buildInfo,
|
|
null!);
|
|
serverStatus.BuildInfo = buildInfoVariable.Value;
|
|
|
|
serverObject.ServerStatus!.MinimumSamplingInterval = 1000;
|
|
serverObject.ServerStatus.CurrentTime!.MinimumSamplingInterval = 1000;
|
|
|
|
NonThreadSafeStatus = new ServerStatusValue(
|
|
serverObject.ServerStatus,
|
|
serverStatus,
|
|
DiagnosticsLock)
|
|
{
|
|
Timestamp = nowUtc,
|
|
OnBeforeRead = OnReadServerStatus
|
|
};
|
|
|
|
// initialize diagnostics.
|
|
ServerDiagnostics = new ServerDiagnosticsSummaryDataType
|
|
{
|
|
ServerViewCount = 0,
|
|
CurrentSessionCount = 0,
|
|
CumulatedSessionCount = 0,
|
|
SecurityRejectedSessionCount = 0,
|
|
RejectedSessionCount = 0,
|
|
SessionTimeoutCount = 0,
|
|
SessionAbortCount = 0,
|
|
PublishingIntervalCount = 0,
|
|
CurrentSubscriptionCount = 0,
|
|
CumulatedSubscriptionCount = 0,
|
|
SecurityRejectedRequestsCount = 0,
|
|
RejectedRequestsCount = 0
|
|
};
|
|
|
|
await DiagnosticsNodeManager.CreateServerDiagnosticsAsync(
|
|
DefaultSystemContext,
|
|
ServerDiagnostics,
|
|
OnUpdateDiagnostics,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
// set the diagnostics enabled state.
|
|
await DiagnosticsNodeManager.SetDiagnosticsEnabledAsync(
|
|
DefaultSystemContext,
|
|
m_configuration.ServerConfiguration.DiagnosticsEnabled,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
var configurationNodeManager = DiagnosticsNodeManager as ConfigurationNodeManager;
|
|
configurationNodeManager?.CreateServerConfiguration(
|
|
DefaultSystemContext,
|
|
m_configuration);
|
|
|
|
// Initialize history capabilities and update Server EventNotifier accordingly
|
|
await DiagnosticsNodeManager.UpdateServerEventNotifierAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
Auditing = m_configuration.ServerConfiguration.AuditingEnabled;
|
|
PropertyState<bool>? auditing = serverObject.Auditing;
|
|
auditing!.OnSimpleWriteValue += OnWriteAuditing;
|
|
auditing.OnSimpleReadValue += OnReadAuditing;
|
|
auditing.Value = Auditing;
|
|
auditing.AccessLevel = AccessLevels.CurrentRead;
|
|
auditing.UserAccessLevel = AccessLevels.CurrentReadOrWrite;
|
|
auditing.MinimumSamplingInterval = 1000;
|
|
|
|
// Wire RoleManager into the well-known role nodes per Part 18 §4.
|
|
// The binding upgrades RoleSet to the typed RoleSetState proxy,
|
|
// wires typed OnCallAsync delegates on each Role's method-state
|
|
// children, applies the SecurityAdmin + SignAndEncrypt
|
|
// authorization gate, and raises RoleMappingRuleChangedAuditEvent
|
|
// on every successful mutation.
|
|
if (DiagnosticsNodeManager is AsyncCustomNodeManager diagnosticsCustom)
|
|
{
|
|
m_roleStateBinding = RoleStateBinding.Bind(diagnosticsCustom, RoleManager, this);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the server status before a read.
|
|
/// </summary>
|
|
private void OnReadServerStatus(
|
|
ISystemContext context,
|
|
BaseVariableValue variable,
|
|
NodeState component)
|
|
{
|
|
lock (NonThreadSafeStatus.Lock)
|
|
{
|
|
DateTime now = m_timeProvider.GetUtcNow().UtcDateTime;
|
|
NonThreadSafeStatus.Timestamp = now;
|
|
NonThreadSafeStatus.Value.CurrentTime = now;
|
|
|
|
// update other timestamps in NodeState objects which are used to derive the source timestamp
|
|
if (variable is ServerStatusValue serverStatusValue &&
|
|
serverStatusValue.Variable is ServerStatusState serverStatusState)
|
|
{
|
|
serverStatusState.Timestamp = now;
|
|
serverStatusState.CurrentTime!.Timestamp = now;
|
|
serverStatusState.State!.Timestamp = now;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a copy of the namespace array.
|
|
/// </summary>
|
|
private ServiceResult OnReadNamespaceArray(
|
|
ISystemContext context,
|
|
NodeState node,
|
|
ref Variant value)
|
|
{
|
|
value = NamespaceUris.ToArrayOf();
|
|
return ServiceResult.Good;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a copy of the server array.
|
|
/// </summary>
|
|
private ServiceResult OnReadServerArray(
|
|
ISystemContext context,
|
|
NodeState node,
|
|
ref Variant value)
|
|
{
|
|
value = ServerUris.ToArrayOf();
|
|
return ServiceResult.Good;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns Diagnostics.EnabledFlag
|
|
/// </summary>
|
|
private ServiceResult OnReadDiagnosticsEnabledFlag(
|
|
ISystemContext context,
|
|
NodeState node,
|
|
ref Variant value)
|
|
{
|
|
value = DiagnosticsNodeManager.DiagnosticsEnabled;
|
|
return ServiceResult.Good;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the Diagnostics.EnabledFlag
|
|
/// </summary>
|
|
private ServiceResult OnWriteDiagnosticsEnabledFlag(
|
|
ISystemContext context,
|
|
NodeState node,
|
|
ref Variant value)
|
|
{
|
|
bool enabled = (bool)value;
|
|
DiagnosticsNodeManager.SetDiagnosticsEnabledAsync(DefaultSystemContext, enabled)
|
|
.AsTask().GetAwaiter().GetResult();
|
|
|
|
return ServiceResult.Good;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the Server.Auditing flag.
|
|
/// </summary>
|
|
private ServiceResult OnWriteAuditing(
|
|
ISystemContext context,
|
|
NodeState node,
|
|
ref Variant value)
|
|
{
|
|
Auditing = Convert.ToBoolean(value, CultureInfo.InvariantCulture);
|
|
return ServiceResult.Good;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the Server.Auditing flag.
|
|
/// </summary>
|
|
private ServiceResult OnReadAuditing(
|
|
ISystemContext context,
|
|
NodeState node,
|
|
ref Variant value)
|
|
{
|
|
value = Auditing;
|
|
return ServiceResult.Good;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a copy of the current diagnostics.
|
|
/// </summary>
|
|
private ServiceResult OnUpdateDiagnostics(
|
|
ISystemContext context,
|
|
NodeState node,
|
|
ref Variant value)
|
|
{
|
|
lock (ServerDiagnostics)
|
|
{
|
|
value = Variant.FromStructure(ServerDiagnostics);
|
|
}
|
|
|
|
return ServiceResult.Good;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set the property to null if the value is zero,
|
|
/// to the value otherwise.
|
|
/// </summary>
|
|
private static PropertyState<uint> SetPropertyValue(
|
|
PropertyState<uint> property,
|
|
uint value)
|
|
{
|
|
if (value != 0)
|
|
{
|
|
property.Value = value;
|
|
}
|
|
else
|
|
{
|
|
property = null!;
|
|
}
|
|
return property;
|
|
}
|
|
|
|
private readonly ServerProperties m_serverDescription;
|
|
private readonly ApplicationConfiguration m_configuration;
|
|
private readonly List<Uri> m_endpointAddresses;
|
|
private readonly TimeProvider m_timeProvider;
|
|
private RoleStateBinding? m_roleStateBinding;
|
|
private volatile IReadOnlyList<ITransportListener>? m_transportListeners;
|
|
}
|
|
}
|