using Opc.Ua; using Opc.Ua.Client; using Opc.Ua.Configuration; namespace OpcUaCli; public static class OpcUaHelper { /// /// Creates an OPC UA client session for the specified endpoint URL. /// /// The OPC UA endpoint URL to connect to. /// Optional username for authentication. /// Optional password for authentication. /// The requested transport security mode: "none", "sign", or "encrypt". /// An active OPC UA client session. public static async Task ConnectAsync(string endpointUrl, string? username = null, string? password = null, string security = "none") { var config = new ApplicationConfiguration { ApplicationName = "OpcUaCli", ApplicationUri = "urn:localhost:OpcUaCli", ApplicationType = ApplicationType.Client, SecurityConfiguration = new SecurityConfiguration { ApplicationCertificate = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "OpcUaCli", "pki", "own") }, TrustedIssuerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "OpcUaCli", "pki", "issuer") }, TrustedPeerCertificates = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "OpcUaCli", "pki", "trusted") }, RejectedCertificateStore = new CertificateTrustList { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "OpcUaCli", "pki", "rejected") }, AutoAcceptUntrustedCertificates = true }, ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 } }; #pragma warning disable CS0618 // Sync/obsolete API is fine for a CLI tool await config.Validate(ApplicationType.Client); config.CertificateValidator.CertificateValidation += (_, e) => e.Accept = true; var requestedMode = ParseSecurityMode(security); EndpointDescription endpoint; if (requestedMode == MessageSecurityMode.None) { endpoint = CoreClientUtils.SelectEndpoint(config, endpointUrl, false); } else { // For secure connections, ensure the client has a certificate var app = new ApplicationInstance { ApplicationName = "OpcUaCli", ApplicationType = ApplicationType.Client, ApplicationConfiguration = config }; await app.CheckApplicationInstanceCertificatesAsync(false, 2048); // Discover endpoints and pick the one matching the requested security mode endpoint = SelectSecureEndpoint(config, endpointUrl, requestedMode); } var endpointConfig = EndpointConfiguration.Create(config); var configuredEndpoint = new ConfiguredEndpoint(null, endpoint, endpointConfig); UserIdentity identity = (username != null) ? new UserIdentity(username, System.Text.Encoding.UTF8.GetBytes(password ?? "")) : new UserIdentity(); var session = await Session.Create( config, configuredEndpoint, false, "OpcUaCli", 60000, identity, null); return session; #pragma warning restore CS0618 } /// /// Parses the security mode string from the CLI option. /// private static MessageSecurityMode ParseSecurityMode(string security) { return (security ?? "none").Trim().ToLowerInvariant() switch { "none" => MessageSecurityMode.None, "sign" => MessageSecurityMode.Sign, "encrypt" or "signandencrypt" => MessageSecurityMode.SignAndEncrypt, _ => throw new ArgumentException( $"Unknown security mode '{security}'. Valid values: none, sign, encrypt") }; } /// /// Discovers server endpoints and selects one matching the requested security mode, /// preferring Basic256Sha256 when multiple matches exist. /// private static EndpointDescription SelectSecureEndpoint(ApplicationConfiguration config, string endpointUrl, MessageSecurityMode requestedMode) { // Use discovery to get all endpoints using var client = DiscoveryClient.Create(new Uri(endpointUrl)); var allEndpoints = client.GetEndpoints(null); EndpointDescription? best = null; foreach (var ep in allEndpoints) { if (ep.SecurityMode != requestedMode) continue; if (best == null) { best = ep; continue; } // Prefer Basic256Sha256 if (ep.SecurityPolicyUri == SecurityPolicies.Basic256Sha256) best = ep; } if (best == null) { var available = string.Join(", ", allEndpoints.Select(e => $"{e.SecurityMode}/{e.SecurityPolicyUri}")); throw new InvalidOperationException( $"No endpoint found with security mode '{requestedMode}'. Available endpoints: {available}"); } // Rewrite endpoint URL to use the user-supplied hostname instead of the server's // internal address (e.g., 0.0.0.0 -> localhost) to handle NAT/hostname differences var serverUri = new Uri(best.EndpointUrl); var requestedUri = new Uri(endpointUrl); if (serverUri.Host != requestedUri.Host) { var builder = new UriBuilder(best.EndpointUrl) { Host = requestedUri.Host }; best.EndpointUrl = builder.ToString(); } return best; } /// /// Converts a raw command-line string into the runtime type expected by the target node. /// /// The raw string supplied by the user. /// The current node value used to infer the target type. /// A typed value suitable for an OPC UA write request. public static object ConvertValue(string rawValue, object? currentValue) { return currentValue switch { bool => bool.Parse(rawValue), byte => byte.Parse(rawValue), short => short.Parse(rawValue), ushort => ushort.Parse(rawValue), int => int.Parse(rawValue), uint => uint.Parse(rawValue), long => long.Parse(rawValue), ulong => ulong.Parse(rawValue), float => float.Parse(rawValue), double => double.Parse(rawValue), _ => rawValue }; } }