/* ======================================================================== * 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.Collections.ObjectModel; using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace Opc.Ua.Server.TestFramework { /// /// Extensions for server fixture. /// public static class ServerFixtureUtils { public const double DefaultSessionTimeout = 120000; public const uint DefaultMaxResponseMessageSize = 128 * 1024; public const int MinTestPort = 50000; public const int MaxTestPort = 65000; /// /// Create and Activate a session without security. /// /// /// The request header is used to call services directly, /// without establishing a session with a client. /// /// The server to connect to. /// A session name. /// The request header for the session. /// public static async Task<(RequestHeader, SecureChannelContext)> CreateAndActivateSessionAsync( this SessionServerBase server, string sessionName, bool useSecurity = false, UserIdentityToken identityToken = null, double sessionTimeout = DefaultSessionTimeout, uint maxResponseMessageSize = DefaultMaxResponseMessageSize) { // Find TCP endpoint ArrayOf endpoints = server.GetEndpoints(); EndpointDescription endpoint = endpoints.Find(e => e.TransportProfileUri .Equals(Profiles.UaTcpTransport, StringComparison.Ordinal) || e.TransportProfileUri .Equals(Profiles.HttpsBinaryTransport, StringComparison.Ordinal) ) ?? throw new NotSupportedException("Unsupported transport profile."); // fake profiles if (useSecurity) { endpoint.SecurityMode = MessageSecurityMode.Sign; endpoint.SecurityPolicyUri = SecurityPolicies.Basic256Sha256; } else { endpoint.SecurityMode = MessageSecurityMode.None; endpoint.SecurityPolicyUri = SecurityPolicies.None; } // set security context var secureChannelContext = new SecureChannelContext( sessionName, endpoint, RequestEncoding.Binary, null, null, null); var requestHeader = new RequestHeader(); // Create session CreateSessionResponse createSessionResponse = await server.CreateSessionAsync( secureChannelContext, requestHeader, null, null, null, sessionName, default, default, sessionTimeout, maxResponseMessageSize, RequestLifetime.None).ConfigureAwait(false); ValidateResponse(createSessionResponse.ResponseHeader); // Activate session requestHeader.AuthenticationToken = createSessionResponse.AuthenticationToken; ActivateSessionResponse activateSessionResponse = await server.ActivateSessionAsync( secureChannelContext, requestHeader, createSessionResponse.ServerSignature, [], [], identityToken != null ? new ExtensionObject(identityToken) : default, null, RequestLifetime.None).ConfigureAwait(false); ValidateResponse(activateSessionResponse.ResponseHeader); return (requestHeader, secureChannelContext); } /// /// Close a session. /// /// The server where the session is active. /// The request header of the session. public static async Task CloseSessionAsync( this SessionServerBase server, SecureChannelContext secureChannelContext, RequestHeader requestHeader, CancellationToken ct) { // close session using var requestLifetime = new RequestLifetime(ct); CloseSessionResponse response = await server.CloseSessionAsync( secureChannelContext, requestHeader, true, requestLifetime).ConfigureAwait(false); ValidateResponse(response.ResponseHeader); } /// /// Validate the response of a service call. /// /// The response header of the service call. /// public static void ValidateResponse(ResponseHeader header) { if (header == null) { throw new ServiceResultException( StatusCodes.BadUnknownResponse, "Null header in response."); } if (StatusCode.IsBad(header.ServiceResult)) { throw new ServiceResultException( new ServiceResult( header.ServiceResult, header.ServiceDiagnostics, header.StringTable)); } } /// /// Validate the response of a service call and validate the number of items returned. /// /// /// /// /// On the client the generated code already validates the response but the /// check is duplicated here to catch also issues when running tests within /// the shared client/server context. /// /// The response header of the service call. /// The list of returned values by the service call. /// The list of requests passed to the service call. /// /// public static void ValidateResponse( ResponseHeader header, ArrayOf response, ArrayOf request) { ValidateResponse(header); if (response is ArrayOf) { throw new ArgumentException( "Must call ValidateDiagnosticInfos() for ArrayOf.", nameof(response)); } if (response.Count != request.Count) { throw ServiceResultException.Unexpected( "The server returned {0} responses but {1} requests were made.", response.Count, request.Count); } for (int ii = 0; ii < response.Count; ii++) { if (response[ii] is null) { throw ServiceResultException.Unexpected( "The server returned responses contain a null response at index {0}.", ii); } } } /// /// Validate the diagnostic response of a service call. /// /// /// public static void ValidateDiagnosticInfos( ArrayOf response, ArrayOf request, ArrayOf stringTable, ILogger logger) { // returning an empty list for diagnostic info arrays is allowed. if (response.Count != 0) { if (response.Count != request.Count) { throw ServiceResultException.Unexpected( "The server forgot to fill in the DiagnosticInfos array correctly when returning an operation level error."); } // now validate the string table if (!stringTable.IsEmpty) { for (int ii = 0; ii < response.Count; ii++) { if (response[ii] is DiagnosticInfo diagnosticInfo && !diagnosticInfo.IsNullDiagnosticInfo) { if (diagnosticInfo.NamespaceUri >= stringTable.Count || diagnosticInfo.SymbolicId >= stringTable.Count || diagnosticInfo.Locale >= stringTable.Count || diagnosticInfo.LocalizedText >= stringTable.Count) { throw ServiceResultException.Unexpected( "The server forgot to fill in string table for the DiagnosticInfos " + "array correctly when returning an operation level error."); } var serviceResult = new ServiceResult( StatusCodes.Good, ii, response, stringTable); logger.LogInformation("DiagnosticInfo: {ServiceResult}", serviceResult); } } } } } /// /// Create a browse description from a node id collection. /// /// The node id collection. /// The template for the browse description for each node id. public static ArrayOf CreateBrowseDescriptionCollectionFromNodeId( ArrayOf nodeIdCollection, BrowseDescription template) { var browseDescriptionCollection = new List(); foreach (NodeId nodeId in nodeIdCollection) { var browseDescription = (BrowseDescription)template.MemberwiseClone(); browseDescription.NodeId = nodeId; browseDescriptionCollection.Add(browseDescription); } return browseDescriptionCollection; } /// /// Create the continuation point collection from the browse result /// collection for the BrowseNext service. /// /// The browse result collection to use. /// The collection of continuation points for the BrowseNext service. public static ArrayOf PrepareBrowseNext( ArrayOf browseResultCollection) { var continuationPoints = new List(); foreach (BrowseResult browseResult in browseResultCollection) { if (!browseResult.ContinuationPoint.IsEmpty) { continuationPoints.Add(browseResult.ContinuationPoint); } } return continuationPoints; } /// /// A dictionary of all node attributes. /// public static readonly ReadOnlyDictionary AttributesIds = new( new SortedDictionary { { Attributes.NodeId, default }, { Attributes.NodeClass, default }, { Attributes.BrowseName, default }, { Attributes.DisplayName, default }, { Attributes.Description, default }, { Attributes.WriteMask, default }, { Attributes.UserWriteMask, default }, { Attributes.DataType, default }, { Attributes.ValueRank, default }, { Attributes.ArrayDimensions, default }, { Attributes.AccessLevel, default }, { Attributes.UserAccessLevel, default }, { Attributes.Historizing, default }, { Attributes.MinimumSamplingInterval, default }, { Attributes.EventNotifier, default }, { Attributes.Executable, default }, { Attributes.UserExecutable, default }, { Attributes.IsAbstract, default }, { Attributes.InverseName, default }, { Attributes.Symmetric, default }, { Attributes.ContainsNoLoops, default }, { Attributes.DataTypeDefinition, default }, { Attributes.RolePermissions, default }, { Attributes.UserRolePermissions, default }, { Attributes.AccessRestrictions, default }, { Attributes.AccessLevelEx, default } }); /// /// Get free IP Port. /// public static int GetNextFreeIPPort() { var endpoint = new IPEndPoint(IPAddress.Any, 0); using var socket = new Socket( endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); socket.Bind(endpoint); if (socket.LocalEndPoint is IPEndPoint ep) { return ep.Port; } return 0; } } }