mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 22:55:08 +08:00
a81d981755
# Description
Pure code-style sweep across the repo — no behavioral changes. Drives
`dotnet format` info-level diagnostics down on a large slice of the
codebase (Stack, Libraries, Tests, Tools).
Six commits, applied bottom-up in safety order so each batch leaves the
build green:
1. **`style: fix whitespace and final-newline formatting (WHITESPACE,
FINALNEWLINE)`**
197 hits across ~30 files; pure mechanical cleanup.
2. **`style: apply dotnet format style fixes (IDE rules)`**
IDE-rule batch via `dotnet format style --severity info` —
`var`/explicit type, simplify names, collection expressions, expression
bodies, unused usings, etc.
3. **`style: apply dotnet format analyzers fixes (RCS blank-line
formatting)`**
Roslynator formatting (RCS0xxx) blank-line and punctuation rules.
4. **`style: convert comments to doc comments and inline local vars
(RCS1181, RCS1124)`**
24 files. RCS1181 promotes `// summary`-style comments above
public/internal members to `/// <summary>` doc comments; RCS1124 inlines
locals used exactly once. Generated `Tools/` files were reverted to keep
the source-generator outputs deterministic.
5. **`docs(agent): correct --diagnostics flag form and verify Roslynator
4.x code fixers`**
Updates `.github/agents/opc-ua-codestyle-enforcer.agent.md` with two
corrections discovered while running this sweep:
- `dotnet format --diagnostics "RULE1 RULE2"` (single quoted,
space-separated) silently fixes nothing on `dotnet format` 10.x — it
parses as one unknown ID and exits 0. The correct form is `--diagnostics
RULE1 --diagnostics RULE2 …`, repeated per rule.
- Roslynator 4.15.0 **does** ship code fixers for
RCS1078/1118/1124/1140/1142/1181/1221/1260 (the previous "no auto-fixer"
list was outdated). RCS1140/1142 default to `Hidden` so need `--severity
info` to surface and fire; RCS1078 requires
`roslynator_empty_string_style = field` (already set in
`.editorconfig`). Only NUnit4002/2046/2010 genuinely lack auto-fixers in
this stack.
6. **`style: format new code from master merge (dotnet format sweep)`**
After merging master into the branch (commit `e3ebc8742`), re-ran the
same three-phase format sweep on the 151 .cs files brought in by the
merge, scoped per project via repeated `--include`. 81 files changed;
same rule families as commits 1–4.
- Hand-fixed `Tests/Opc.Ua.MigrationAnalyzer.Tests/AnalyzerHarness.cs`
for `RCS1174` (redundant `async`/`await` removal) because the auto-fixer
didn't apply for that specific case.
- Reverted files that the formatter would break:
- `Libraries/Opc.Ua.Server/Server/ServerInternalData.cs` — multi-TFM
analysis disagreement injected `<<<<<<<` / `=======` / `>>>>>>>`
markers.
-
`Tools/Opc.Ua.MigrationAnalyzer.Core/{Core/Stack/Client/ChannelBase.cs,
Core/Stack/Types/IUserIdentityTokenHandler.cs,
Client/Session/Session.cs}` — IDE0005 removed `using
Opc.Ua.Security.Certificates` that IS used (the files are 1.5.378
reference fixtures for the analyzer).
- `Tests/Opc.Ua.Gds.Tests/PushTest.cs` — IDE0002 over-simplified
`OpcUa.ObjectTypeIds` to `ObjectTypeIds`, which then resolves to a
conflicting type.
### What this PR does **not** touch
- No behavioral or semantic logic changes.
- No `.editorconfig` rule promotions (rules stay at their current
severities; this is a backlog-burndown pass, not a rule tightening).
- Generated source under `Tools/` is intentionally excluded.
- Higher-risk rule families (`RCS1090`/`CA2007` `ConfigureAwait`,
`RCS1249` null-forgiving cleanup, `IDE0270`/`IDE0059`) are left for
follow-up sweeps because they need per-TFM build verification or
semantic review.
## Related Issues
_No tracking issue — this is a non-functional style sweep and the
agent-doc correction is a follow-up to the same work. Happy to open one
if the maintainers prefer._
## 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. _(N/A — style-only, no
behavioral change)_
- [x] I have added all necessary documentation. _(agent doc updated with
the two corrections)_
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings. _(verified clean on `net10.0` and `net48`; only
pre-existing `NETSDK1005`/`NETSDK1207` failures on Tools/AOT projects)_
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed. _(build
verified on net10 and net48 per batch; full test suite not yet run
locally — relying on CI)_
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
---------
Co-authored-by: agent <agent@local>
4140 lines
167 KiB
C#
4140 lines
167 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using Opc.Ua.Bindings;
|
|
using Opc.Ua.Security.Certificates;
|
|
|
|
namespace Opc.Ua.Server
|
|
{
|
|
/// <inheritdoc/>
|
|
public class StandardServer : SessionServerBase, IStandardServer
|
|
{
|
|
/// <inheritdoc/>
|
|
public StandardServer(ITelemetryContext telemetry)
|
|
: this(telemetry, null)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of <see cref="StandardServer"/>
|
|
/// with an explicit <see cref="TimeProvider"/> so the server's
|
|
/// timers and duration calculations can be mocked in tests.
|
|
/// </summary>
|
|
/// <param name="telemetry">The telemetry 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 StandardServer(ITelemetryContext telemetry, TimeProvider? timeProvider)
|
|
: base(telemetry)
|
|
{
|
|
TimeProvider = timeProvider ?? TimeProvider.System;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The <see cref="TimeProvider"/> used by the server for all
|
|
/// time / duration calculations and timer scheduling.
|
|
/// </summary>
|
|
protected TimeProvider TimeProvider { get; }
|
|
|
|
/// <inheritdoc/>
|
|
protected override void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
// halt any outstanding timer.
|
|
m_registrationTimer?.Dispose();
|
|
m_registrationTimer = null;
|
|
|
|
// close the watcher.
|
|
m_configurationWatcher?.Dispose();
|
|
m_configurationWatcher = null;
|
|
|
|
// close the server.
|
|
m_serverInternal?.Dispose();
|
|
m_serverInternal = null;
|
|
|
|
m_certManagerSubscription?.Dispose();
|
|
|
|
m_semaphoreSlim.Dispose();
|
|
}
|
|
|
|
base.Dispose(disposing);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<FindServersResponse> FindServersAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
string? endpointUrl,
|
|
ArrayOf<string> localeIds,
|
|
ArrayOf<string> serverUris,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
List<ApplicationDescription> servers = [];
|
|
|
|
ValidateRequest(requestHeader);
|
|
|
|
await m_semaphoreSlim.WaitAsync(requestLifetime.CancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
// parse the url provided by the client.
|
|
IList<BaseAddress> baseAddresses = BaseAddresses;
|
|
|
|
Uri? parsedEndpointUrl = Utils.ParseUri(endpointUrl);
|
|
|
|
if (parsedEndpointUrl != null)
|
|
{
|
|
baseAddresses = FilterByEndpointUrl(parsedEndpointUrl, baseAddresses);
|
|
}
|
|
|
|
// check if nothing to do.
|
|
if (baseAddresses.Count == 0)
|
|
{
|
|
return new FindServersResponse
|
|
{
|
|
ResponseHeader = CreateResponse(requestHeader, StatusCodes.Good),
|
|
Servers = servers
|
|
};
|
|
}
|
|
|
|
// build list of unique servers.
|
|
var uniqueServers = new Dictionary<string, ApplicationDescription>();
|
|
|
|
foreach (EndpointDescription description in GetEndpoints())
|
|
{
|
|
ApplicationDescription server = description.Server;
|
|
|
|
// skip servers that have been processed.
|
|
if (uniqueServers.ContainsKey(server.ApplicationUri!))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// check client is filtering by server uri.
|
|
if (serverUris.Count > 0 &&
|
|
!serverUris.Contains(f => f == server.ApplicationUri))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// localize the application name if requested.
|
|
LocalizedText applicationName = server.ApplicationName;
|
|
|
|
if (localeIds.Count > 0)
|
|
{
|
|
applicationName = ServerInternal.ResourceManager
|
|
.Translate(localeIds, applicationName);
|
|
}
|
|
|
|
// get the application description.
|
|
ApplicationDescription application = TranslateApplicationDescription(
|
|
parsedEndpointUrl!,
|
|
server,
|
|
baseAddresses,
|
|
applicationName);
|
|
|
|
uniqueServers.Add(server.ApplicationUri!, application);
|
|
|
|
// add to list of servers to return.
|
|
servers.Add(application);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
m_semaphoreSlim.Release();
|
|
}
|
|
|
|
return new FindServersResponse
|
|
{
|
|
ResponseHeader = CreateResponse(requestHeader, StatusCodes.Good),
|
|
Servers = servers
|
|
};
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<GetEndpointsResponse> GetEndpointsAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
string? endpointUrl,
|
|
ArrayOf<string> localeIds,
|
|
ArrayOf<string> profileUris,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
ArrayOf<EndpointDescription> endpoints = default;
|
|
|
|
ValidateRequest(requestHeader);
|
|
|
|
await m_semaphoreSlim.WaitAsync(requestLifetime.CancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
// filter by profile.
|
|
IList<BaseAddress> baseAddresses = FilterByProfile(profileUris, BaseAddresses);
|
|
|
|
// get the descriptions.
|
|
endpoints = GetEndpointDescriptions(endpointUrl, baseAddresses, localeIds);
|
|
}
|
|
finally
|
|
{
|
|
m_semaphoreSlim.Release();
|
|
}
|
|
|
|
return new GetEndpointsResponse
|
|
{
|
|
ResponseHeader = CreateResponse(requestHeader, StatusCodes.Good),
|
|
Endpoints = endpoints
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the endpoints that match the base address and endpoint url.
|
|
/// </summary>
|
|
protected ArrayOf<EndpointDescription> GetEndpointDescriptions(
|
|
string? endpointUrl,
|
|
IList<BaseAddress> baseAddresses,
|
|
ArrayOf<string> localeIds)
|
|
{
|
|
ArrayOf<EndpointDescription> endpoints = default;
|
|
|
|
// parse the url provided by the client.
|
|
Uri? parsedEndpointUrl = Utils.ParseUri(endpointUrl);
|
|
|
|
if (parsedEndpointUrl != null)
|
|
{
|
|
baseAddresses = FilterByEndpointUrl(parsedEndpointUrl, baseAddresses);
|
|
}
|
|
|
|
// check if nothing to do.
|
|
if (baseAddresses.Count != 0)
|
|
{
|
|
// localize the application name if requested.
|
|
LocalizedText applicationName = ServerDescription!.ApplicationName;
|
|
|
|
if (localeIds.Count > 0)
|
|
{
|
|
applicationName = ServerInternal.ResourceManager
|
|
.Translate(localeIds, applicationName);
|
|
}
|
|
|
|
// translate the application description.
|
|
ApplicationDescription application = TranslateApplicationDescription(
|
|
parsedEndpointUrl!,
|
|
ServerDescription,
|
|
baseAddresses,
|
|
applicationName);
|
|
|
|
// translate the endpoint descriptions.
|
|
endpoints = TranslateEndpointDescriptions(
|
|
parsedEndpointUrl!,
|
|
baseAddresses,
|
|
Endpoints,
|
|
application);
|
|
}
|
|
|
|
return endpoints;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override void ReportAuditOpenSecureChannelEvent(
|
|
string globalChannelId,
|
|
EndpointDescription endpointDescription,
|
|
OpenSecureChannelRequest request,
|
|
Certificate clientCertificate,
|
|
Exception exception)
|
|
{
|
|
ServerInternal?.ReportAuditOpenSecureChannelEvent(
|
|
globalChannelId,
|
|
endpointDescription,
|
|
request,
|
|
clientCertificate,
|
|
exception,
|
|
m_logger);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override void ReportAuditCloseSecureChannelEvent(
|
|
string globalChannelId,
|
|
Exception exception)
|
|
{
|
|
ServerInternal?.ReportAuditCloseSecureChannelEvent(globalChannelId, exception, m_logger);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override void ReportAuditCertificateEvent(
|
|
Certificate clientCertificate,
|
|
Exception exception)
|
|
{
|
|
ServerInternal?.ReportAuditCertificateEvent(clientCertificate, exception, m_logger);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<CreateSessionResponse> CreateSessionAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
ApplicationDescription? clientDescription,
|
|
string? serverUri,
|
|
string? endpointUrl,
|
|
string? sessionName,
|
|
ByteString clientNonce,
|
|
ByteString clientCertificate,
|
|
double requestedSessionTimeout,
|
|
uint maxResponseMessageSize,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
NodeId sessionId;
|
|
NodeId authenticationToken;
|
|
double revisedSessionTimeout = 0;
|
|
ByteString serverNonce;
|
|
ByteString serverCertificate = default;
|
|
ArrayOf<EndpointDescription> serverEndpoints = default;
|
|
SignatureData? serverSignature = null;
|
|
uint maxRequestMessageSize = (uint)MessageContext.MaxMessageSize;
|
|
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.CreateSession,
|
|
requestLifetime).ConfigureAwait(false);
|
|
ISession? session = null;
|
|
try
|
|
{
|
|
// check the server uri.
|
|
if (!string.IsNullOrEmpty(serverUri) && serverUri != Configuration!.ApplicationUri)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadServerUriInvalid);
|
|
}
|
|
|
|
bool requireEncryption = RequireEncryption(
|
|
context.ChannelContext!.EndpointDescription!);
|
|
|
|
if (!requireEncryption && !clientCertificate.IsEmpty)
|
|
{
|
|
requireEncryption = true;
|
|
}
|
|
|
|
CertificateCollection? clientIssuerCertificates = null;
|
|
|
|
// validate client application instance certificate.
|
|
Certificate? parsedClientCertificate = null;
|
|
|
|
if (requireEncryption && clientCertificate.Length > 0)
|
|
{
|
|
try
|
|
{
|
|
using CertificateCollection clientCertificateChain
|
|
= Utils.ParseCertificateChainBlob(
|
|
clientCertificate,
|
|
ServerInternal.Telemetry);
|
|
parsedClientCertificate = clientCertificateChain[0].AddRef();
|
|
|
|
if (clientCertificateChain.Count > 1)
|
|
{
|
|
clientIssuerCertificates = [];
|
|
for (int i = 1; i < clientCertificateChain.Count; i++)
|
|
{
|
|
clientIssuerCertificates.Add(clientCertificateChain[i]);
|
|
}
|
|
}
|
|
|
|
if (context.SecurityPolicyUri != SecurityPolicies.None)
|
|
{
|
|
// verify if applicationUri from ApplicationDescription matches the applicationUris in the client certificate.
|
|
if (!string.IsNullOrEmpty(clientDescription?.ApplicationUri))
|
|
{
|
|
if (!X509Utils.CompareApplicationUriWithCertificate(parsedClientCertificate!, clientDescription!.ApplicationUri!))
|
|
{
|
|
// report the AuditCertificateDataMismatch event for invalid uri
|
|
ServerInternal?.ReportAuditCertificateDataMismatchEvent(
|
|
parsedClientCertificate,
|
|
null,
|
|
clientDescription.ApplicationUri,
|
|
StatusCodes.BadCertificateUriInvalid,
|
|
m_logger);
|
|
|
|
throw ServiceResultException.Create(
|
|
StatusCodes.BadCertificateUriInvalid,
|
|
"The URI specified in the ApplicationDescription {0} does not match the URIs in the Certificate.",
|
|
clientDescription.ApplicationUri!);
|
|
}
|
|
|
|
CertificateValidationResult clientCertResult = await CertificateManager!
|
|
.ValidateAsync(
|
|
clientCertificateChain,
|
|
TrustListIdentifier.Peers,
|
|
options: null,
|
|
ct: requestLifetime.CancellationToken)
|
|
.ConfigureAwait(false);
|
|
if (!clientCertResult.IsValid)
|
|
{
|
|
throw new ServiceResultException(clientCertResult.StatusCode);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
// report audit event for client certificate
|
|
ReportAuditCertificateEvent(parsedClientCertificate!, e);
|
|
|
|
OnApplicationCertificateError(clientCertificate, new ServiceResult(e));
|
|
}
|
|
}
|
|
|
|
// verify the nonce provided by the client.
|
|
if (!clientNonce.IsEmpty)
|
|
{
|
|
if (clientNonce.Length < m_minNonceLength)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadNonceInvalid);
|
|
}
|
|
|
|
// ignore nonce if security policy set to none
|
|
if (context.SecurityPolicyUri == SecurityPolicies.None)
|
|
{
|
|
clientNonce = default;
|
|
}
|
|
}
|
|
|
|
// load the certificate for the security profile
|
|
Certificate instanceCertificate = CertificateManager!
|
|
.GetInstanceCertificate(
|
|
context.SecurityPolicyUri)?.Certificate!;
|
|
|
|
// create the session.
|
|
CreateSessionResult result = await ServerInternal.SessionManager.CreateSessionAsync(
|
|
context,
|
|
instanceCertificate,
|
|
sessionName,
|
|
clientNonce,
|
|
clientDescription,
|
|
endpointUrl,
|
|
parsedClientCertificate,
|
|
clientIssuerCertificates,
|
|
requestedSessionTimeout,
|
|
maxResponseMessageSize,
|
|
requestLifetime.CancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
session = result.Session;
|
|
sessionId = result.SessionId;
|
|
authenticationToken = result.AuthenticationToken;
|
|
serverNonce = result.ServerNonce;
|
|
revisedSessionTimeout = result.RevisedSessionTimeout;
|
|
|
|
if (endpointUrl != null)
|
|
{
|
|
try
|
|
{
|
|
// check the endpointurl
|
|
var configuredEndpoint = new ConfiguredEndpoint
|
|
{
|
|
EndpointUrl = new Uri(endpointUrl)
|
|
};
|
|
|
|
CertificateManager.ValidateDomains(
|
|
instanceCertificate,
|
|
configuredEndpoint,
|
|
serverValidation: true);
|
|
}
|
|
catch (ServiceResultException sre)
|
|
when (sre.StatusCode == StatusCodes.BadCertificateHostNameInvalid)
|
|
{
|
|
m_logger.LogWarning(
|
|
"Server - Client connects with an endpointUrl [{EndpointUrl}] which does not match Server hostnames.",
|
|
endpointUrl);
|
|
ServerInternal.ReportAuditUrlMismatchEvent(
|
|
context.AuditEntryId!,
|
|
session,
|
|
revisedSessionTimeout,
|
|
endpointUrl,
|
|
m_logger);
|
|
}
|
|
}
|
|
|
|
AdditionalParametersType? parameters = CreateSessionProcessAdditionalParameters(
|
|
session,
|
|
requestHeader.AdditionalHeader);
|
|
|
|
await m_semaphoreSlim.WaitAsync(requestLifetime.CancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
// return the application instance certificate for the server.
|
|
if (requireEncryption)
|
|
{
|
|
// check if complete chain should be sent.
|
|
if (CertificateManager.SendCertificateChain)
|
|
{
|
|
serverCertificate = CertificateManager
|
|
.LoadCertificateChainRaw(instanceCertificate).ToByteString();
|
|
}
|
|
else
|
|
{
|
|
serverCertificate = instanceCertificate.RawData.ToByteString();
|
|
}
|
|
}
|
|
|
|
// return the endpoints supported by the server.
|
|
serverEndpoints = GetEndpointDescriptions(endpointUrl, BaseAddresses, default);
|
|
|
|
// sign the nonce provided by the client.
|
|
serverSignature = CreateSessionServerSignature(
|
|
context,
|
|
instanceCertificate,
|
|
parsedClientCertificate,
|
|
clientNonce,
|
|
serverNonce);
|
|
}
|
|
finally
|
|
{
|
|
m_semaphoreSlim.Release();
|
|
}
|
|
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.CurrentSessionCount++;
|
|
ServerInternal.ServerDiagnostics.CumulatedSessionCount++;
|
|
}
|
|
|
|
m_logger.LogInformation("Server - SESSION CREATED. SessionId={SessionId}", sessionId);
|
|
|
|
// report audit for successful create session
|
|
ServerInternal.ReportAuditCreateSessionEvent(
|
|
context.AuditEntryId!,
|
|
session,
|
|
revisedSessionTimeout,
|
|
m_logger);
|
|
|
|
ResponseHeader responseHeader = CreateResponse(requestHeader, StatusCodes.Good);
|
|
|
|
if (parameters != null)
|
|
{
|
|
responseHeader.AdditionalHeader = new ExtensionObject(parameters);
|
|
}
|
|
|
|
return new CreateSessionResponse
|
|
{
|
|
ResponseHeader = responseHeader,
|
|
SessionId = sessionId,
|
|
AuthenticationToken = authenticationToken,
|
|
RevisedSessionTimeout = revisedSessionTimeout,
|
|
ServerNonce = serverNonce,
|
|
ServerCertificate = serverCertificate,
|
|
ServerEndpoints = serverEndpoints,
|
|
ServerSignature = serverSignature!,
|
|
MaxRequestMessageSize = maxRequestMessageSize
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
m_logger.LogError("Server - SESSION CREATE failed. {ErrorMessage}", e.Message);
|
|
|
|
// report the failed AuditCreateSessionEvent
|
|
ServerInternal.ReportAuditCreateSessionEvent(
|
|
context.AuditEntryId!,
|
|
session!,
|
|
revisedSessionTimeout,
|
|
m_logger,
|
|
e);
|
|
|
|
if (session != null)
|
|
{
|
|
await ServerInternal.SessionManager.CloseSessionAsync(session.Id, requestLifetime.CancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedSessionCount++;
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedSessionCount++;
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException((DiagnosticsMasks)requestHeader.ReturnDiagnostics, [], e)!;
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the server signature returned during CreateSession.
|
|
/// </summary>
|
|
/// <param name="context">The request context.</param>
|
|
/// <param name="instanceCertificate">The instance certificate used to sign data.</param>
|
|
/// <param name="parsedClientCertificate">The client certificate supplied in the request.</param>
|
|
/// <param name="clientNonce">The client nonce supplied in the request.</param>
|
|
/// <param name="serverNonce">The server nonce generated for the response.</param>
|
|
/// <returns>The server signature or <c>null</c> when signing is not required.</returns>
|
|
protected virtual SignatureData? CreateSessionServerSignature(
|
|
OperationContext context,
|
|
Certificate instanceCertificate,
|
|
Certificate? parsedClientCertificate,
|
|
ByteString clientNonce,
|
|
ByteString serverNonce)
|
|
{
|
|
return SessionSecurityPolicyHelper.CreateServerSignature(
|
|
context,
|
|
instanceCertificate,
|
|
parsedClientCertificate,
|
|
clientNonce,
|
|
serverNonce);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process additional header values during session creation for ephemeral-key user token policies.
|
|
/// </summary>
|
|
/// <param name="session">The session.</param>
|
|
/// <param name="additionalHeader">The additional request header.</param>
|
|
/// <returns>An AdditionalParametersType object containing the processed parameters.</returns>
|
|
protected virtual AdditionalParametersType? CreateSessionProcessAdditionalParameters(
|
|
ISession session,
|
|
ExtensionObject additionalHeader)
|
|
{
|
|
if (additionalHeader.TryGetValue(out AdditionalParametersType? parameters))
|
|
{
|
|
parameters = CreateSessionProcessAdditionalParameters(session, parameters);
|
|
}
|
|
return parameters;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process additional parameters during session creation and set the session's user token security policy.
|
|
/// </summary>
|
|
/// <param name="session">The session</param>
|
|
/// <param name="parameters">The additional parameters for the session</param>
|
|
/// <returns>An AdditionalParametersType object containing the processed parameters</returns>
|
|
protected virtual AdditionalParametersType CreateSessionProcessAdditionalParameters(
|
|
ISession session,
|
|
AdditionalParametersType parameters)
|
|
{
|
|
return SessionSecurityPolicyHelper.ProcessCreateSessionAdditionalParameters(
|
|
session,
|
|
parameters,
|
|
m_logger)!;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process additional header values during session activation for ephemeral-key user token policies.
|
|
/// </summary>
|
|
/// <param name="session">The session.</param>
|
|
/// <param name="additionalHeader">The additional request header.</param>
|
|
/// <returns>An AdditionalParametersType object containing the processed parameters.</returns>
|
|
protected virtual AdditionalParametersType? ActivateSessionProcessAdditionalParameters(
|
|
ISession session,
|
|
ExtensionObject additionalHeader)
|
|
{
|
|
if (additionalHeader.TryGetValue(out AdditionalParametersType? parameters))
|
|
{
|
|
parameters = ActivateSessionProcessAdditionalParameters(session, parameters);
|
|
}
|
|
return parameters;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process additional parameters during session activation for ephemeral-key user token policies.
|
|
/// </summary>
|
|
/// <param name="session">The session</param>
|
|
/// <param name="parameters">The additional parameters for the session</param>
|
|
/// <returns>An AdditionalParametersType object containing the processed parameters</returns>
|
|
protected virtual AdditionalParametersType ActivateSessionProcessAdditionalParameters(
|
|
ISession session,
|
|
AdditionalParametersType parameters)
|
|
{
|
|
return SessionSecurityPolicyHelper.ProcessActivateSessionAdditionalParameters(
|
|
session,
|
|
parameters);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<ActivateSessionResponse> ActivateSessionAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
SignatureData? clientSignature,
|
|
ArrayOf<SignedSoftwareCertificate> clientSoftwareCertificates,
|
|
ArrayOf<string> localeIds,
|
|
ExtensionObject userIdentityToken,
|
|
SignatureData? userTokenSignature,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
ByteString serverNonce;
|
|
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.ActivateSession,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
// activate the session.
|
|
(bool identityChanged, serverNonce, ServiceResult activationStatus) = await ServerInternal.SessionManager.ActivateSessionAsync(
|
|
context,
|
|
requestHeader.AuthenticationToken,
|
|
clientSignature,
|
|
userIdentityToken,
|
|
userTokenSignature,
|
|
localeIds,
|
|
requestLifetime.CancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
if (identityChanged)
|
|
{
|
|
ISession? activatedSession = ServerInternal.SessionManager
|
|
.GetSession(requestHeader.AuthenticationToken);
|
|
|
|
if (activatedSession != null)
|
|
{
|
|
await ServerInternal.NodeManager.SessionActivatedAsync(
|
|
context,
|
|
activatedSession.Id,
|
|
requestLifetime.CancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
ISession? session = ServerInternal.SessionManager
|
|
.GetSession(requestHeader.AuthenticationToken);
|
|
|
|
AdditionalParametersType? parameters = ActivateSessionProcessAdditionalParameters(
|
|
session!,
|
|
requestHeader.AdditionalHeader);
|
|
|
|
m_logger.LogInformation("Server - SESSION ACTIVATED.");
|
|
|
|
// report the audit event for session activate
|
|
ServerInternal.ReportAuditActivateSessionEvent(
|
|
m_logger,
|
|
context.AuditEntryId!,
|
|
session!);
|
|
|
|
// Use the activation status from SessionManager so spec-defined
|
|
// success codes (e.g. Good_PasswordChangeRequired per Part 18
|
|
// §5.2.8) reach the client.
|
|
StatusCode resultStatus = ServiceResult.IsGood(activationStatus)
|
|
? activationStatus.StatusCode
|
|
: StatusCodes.Good;
|
|
ResponseHeader responseHeader = CreateResponse(requestHeader, resultStatus);
|
|
|
|
if (parameters != null)
|
|
{
|
|
responseHeader.AdditionalHeader = new ExtensionObject(parameters);
|
|
}
|
|
return new ActivateSessionResponse
|
|
{
|
|
ResponseHeader = responseHeader,
|
|
ServerNonce = serverNonce,
|
|
Results = [],
|
|
DiagnosticInfos = []
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
m_logger.LogInformation("Server - SESSION ACTIVATE failed. {ErrorMessage}", e.Message);
|
|
|
|
// report the audit event for failed session activate
|
|
ISession? session = ServerInternal.SessionManager
|
|
.GetSession(requestHeader.AuthenticationToken);
|
|
ServerInternal.ReportAuditActivateSessionEvent(
|
|
m_logger,
|
|
context.AuditEntryId!,
|
|
session!,
|
|
e);
|
|
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedSessionCount++;
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedSessionCount++;
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(
|
|
(DiagnosticsMasks)requestHeader.ReturnDiagnostics,
|
|
localeIds,
|
|
e)!;
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns whether the error is a security error.
|
|
/// </summary>
|
|
/// <param name="error">The error.</param>
|
|
/// <returns>
|
|
/// <c>true</c> if the error is one of the security errors, otherwise <c>false</c>.
|
|
/// </returns>
|
|
protected bool IsSecurityError(StatusCode error)
|
|
{
|
|
return
|
|
error == StatusCodes.BadUserSignatureInvalid ||
|
|
error == StatusCodes.BadUserAccessDenied ||
|
|
error == StatusCodes.BadSecurityPolicyRejected ||
|
|
error == StatusCodes.BadSecurityModeRejected ||
|
|
error == StatusCodes.BadSecurityChecksFailed ||
|
|
error == StatusCodes.BadSecureChannelTokenUnknown ||
|
|
error == StatusCodes.BadSecureChannelIdInvalid ||
|
|
error == StatusCodes.BadNoValidCertificates ||
|
|
error == StatusCodes.BadIdentityTokenInvalid ||
|
|
error == StatusCodes.BadIdentityTokenRejected ||
|
|
error == StatusCodes.BadIdentityChangeNotSupported ||
|
|
error == StatusCodes.BadCertificateUseNotAllowed ||
|
|
error == StatusCodes.BadCertificateUriInvalid ||
|
|
error == StatusCodes.BadCertificateUntrusted ||
|
|
error == StatusCodes.BadCertificateTimeInvalid ||
|
|
error == StatusCodes.BadCertificateRevoked ||
|
|
error == StatusCodes.BadCertificateRevocationUnknown ||
|
|
error == StatusCodes.BadCertificateIssuerUseNotAllowed ||
|
|
error == StatusCodes.BadCertificateIssuerTimeInvalid ||
|
|
error == StatusCodes.BadCertificateIssuerRevoked ||
|
|
error == StatusCodes.BadCertificateIssuerRevocationUnknown ||
|
|
error == StatusCodes.BadCertificateInvalid ||
|
|
error == StatusCodes.BadCertificateHostNameInvalid ||
|
|
error == StatusCodes.BadCertificatePolicyCheckFailed ||
|
|
error == StatusCodes.BadApplicationSignatureInvalid;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the response header.
|
|
/// </summary>
|
|
/// <param name="requestHeader">The object that contains description for the RequestHeader DataType.</param>
|
|
/// <param name="exception">The exception used to create DiagnosticInfo assigned to the ServiceDiagnostics.</param>
|
|
/// <returns>Returns a description for the ResponseHeader DataType. </returns>
|
|
protected ResponseHeader CreateResponse(
|
|
RequestHeader? requestHeader,
|
|
ServiceResultException exception)
|
|
{
|
|
var responseHeader = new ResponseHeader
|
|
{
|
|
ServiceResult = exception.StatusCode,
|
|
|
|
Timestamp = TimeProvider.GetUtcNow().UtcDateTime,
|
|
RequestHandle = requestHeader!.RequestHandle
|
|
};
|
|
|
|
var stringTable = new StringTable();
|
|
responseHeader.ServiceDiagnostics = new DiagnosticInfo(
|
|
exception,
|
|
(DiagnosticsMasks)requestHeader.ReturnDiagnostics,
|
|
true,
|
|
stringTable,
|
|
m_logger);
|
|
responseHeader.StringTable = stringTable.ToArray();
|
|
|
|
return responseHeader;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<CloseSessionResponse> CloseSessionAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
bool deleteSubscriptions,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.CloseSession,
|
|
requestLifetime).ConfigureAwait(false);
|
|
try
|
|
{
|
|
ISession? session = ServerInternal.SessionManager
|
|
.GetSession(requestHeader.AuthenticationToken);
|
|
|
|
await ServerInternal.CloseSessionAsync(context, context.Session.Id, deleteSubscriptions, requestLifetime.CancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
// report the audit event for close session
|
|
ServerInternal.ReportAuditCloseSessionEvent(
|
|
context.AuditEntryId!,
|
|
session!,
|
|
m_logger,
|
|
"Session/CloseSession");
|
|
|
|
return new CloseSessionResponse
|
|
{
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable)
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<CancelResponse> CancelAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
uint requestHandle,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.Cancel,
|
|
requestLifetime).ConfigureAwait(false);
|
|
CancelResponse response;
|
|
try
|
|
{
|
|
ServerInternal.RequestManager.CancelRequests(requestHandle, out uint cancelCount);
|
|
|
|
response = new CancelResponse
|
|
{
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable),
|
|
CancelCount = cancelCount
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
return response;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<BrowseResponse> BrowseAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
ViewDescription? view,
|
|
uint requestedMaxReferencesPerNode,
|
|
ArrayOf<BrowseDescription> nodesToBrowse,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.Browse,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
ValidateOperationLimits(nodesToBrowse, OperationLimits.MaxNodesPerBrowse);
|
|
|
|
(ArrayOf<BrowseResult> results, ArrayOf<DiagnosticInfo> diagnosticInfos) =
|
|
await ServerInternal.NodeManager.BrowseAsync(
|
|
context,
|
|
view!,
|
|
requestedMaxReferencesPerNode,
|
|
nodesToBrowse,
|
|
requestLifetime.CancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
return new BrowseResponse
|
|
{
|
|
Results = results,
|
|
DiagnosticInfos = diagnosticInfos,
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable)
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<BrowseNextResponse> BrowseNextAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
bool releaseContinuationPoints,
|
|
ArrayOf<ByteString> continuationPoints,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.BrowseNext,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
ValidateOperationLimits(continuationPoints, OperationLimits.MaxNodesPerBrowse);
|
|
|
|
(ArrayOf<BrowseResult> results, ArrayOf<DiagnosticInfo> diagnosticInfos) =
|
|
await ServerInternal.NodeManager.BrowseNextAsync(
|
|
context,
|
|
releaseContinuationPoints,
|
|
continuationPoints,
|
|
requestLifetime.CancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
return new BrowseNextResponse
|
|
{
|
|
Results = results,
|
|
DiagnosticInfos = diagnosticInfos,
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable)
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<AddNodesResponse> AddNodesAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
ArrayOf<AddNodesItem> nodesToAdd,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.AddNodes,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
ValidateOperationLimits(nodesToAdd, OperationLimits.MaxNodesPerNodeManagement);
|
|
|
|
(ArrayOf<AddNodesResult> results, ArrayOf<DiagnosticInfo> diagnosticInfos) =
|
|
await ServerInternal.NodeManager.AddNodesAsync(
|
|
context,
|
|
nodesToAdd,
|
|
requestLifetime.CancellationToken).ConfigureAwait(false);
|
|
|
|
StatusCode auditStatus = AggregateStatusCode(results);
|
|
ServerInternal.ReportAuditAddNodesEvent(
|
|
new ServerSystemContext(ServerInternal, context),
|
|
nodesToAdd,
|
|
"NodeManagement/AddNodes",
|
|
auditStatus,
|
|
m_logger);
|
|
|
|
return new AddNodesResponse
|
|
{
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable),
|
|
Results = results,
|
|
DiagnosticInfos = diagnosticInfos
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
ServerInternal.ReportAuditAddNodesEvent(
|
|
new ServerSystemContext(ServerInternal, context),
|
|
nodesToAdd,
|
|
"NodeManagement/AddNodes",
|
|
e.StatusCode,
|
|
m_logger);
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<DeleteNodesResponse> DeleteNodesAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
ArrayOf<DeleteNodesItem> nodesToDelete,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.DeleteNodes,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
ValidateOperationLimits(nodesToDelete, OperationLimits.MaxNodesPerNodeManagement);
|
|
|
|
(ArrayOf<StatusCode> results, ArrayOf<DiagnosticInfo> diagnosticInfos) =
|
|
await ServerInternal.NodeManager.DeleteNodesAsync(
|
|
context,
|
|
nodesToDelete,
|
|
requestLifetime.CancellationToken).ConfigureAwait(false);
|
|
|
|
StatusCode auditStatus = AggregateStatusCode(results);
|
|
ServerInternal.ReportAuditDeleteNodesEvent(
|
|
new ServerSystemContext(ServerInternal, context),
|
|
nodesToDelete,
|
|
"NodeManagement/DeleteNodes",
|
|
auditStatus,
|
|
m_logger);
|
|
|
|
return new DeleteNodesResponse
|
|
{
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable),
|
|
Results = results,
|
|
DiagnosticInfos = diagnosticInfos
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
ServerInternal.ReportAuditDeleteNodesEvent(
|
|
new ServerSystemContext(ServerInternal, context),
|
|
nodesToDelete,
|
|
"NodeManagement/DeleteNodes",
|
|
e.StatusCode,
|
|
m_logger);
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<AddReferencesResponse> AddReferencesAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
ArrayOf<AddReferencesItem> referencesToAdd,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.AddReferences,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
ValidateOperationLimits(referencesToAdd, OperationLimits.MaxNodesPerNodeManagement);
|
|
|
|
(ArrayOf<StatusCode> results, ArrayOf<DiagnosticInfo> diagnosticInfos) =
|
|
await ServerInternal.NodeManager.AddReferencesAsync(
|
|
context,
|
|
referencesToAdd,
|
|
requestLifetime.CancellationToken).ConfigureAwait(false);
|
|
|
|
StatusCode auditStatus = AggregateStatusCode(results);
|
|
ServerInternal.ReportAuditAddReferencesEvent(
|
|
new ServerSystemContext(ServerInternal, context),
|
|
referencesToAdd,
|
|
"NodeManagement/AddReferences",
|
|
auditStatus,
|
|
m_logger);
|
|
|
|
return new AddReferencesResponse
|
|
{
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable),
|
|
Results = results,
|
|
DiagnosticInfos = diagnosticInfos
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
ServerInternal.ReportAuditAddReferencesEvent(
|
|
new ServerSystemContext(ServerInternal, context),
|
|
referencesToAdd,
|
|
"NodeManagement/AddReferences",
|
|
e.StatusCode,
|
|
m_logger);
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<DeleteReferencesResponse> DeleteReferencesAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
ArrayOf<DeleteReferencesItem> referencesToDelete,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.DeleteReferences,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
ValidateOperationLimits(referencesToDelete, OperationLimits.MaxNodesPerNodeManagement);
|
|
|
|
(ArrayOf<StatusCode> results, ArrayOf<DiagnosticInfo> diagnosticInfos) =
|
|
await ServerInternal.NodeManager.DeleteReferencesAsync(
|
|
context,
|
|
referencesToDelete,
|
|
requestLifetime.CancellationToken).ConfigureAwait(false);
|
|
|
|
StatusCode auditStatus = AggregateStatusCode(results);
|
|
ServerInternal.ReportAuditDeleteReferencesEvent(
|
|
new ServerSystemContext(ServerInternal, context),
|
|
referencesToDelete,
|
|
"NodeManagement/DeleteReferences",
|
|
auditStatus,
|
|
m_logger);
|
|
|
|
return new DeleteReferencesResponse
|
|
{
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable),
|
|
Results = results,
|
|
DiagnosticInfos = diagnosticInfos
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
ServerInternal.ReportAuditDeleteReferencesEvent(
|
|
new ServerSystemContext(ServerInternal, context),
|
|
referencesToDelete,
|
|
"NodeManagement/DeleteReferences",
|
|
e.StatusCode,
|
|
m_logger);
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Aggregates per-item status codes into a single audit status:
|
|
/// <see cref="StatusCodes.Good"/> when every item succeeded,
|
|
/// the first bad status otherwise.
|
|
/// </summary>
|
|
private static StatusCode AggregateStatusCode(ArrayOf<StatusCode> results)
|
|
{
|
|
for (int ii = 0; ii < results.Count; ii++)
|
|
{
|
|
StatusCode code = results[ii];
|
|
if (StatusCode.IsBad(code))
|
|
{
|
|
return code;
|
|
}
|
|
}
|
|
return StatusCodes.Good;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Aggregates AddNodes per-item statuses into a single audit status.
|
|
/// </summary>
|
|
private static StatusCode AggregateStatusCode(ArrayOf<AddNodesResult> results)
|
|
{
|
|
for (int ii = 0; ii < results.Count; ii++)
|
|
{
|
|
AddNodesResult result = results[ii];
|
|
if (result == null)
|
|
{
|
|
continue;
|
|
}
|
|
if (StatusCode.IsBad(result.StatusCode))
|
|
{
|
|
return result.StatusCode;
|
|
}
|
|
}
|
|
return StatusCodes.Good;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<RegisterNodesResponse> RegisterNodesAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
ArrayOf<NodeId> nodesToRegister,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.RegisterNodes,
|
|
requestLifetime).ConfigureAwait(false);
|
|
RegisterNodesResponse response;
|
|
try
|
|
{
|
|
ValidateOperationLimits(nodesToRegister, OperationLimits.MaxNodesPerRegisterNodes);
|
|
|
|
ServerInternal.NodeManager
|
|
.RegisterNodes(context, nodesToRegister, out ArrayOf<NodeId> registeredNodeIds);
|
|
|
|
response = new RegisterNodesResponse
|
|
{
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable),
|
|
RegisteredNodeIds = registeredNodeIds
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
return response;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<UnregisterNodesResponse> UnregisterNodesAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
ArrayOf<NodeId> nodesToUnregister,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.UnregisterNodes,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
UnregisterNodesResponse response;
|
|
try
|
|
{
|
|
ValidateOperationLimits(
|
|
nodesToUnregister,
|
|
OperationLimits.MaxNodesPerRegisterNodes);
|
|
|
|
ServerInternal.NodeManager.UnregisterNodes(context, nodesToUnregister);
|
|
|
|
response = new UnregisterNodesResponse
|
|
{
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable)
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
return response;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<TranslateBrowsePathsToNodeIdsResponse> TranslateBrowsePathsToNodeIdsAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
ArrayOf<BrowsePath> browsePaths,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.TranslateBrowsePathsToNodeIds,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
ValidateOperationLimits(
|
|
browsePaths,
|
|
OperationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds);
|
|
|
|
foreach (BrowsePath bp in browsePaths)
|
|
{
|
|
ValidateOperationLimits(
|
|
bp.RelativePath.Elements.Count,
|
|
OperationLimits.MaxNodesPerTranslateBrowsePathsToNodeIds);
|
|
}
|
|
|
|
(ArrayOf<BrowsePathResult> results, ArrayOf<DiagnosticInfo> diagnosticInfos) =
|
|
await ServerInternal.NodeManager.TranslateBrowsePathsToNodeIdsAsync(
|
|
context,
|
|
browsePaths,
|
|
requestLifetime.CancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
return new TranslateBrowsePathsToNodeIdsResponse
|
|
{
|
|
Results = results,
|
|
DiagnosticInfos = diagnosticInfos,
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable)
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<ReadResponse> ReadAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
double maxAge,
|
|
TimestampsToReturn timestampsToReturn,
|
|
ArrayOf<ReadValueId> nodesToRead,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.Read,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
ValidateOperationLimits(nodesToRead, OperationLimits.MaxNodesPerRead);
|
|
|
|
(ArrayOf<DataValue> results, ArrayOf<DiagnosticInfo> diagnosticInfos) =
|
|
await ServerInternal.NodeManager.ReadAsync(
|
|
context,
|
|
maxAge,
|
|
timestampsToReturn,
|
|
nodesToRead,
|
|
requestLifetime.CancellationToken).ConfigureAwait(false);
|
|
|
|
return new ReadResponse
|
|
{
|
|
Results = results,
|
|
DiagnosticInfos = diagnosticInfos,
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable)
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
ServerInternal.ReportAuditEvent(context, "Read", e, m_logger);
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<HistoryReadResponse> HistoryReadAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
ExtensionObject historyReadDetails,
|
|
TimestampsToReturn timestampsToReturn,
|
|
bool releaseContinuationPoints,
|
|
ArrayOf<HistoryReadValueId> nodesToRead,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.HistoryRead,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
if (historyReadDetails.TryGetValue(out ReadEventDetails? _))
|
|
{
|
|
ValidateOperationLimits(
|
|
nodesToRead,
|
|
OperationLimits.MaxNodesPerHistoryReadEvents);
|
|
}
|
|
else
|
|
{
|
|
ValidateOperationLimits(
|
|
nodesToRead,
|
|
OperationLimits.MaxNodesPerHistoryReadData);
|
|
}
|
|
|
|
(ArrayOf<HistoryReadResult> results, ArrayOf<DiagnosticInfo> diagnosticInfos) =
|
|
await ServerInternal.NodeManager.HistoryReadAsync(
|
|
context,
|
|
historyReadDetails,
|
|
timestampsToReturn,
|
|
releaseContinuationPoints,
|
|
nodesToRead,
|
|
requestLifetime.CancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
return new HistoryReadResponse
|
|
{
|
|
Results = results,
|
|
DiagnosticInfos = diagnosticInfos,
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable)
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
ServerInternal.ReportAuditEvent(context, "HistoryRead", e, m_logger);
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<WriteResponse> WriteAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
ArrayOf<WriteValue> nodesToWrite,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.Write,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
ValidateOperationLimits(nodesToWrite, OperationLimits.MaxNodesPerWrite);
|
|
|
|
(ArrayOf<StatusCode> results, ArrayOf<DiagnosticInfo> diagnosticInfos) =
|
|
await ServerInternal.NodeManager
|
|
.WriteAsync(context, nodesToWrite, requestLifetime.CancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
return new WriteResponse
|
|
{
|
|
Results = results,
|
|
DiagnosticInfos = diagnosticInfos,
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable)
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<HistoryUpdateResponse> HistoryUpdateAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
ArrayOf<ExtensionObject> historyUpdateDetails,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.HistoryUpdate,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
// check only for BadNothingToDo here
|
|
// MaxNodesPerHistoryUpdateEvents & MaxNodesPerHistoryUpdateData
|
|
// must be checked in NodeManager (TODO)
|
|
ValidateOperationLimits(historyUpdateDetails);
|
|
|
|
(ArrayOf<HistoryUpdateResult> results, ArrayOf<DiagnosticInfo> diagnosticInfos) =
|
|
await ServerInternal.NodeManager.HistoryUpdateAsync(
|
|
context,
|
|
historyUpdateDetails,
|
|
requestLifetime.CancellationToken).ConfigureAwait(false);
|
|
|
|
return new HistoryUpdateResponse
|
|
{
|
|
Results = results,
|
|
DiagnosticInfos = diagnosticInfos,
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable)
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<CreateSubscriptionResponse> CreateSubscriptionAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
double requestedPublishingInterval,
|
|
uint requestedLifetimeCount,
|
|
uint requestedMaxKeepAliveCount,
|
|
uint maxNotificationsPerPublish,
|
|
bool publishingEnabled,
|
|
byte priority,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.CreateSubscription,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
CreateSubscriptionResponse response = await ServerInternal.SubscriptionManager.CreateSubscriptionAsync(
|
|
context,
|
|
requestedPublishingInterval,
|
|
requestedLifetimeCount,
|
|
requestedMaxKeepAliveCount,
|
|
maxNotificationsPerPublish,
|
|
publishingEnabled,
|
|
priority,
|
|
requestLifetime.CancellationToken).ConfigureAwait(false);
|
|
|
|
response.ResponseHeader = CreateResponse(requestHeader, context.StringTable);
|
|
|
|
return response;
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<TransferSubscriptionsResponse> TransferSubscriptionsAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
ArrayOf<uint> subscriptionIds,
|
|
bool sendInitialValues,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.TransferSubscriptions,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
ValidateOperationLimits(subscriptionIds);
|
|
|
|
TransferSubscriptionsResponse response = await ServerInternal.SubscriptionManager.TransferSubscriptionsAsync(
|
|
context,
|
|
subscriptionIds,
|
|
sendInitialValues,
|
|
requestLifetime.CancellationToken).ConfigureAwait(false);
|
|
|
|
response.ResponseHeader = CreateResponse(requestHeader, context.StringTable);
|
|
|
|
return response;
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<DeleteSubscriptionsResponse> DeleteSubscriptionsAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
ArrayOf<uint> subscriptionIds,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.DeleteSubscriptions,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
ValidateOperationLimits(subscriptionIds);
|
|
|
|
DeleteSubscriptionsResponse response = await ServerInternal.SubscriptionManager.DeleteSubscriptionsAsync(
|
|
context,
|
|
subscriptionIds,
|
|
requestLifetime.CancellationToken).ConfigureAwait(false);
|
|
|
|
response.ResponseHeader = CreateResponse(requestHeader, context.StringTable);
|
|
|
|
return response;
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<PublishResponse> PublishAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
ArrayOf<SubscriptionAcknowledgement> subscriptionAcknowledgements,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.Publish,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
/*
|
|
// check if there is an odd delay.
|
|
if (DateTime.UtcNow > requestHeader.Timestamp.AddMilliseconds(100))
|
|
{
|
|
m_logger.LogTrace(m_eventId,
|
|
"WARNING. Unexpected delay receiving Publish request. Time={0:hh:mm:ss.fff}, ReceiveTime={1:hh:mm:ss.fff}",
|
|
DateTime.UtcNow,
|
|
requestHeader.Timestamp);
|
|
}
|
|
*/
|
|
|
|
m_logger.LogTrace(
|
|
"PUBLISH #{RequestHandle} RECEIVED. TIME={Timestamp:hh:mm:ss.fff}",
|
|
requestHeader.RequestHandle,
|
|
requestHeader.Timestamp);
|
|
|
|
PublishResponse response = await ServerInternal.SubscriptionManager.PublishAsync(
|
|
context,
|
|
subscriptionAcknowledgements,
|
|
requestLifetime.CancellationToken).ConfigureAwait(false);
|
|
|
|
response.ResponseHeader = CreateResponse(requestHeader, context.StringTable);
|
|
|
|
/*
|
|
if (response.NotificationMessage != null)
|
|
{
|
|
m_logger.LogTrace(m_eventId,
|
|
"PublishResponse: SubId={0} SeqNo={1}, PublishTime={2:mm:ss.fff}, Time={3:mm:ss.fff}",
|
|
subscriptionId,
|
|
notificationMessage.SequenceNumber,
|
|
notificationMessage.PublishTime,
|
|
DateTime.UtcNow);
|
|
}
|
|
*/
|
|
|
|
return response;
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<RepublishResponse> RepublishAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
uint subscriptionId,
|
|
uint retransmitSequenceNumber,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.Republish,
|
|
requestLifetime).ConfigureAwait(false);
|
|
RepublishResponse response;
|
|
try
|
|
{
|
|
NotificationMessage notificationMessage = ServerInternal.SubscriptionManager.Republish(
|
|
context,
|
|
subscriptionId,
|
|
retransmitSequenceNumber);
|
|
|
|
response = new RepublishResponse
|
|
{
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable),
|
|
NotificationMessage = notificationMessage
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
return response;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<ModifySubscriptionResponse> ModifySubscriptionAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
uint subscriptionId,
|
|
double requestedPublishingInterval,
|
|
uint requestedLifetimeCount,
|
|
uint requestedMaxKeepAliveCount,
|
|
uint maxNotificationsPerPublish,
|
|
byte priority,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.ModifySubscription,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
ModifySubscriptionResponse response;
|
|
try
|
|
{
|
|
ServerInternal.SubscriptionManager.ModifySubscription(
|
|
context,
|
|
subscriptionId,
|
|
requestedPublishingInterval,
|
|
requestedLifetimeCount,
|
|
requestedMaxKeepAliveCount,
|
|
maxNotificationsPerPublish,
|
|
priority,
|
|
out double revisedPublishingInterval,
|
|
out uint revisedLifetimeCount,
|
|
out uint revisedMaxKeepAliveCount);
|
|
|
|
response = new ModifySubscriptionResponse
|
|
{
|
|
RevisedPublishingInterval = revisedPublishingInterval,
|
|
RevisedLifetimeCount = revisedLifetimeCount,
|
|
RevisedMaxKeepAliveCount = revisedMaxKeepAliveCount,
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable)
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
return response;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<SetPublishingModeResponse> SetPublishingModeAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
bool publishingEnabled,
|
|
ArrayOf<uint> subscriptionIds,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.SetPublishingMode,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
SetPublishingModeResponse response;
|
|
try
|
|
{
|
|
ValidateOperationLimits(subscriptionIds);
|
|
|
|
ServerInternal.SubscriptionManager.SetPublishingMode(
|
|
context,
|
|
publishingEnabled,
|
|
subscriptionIds,
|
|
out ArrayOf<StatusCode> results,
|
|
out ArrayOf<DiagnosticInfo> diagnosticInfos);
|
|
|
|
response = new SetPublishingModeResponse
|
|
{
|
|
Results = results,
|
|
DiagnosticInfos = diagnosticInfos,
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable)
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
return response;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<SetTriggeringResponse> SetTriggeringAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
uint subscriptionId,
|
|
uint triggeringItemId,
|
|
ArrayOf<uint> linksToAdd,
|
|
ArrayOf<uint> linksToRemove,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.SetTriggering,
|
|
requestLifetime).ConfigureAwait(false);
|
|
SetTriggeringResponse response;
|
|
try
|
|
{
|
|
if (linksToAdd.Count == 0 && linksToRemove.Count == 0)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadNothingToDo);
|
|
}
|
|
|
|
int monitoredItemsCount = 0;
|
|
monitoredItemsCount += linksToAdd.Count;
|
|
monitoredItemsCount += linksToRemove.Count;
|
|
ValidateOperationLimits(
|
|
monitoredItemsCount,
|
|
OperationLimits.MaxMonitoredItemsPerCall);
|
|
|
|
ServerInternal.SubscriptionManager.SetTriggering(
|
|
context,
|
|
subscriptionId,
|
|
triggeringItemId,
|
|
linksToAdd,
|
|
linksToRemove,
|
|
out ArrayOf<StatusCode> addResults,
|
|
out ArrayOf<DiagnosticInfo> addDiagnosticInfos,
|
|
out ArrayOf<StatusCode> removeResults,
|
|
out ArrayOf<DiagnosticInfo> removeDiagnosticInfos);
|
|
|
|
response = new SetTriggeringResponse
|
|
{
|
|
AddResults = addResults,
|
|
AddDiagnosticInfos = addDiagnosticInfos,
|
|
RemoveResults = removeResults,
|
|
RemoveDiagnosticInfos = removeDiagnosticInfos,
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable)
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
return response;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<CreateMonitoredItemsResponse> CreateMonitoredItemsAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
uint subscriptionId,
|
|
TimestampsToReturn timestampsToReturn,
|
|
ArrayOf<MonitoredItemCreateRequest> itemsToCreate,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.CreateMonitoredItems,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
ValidateOperationLimits(itemsToCreate, OperationLimits.MaxMonitoredItemsPerCall);
|
|
|
|
CreateMonitoredItemsResponse result = await ServerInternal.SubscriptionManager.CreateMonitoredItemsAsync(
|
|
context,
|
|
subscriptionId,
|
|
timestampsToReturn,
|
|
itemsToCreate,
|
|
requestLifetime.CancellationToken).ConfigureAwait(false);
|
|
|
|
result.ResponseHeader = CreateResponse(requestHeader, context.StringTable);
|
|
|
|
return result;
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<ModifyMonitoredItemsResponse> ModifyMonitoredItemsAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
uint subscriptionId,
|
|
TimestampsToReturn timestampsToReturn,
|
|
ArrayOf<MonitoredItemModifyRequest> itemsToModify,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.ModifyMonitoredItems,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
ValidateOperationLimits(itemsToModify, OperationLimits.MaxMonitoredItemsPerCall);
|
|
|
|
ModifyMonitoredItemsResponse response = await ServerInternal.SubscriptionManager.ModifyMonitoredItemsAsync(
|
|
context,
|
|
subscriptionId,
|
|
timestampsToReturn,
|
|
itemsToModify,
|
|
requestLifetime.CancellationToken).ConfigureAwait(false);
|
|
|
|
response.ResponseHeader = CreateResponse(requestHeader, context.StringTable);
|
|
|
|
return response;
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<DeleteMonitoredItemsResponse> DeleteMonitoredItemsAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
uint subscriptionId,
|
|
ArrayOf<uint> monitoredItemIds,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.DeleteMonitoredItems,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
ValidateOperationLimits(monitoredItemIds, OperationLimits.MaxMonitoredItemsPerCall);
|
|
|
|
DeleteMonitoredItemsResponse response = await ServerInternal.SubscriptionManager.DeleteMonitoredItemsAsync(
|
|
context,
|
|
subscriptionId,
|
|
monitoredItemIds,
|
|
requestLifetime.CancellationToken).ConfigureAwait(false);
|
|
|
|
response.ResponseHeader = CreateResponse(requestHeader, context.StringTable);
|
|
|
|
return response;
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<SetMonitoringModeResponse> SetMonitoringModeAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
uint subscriptionId,
|
|
MonitoringMode monitoringMode,
|
|
ArrayOf<uint> monitoredItemIds,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.SetMonitoringMode,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
ValidateOperationLimits(monitoredItemIds, OperationLimits.MaxMonitoredItemsPerCall);
|
|
|
|
(ArrayOf<StatusCode> results, ArrayOf<DiagnosticInfo> diagnosticInfos) =
|
|
await ServerInternal.SubscriptionManager.SetMonitoringModeAsync(
|
|
context,
|
|
subscriptionId,
|
|
monitoringMode,
|
|
monitoredItemIds,
|
|
requestLifetime.CancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
return new SetMonitoringModeResponse
|
|
{
|
|
Results = results,
|
|
DiagnosticInfos = diagnosticInfos,
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable)
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public override async ValueTask<CallResponse> CallAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
RequestHeader? requestHeader,
|
|
ArrayOf<CallMethodRequest> methodsToCall,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
OperationContext context = await ValidateRequestAsync(
|
|
secureChannelContext,
|
|
requestHeader,
|
|
RequestType.Call,
|
|
requestLifetime).ConfigureAwait(false);
|
|
|
|
try
|
|
{
|
|
ValidateOperationLimits(methodsToCall, OperationLimits.MaxNodesPerMethodCall);
|
|
|
|
(ArrayOf<CallMethodResult> results, ArrayOf<DiagnosticInfo> diagnosticInfos) =
|
|
await ServerInternal.NodeManager.CallAsync(context, methodsToCall, requestLifetime.CancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
return new CallResponse
|
|
{
|
|
Results = results,
|
|
DiagnosticInfos = diagnosticInfos,
|
|
ResponseHeader = CreateResponse(requestHeader, context.StringTable)
|
|
};
|
|
}
|
|
catch (ServiceResultException e)
|
|
{
|
|
lock (ServerInternal.DiagnosticsWriteLock)
|
|
{
|
|
ServerInternal.ServerDiagnostics.RejectedRequestsCount++;
|
|
|
|
if (IsSecurityError(e.StatusCode))
|
|
{
|
|
ServerInternal.ServerDiagnostics.SecurityRejectedRequestsCount++;
|
|
}
|
|
}
|
|
|
|
throw TranslateException(context, e);
|
|
}
|
|
finally
|
|
{
|
|
OnRequestComplete(context);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public IServerInternal CurrentInstance
|
|
{
|
|
get
|
|
{
|
|
m_semaphoreSlim.Wait();
|
|
try
|
|
{
|
|
if (m_serverInternal == null)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadServerHalted);
|
|
}
|
|
return m_serverInternal;
|
|
}
|
|
finally
|
|
{
|
|
m_semaphoreSlim.Release();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the current status of the server.
|
|
/// </summary>
|
|
/// <returns>Returns a ServerStatusDataType object</returns>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
[Obsolete(
|
|
"No longer thread safe. To read the value use CurrentState, to write use CurrentInstance.UpdateServerStatus."
|
|
)]
|
|
public ServerStatusDataType GetStatus()
|
|
{
|
|
lock (Lock!)
|
|
{
|
|
if (m_serverInternal == null)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadServerHalted);
|
|
}
|
|
return ServerInternal.Status.Value;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public ServerState CurrentState => ServerInternal.CurrentState;
|
|
|
|
/// <summary>
|
|
/// Registers the server with the discovery server.
|
|
/// </summary>
|
|
/// <returns>Boolean value.</returns>
|
|
[Obsolete("Use RegisterWithDiscoveryServerAsync instead.")]
|
|
public bool RegisterWithDiscoveryServer()
|
|
{
|
|
return RegisterWithDiscoveryServerAsync().AsTask().GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public async ValueTask<bool> RegisterWithDiscoveryServerAsync(CancellationToken ct = default)
|
|
{
|
|
var configuration = new ApplicationConfiguration(Configuration!)
|
|
{
|
|
// share the server's CertificateManager so the registration channel uses
|
|
// the same trust list, rejected store, and cached validation results.
|
|
// The base copy ctor already propagates the legacy CertificateValidator.
|
|
CertificateManager = CertificateManager!
|
|
};
|
|
|
|
// try each endpoint.
|
|
if (m_registrationEndpoints != null)
|
|
{
|
|
foreach (ConfiguredEndpoint endpoint in m_registrationEndpoints.Endpoints)
|
|
{
|
|
RegistrationClient? client = null;
|
|
int i = 0;
|
|
|
|
while (i++ < 2)
|
|
{
|
|
try
|
|
{
|
|
// update from the server.
|
|
bool updateRequired = true;
|
|
|
|
lock (m_registrationLock)
|
|
{
|
|
updateRequired = endpoint.UpdateBeforeConnect;
|
|
}
|
|
|
|
if (updateRequired)
|
|
{
|
|
await endpoint.UpdateFromServerAsync(MessageContext.Telemetry, ct).ConfigureAwait(false);
|
|
}
|
|
|
|
lock (m_registrationLock)
|
|
{
|
|
endpoint.UpdateBeforeConnect = false;
|
|
}
|
|
|
|
var requestHeader = new RequestHeader
|
|
{
|
|
Timestamp = TimeProvider.GetUtcNow().UtcDateTime
|
|
};
|
|
|
|
// create the client.
|
|
Certificate? instanceCertificate =
|
|
CertificateManager!.GetInstanceCertificate(
|
|
endpoint.Description?.SecurityPolicyUri ??
|
|
SecurityPolicies.None)?.Certificate;
|
|
client = await RegistrationClient.CreateAsync(
|
|
configuration,
|
|
endpoint.Description!,
|
|
endpoint.Configuration!,
|
|
instanceCertificate!,
|
|
ct: ct).ConfigureAwait(false);
|
|
|
|
client.OperationTimeout = 10000;
|
|
|
|
// register the server.
|
|
if (m_useRegisterServer2)
|
|
{
|
|
var mdnsDiscoveryConfig = new MdnsDiscoveryConfiguration
|
|
{
|
|
ServerCapabilities = configuration.ServerConfiguration!
|
|
.ServerCapabilities,
|
|
MdnsServerName = Utils.GetHostName()
|
|
};
|
|
var extensionObject = new ExtensionObject(mdnsDiscoveryConfig);
|
|
ArrayOf<ExtensionObject> discoveryConfiguration = [extensionObject];
|
|
await client.RegisterServer2Async(
|
|
requestHeader,
|
|
m_registrationInfo,
|
|
discoveryConfiguration,
|
|
ct).ConfigureAwait(false);
|
|
}
|
|
else
|
|
{
|
|
await client.RegisterServerAsync(
|
|
requestHeader,
|
|
m_registrationInfo,
|
|
ct)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
m_registeredWithDiscoveryServer = m_registrationInfo!.IsOnline;
|
|
return true;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
m_logger.LogWarning(
|
|
"RegisterServer{Api} failed for {EndpointUrl}. Exception={ErrorMessage}",
|
|
m_useRegisterServer2 ? "2" : string.Empty,
|
|
endpoint.EndpointUrl,
|
|
e.Message);
|
|
m_useRegisterServer2 = !m_useRegisterServer2;
|
|
}
|
|
finally
|
|
{
|
|
if (client != null)
|
|
{
|
|
try
|
|
{
|
|
await client.CloseAsync(ct).ConfigureAwait(false);
|
|
client = null;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
m_logger.LogWarning(
|
|
"Could not cleanly close connection with LDS. Exception={ErrorMessage}",
|
|
e.Message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// retry to start with RegisterServer2 if both failed
|
|
m_useRegisterServer2 = true;
|
|
}
|
|
|
|
m_registeredWithDiscoveryServer = false;
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers the server endpoints with the LDS.
|
|
/// </summary>
|
|
/// <param name="state">The state.</param>
|
|
private async void OnRegisterServerAsync(object? state)
|
|
{
|
|
try
|
|
{
|
|
lock (m_registrationLock)
|
|
{
|
|
// halt any outstanding timer.
|
|
m_registrationTimer?.Dispose();
|
|
m_registrationTimer = null;
|
|
}
|
|
|
|
if (await RegisterWithDiscoveryServerAsync().ConfigureAwait(false))
|
|
{
|
|
// schedule next registration.
|
|
lock (m_registrationLock)
|
|
{
|
|
if (m_maxRegistrationInterval > 0)
|
|
{
|
|
m_registrationTimer = TimeProvider.CreateTimer(
|
|
OnRegisterServerAsync,
|
|
this,
|
|
TimeSpan.FromMilliseconds(m_maxRegistrationInterval),
|
|
Timeout.InfiniteTimeSpan);
|
|
|
|
m_lastRegistrationInterval = m_minRegistrationInterval;
|
|
m_logger.LogInformation(
|
|
"Register server succeeded. Registering again in {RegistrationInterval} ms",
|
|
m_maxRegistrationInterval);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
lock (m_registrationLock)
|
|
{
|
|
if (m_registrationTimer == null)
|
|
{
|
|
// calculate next registration attempt.
|
|
m_lastRegistrationInterval *= 2;
|
|
|
|
if (m_lastRegistrationInterval > m_maxRegistrationInterval)
|
|
{
|
|
m_lastRegistrationInterval = m_maxRegistrationInterval;
|
|
}
|
|
|
|
m_logger.LogInformation(
|
|
"Register server failed. Trying again in {RegistrationInterval} ms",
|
|
m_lastRegistrationInterval);
|
|
|
|
// create timer.
|
|
m_registrationTimer = TimeProvider.CreateTimer(
|
|
OnRegisterServerAsync,
|
|
this,
|
|
TimeSpan.FromMilliseconds(m_lastRegistrationInterval),
|
|
Timeout.InfiniteTimeSpan);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
m_logger.LogError(e, "Unexpected exception handling registration timer.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The synchronization object.
|
|
/// </summary>
|
|
[Obsolete("Use your own synchronization mechanism.")]
|
|
protected object? Lock => null;
|
|
|
|
/// <summary>
|
|
/// The state object associated with the server.
|
|
/// </summary>
|
|
/// <value>The server internal data.</value>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
protected IServerInternal ServerInternal =>
|
|
m_serverInternal ?? throw new ServiceResultException(StatusCodes.BadServerHalted);
|
|
|
|
/// <summary>
|
|
/// Verifies that the request header is valid.
|
|
/// </summary>
|
|
/// <param name="requestHeader">The request header.</param>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
protected override void ValidateRequest([NotNull] RequestHeader? requestHeader)
|
|
{
|
|
// check for server error.
|
|
ServiceResult error = ServerError;
|
|
|
|
if (ServiceResult.IsBad(error))
|
|
{
|
|
throw new ServiceResultException(error);
|
|
}
|
|
|
|
// check server state.
|
|
ServerInternalData? serverInternal = m_serverInternal;
|
|
|
|
if (serverInternal == null || !serverInternal.IsRunning)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadServerHalted);
|
|
}
|
|
|
|
base.ValidateRequest(requestHeader);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the server state.
|
|
/// </summary>
|
|
/// <param name="state">The state.</param>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
protected virtual void SetServerState(ServerState state)
|
|
{
|
|
m_semaphoreSlim.Wait();
|
|
try
|
|
{
|
|
if (ServiceResult.IsBad(ServerError))
|
|
{
|
|
throw new ServiceResultException(ServerError);
|
|
}
|
|
|
|
if (m_serverInternal == null)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadServerHalted);
|
|
}
|
|
|
|
m_logger.LogInformation(Utils.TraceMasks.StartStop, "Server - Enter {State} state.", state);
|
|
|
|
ServerInternal.CurrentState = state;
|
|
}
|
|
finally
|
|
{
|
|
m_semaphoreSlim.Release();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reports an error during initialization after the base server object has been started.
|
|
/// </summary>
|
|
/// <param name="error">The error.</param>
|
|
protected virtual void SetServerError(ServiceResult error)
|
|
{
|
|
m_semaphoreSlim.Wait();
|
|
try
|
|
{
|
|
ServerError = error;
|
|
}
|
|
finally
|
|
{
|
|
m_semaphoreSlim.Release();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles an error when validating the application instance certificate provided by a client.
|
|
/// </summary>
|
|
/// <param name="clientCertificate">The client certificate.</param>
|
|
/// <param name="result">The result.</param>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
protected virtual void OnApplicationCertificateError(
|
|
ByteString clientCertificate,
|
|
ServiceResult result)
|
|
{
|
|
// see https://reference.opcfoundation.org/Core/Part4/v105/docs/6.1.3
|
|
StatusCode resultCode = result.StatusCode;
|
|
if (resultCode == StatusCodes.BadCertificateInvalid ||
|
|
resultCode == StatusCodes.BadCertificateRevoked ||
|
|
resultCode == StatusCodes.BadCertificateUntrusted ||
|
|
resultCode == StatusCodes.BadCertificateIssuerRevoked ||
|
|
resultCode == StatusCodes.BadCertificateRevocationUnknown ||
|
|
resultCode == StatusCodes.BadCertificateChainIncomplete ||
|
|
resultCode == StatusCodes.BadCertificateIssuerRevocationUnknown)
|
|
{
|
|
resultCode = StatusCodes.BadSecurityChecksFailed;
|
|
}
|
|
|
|
throw new ServiceResultException(new ServiceResult(resultCode, result));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the request header is valid.
|
|
/// </summary>
|
|
/// <param name="secureChannelContext">The secure channel context.</param>
|
|
/// <param name="requestHeader">The request header.</param>
|
|
/// <param name="requestType">Type of the request.</param>
|
|
/// <param name="requestLifetime">The request lifetime.</param>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
protected virtual async ValueTask<OperationContext> ValidateRequestAsync(
|
|
SecureChannelContext secureChannelContext,
|
|
[NotNull] RequestHeader? requestHeader,
|
|
RequestType requestType,
|
|
RequestLifetime requestLifetime)
|
|
{
|
|
base.ValidateRequest(requestHeader);
|
|
|
|
if (!ServerInternal.IsRunning)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadServerHalted);
|
|
}
|
|
|
|
OperationContext context = await ServerInternal.SessionManager
|
|
.ValidateRequestAsync(requestHeader, secureChannelContext, requestType, requestLifetime).ConfigureAwait(false);
|
|
|
|
if (ServerUtils.EventLog.IsEnabled())
|
|
{
|
|
string? requestTypeString = Enum.GetName(
|
|
#if !NET8_0_OR_GREATER
|
|
typeof(RequestType),
|
|
#endif
|
|
context.RequestType);
|
|
ServerUtils.EventLog.ServerCall(requestTypeString!, context.RequestId);
|
|
}
|
|
m_logger.LogTrace("Server Call={RequestType}, Id={RequestId}", context.RequestType, context.RequestId);
|
|
|
|
// notify the request manager.
|
|
ServerInternal.RequestManager.RequestReceived(context);
|
|
|
|
return context;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validate operation limits.
|
|
/// </summary>
|
|
/// <typeparam name="T"></typeparam>
|
|
/// <param name="operation">A list of operations.</param>
|
|
/// <param name="operationLimit">The operation limit property.</param>
|
|
/// <exception cref="ServiceResultException">BadNothingToDo if list is null or empty.</exception>
|
|
/// <exception cref="ServiceResultException">BadTooManyOperations if list is larger than operation limit property.</exception>
|
|
protected void ValidateOperationLimits<T>(
|
|
ArrayOf<T> operation,
|
|
PropertyState<uint>? operationLimit = null)
|
|
{
|
|
if (operation.IsEmpty)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadNothingToDo);
|
|
}
|
|
ValidateOperationLimits(operation.Count, operationLimit);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validate operation limits.
|
|
/// </summary>
|
|
/// <param name="count">A count of operations.</param>
|
|
/// <param name="operationLimit">The operation limit property.</param>
|
|
/// <exception cref="ServiceResultException">BadTooManyOperations if count is larger than operation limit property.</exception>
|
|
protected void ValidateOperationLimits(int count, PropertyState<uint>? operationLimit)
|
|
{
|
|
uint operationLimitValue = operationLimit != null ? operationLimit.Value : 0;
|
|
if (operationLimitValue > 0 && count > operationLimitValue)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadTooManyOperations);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Translates an exception.
|
|
/// </summary>
|
|
/// <param name="context">The context.</param>
|
|
/// <param name="e">The ServiceResultException e.</param>
|
|
/// <returns>Returns an exception thrown when a UA defined error occurs, the return type is <seealso cref="ServiceResultException"/>.</returns>
|
|
protected virtual ServiceResultException TranslateException(
|
|
OperationContext context,
|
|
ServiceResultException e)
|
|
{
|
|
ArrayOf<string> preferredLocales = default;
|
|
|
|
if (context != null && context.Session != null)
|
|
{
|
|
preferredLocales = context.Session.PreferredLocales;
|
|
}
|
|
|
|
return TranslateException(context!.DiagnosticsMask, preferredLocales, e);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Translates an exception.
|
|
/// </summary>
|
|
/// <param name="diagnosticsMasks">The fields to return.</param>
|
|
/// <param name="preferredLocales">The preferred locales.</param>
|
|
/// <param name="e">The ServiceResultException e.</param>
|
|
/// <returns>Returns an exception thrown when a UA defined error occurs, the return type is <seealso cref="ServiceResultException"/>.</returns>
|
|
/// <exception cref="ArgumentNullException">
|
|
/// <paramref name="e"/> is null.
|
|
/// </exception>
|
|
protected virtual ServiceResultException TranslateException(
|
|
DiagnosticsMasks diagnosticsMasks,
|
|
ArrayOf<string> preferredLocales,
|
|
ServiceResultException e)
|
|
{
|
|
if (e == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(e));
|
|
}
|
|
|
|
// check if inner result required.
|
|
ServiceResult? innerResult = null;
|
|
|
|
if ((
|
|
diagnosticsMasks &
|
|
(DiagnosticsMasks.ServiceInnerDiagnostics |
|
|
DiagnosticsMasks.ServiceInnerStatusCode)
|
|
) != 0)
|
|
{
|
|
innerResult = e.InnerResult;
|
|
}
|
|
|
|
// check if translated text required.
|
|
LocalizedText translatedText = default;
|
|
|
|
if ((diagnosticsMasks & DiagnosticsMasks.ServiceLocalizedText) != 0)
|
|
{
|
|
translatedText = e.LocalizedText;
|
|
}
|
|
|
|
// create new result object.
|
|
var result = new ServiceResult(
|
|
e.NamespaceUri,
|
|
new StatusCode(e.StatusCode.Code, e.SymbolicId),
|
|
translatedText,
|
|
e.AdditionalInfo,
|
|
innerResult);
|
|
|
|
// translate result.
|
|
result = ServerInternal.ResourceManager.Translate(preferredLocales, result)!;
|
|
return new ServiceResultException(result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Translates a service result.
|
|
/// </summary>
|
|
/// <param name="diagnosticsMasks">The fields to return.</param>
|
|
/// <param name="preferredLocales">The preferred locales.</param>
|
|
/// <param name="result">The result.</param>
|
|
/// <returns>Returns a class that combines the status code and diagnostic info structures.</returns>
|
|
protected virtual ServiceResult? TranslateResult(
|
|
DiagnosticsMasks diagnosticsMasks,
|
|
ArrayOf<string> preferredLocales,
|
|
ServiceResult result)
|
|
{
|
|
if (result == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return ServerInternal.ResourceManager.Translate(preferredLocales, result);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies that the request header is valid.
|
|
/// </summary>
|
|
/// <param name="context">The operation context.</param>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
protected virtual void OnRequestComplete(OperationContext context)
|
|
{
|
|
m_semaphoreSlim.Wait();
|
|
try
|
|
{
|
|
if (m_serverInternal == null)
|
|
{
|
|
throw new ServiceResultException(StatusCodes.BadServerHalted);
|
|
}
|
|
|
|
ServerInternal.RequestManager.RequestCompleted(context);
|
|
}
|
|
finally
|
|
{
|
|
m_semaphoreSlim.Release();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raised when the configuration changes.
|
|
/// </summary>
|
|
/// <param name="sender">The sender.</param>
|
|
/// <param name="args">The <see cref="ConfigurationWatcherEventArgs"/> instance containing the event data.</param>
|
|
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2072",
|
|
Justification = "Configuration.GetType() returns a concrete type whose constructor is preserved at the call site.")]
|
|
protected virtual async void OnConfigurationChangedAsync(
|
|
object? sender,
|
|
ConfigurationWatcherEventArgs args)
|
|
{
|
|
try
|
|
{
|
|
ApplicationConfiguration configuration = await ApplicationConfiguration
|
|
.LoadAsync(
|
|
new FileInfo(args.FilePath),
|
|
Configuration!.ApplicationType,
|
|
Configuration!.GetType(),
|
|
MessageContext.Telemetry)
|
|
.ConfigureAwait(false);
|
|
|
|
await OnUpdateConfigurationAsync(configuration)
|
|
.ConfigureAwait(false);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
m_logger.LogError(e, "Could not load updated configuration file from: {FilePath}", args.FilePath);
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override async ValueTask OnUpdateConfigurationAsync(
|
|
ApplicationConfiguration configuration,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await m_semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
// update security configuration.
|
|
configuration.SecurityConfiguration.Validate(MessageContext.Telemetry);
|
|
|
|
ApplicationConfiguration currentConfiguration = Configuration!;
|
|
SecurityConfiguration currentSecurityConfiguration = currentConfiguration
|
|
.SecurityConfiguration;
|
|
|
|
currentSecurityConfiguration.TrustedIssuerCertificates = configuration
|
|
.SecurityConfiguration
|
|
.TrustedIssuerCertificates;
|
|
currentSecurityConfiguration.TrustedPeerCertificates = configuration
|
|
.SecurityConfiguration
|
|
.TrustedPeerCertificates;
|
|
currentSecurityConfiguration.RejectedCertificateStore = configuration
|
|
.SecurityConfiguration
|
|
.RejectedCertificateStore;
|
|
|
|
if (CertificateManager != null)
|
|
{
|
|
await CertificateManager.UpdateAsync(
|
|
Configuration!.SecurityConfiguration,
|
|
Configuration.ApplicationUri,
|
|
cancellationToken)
|
|
.ConfigureAwait(false);
|
|
}
|
|
|
|
// update trace configuration.
|
|
currentConfiguration.TraceConfiguration = configuration.TraceConfiguration ??
|
|
new TraceConfiguration();
|
|
|
|
// Legacy Utils trace pipeline; kept for 1.5.378 -> 2.0 migration.
|
|
#pragma warning disable CS0618 // Type or member is obsolete
|
|
currentConfiguration.TraceConfiguration.ApplySettings();
|
|
#pragma warning restore CS0618 // Type or member is obsolete
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
m_logger.LogError(e, "Failed to update configuration.");
|
|
}
|
|
finally
|
|
{
|
|
m_semaphoreSlim.Release();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called before the server starts.
|
|
/// </summary>
|
|
/// <param name="configuration">The configuration.</param>
|
|
protected override void OnServerStarting(ApplicationConfiguration configuration)
|
|
{
|
|
m_semaphoreSlim.Wait();
|
|
try
|
|
{
|
|
base.OnServerStarting(configuration);
|
|
|
|
// save minimum nonce length.
|
|
m_minNonceLength = configuration.SecurityConfiguration.NonceLength;
|
|
|
|
// try first RegisterServer2
|
|
m_useRegisterServer2 = true;
|
|
}
|
|
finally
|
|
{
|
|
m_semaphoreSlim.Release();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the endpoints and creates the hosts.
|
|
/// </summary>
|
|
/// <param name="configuration">The configuration.</param>
|
|
/// <param name="bindingFactory">The transport listener binding factory.</param>
|
|
/// <param name="serverDescription">The server description.</param>
|
|
/// <param name="endpoints">The endpoints.</param>
|
|
/// <returns>
|
|
/// Returns IList of a host for a UA service.
|
|
/// </returns>
|
|
protected override IList<ServiceHost> InitializeServiceHosts(
|
|
ApplicationConfiguration configuration,
|
|
ITransportListenerBindings bindingFactory,
|
|
out ApplicationDescription serverDescription,
|
|
out ArrayOf<EndpointDescription> endpoints)
|
|
{
|
|
var hosts = new Dictionary<string, ServiceHost>();
|
|
|
|
// ensure at least one security policy exists.
|
|
if (configuration.ServerConfiguration!.SecurityPolicies.IsEmpty)
|
|
{
|
|
configuration.ServerConfiguration.SecurityPolicies =
|
|
configuration.ServerConfiguration.SecurityPolicies.AddItem(new ServerSecurityPolicy());
|
|
}
|
|
|
|
// ensure at least one user token policy exists.
|
|
if (configuration.ServerConfiguration.UserTokenPolicies.IsEmpty)
|
|
{
|
|
var userTokenPolicy = new UserTokenPolicy { TokenType = UserTokenType.Anonymous };
|
|
userTokenPolicy.PolicyId = userTokenPolicy.TokenType.ToString();
|
|
|
|
configuration.ServerConfiguration.UserTokenPolicies += userTokenPolicy;
|
|
}
|
|
|
|
// set server description.
|
|
serverDescription = new ApplicationDescription
|
|
{
|
|
ApplicationUri = configuration.ApplicationUri,
|
|
ApplicationName = new LocalizedText("en-US", configuration.ApplicationName),
|
|
ApplicationType = configuration.ApplicationType,
|
|
ProductUri = configuration.ProductUri,
|
|
DiscoveryUrls = GetDiscoveryUrls()
|
|
};
|
|
|
|
List<EndpointDescription> endpointsList = [];
|
|
ArrayOf<string> baseAddresses = configuration.ServerConfiguration.BaseAddresses;
|
|
foreach (
|
|
string scheme in Utils.DefaultUriSchemes.Where(scheme =>
|
|
baseAddresses.Contains(a => a.StartsWith(scheme, StringComparison.Ordinal))))
|
|
{
|
|
ITransportListenerFactory? binding = bindingFactory.GetBinding(scheme, MessageContext.Telemetry);
|
|
if (binding != null)
|
|
{
|
|
List<EndpointDescription> endpointsForHost = binding.CreateServiceHost(
|
|
this,
|
|
hosts,
|
|
configuration,
|
|
configuration.ServerConfiguration.BaseAddresses,
|
|
serverDescription,
|
|
configuration.ServerConfiguration.SecurityPolicies,
|
|
CertificateManager!,
|
|
configuration.CertificateManager!);
|
|
endpointsList.AddRange(endpointsForHost);
|
|
}
|
|
}
|
|
endpoints = endpointsList;
|
|
return [.. hosts.Values];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates an instance of the service host.
|
|
/// </summary>
|
|
public override ServiceHost CreateServiceHost(ServerBase server, params Uri[] addresses)
|
|
{
|
|
return new ServiceHost(this, addresses);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns an instance of the endpoint to use.
|
|
/// </summary>
|
|
protected override EndpointBase GetEndpointInstance(ServerBase server)
|
|
{
|
|
return new SessionEndpoint(server);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override async ValueTask StartApplicationAsync(ApplicationConfiguration configuration, CancellationToken cancellationToken = default)
|
|
{
|
|
await base.StartApplicationAsync(configuration, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
await m_semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
m_logger.LogInformation(
|
|
Utils.TraceMasks.StartStop,
|
|
"Server - Start application {ApplicationName}.",
|
|
configuration.ApplicationName);
|
|
|
|
// Setup the minimum nonce length
|
|
Nonce.SetMinNonceValue((uint)configuration.SecurityConfiguration.NonceLength);
|
|
|
|
// create the datastore for the instance.
|
|
m_serverInternal = new ServerInternalData(
|
|
ServerProperties!,
|
|
configuration,
|
|
MessageContext,
|
|
TimeProvider);
|
|
|
|
// create the manager responsible for providing localized string resources.
|
|
m_logger.LogInformation(Utils.TraceMasks.StartStop, "Server - CreateResourceManager.");
|
|
ResourceManager resourceManager = CreateResourceManager(
|
|
m_serverInternal,
|
|
configuration);
|
|
|
|
// create the manager responsible for incoming requests.
|
|
m_logger.LogInformation(Utils.TraceMasks.StartStop, "Server - CreateRequestManager.");
|
|
RequestManager requestManager = CreateRequestManager(
|
|
m_serverInternal,
|
|
configuration);
|
|
|
|
//create the main node manager factory
|
|
IMainNodeManagerFactory mainNodeManagerFactory = CreateMainNodeManagerFactory(m_serverInternal, configuration);
|
|
|
|
ServerInternal.SetMainNodeManagerFactory(mainNodeManagerFactory);
|
|
|
|
// create the master node manager.
|
|
m_logger.LogInformation(Utils.TraceMasks.StartStop, "Server - CreateMasterNodeManager.");
|
|
IMasterNodeManager masterNodeManager = CreateMasterNodeManager(
|
|
m_serverInternal,
|
|
configuration);
|
|
|
|
// add the node manager to the datastore.
|
|
ServerInternal.SetNodeManager(masterNodeManager);
|
|
|
|
// put the node manager into a state that allows it to be used by other objects.
|
|
await masterNodeManager.StartupAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
// create the manager responsible for handling events.
|
|
m_logger.LogInformation(Utils.TraceMasks.StartStop, "Server - CreateEventManager.");
|
|
EventManager eventManager = CreateEventManager(m_serverInternal, configuration);
|
|
|
|
// creates the server object.
|
|
await ServerInternal.CreateServerObjectAsync(
|
|
eventManager,
|
|
resourceManager,
|
|
requestManager,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
// do any additional processing now that the node manager is up and running.
|
|
OnNodeManagerStarted(m_serverInternal);
|
|
|
|
// create the manager responsible for aggregates.
|
|
m_logger.LogInformation(Utils.TraceMasks.StartStop, "Server - CreateAggregateManager.");
|
|
ServerInternal.SetAggregateManager(
|
|
await CreateAggregateManagerAsync(m_serverInternal, configuration, cancellationToken).ConfigureAwait(false));
|
|
|
|
// create the manager responsible for modelling rules.
|
|
m_logger.LogInformation(Utils.TraceMasks.StartStop, "Server - CreateModellingRulesManager.");
|
|
ServerInternal.SetModellingRulesManager(
|
|
await CreateModellingRulesManagerAsync(m_serverInternal, configuration, cancellationToken).ConfigureAwait(false));
|
|
|
|
// start the session manager.
|
|
m_logger.LogInformation(Utils.TraceMasks.StartStop, "Server - CreateSessionManager.");
|
|
ISessionManager sessionManager = CreateSessionManager(
|
|
m_serverInternal,
|
|
configuration);
|
|
await sessionManager.StartupAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
// use event to trigger channel that should not be closed.
|
|
sessionManager.SessionChannelKeepAlive += SessionChannelKeepAliveEvent;
|
|
|
|
//create the MonitoredItemQueueFactory
|
|
IMonitoredItemQueueFactory? monitoredItemQueueFactory
|
|
= CreateMonitoredItemQueueFactory(
|
|
m_serverInternal,
|
|
configuration);
|
|
|
|
//add the MonitoredItemQueueFactory to the datastore.
|
|
ServerInternal.SetMonitoredItemQueueFactory(monitoredItemQueueFactory!);
|
|
|
|
//create the SubscriptionStore
|
|
ISubscriptionStore? subscriptionStore = CreateSubscriptionStore(
|
|
m_serverInternal,
|
|
configuration);
|
|
|
|
//add the SubscriptionStore to the datastore
|
|
ServerInternal.SetSubscriptionStore(subscriptionStore!);
|
|
|
|
// start the subscription manager.
|
|
m_logger.LogInformation(Utils.TraceMasks.StartStop, "Server - CreateSubscriptionManager.");
|
|
ISubscriptionManager subscriptionManager = CreateSubscriptionManager(
|
|
m_serverInternal,
|
|
configuration);
|
|
await subscriptionManager.StartupAsync(cancellationToken)
|
|
.ConfigureAwait(false);
|
|
|
|
// add the session manager to the datastore.
|
|
ServerInternal.SetSessionManager(sessionManager, subscriptionManager);
|
|
|
|
ServerError = null!;
|
|
|
|
// setup registration information.
|
|
lock (m_registrationLock)
|
|
{
|
|
m_maxRegistrationInterval = configuration.ServerConfiguration!
|
|
.MaxRegistrationInterval;
|
|
|
|
ApplicationDescription serverDescription = ServerDescription!;
|
|
|
|
m_registrationInfo = new RegisteredServer
|
|
{
|
|
ServerUri = serverDescription.ApplicationUri!
|
|
};
|
|
m_registrationInfo.ServerNames += serverDescription.ApplicationName;
|
|
m_registrationInfo.ProductUri = serverDescription.ProductUri;
|
|
m_registrationInfo.ServerType = serverDescription.ApplicationType;
|
|
m_registrationInfo.GatewayServerUri = null;
|
|
m_registrationInfo.IsOnline = true;
|
|
m_registrationInfo.SemaphoreFilePath = null;
|
|
|
|
// add all discovery urls.
|
|
string computerName = Utils.GetHostName();
|
|
|
|
var discoveryUrls = new List<string>();
|
|
for (int ii = 0; ii < BaseAddresses.Count; ii++)
|
|
{
|
|
var uri = new UriBuilder(BaseAddresses[ii].DiscoveryUrl!);
|
|
|
|
if (string.Equals(
|
|
uri.Host,
|
|
"localhost",
|
|
StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
uri.Host = computerName;
|
|
}
|
|
|
|
discoveryUrls.Add(uri.ToString());
|
|
}
|
|
m_registrationInfo.DiscoveryUrls = discoveryUrls;
|
|
|
|
// build list of registration endpoints.
|
|
m_registrationEndpoints = new ConfiguredEndpointCollection(configuration);
|
|
|
|
EndpointDescription? endpoint = configuration.ServerConfiguration
|
|
.RegistrationEndpoint;
|
|
|
|
if (endpoint == null)
|
|
{
|
|
endpoint = new EndpointDescription
|
|
{
|
|
EndpointUrl = Utils.Format(Utils.DiscoveryUrls[0], "localhost"),
|
|
SecurityLevel = ServerSecurityPolicy.CalculateSecurityLevel(
|
|
MessageSecurityMode.SignAndEncrypt,
|
|
SecurityPolicies.Basic256Sha256,
|
|
m_logger),
|
|
SecurityMode = MessageSecurityMode.SignAndEncrypt,
|
|
SecurityPolicyUri = SecurityPolicies.Basic256Sha256
|
|
};
|
|
endpoint.Server.ApplicationType = ApplicationType.DiscoveryServer;
|
|
}
|
|
|
|
m_registrationEndpoints.Add(endpoint);
|
|
|
|
m_registeredWithDiscoveryServer = false;
|
|
m_minRegistrationInterval = 1000;
|
|
m_lastRegistrationInterval = m_minRegistrationInterval;
|
|
|
|
// start registration timer.
|
|
m_registrationTimer?.Dispose();
|
|
m_registrationTimer = null;
|
|
|
|
if (m_maxRegistrationInterval > 0)
|
|
{
|
|
m_logger.LogInformation(Utils.TraceMasks.StartStop, "Server - Registration Timer started.");
|
|
m_registrationTimer = TimeProvider.CreateTimer(
|
|
OnRegisterServerAsync,
|
|
this,
|
|
TimeSpan.FromMilliseconds(m_minRegistrationInterval),
|
|
Timeout.InfiniteTimeSpan);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
const string message = "Unexpected error starting application";
|
|
m_logger.LogCritical(Utils.TraceMasks.StartStop, e, message);
|
|
m_serverInternal?.Dispose();
|
|
m_serverInternal = null;
|
|
var error = ServiceResult.Create(e, StatusCodes.BadInternalError, message);
|
|
ServerError = error;
|
|
throw new ServiceResultException(error);
|
|
}
|
|
finally
|
|
{
|
|
m_semaphoreSlim.Release();
|
|
}
|
|
|
|
// set the server status as running.
|
|
SetServerState(ServerState.Running);
|
|
|
|
// all initialization is complete.
|
|
m_logger.LogInformation(Utils.TraceMasks.StartStop, "Server - Started.");
|
|
|
|
// Surface the live transport listener registry through the
|
|
// ITransportListenerRegistryProvider so consumers such as
|
|
// ConfigurationNodeManager can drive post-response channel
|
|
// cuts after ApplyChanges (OPC UA Part 12 §7.10.9).
|
|
m_serverInternal.SetTransportListenerRegistry(TransportListeners.AsReadOnly());
|
|
|
|
OnServerStarted(m_serverInternal);
|
|
|
|
// monitor the configuration file.
|
|
if (!string.IsNullOrEmpty(configuration.SourceFilePath))
|
|
{
|
|
m_logger.LogInformation(Utils.TraceMasks.StartStop, "Server - Configuration watcher started.");
|
|
m_configurationWatcher = new ConfigurationWatcher(configuration, MessageContext.Telemetry);
|
|
m_configurationWatcher!.Changed += OnConfigurationChangedAsync;
|
|
}
|
|
|
|
// Log availability of the new CertificateManager
|
|
if (CertificateManager != null)
|
|
{
|
|
m_logger.LogInformation(Utils.TraceMasks.StartStop,
|
|
"CertificateManager initialized with {Count} trust lists.",
|
|
CertificateManager.TrustLists.Count);
|
|
|
|
// Subscribe to CertificateManager change notifications and
|
|
// fan-out to OnCertificateUpdateAsync so endpoint descriptions
|
|
// and transport listeners pick up cert hot-updates.
|
|
m_certManagerSubscription = CertificateManager.CertificateChanges
|
|
.Subscribe(new CertificateManagerChangeObserver(this, m_logger));
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
protected override async ValueTask OnServerStoppingAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
m_logger.LogInformation(Utils.TraceMasks.StartStop, "Server - Stopping.");
|
|
|
|
ShutDownDelay();
|
|
|
|
// halt any outstanding timer.
|
|
lock (m_registrationLock)
|
|
{
|
|
m_registrationTimer?.Dispose();
|
|
m_registrationTimer = null;
|
|
}
|
|
|
|
// attempt graceful shutdown the server.
|
|
try
|
|
{
|
|
if (m_maxRegistrationInterval > 0 && m_registeredWithDiscoveryServer)
|
|
{
|
|
// unregister from Discovery Server if registered before
|
|
m_registrationInfo!.IsOnline = false;
|
|
await RegisterWithDiscoveryServerAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
await m_semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
try
|
|
{
|
|
if (m_serverInternal != null)
|
|
{
|
|
ServerInternal.SessionManager.SessionChannelKeepAlive
|
|
-= SessionChannelKeepAliveEvent;
|
|
await ServerInternal.SubscriptionManager.ShutdownAsync(cancellationToken).ConfigureAwait(false);
|
|
ServerInternal.SessionManager.Shutdown();
|
|
await ServerInternal.NodeManager.ShutdownAsync(cancellationToken).ConfigureAwait(false);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
m_semaphoreSlim.Release();
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
ServerError = new ServiceResult(e);
|
|
}
|
|
finally
|
|
{
|
|
// ensure that everything is cleaned up.
|
|
m_serverInternal?.Dispose();
|
|
m_serverInternal = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Trys to get the secure channel id for an AuthenticationToken.
|
|
/// The ChannelId is known to the sessions of the Server.
|
|
/// Each session has an AuthenticationToken which can be used to identify the session.
|
|
/// </summary>
|
|
/// <param name="authenticationToken">The AuthenticationToken from the RequestHeader</param>
|
|
/// <param name="channelId">The Channel id</param>
|
|
/// <returns>returns true if a channelId was found for the provided AuthenticationToken</returns>
|
|
public override bool TryGetSecureChannelIdForAuthenticationToken(
|
|
NodeId authenticationToken,
|
|
out uint channelId)
|
|
{
|
|
ISession? session = ServerInternal.SessionManager.GetSession(authenticationToken);
|
|
|
|
if (session == null)
|
|
{
|
|
channelId = 0;
|
|
return false;
|
|
}
|
|
|
|
return uint.TryParse(session.SecureChannelId, out channelId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Implements the server shutdown delay if session are connected.
|
|
/// </summary>
|
|
protected void ShutDownDelay()
|
|
{
|
|
try
|
|
{
|
|
// check for connected clients.
|
|
IList<ISession> currentessions = ServerInternal.SessionManager.GetSessions();
|
|
|
|
if (currentessions.Count > 0)
|
|
{
|
|
// provide some time for the connected clients to detect the shutdown state.
|
|
ServerInternal.UpdateServerStatus(
|
|
(status) =>
|
|
{
|
|
// set the shutdown reason and state.
|
|
status.Value.ShutdownReason = new LocalizedText(
|
|
"en-US",
|
|
"Application is shutting down.");
|
|
ServerStatusState statusVariable = status!.Variable!;
|
|
statusVariable.ShutdownReason!.Value = new LocalizedText(
|
|
"en-US",
|
|
"Application is shutting down.");
|
|
status.Value.State = ServerState.Shutdown;
|
|
statusVariable.State!.Value = ServerState.Shutdown;
|
|
statusVariable
|
|
.ClearChangeMasks(ServerInternal.DefaultSystemContext, true);
|
|
});
|
|
|
|
foreach (ISession session in currentessions)
|
|
{
|
|
// raise close session audit event
|
|
ServerInternal.ReportAuditCloseSessionEvent(
|
|
null!,
|
|
session,
|
|
m_logger,
|
|
"Session/Terminated");
|
|
}
|
|
|
|
for (int timeTillShutdown = Configuration!.ServerConfiguration!.ShutdownDelay;
|
|
timeTillShutdown > 0;
|
|
timeTillShutdown--)
|
|
{
|
|
ServerInternal.UpdateServerStatus(
|
|
(status) =>
|
|
{
|
|
status.Value.SecondsTillShutdown = (uint)timeTillShutdown;
|
|
status!.Variable!.SecondsTillShutdown!.Value = (uint)timeTillShutdown;
|
|
status.Variable
|
|
.ClearChangeMasks(ServerInternal.DefaultSystemContext, true);
|
|
});
|
|
|
|
// exit if all client connections are closed.
|
|
int sessionCount = ServerInternal.SessionManager.GetSessions().Count;
|
|
if (sessionCount == 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
m_logger.LogInformation(
|
|
Utils.TraceMasks.StartStop,
|
|
"{SessionCount} active sessions. Seconds until shutdown: {TimeTillShutdown}s",
|
|
sessionCount,
|
|
timeTillShutdown);
|
|
|
|
Thread.Sleep(1000);
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore error during shutdown procedure.
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the request manager for the server.
|
|
/// </summary>
|
|
/// <param name="server">The server.</param>
|
|
/// <param name="configuration">The configuration.</param>
|
|
/// <returns>
|
|
/// Returns an object that manages requests from within the server, return type is <seealso cref="RequestManager"/>.
|
|
/// </returns>
|
|
protected virtual RequestManager CreateRequestManager(
|
|
IServerInternal server,
|
|
ApplicationConfiguration configuration)
|
|
{
|
|
return new RequestManager(server);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the aggregate manager used by the server.
|
|
/// </summary>
|
|
/// <param name="server">The server.</param>
|
|
/// <param name="configuration">The application configuration.</param>
|
|
/// <param name="cancellationToken">A token to monitor for cancellation requests.</param>
|
|
/// <returns>The manager.</returns>
|
|
protected virtual async ValueTask<AggregateManager> CreateAggregateManagerAsync(
|
|
IServerInternal server,
|
|
ApplicationConfiguration configuration,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var manager = new AggregateManager(server);
|
|
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_Interpolative,
|
|
BrowseNames.AggregateFunction_Interpolative,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_Average,
|
|
BrowseNames.AggregateFunction_Average,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_TimeAverage,
|
|
BrowseNames.AggregateFunction_TimeAverage,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_TimeAverage2,
|
|
BrowseNames.AggregateFunction_TimeAverage2,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_Total,
|
|
BrowseNames.AggregateFunction_Total,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_Total2,
|
|
BrowseNames.AggregateFunction_Total2,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_Minimum,
|
|
BrowseNames.AggregateFunction_Minimum,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_Maximum,
|
|
BrowseNames.AggregateFunction_Maximum,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_MinimumActualTime,
|
|
BrowseNames.AggregateFunction_MinimumActualTime,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_MaximumActualTime,
|
|
BrowseNames.AggregateFunction_MaximumActualTime,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_Range,
|
|
BrowseNames.AggregateFunction_Range,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_Minimum2,
|
|
BrowseNames.AggregateFunction_Minimum2,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_Maximum2,
|
|
BrowseNames.AggregateFunction_Maximum2,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_MinimumActualTime2,
|
|
BrowseNames.AggregateFunction_MinimumActualTime2,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_MaximumActualTime2,
|
|
BrowseNames.AggregateFunction_MaximumActualTime2,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_Range2,
|
|
BrowseNames.AggregateFunction_Range2,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_Count,
|
|
BrowseNames.AggregateFunction_Count,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_AnnotationCount,
|
|
BrowseNames.AggregateFunction_AnnotationCount,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_DurationInStateZero,
|
|
BrowseNames.AggregateFunction_DurationInStateZero,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_DurationInStateNonZero,
|
|
BrowseNames.AggregateFunction_DurationInStateNonZero,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_NumberOfTransitions,
|
|
BrowseNames.AggregateFunction_NumberOfTransitions,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_Start,
|
|
BrowseNames.AggregateFunction_Start,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_End,
|
|
BrowseNames.AggregateFunction_End,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_Delta,
|
|
BrowseNames.AggregateFunction_Delta,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_StartBound,
|
|
BrowseNames.AggregateFunction_StartBound,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_EndBound,
|
|
BrowseNames.AggregateFunction_EndBound,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_DeltaBounds,
|
|
BrowseNames.AggregateFunction_DeltaBounds,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_DurationGood,
|
|
BrowseNames.AggregateFunction_DurationGood,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_DurationBad,
|
|
BrowseNames.AggregateFunction_DurationBad,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_PercentGood,
|
|
BrowseNames.AggregateFunction_PercentGood,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_PercentBad,
|
|
BrowseNames.AggregateFunction_PercentBad,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_WorstQuality,
|
|
BrowseNames.AggregateFunction_WorstQuality,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_WorstQuality2,
|
|
BrowseNames.AggregateFunction_WorstQuality2,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_StandardDeviationPopulation,
|
|
BrowseNames.AggregateFunction_StandardDeviationPopulation,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_VariancePopulation,
|
|
BrowseNames.AggregateFunction_VariancePopulation,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_StandardDeviationSample,
|
|
BrowseNames.AggregateFunction_StandardDeviationSample,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterFactoryAsync(
|
|
ObjectIds.AggregateFunction_VarianceSample,
|
|
BrowseNames.AggregateFunction_VarianceSample,
|
|
Aggregators.CreateStandardCalculator,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
return manager;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the modelling rules manager used by the server.
|
|
/// </summary>
|
|
/// <param name="server">The server.</param>
|
|
/// <param name="configuration">The application configuration.</param>
|
|
/// <param name="cancellationToken">The cancellation Token</param>
|
|
/// <returns>The manager.</returns>
|
|
protected virtual async ValueTask<ModellingRulesManager> CreateModellingRulesManagerAsync(
|
|
IServerInternal server,
|
|
ApplicationConfiguration configuration,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var manager = new ModellingRulesManager(server);
|
|
|
|
await manager.RegisterModellingRuleAsync(ObjectIds.ModellingRule_Mandatory, BrowseNames.ModellingRule_Mandatory, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
await manager.RegisterModellingRuleAsync(ObjectIds.ModellingRule_Optional, BrowseNames.ModellingRule_Optional, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
await manager.RegisterModellingRuleAsync(ObjectIds.ModellingRule_ExposesItsArray, BrowseNames.ModellingRule_ExposesItsArray, cancellationToken)
|
|
.ConfigureAwait(false);
|
|
await manager.RegisterModellingRuleAsync(
|
|
ObjectIds.ModellingRule_OptionalPlaceholder,
|
|
BrowseNames.ModellingRule_OptionalPlaceholder,
|
|
cancellationToken).ConfigureAwait(false);
|
|
await manager.RegisterModellingRuleAsync(
|
|
ObjectIds.ModellingRule_MandatoryPlaceholder,
|
|
BrowseNames.ModellingRule_MandatoryPlaceholder,
|
|
cancellationToken).ConfigureAwait(false);
|
|
|
|
return manager;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the resource manager for the server.
|
|
/// </summary>
|
|
/// <param name="server">The server.</param>
|
|
/// <param name="configuration">The configuration.</param>
|
|
/// <returns>Returns an object that manages access to localized resources, the return type is <seealso cref="ResourceManager"/>.</returns>
|
|
protected virtual ResourceManager CreateResourceManager(
|
|
IServerInternal server,
|
|
ApplicationConfiguration configuration)
|
|
{
|
|
var resourceManager = new ResourceManager(configuration);
|
|
|
|
// load default text for all status codes.
|
|
resourceManager.LoadDefaultText();
|
|
|
|
return resourceManager;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the master node manager for the server (async).
|
|
/// </summary>
|
|
/// <param name="server">The server.</param>
|
|
/// <param name="configuration">The configuration.</param>
|
|
/// <param name="cancellationToken">The cancellation token.</param>
|
|
/// <returns>The configured master node manager.</returns>
|
|
/// <remarks>
|
|
/// Subclasses that need to plug in custom async node-manager factories should
|
|
/// override this method. The default implementation awaits each registered
|
|
/// <see cref="IAsyncNodeManagerFactory"/> and delegates the synchronous
|
|
/// factories to <see cref="CreateMasterNodeManager"/>.
|
|
/// </remarks>
|
|
protected virtual async ValueTask<IMasterNodeManager> CreateMasterNodeManagerAsync(
|
|
IServerInternal server,
|
|
ApplicationConfiguration configuration,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var nodeManagers = new List<INodeManager>();
|
|
|
|
foreach (INodeManagerFactory nodeManagerFactory in m_nodeManagerFactories)
|
|
{
|
|
nodeManagers.Add(nodeManagerFactory.Create(server, configuration));
|
|
}
|
|
|
|
var asyncNodeManagers = new List<IAsyncNodeManager>();
|
|
|
|
foreach (IAsyncNodeManagerFactory nodeManagerFactory in m_asyncNodeManagerFactories)
|
|
{
|
|
asyncNodeManagers.Add(await nodeManagerFactory.CreateAsync(server, configuration, cancellationToken)
|
|
.ConfigureAwait(false));
|
|
}
|
|
|
|
return new MasterNodeManager(server, configuration, null, asyncNodeManagers, nodeManagers);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the master node manager for the server.
|
|
/// </summary>
|
|
/// <param name="server">The server.</param>
|
|
/// <param name="configuration">The configuration.</param>
|
|
/// <returns>Returns the master node manager for the server, the return type is <seealso cref="MasterNodeManager"/>.</returns>
|
|
protected virtual IMasterNodeManager CreateMasterNodeManager(
|
|
IServerInternal server,
|
|
ApplicationConfiguration configuration)
|
|
{
|
|
return CreateMasterNodeManagerAsync(server, configuration).AsTask().GetAwaiter().GetResult();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the master node manager for the server.
|
|
/// </summary>
|
|
/// <param name="server">The server.</param>
|
|
/// <param name="configuration">The configuration.</param>
|
|
/// <returns>Returns the master node manager for the server, the return type is <seealso cref="MasterNodeManager"/>.</returns>
|
|
protected virtual IMainNodeManagerFactory CreateMainNodeManagerFactory(
|
|
IServerInternal server,
|
|
ApplicationConfiguration configuration)
|
|
{
|
|
return new MainNodeManagerFactory(configuration, server);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the event manager for the server.
|
|
/// </summary>
|
|
/// <param name="server">The server.</param>
|
|
/// <param name="configuration">The configuration.</param>
|
|
/// <returns>Returns an object that manages all events raised within the server, the return type is <seealso cref="EventManager"/>.</returns>
|
|
protected virtual EventManager CreateEventManager(
|
|
IServerInternal server,
|
|
ApplicationConfiguration configuration)
|
|
{
|
|
return new EventManager(
|
|
server,
|
|
(uint)configuration.ServerConfiguration!.MaxEventQueueSize,
|
|
(uint)configuration.ServerConfiguration.MaxDurableEventQueueSize);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the session manager for the server.
|
|
/// </summary>
|
|
/// <param name="server">The server.</param>
|
|
/// <param name="configuration">The configuration.</param>
|
|
/// <returns>Returns a generic session manager object for a server, the return type is <seealso cref="ISessionManager"/>.</returns>
|
|
protected virtual ISessionManager CreateSessionManager(
|
|
IServerInternal server,
|
|
ApplicationConfiguration configuration)
|
|
{
|
|
return new SessionManager(server, configuration, TimeProvider);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the session manager for the server.
|
|
/// </summary>
|
|
/// <param name="server">The server.</param>
|
|
/// <param name="configuration">The configuration.</param>
|
|
/// <returns>Returns a generic session manager object for a server, the return type is <seealso cref="SubscriptionManager"/>.</returns>
|
|
protected virtual ISubscriptionManager CreateSubscriptionManager(
|
|
IServerInternal server,
|
|
ApplicationConfiguration configuration)
|
|
{
|
|
return new SubscriptionManager(server, configuration);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the (durable) monitored item queue factory for the server.
|
|
/// </summary>
|
|
/// <param name="server">The server.</param>
|
|
/// <param name="configuration">The configuration.</param>
|
|
/// <returns>Returns a (durable) monitored item queue factory for a server, the return type is <seealso cref="IMonitoredItemQueueFactory"/>.</returns>
|
|
protected virtual IMonitoredItemQueueFactory? CreateMonitoredItemQueueFactory(
|
|
IServerInternal server,
|
|
ApplicationConfiguration configuration)
|
|
{
|
|
return new MonitoredItemQueueFactory(MessageContext.Telemetry);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates the subscriptionStore for the server.
|
|
/// </summary>
|
|
/// <param name="server">The server.</param>
|
|
/// <param name="configuration">The configuration.</param>
|
|
/// <returns>Returns a subscriptionStore for a server, the return type is <seealso cref="ISubscriptionStore"/>.</returns>
|
|
protected virtual ISubscriptionStore? CreateSubscriptionStore(
|
|
IServerInternal server,
|
|
ApplicationConfiguration configuration)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called after the node managers have been started.
|
|
/// </summary>
|
|
/// <param name="server">The server.</param>
|
|
protected virtual void OnNodeManagerStarted(IServerInternal server)
|
|
{
|
|
// may be overridden by the subclass.
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called after the server has been started.
|
|
/// </summary>
|
|
/// <param name="server">The server.</param>
|
|
protected virtual void OnServerStarted(IServerInternal server)
|
|
{
|
|
// may be overridden by the subclass.
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public IEnumerable<INodeManagerFactory> NodeManagerFactories => m_nodeManagerFactories;
|
|
|
|
/// <inheritdoc/>
|
|
public IEnumerable<IAsyncNodeManagerFactory> AsyncNodeManagerFactories
|
|
=> m_asyncNodeManagerFactories;
|
|
|
|
/// <inheritdoc/>
|
|
public virtual void AddNodeManager(INodeManagerFactory nodeManagerFactory)
|
|
{
|
|
m_nodeManagerFactories.Add(nodeManagerFactory);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual void AddNodeManager(IAsyncNodeManagerFactory nodeManagerFactory)
|
|
{
|
|
m_asyncNodeManagerFactories.Add(nodeManagerFactory);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual void RemoveNodeManager(INodeManagerFactory nodeManagerFactory)
|
|
{
|
|
m_nodeManagerFactories.Remove(nodeManagerFactory);
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public virtual void RemoveNodeManager(IAsyncNodeManagerFactory nodeManagerFactory)
|
|
{
|
|
m_asyncNodeManagerFactories.Remove(nodeManagerFactory);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reacts to a session channel keep alive event to signal
|
|
/// a listener channel that a session is still active.
|
|
/// </summary>
|
|
private void SessionChannelKeepAliveEvent(ISession session, SessionEventReason reason)
|
|
{
|
|
Debug.Assert(reason == SessionEventReason.ChannelKeepAlive);
|
|
|
|
string? secureChannelId = session?.SecureChannelId;
|
|
if (!string.IsNullOrEmpty(secureChannelId))
|
|
{
|
|
ITransportListener? transportListener = TransportListeners.FirstOrDefault(tl =>
|
|
secureChannelId!.StartsWith(tl.ListenerId, StringComparison.Ordinal));
|
|
transportListener?.UpdateChannelLastActiveTime(secureChannelId!);
|
|
}
|
|
}
|
|
|
|
private OperationLimitsState OperationLimits
|
|
=> ServerInternal.ServerObject.ServerCapabilities!.OperationLimits!;
|
|
|
|
private readonly Lock m_registrationLock = new();
|
|
private readonly SemaphoreSlim m_semaphoreSlim = new(1, 1);
|
|
private ServerInternalData? m_serverInternal;
|
|
private ConfigurationWatcher? m_configurationWatcher;
|
|
private ConfiguredEndpointCollection? m_registrationEndpoints;
|
|
private RegisteredServer? m_registrationInfo;
|
|
private ITimer? m_registrationTimer;
|
|
private int m_minRegistrationInterval;
|
|
private int m_maxRegistrationInterval;
|
|
private int m_lastRegistrationInterval;
|
|
private bool m_registeredWithDiscoveryServer;
|
|
private int m_minNonceLength;
|
|
private bool m_useRegisterServer2;
|
|
private readonly List<INodeManagerFactory> m_nodeManagerFactories = [];
|
|
private readonly List<IAsyncNodeManagerFactory> m_asyncNodeManagerFactories = [];
|
|
private IDisposable? m_certManagerSubscription;
|
|
|
|
private sealed class CertificateManagerChangeObserver : IObserver<CertificateChangeEvent>
|
|
{
|
|
private readonly StandardServer _server;
|
|
private readonly ILogger _logger;
|
|
|
|
public CertificateManagerChangeObserver(StandardServer server, ILogger logger)
|
|
{
|
|
_server = server;
|
|
_logger = logger;
|
|
}
|
|
|
|
public void OnNext(CertificateChangeEvent value)
|
|
{
|
|
if (value.Kind == CertificateChangeKind.ApplicationCertificateUpdated)
|
|
{
|
|
_logger.LogInformation(
|
|
"CertificateManager: Application certificate updated for type {CertType}.",
|
|
value.CertificateType);
|
|
|
|
// Fan-out the cert update to endpoint descriptions and
|
|
// transport listeners. The reload itself was performed by
|
|
// CertificateManager.UpdateAsync /
|
|
// ReloadApplicationCertificatesAsync before this notification
|
|
// fired, so we only need to refresh downstream consumers.
|
|
try
|
|
{
|
|
ICertificateValidatorEx validator = _server.CertificateManager!;
|
|
var args = new CertificateUpdateEventArgs(
|
|
_server.Configuration?.SecurityConfiguration!,
|
|
validator);
|
|
_server.OnCertificateUpdateAsync(_server, args);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex,
|
|
"CertificateManager change observer failed to fan-out cert update.");
|
|
}
|
|
}
|
|
}
|
|
|
|
public void OnError(Exception error)
|
|
{
|
|
}
|
|
|
|
public void OnCompleted()
|
|
{
|
|
}
|
|
}
|
|
}
|
|
}
|