Files
UA-.NETStandard/Applications/ConsoleReferenceClient/RunConnectAll.cs
T
Randy Armstrong 5e627f2ca1 Post IOP Fixes.
2026-02-18 17:25:42 -08:00

701 lines
29 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
namespace SecurityTestClient
{
internal sealed class RunConnectAll
{
private readonly Lock m_lock = new();
private SessionReconnectHandler m_reconnectHandler;
private ILogger m_logger;
private ITelemetryContext m_context;
private ApplicationConfiguration m_configuration;
private ISession m_session;
const string ServerUrl = "opc.tcp://localhost:62541";
const string UserName = "sysadmin";
const string Password = "demo12";
const bool supportsX509 = true;
//const string ServerUrl = "opc.tcp://10.103.19.71:52520/OPCUA/SampleConsoleServer";
//const string UserName = "opcua";
//const string Password = "opcua";
//static bool supportsX509 = false;
//const string ServerUrl = "opc.tcp://10.103.141.179:4840";
//const string UserName = "iop";
//const string Password = "test";
//static bool supportsX509 = false;
//const string ServerUrl = "opc.tcp://78.46.151.116:4840";
//const string UserName = "user1";
//const string Password = "passsword";
//static bool supportsX509 = false;
//const string ServerUrl = "opc.tcp://10.103.119.62:4888/Softing/OpcUa/TestServer";
//const string UserName = "usr";
//const string Password = "pwd";
//const bool supportsX509 = true;
static string TargetPolicy = SecurityPolicies.ECC_nistP256_AesGcm;
const int kMaxSearchDepth = 128;
const int ReconnectPeriod = 1000;
const int ReconnectPeriodExponentialBackoff = 15000;
public RunConnectAll(ApplicationConfiguration configuration, ITelemetryContext context)
{
CryptoTrace.Enabled = true;
m_context = context;
m_configuration = configuration;
m_logger = context.CreateLogger("Test");
m_reconnectHandler = new SessionReconnectHandler(
context,
true,
ReconnectPeriodExponentialBackoff);
}
private string GetUserCertificateFile(string securityPolicyUri)
{
var securityPolicy = SecurityPolicies.GetInfo(securityPolicyUri);
switch (securityPolicy.CertificateKeyAlgorithm)
{
default:
case CertificateKeyAlgorithm.RSA:
case CertificateKeyAlgorithm.RSADH:
return $"iama.tester.rsa.der";
case CertificateKeyAlgorithm.BrainpoolP256r1:
return $"iama.tester.brainpoolP256r1.der";
case CertificateKeyAlgorithm.BrainpoolP384r1:
return $"iama.tester.brainpoolP384r1.der";
case CertificateKeyAlgorithm.NistP256:
return $"iama.tester.nistP256.der";
case CertificateKeyAlgorithm.NistP384:
return $"iama.tester.nistP384.der";
}
}
public async Task<bool> RunAsync(ManualResetEvent quitEvent, CancellationToken ct)
{
try
{
m_logger.LogInformation("OPC UA Security Test Client");
// The application name and config file names
const string applicationName = "ConsoleReferenceClient";
const string configSectionName = "Quickstarts.ReferenceClient";
// Define the UA Client application
var passwordProvider = new CertificatePasswordProvider([]);
var application = new ApplicationInstance(m_context)
{
ApplicationName = applicationName,
ApplicationType = ApplicationType.Client,
ConfigSectionName = configSectionName,
CertificatePasswordProvider = passwordProvider
};
// load the application configuration.
var configuration = m_configuration = await application
.LoadApplicationConfigurationAsync(silent: false, ct: ct)
.ConfigureAwait(false);
m_configuration.CertificateValidator.CertificateValidation += CertificateValidation;
// check the application certificate.
bool haveAppCertificate = await application
.CheckApplicationInstanceCertificatesAsync(false, ct: ct)
.ConfigureAwait(false);
if (!haveAppCertificate)
{
throw new InvalidOperationException("Application instance certificate invalid!");
}
m_logger.LogInformation("Connecting to... {ServerUrl}", ServerUrl);
var endpoints = await GetEndpoints(
m_configuration,
ServerUrl,
ct).ConfigureAwait(false);
endpoints = endpoints.Where(x => x.SecurityPolicyUri == TargetPolicy).ToList();
var endpointConfiguration = EndpointConfiguration.Create(m_configuration);
var sessionFactory = new DefaultSessionFactory(m_context);
var userNameidentity = new UserIdentity(UserName, new UTF8Encoding(false).GetBytes(Password));
foreach (var ii in endpoints)
{
var userCertificateFile = GetUserCertificateFile(ii.SecurityPolicyUri);
var x509 = X509CertificateLoader.LoadCertificateFromFile(Path.Combine("..\\..\\pki\\trustedUser\\certs", userCertificateFile));
var thumbprint = x509.Thumbprint;
var certificateIdentity = await LoadUserCertificateAsync(thumbprint, "password", ct).ConfigureAwait(false);
var identities = new List<UserIdentity>
{
new UserIdentity()
};
if (!String.IsNullOrEmpty(UserName)) { identities.Add(userNameidentity); }
if (supportsX509) { identities.Add(certificateIdentity); }
foreach (var identity in identities)
{
try
{
m_logger.LogWarning("{Line}", new string('=', 80));
m_logger.LogWarning(
"SECURITY-POLICY={SecurityPolicyUri} {SecurityMode}",
SecurityPolicies.GetDisplayName(ii.SecurityPolicyUri),
ii.SecurityMode);
m_logger.LogWarning(
"IDENTITY={DisplayName} {TokenType}",
identity.DisplayName,
identity.TokenType);
ISession session = await RunTestAsync(
endpointConfiguration,
sessionFactory,
ii,
identity,
ct).ConfigureAwait(false);
m_logger.LogWarning("Waiting for SecureChannel renew");
await session.UpdateSessionAsync(identity, null, ct).ConfigureAwait(false);
for (int count = 0; count < 20; count++)
{
var result = await session.ReadAsync(
null,
0,
TimestampsToReturn.Neither,
new ReadValueIdCollection()
{
new ReadValueId()
{
NodeId = Opc.Ua.VariableIds.Server_ServerStatus_CurrentTime,
AttributeId = Attributes.Value
}
},
ct).ConfigureAwait(false);
m_logger.LogWarning(
"CurrentTime: {CurrentTime}",
result.Results[0].GetValueOrDefault<DateTime>().ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture));
await Task.Delay(5000, ct).ConfigureAwait(false);
}
await session.UpdateSessionAsync(identity, null, ct).ConfigureAwait(false);
await session.CloseAsync(true, ct: ct).ConfigureAwait(false);
}
catch (Exception e)
{
Console.WriteLine("Exception: {0}", e.Message);
Console.WriteLine("StackTrace: {0}", e.StackTrace);
quitEvent.WaitOne(20000);
}
m_logger.LogWarning(
"TEST COMPLETE: {SecurityPolicyUri} {SecurityMode}",
SecurityPolicies.GetDisplayName(ii.SecurityPolicyUri),
ii.SecurityMode);
m_logger.LogWarning("{Line}", new string('=', 80));
//break;
}
//break;
}
Console.WriteLine("Ctrl-C to stop.");
quitEvent.WaitOne();
}
catch (Exception e)
{
m_logger.LogError("Exception: {Message}", e.Message);
m_logger.LogTrace("StackTrace: {StackTrace}", e.StackTrace);
}
return true;
}
private async Task<UserIdentity> LoadUserCertificateAsync(string thumbprint, string password, CancellationToken ct)
{
#if NET8_0_OR_GREATER
var store = m_configuration.SecurityConfiguration.TrustedUserCertificates;
// get user certificate with matching thumbprint
var hit = (
await store.GetCertificatesAsync(m_context, ct).ConfigureAwait(false)
).Find(X509FindType.FindByThumbprint, thumbprint, false).FirstOrDefault();
// create Certificate Identifier
var cid = new CertificateIdentifier(hit)
{
StorePath = store.StorePath,
StoreType = store.StoreType
};
return await UserIdentity.CreateAsync(
cid,
new CertificatePasswordProvider(new UTF8Encoding(false).GetBytes(password)),
m_context,
ct
).ConfigureAwait(false);
#else
await Task.Delay(1, ct).ConfigureAwait(false);
throw new NotSupportedException("User certificate identity is only supported on .NET 8 or greater.");
#endif
}
public async Task<ISession> RunTestAsync(
EndpointConfiguration endpointConfiguration,
DefaultSessionFactory sessionFactory,
EndpointDescription endpointDescription,
UserIdentity identity,
CancellationToken ct)
{
var endpoint = new ConfiguredEndpoint(
null,
endpointDescription,
endpointConfiguration);
// Create the session
ISession session = await sessionFactory
.CreateAsync(
m_configuration,
endpoint,
false,
false,
m_configuration.ApplicationName,
600000,
//new UserIdentity(),
(endpointDescription.SecurityMode != MessageSecurityMode.None) ? identity : new UserIdentity(),
null,
ct
)
.ConfigureAwait(false);
// Assign the created session
if (session == null || !session.Connected)
{
throw new InvalidOperationException("Could not connect to server at " + ServerUrl);
}
session.KeepAliveInterval = 10000;
session.KeepAlive += Session_KeepAlive;
var nodes = await BrowseFullAddressSpaceAsync(
session,
ObjectIds.ObjectsFolder,
null,
ct).ConfigureAwait(false);
return session;
}
private async ValueTask<IList<EndpointDescription>> GetEndpoints(
ApplicationConfiguration application,
string discoveryUrl,
CancellationToken ct = default)
{
var endpointConfiguration = EndpointConfiguration.Create(application);
using var client = await DiscoveryClient.CreateAsync(
application,
new Uri(discoveryUrl),
endpointConfiguration,
ct: ct).ConfigureAwait(false);
return await client.GetEndpointsAsync(null, ct).ConfigureAwait(false);
}
private void CertificateValidation(
CertificateValidator sender,
CertificateValidationEventArgs e)
{
bool certificateAccepted = false;
// ****
// Implement a custom logic to decide if the certificate should be
// accepted or not and set certificateAccepted flag accordingly.
// The certificate can be retrieved from the e.Certificate field
// ***
ServiceResult error = e.Error;
m_logger.LogInformation("{ServiceResult}", error);
if (error.StatusCode == StatusCodes.BadCertificateUntrusted)
{
certificateAccepted = true;
}
if (certificateAccepted)
{
m_logger.LogInformation(
"Untrusted Certificate accepted. Subject = {Subject}",
e.Certificate.Subject);
e.Accept = true;
}
else
{
m_logger.LogInformation(
"Untrusted Certificate rejected. Subject = {Subject}",
e.Certificate.Subject);
}
}
private void Session_KeepAlive(ISession session, KeepAliveEventArgs e)
{
try
{
// check for events from discarded sessions.
if (m_session == null || !m_session.Equals(session))
{
return;
}
// start reconnect sequence on communication error.
if (ServiceResult.IsBad(e.Status))
{
SessionReconnectHandler.ReconnectState state = m_reconnectHandler
.BeginReconnect(
m_session,
null,
ReconnectPeriod,
Client_ReconnectComplete);
if (state == SessionReconnectHandler.ReconnectState.Triggered)
{
m_logger.LogInformation(
"KeepAlive status {Status}, reconnect status {State}, reconnect period {ReconnectPeriod}ms.",
e.Status,
state,
ReconnectPeriod
);
}
else
{
m_logger.LogInformation(
"KeepAlive status {Status}, reconnect status {State}.",
e.Status,
state);
}
// cancel sending a new keep alive request, because reconnect is triggered.
e.CancelKeepAlive = true;
}
}
catch (Exception exception)
{
m_logger.LogError(exception, "Error in OnKeepAlive.");
}
}
private void Client_ReconnectComplete(object sender, EventArgs e)
{
// ignore callbacks from discarded objects.
if (!ReferenceEquals(sender, m_reconnectHandler))
{
return;
}
lock (m_lock)
{
// if session recovered, Session property is null
if (m_reconnectHandler.Session != null)
{
// ensure only a new instance is disposed
// after reactivate, the same session instance may be returned
if (!ReferenceEquals(m_session, m_reconnectHandler.Session))
{
m_logger.LogInformation(
"--- RECONNECTED TO NEW SESSION --- {SessionId}",
m_reconnectHandler.Session.SessionId
);
ISession session = m_session;
m_session = m_reconnectHandler.Session;
Utils.SilentDispose(session);
}
else
{
m_logger.LogInformation(
"--- REACTIVATED SESSION --- {SessionId}",
m_reconnectHandler.Session.SessionId);
}
}
else
{
m_logger.LogInformation("--- RECONNECT KeepAlive recovered ---");
}
}
}
private async Task<ReferenceDescriptionCollection> BrowseFullAddressSpaceAsync(
ISession session,
NodeId startingNode = null,
BrowseDescription browseDescription = null,
CancellationToken ct = default)
{
var stopWatch = new Stopwatch();
stopWatch.Start();
// Browse template
const int kMaxReferencesPerNode = 1000;
BrowseDescription browseTemplate =
browseDescription
?? new BrowseDescription
{
NodeId = startingNode ?? ObjectIds.RootFolder,
BrowseDirection = BrowseDirection.Forward,
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
IncludeSubtypes = true,
NodeClassMask = 0,
ResultMask = (uint)BrowseResultMask.All
};
BrowseDescriptionCollection browseDescriptionCollection
= CreateBrowseDescriptionCollectionFromNodeId(
[.. new NodeId[] { startingNode ?? ObjectIds.RootFolder }],
browseTemplate);
// Browse
var referenceDescriptions = new Dictionary<ExpandedNodeId, ReferenceDescription>();
var random = new Random(11211); // use a fixed seed for test reproducibility
int searchDepth = 0;
uint maxNodesPerBrowse = session.OperationLimits.MaxNodesPerBrowse;
while (browseDescriptionCollection.Count > 0 && searchDepth < kMaxSearchDepth)
{
searchDepth++;
m_logger.LogInformation(
"{SearchDepth}: Browse {Count} nodes after {ElapsedMilliseconds}ms",
searchDepth,
browseDescriptionCollection.Count,
stopWatch.ElapsedMilliseconds);
var allBrowseResults = new BrowseResultCollection();
bool repeatBrowse;
var browseResultCollection = new BrowseResultCollection();
var unprocessedOperations = new BrowseDescriptionCollection();
DiagnosticInfoCollection diagnosticsInfoCollection;
do
{
BrowseDescriptionCollection browseCollection =
maxNodesPerBrowse == 0
? browseDescriptionCollection
: browseDescriptionCollection.Take((int)maxNodesPerBrowse).ToArray();
repeatBrowse = false;
try
{
RequestHeader requestHeader = null;
// a random pattern to obscure the message size
// (only useful for application running over untrusted networks).
if (session.ConfiguredEndpoint.Description.SecurityMode == MessageSecurityMode.SignAndEncrypt)
{
// a real application needs to use secure randomness
#pragma warning disable CA5394 // Do not use insecure randomness
var padding = new byte[random.Next() % 128];
random.NextBytes(padding);
#pragma warning restore CA5394 // Do not use insecure randomness
m_logger.LogWarning("Sending Padding with {Length} Bytes", padding.Length);
requestHeader = new RequestHeader
{
AdditionalHeader = new ExtensionObject(new Opc.Ua.AdditionalParametersType()
{
Parameters = new KeyValuePairCollection([
new Opc.Ua.KeyValuePair() {
Key = AdditionalParameterNames.Padding,
Value = new Variant(padding)
}
])
})
};
}
BrowseResponse browseResponse = await
session.BrowseAsync(
requestHeader,
null,
kMaxReferencesPerNode,
browseCollection,
ct)
.ConfigureAwait(false);
browseResultCollection = browseResponse.Results;
diagnosticsInfoCollection = browseResponse.DiagnosticInfos;
ClientBase.ValidateResponse(browseResultCollection, browseCollection);
ClientBase.ValidateDiagnosticInfos(
diagnosticsInfoCollection,
browseCollection);
// separate unprocessed nodes for later
int ii = 0;
foreach (BrowseResult browseResult in browseResultCollection)
{
// check for error.
StatusCode statusCode = browseResult.StatusCode;
if (StatusCode.IsBad(statusCode))
{
// this error indicates that the server does not have enough simultaneously active
// continuation points. This request will need to be resent after the other operations
// have been completed and their continuation points released.
if (statusCode == StatusCodes.BadNoContinuationPoints)
{
unprocessedOperations.Add(browseCollection[ii++]);
continue;
}
}
// save results.
allBrowseResults.Add(browseResult);
ii++;
}
}
catch (ServiceResultException sre)
{
if (sre.StatusCode is StatusCodes.BadEncodingLimitsExceeded or StatusCodes
.BadResponseTooLarge)
{
// try to address by overriding operation limit
maxNodesPerBrowse =
maxNodesPerBrowse == 0
? (uint)browseCollection.Count / 2
: maxNodesPerBrowse / 2;
repeatBrowse = true;
}
else
{
m_logger.LogError("Browse error: {Message}", sre.Message);
throw;
}
}
} while (repeatBrowse);
if (maxNodesPerBrowse == 0)
{
browseDescriptionCollection.Clear();
}
else
{
browseDescriptionCollection = browseDescriptionCollection
.Skip(browseResultCollection.Count)
.ToArray();
}
// Browse next
ByteStringCollection continuationPoints = PrepareBrowseNext(browseResultCollection);
while (continuationPoints.Count > 0)
{
m_logger.LogInformation("BrowseNext {Count} continuation points.", continuationPoints.Count);
BrowseNextResponse browseNextResult = await
session.BrowseNextAsync(null, false, continuationPoints, ct)
.ConfigureAwait(false);
BrowseResultCollection browseNextResultCollection = browseNextResult.Results;
diagnosticsInfoCollection = browseNextResult.DiagnosticInfos;
ClientBase.ValidateResponse(browseNextResultCollection, continuationPoints);
ClientBase.ValidateDiagnosticInfos(
diagnosticsInfoCollection,
continuationPoints);
allBrowseResults.AddRange(browseNextResultCollection);
continuationPoints = PrepareBrowseNext(browseNextResultCollection);
}
// Build browse request for next level
var browseTable = new NodeIdCollection();
int duplicates = 0;
foreach (BrowseResult browseResult in allBrowseResults)
{
foreach (ReferenceDescription reference in browseResult.References)
{
if (!referenceDescriptions.ContainsKey(reference.NodeId))
{
referenceDescriptions[reference.NodeId] = reference;
if (reference.ReferenceTypeId != ReferenceTypeIds.HasProperty)
{
browseTable.Add(
ExpandedNodeId.ToNodeId(
reference.NodeId,
session.NamespaceUris));
}
}
else
{
duplicates++;
}
}
}
if (duplicates > 0)
{
m_logger.LogInformation("Browse Result {Duplicates} duplicate nodes were ignored.", duplicates);
}
browseDescriptionCollection.AddRange(
CreateBrowseDescriptionCollectionFromNodeId(browseTable, browseTemplate));
// add unprocessed nodes if any
browseDescriptionCollection.AddRange(unprocessedOperations);
}
stopWatch.Stop();
var result = new ReferenceDescriptionCollection(referenceDescriptions.Values);
result.Sort((x, y) => x.NodeId.CompareTo(y.NodeId));
m_logger.LogWarning(
"BrowseFullAddressSpace found {Count} references on server in {ElapsedMilliseconds} ms.",
referenceDescriptions.Count,
stopWatch.ElapsedMilliseconds);
return result;
}
private static BrowseDescriptionCollection CreateBrowseDescriptionCollectionFromNodeId(
NodeIdCollection nodeIdCollection,
BrowseDescription template)
{
var browseDescriptionCollection = new BrowseDescriptionCollection();
foreach (NodeId nodeId in nodeIdCollection)
{
var browseDescription = (BrowseDescription)template.MemberwiseClone();
browseDescription.NodeId = nodeId;
browseDescriptionCollection.Add(browseDescription);
}
return browseDescriptionCollection;
}
private static ByteStringCollection PrepareBrowseNext(
BrowseResultCollection browseResultCollection)
{
var continuationPoints = new ByteStringCollection();
foreach (BrowseResult browseResult in browseResultCollection)
{
if (browseResult.ContinuationPoint != null)
{
continuationPoints.Add(browseResult.ContinuationPoint);
}
}
return continuationPoints;
}
}
}