/* ========================================================================
* Copyright (c) 2005-2026 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.IO;
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Client.Subscriptions.Streaming;
using Opc.Ua.Configuration;
namespace AmbSample.Client
{
///
/// Connects a demo session. Certificate handling is deliberately the
/// minimum that lets the sample run against its own server.
///
internal sealed class SampleSession : IAsyncDisposable
{
private SampleSession(
ManagedSession session,
ApplicationInstance application)
{
Session = session;
m_application = application;
}
public ManagedSession Session { get; }
public IStreamingSubscription Streaming => Session.DefaultStreaming;
public static async Task ConnectAsync(
AmbClientOptions options,
ITelemetryContext telemetry,
CancellationToken cancellationToken)
{
string pkiRoot = GetPrivateStateRoot();
var configuration = new ApplicationConfiguration(telemetry)
{
ApplicationName = "AmbClient",
ApplicationUri = "urn:localhost:OPCFoundation:AmbClient",
ApplicationType = ApplicationType.Client,
SecurityConfiguration = new SecurityConfiguration
{
ApplicationCertificate = new CertificateIdentifier
{
StoreType = CertificateStoreType.Directory,
StorePath = Path.Combine(pkiRoot, "own"),
SubjectName = "CN=AmbClient, O=OPC Foundation"
},
TrustedIssuerCertificates = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = Path.Combine(pkiRoot, "issuer")
},
TrustedPeerCertificates = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = Path.Combine(pkiRoot, "trusted")
},
RejectedCertificateStore = new CertificateTrustList
{
StoreType = CertificateStoreType.Directory,
StorePath = Path.Combine(pkiRoot, "rejected")
},
AutoAcceptUntrustedCertificates = options.Insecure
},
TransportQuotas = new TransportQuotas { MaxMessageSize = 8 * 1024 * 1024 },
ClientConfiguration = new ClientConfiguration(),
ServerConfiguration = new ServerConfiguration()
};
await configuration.ValidateAsync(ApplicationType.Client, cancellationToken)
.ConfigureAwait(false);
var appInstance = new ApplicationInstance(configuration, telemetry);
await appInstance
.CheckApplicationInstanceCertificatesAsync(true, ct: cancellationToken)
.ConfigureAwait(false);
// The application instance owns the certificate manager the
// session validates with, so it lives as long as the session and
// is disposed with it. Disposing it here would leave the session
// with a disposed CertificateManager and every connect attempt
// failing with ObjectDisposedException.
configuration.CertificateManager ??= CertificateManagerFactory.Create(
configuration.SecurityConfiguration,
telemetry);
if (options.Insecure)
{
configuration.CertificateManager.AcceptError = static (_, _) => true;
Console.Error.WriteLine(
"WARNING: --insecure is demo-only: any server certificate is accepted.");
}
try
{
EndpointDescription? endpointDescription = await CoreClientUtils.SelectEndpointAsync(
configuration,
options.ServerUrl,
useSecurity: true,
discoverTimeout: 15000,
telemetry,
cancellationToken).ConfigureAwait(false);
if (endpointDescription is null)
{
throw ServiceResultException.Create(
StatusCodes.BadTimeout,
"Could not reach {0}.",
options.ServerUrl);
}
var endpoint = new ConfiguredEndpoint(
null,
endpointDescription,
EndpointConfiguration.Create(configuration));
ManagedSession session = await new ManagedSessionBuilder(configuration, telemetry)
.UseEndpoint(endpoint)
.WithSessionName("AmbClient")
.WithSessionTimeout(TimeSpan.FromSeconds(60))
.WithUserIdentity(new UserIdentity(new AnonymousIdentityToken()))
.ConnectAsync(cancellationToken).ConfigureAwait(false);
return new SampleSession(session, appInstance);
}
catch
{
await appInstance.DisposeAsync().ConfigureAwait(false);
throw;
}
}
public async ValueTask DisposeAsync()
{
// Tear the streaming subscription down while the channel is still
// open, so removing its monitored items does not race the close.
// A failing step does not keep the later ones from running.
try
{
await Streaming.DisposeAsync().ConfigureAwait(false);
}
finally
{
try
{
await Session.CloseAsync(CancellationToken.None).ConfigureAwait(false);
}
finally
{
try
{
await Session.DisposeAsync().ConfigureAwait(false);
}
finally
{
await m_application.DisposeAsync().ConfigureAwait(false);
}
}
}
}
private static string GetPrivateStateRoot()
{
string baseDirectory = Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData);
if (string.IsNullOrEmpty(baseDirectory))
{
baseDirectory = AppContext.BaseDirectory;
}
string root = Path.Combine(
baseDirectory,
"OPC Foundation",
"AmbClient",
"pki");
Directory.CreateDirectory(root);
return root;
}
private readonly ApplicationInstance m_application;
}
}