/* ======================================================================== * 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.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Opc.Ua.Configuration; using Opc.Ua.Tests; using Quickstarts.ReferenceServer; namespace Opc.Ua.Server.TestFramework { /// /// Server fixture for testing. /// /// A server class T used for testing. // CA1001: test fixture; lifecycle is handled via async StopAsync // (Application.DisposeAsync at line 445), not the IDisposable pattern. #pragma warning disable CA1001 public class ServerFixture #pragma warning restore CA1001 where T : ServerBase { public IApplicationInstance Application { get; private set; } public ApplicationConfiguration Config { get; private set; } public T Server { get; private set; } public bool AutoAccept { get; set; } public bool OperationLimits { get; set; } public int MaxChannelCount { get; set; } = 10; public int ReverseConnectTimeout { get; set; } public bool AllNodeManagers { get; set; } public int TraceMasks { get; set; } = Utils.TraceMasks.Error | Utils.TraceMasks.StackTrace | Utils.TraceMasks.Security | Utils.TraceMasks.Information; public bool SecurityNone { get; set; } public string UriScheme { get; set; } = Utils.UriSchemeOpcTcp; public int Port { get; private set; } public bool UseTracing { get; } public bool DurableSubscriptionsEnabled { get; set; } public bool UseSamplingGroupsInReferenceNodeManager { get; set; } public bool ProvisioningMode { get; set; } public ActivityListener ActivityListener { get; private set; } public ServerFixture( Func factory, bool useTracing, bool disableActivityLogging) : this(factory) { UseTracing = useTracing; if (UseTracing) { StartActivityListenerInternal(disableActivityLogging); } } public ServerFixture(Func factory) { m_factory = factory; m_telemetry = NUnitTelemetryContext.Create(true); m_logger = m_telemetry.CreateLogger>(); } public async Task LoadConfigurationAsync(string pkiRoot = null) { Application = new ApplicationInstance(m_telemetry) { ApplicationName = typeof(T).Name, ApplicationType = ApplicationType.Server }; // create the application configuration. Use temp path for cert stores. pkiRoot ??= Path.GetTempPath() + Path.GetRandomFileName(); string endpointUrl = $"{UriScheme}://localhost:0/" + typeof(T).Name; IApplicationConfigurationBuilderServerSelected serverConfig = Application .Build( "urn:localhost:UA:" + typeof(T).Name, "uri:opcfoundation.org:" + typeof(T).Name) .SetMaxByteStringLength(4 * 1024 * 1024) .SetMaxArrayLength(1024 * 1024) .SetMaxMessageSize(16 * 1024 * 1024) .SetChannelLifetime(30000) .AsServer([endpointUrl]); if (SecurityNone) { serverConfig.AddUnsecurePolicyNone(); } if (Utils.IsUriHttpsScheme(endpointUrl)) { serverConfig.AddPolicy( MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic256Sha256); } else if (endpointUrl.StartsWith(Utils.UriSchemeOpcTcp, StringComparison.Ordinal)) { // add deprecated policies for opc.tcp tests serverConfig .AddSignPolicies() .AddSignAndEncryptPolicies() .AddEccSignPolicies() .AddEccSignAndEncryptPolicies(); AddPolicyIfSupported( serverConfig, MessageSecurityMode.Sign, SecurityPolicies.Basic128Rsa15); AddPolicyIfSupported( serverConfig, MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic128Rsa15); AddPolicyIfSupported( serverConfig, MessageSecurityMode.Sign, SecurityPolicies.Basic256); AddPolicyIfSupported( serverConfig, MessageSecurityMode.SignAndEncrypt, SecurityPolicies.Basic256); AddPolicyIfSupported( serverConfig, MessageSecurityMode.Sign, SecurityPolicies.RSA_DH_AesGcm); AddPolicyIfSupported( serverConfig, MessageSecurityMode.SignAndEncrypt, SecurityPolicies.RSA_DH_AesGcm); AddPolicyIfSupported( serverConfig, MessageSecurityMode.Sign, SecurityPolicies.RSA_DH_ChaChaPoly); AddPolicyIfSupported( serverConfig, MessageSecurityMode.SignAndEncrypt, SecurityPolicies.RSA_DH_ChaChaPoly); AddPolicyIfSupported( serverConfig, MessageSecurityMode.Sign, SecurityPolicies.ECC_nistP256_AesGcm); AddPolicyIfSupported( serverConfig, MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_nistP256_AesGcm); AddPolicyIfSupported( serverConfig, MessageSecurityMode.Sign, SecurityPolicies.ECC_nistP256_ChaChaPoly); AddPolicyIfSupported( serverConfig, MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_nistP256_ChaChaPoly); AddPolicyIfSupported( serverConfig, MessageSecurityMode.Sign, SecurityPolicies.ECC_nistP384_AesGcm); AddPolicyIfSupported( serverConfig, MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_nistP384_AesGcm); AddPolicyIfSupported( serverConfig, MessageSecurityMode.Sign, SecurityPolicies.ECC_nistP384_ChaChaPoly); AddPolicyIfSupported( serverConfig, MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_nistP384_ChaChaPoly); AddPolicyIfSupported( serverConfig, MessageSecurityMode.Sign, SecurityPolicies.ECC_brainpoolP256r1_AesGcm); AddPolicyIfSupported( serverConfig, MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_brainpoolP256r1_AesGcm); AddPolicyIfSupported( serverConfig, MessageSecurityMode.Sign, SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly); AddPolicyIfSupported( serverConfig, MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_brainpoolP256r1_ChaChaPoly); AddPolicyIfSupported( serverConfig, MessageSecurityMode.Sign, SecurityPolicies.ECC_brainpoolP384r1_AesGcm); AddPolicyIfSupported( serverConfig, MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_brainpoolP384r1_AesGcm); AddPolicyIfSupported( serverConfig, MessageSecurityMode.Sign, SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly); AddPolicyIfSupported( serverConfig, MessageSecurityMode.SignAndEncrypt, SecurityPolicies.ECC_brainpoolP384r1_ChaChaPoly); } if (OperationLimits) { serverConfig.SetOperationLimits( new OperationLimits { MaxNodesPerBrowse = 2500, MaxNodesPerRead = 1000, MaxNodesPerWrite = 1000, MaxNodesPerMethodCall = 1000, MaxMonitoredItemsPerCall = 1000, MaxNodesPerHistoryReadData = 1000, MaxNodesPerHistoryReadEvents = 1000, MaxNodesPerHistoryUpdateData = 1000, MaxNodesPerHistoryUpdateEvents = 1000, MaxNodesPerNodeManagement = 1000, MaxNodesPerRegisterNodes = 1000, MaxNodesPerTranslateBrowsePathsToNodeIds = 1000 }); } serverConfig .SetMaxChannelCount(MaxChannelCount) .SetMaxMessageQueueSize(20) .SetDiagnosticsEnabled(true) .SetAuditingEnabled(true); if (ReverseConnectTimeout != 0) { serverConfig.SetReverseConnect( new ReverseConnectServerConfiguration { ConnectInterval = ReverseConnectTimeout / 4, ConnectTimeout = ReverseConnectTimeout, RejectTimeout = ReverseConnectTimeout / 4 }); } ArrayOf applicationCerts = ApplicationConfigurationBuilder.CreateDefaultApplicationCertificates( "CN=" + typeof(T).Name + ", C=US, S=Arizona, O=OPC Foundation, DC=localhost", CertificateStoreType.Directory, pkiRoot); if (DurableSubscriptionsEnabled) { serverConfig .SetDurableSubscriptionsEnabled(true) .SetMaxDurableEventQueueSize(10000) .SetMaxDurableNotificationQueueSize(1000) .SetMaxDurableSubscriptionLifetime(3600); } Config = await serverConfig .AddSecurityConfiguration(applicationCerts, pkiRoot) .SetAutoAcceptUntrustedCertificates(AutoAccept) .CreateAsync() .ConfigureAwait(false); } /// /// Start server fixture on random or fixed port. /// public Task StartAsync(int port = 0) { return StartAsync(null, port); } /// /// Start server fixture on random or fixed port with dedicated PKI. /// public async Task StartAsync(string pkiRoot, int port = 0) { int testPort = port; int serverStartRetries = 1; if (Application == null) { await LoadConfigurationAsync(pkiRoot).ConfigureAwait(false); } if (port <= 0) { testPort = ServerFixtureUtils.GetNextFreeIPPort(); serverStartRetries = 25; } bool retryStartServer; do { retryStartServer = false; try { await InternalStartServerAsync(testPort).ConfigureAwait(false); } catch (ServiceResultException sre) when (serverStartRetries > 0 && sre.StatusCode == StatusCodes.BadNoCommunication) { serverStartRetries--; testPort = UnsecureRandom.Shared.Next( ServerFixtureUtils.MinTestPort, ServerFixtureUtils.MaxTestPort); retryStartServer = true; } await Task.Delay(UnsecureRandom.Shared.Next(100, 1000)).ConfigureAwait(false); } while (retryStartServer); return Server; } /// /// Create the configuration and start the server. /// /// private async Task InternalStartServerAsync(int port) { Config.ServerConfiguration.BaseAddresses = [$"{UriScheme}://localhost:{port}/{typeof(T).Name}"]; // check the application certificate. bool haveAppCertificate = await Application .CheckApplicationInstanceCertificatesAsync(true, CertificateFactory.DefaultLifeTime) .ConfigureAwait(false); if (!haveAppCertificate) { throw new InvalidOperationException("Application instance certificate invalid!"); } // start the server. T server = m_factory(m_telemetry); if (AllNodeManagers && server is StandardServer standardServer) { Quickstarts.Servers.Utils.AddDefaultNodeManagers(standardServer); } if (UseSamplingGroupsInReferenceNodeManager && server is ReferenceServer referenceServer) { Quickstarts.Servers.Utils.UseSamplingGroupsInReferenceNodeManager(referenceServer); } if (ProvisioningMode && server is ReferenceServer provisioningReferenceServer) { Quickstarts.Servers.Utils.EnableProvisioningMode(provisioningReferenceServer); } m_startupCts = new CancellationTokenSource(); await Application.StartAsync(server, m_startupCts.Token).ConfigureAwait(false); Server = server; Port = port; } /// /// Configures Activity Listener and registers with Activity Source. /// public void StartActivityListenerInternal(bool disableActivityLogging = false) { if (disableActivityLogging) { // Create an instance of ActivityListener without logging ActivityListener = new ActivityListener { ShouldListenTo = (source) => source.Name == m_telemetry.GetActivitySource().Name, Sample = (ref _) => ActivitySamplingResult.AllDataAndRecorded, ActivityStarted = _ => { }, ActivityStopped = _ => { } }; } else { // Create an instance of ActivityListener and configure its properties with logging ActivityListener = new ActivityListener { ShouldListenTo = (source) => source.Name == m_telemetry.GetActivitySource().Name, Sample = (ref _) => ActivitySamplingResult.AllDataAndRecorded, ActivityStarted = activity => m_logger.LogInformation( "Server Started: {OperationName,-15} - TraceId: {TraceId,-32} SpanId: {SpanId,-16} ParentId: {ParentId,-32}", activity.OperationName, activity.TraceId, activity.SpanId, activity.ParentId ), ActivityStopped = activity => m_logger.LogInformation( "Server Stopped: {OperationName,-15} - TraceId: {TraceId,-32} SpanId: {SpanId,-16} ParentId: {ParentId,-32} Duration: {Duration}", activity.OperationName, activity.TraceId, activity.SpanId, activity.ParentId, activity.Duration) }; } ActivitySource.AddActivityListener(ActivityListener); } /// /// Stop the server. /// public async Task StopAsync() { // Cancel any in-progress startup (e.g., address space creation) m_startupCts?.Dispose(); m_startupCts = null; if (Server != null) { await Server.StopAsync().ConfigureAwait(false); Server.Dispose(); Server = null; } if (Application != null) { await Application.DisposeAsync().ConfigureAwait(false); Application = null; } Config = null; ActivityListener?.Dispose(); ActivityListener = null; await Task.Delay(100).ConfigureAwait(false); } private static void AddPolicyIfSupported( IApplicationConfigurationBuilderServerSelected serverConfig, MessageSecurityMode securityMode, string securityPolicyUri) { if (SecurityPolicies.GetInfo(securityPolicyUri) != null) { serverConfig.AddPolicy(securityMode, securityPolicyUri); } } private CancellationTokenSource m_startupCts; private readonly Func m_factory; private readonly ITelemetryContext m_telemetry; private readonly ILogger m_logger; } }