using System.Net; using System.Net.Sockets; using System.Reflection; using Microsoft.Extensions.Logging.Abstractions; using Opc.Ua; using ZB.MOM.WW.OtOpcUa.Commons.OpcUa; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; using ZB.MOM.WW.OtOpcUa.Configuration.Enums; using ZB.MOM.WW.OtOpcUa.Core.Authorization; using ZB.MOM.WW.OtOpcUa.OpcUaServer; using ZB.MOM.WW.OtOpcUa.OpcUaServer.Security; internal sealed class BaselineRig : IAsyncDisposable { internal const string Cluster = "SITE-A", RawDeviceFolder = "Plant/Modbus/dev1", RawAlarmPath = "Plant/Modbus/dev1/temp_hi"; internal const string EqA = "EqA", EqB = "EqB", KeyPrincipal = "event-probe", KeyId = "k-server-events"; internal static string KeyPassword => Environment.GetEnvironmentVariable("OTOPCUA_BASELINE_CANONICAL_KEY") ?? throw new InvalidOperationException("An ephemeral fixture credential is required"); private readonly OpcUaApplicationHostOptions _options; private OpcUaApplicationHost _host = null!; private bool _disposed; internal OtOpcUaSdkServer Server { get; private set; } = null!; internal OtOpcUaNodeManager Nm => Server.NodeManager!; internal SdkAddressSpaceSink Sink { get; private set; } = null!; internal string EndpointUrl => $"opc.tcp://127.0.0.1:{_options.OpcUaPort}/OtOpcUa"; internal string PkiRoot { get; } internal CertificateValidator Validator { get { var applicationField = typeof(OpcUaApplicationHost).GetField("_application", BindingFlags.NonPublic | BindingFlags.Instance) ?? throw new InvalidOperationException("Original host application field is missing"); return applicationField.GetValue(_host) is Opc.Ua.Configuration.ApplicationInstance application && application.ApplicationConfiguration.CertificateValidator is CertificateValidator validator ? validator : throw new InvalidOperationException("Original host application field type changed"); } } internal BaselineRig(string directory) { PkiRoot = Path.Combine(directory, "server-pki"); using var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); _options = new OpcUaApplicationHostOptions { ApplicationName = "OtOpcUa.SDK2LifecycleSoak", ApplicationUri = $"urn:OtOpcUa.SDK2LifecycleSoak.{Guid.NewGuid():N}", OpcUaPort = ((IPEndPoint)listener.LocalEndpoint).Port, PublicHostname = "127.0.0.1", PkiStoreRoot = PkiRoot, EnabledSecurityProfiles = [OpcUaSecurityProfile.None, OpcUaSecurityProfile.Basic256Sha256SignAndEncrypt], AutoAcceptUntrustedClientCertificates = true, }; } internal async Task BootAsync(CancellationToken ct) { Server = new OtOpcUaSdkServer(); _host = new OpcUaApplicationHost(_options, NullLogger.Instance, new Auth()); await _host.StartAsync(Server, ct); Canonical.Ledger.Record("host-start"); Sink = new SdkAddressSpaceSink(Nm); } internal async Task RestartAsync(CancellationToken ct) { await _host.DisposeAsync(); Canonical.Ledger.Record("host-stop"); await BootAsync(ct); } internal DataPlaneAuthorityState Enforce(bool enabled, ApiKey[] keys, params NodeAcl[] grants) { var state = new DataPlaneAuthorityState(); state.Apply(Cluster, "rev-1", keys, grants, new Dictionary> { [new AclScopeRef(NodeAclScopeKind.Equipment, "eq-" + EqA)] = [new NodeAclPath([NodeAclRealms.Uns, EqA])], [new AclScopeRef(NodeAclScopeKind.Equipment, "eq-" + EqB)] = [new NodeAclPath([NodeAclRealms.Uns, EqB])] }); Nm.DataPlaneAuthority = state; Nm.Authorizer = new DataPlaneAuthorizer(state, new TriePermissionEvaluator(state.Tries), enabled, NullLogger.Instance, TimeProvider.System); Canonical.Ledger.Record("authority-restore"); return state; } internal static ApiKey Key() => new() { ApiKeyId = KeyId, Name = KeyId, SecretHash = "hash", Principal = KeyPrincipal, Enabled = true, CreatedUtc = DateTime.UtcNow.AddDays(-1) }; internal static NodeAcl ClusterGrant(string principal, string id, NodePermissions permissions) => new() { NodeAclId = id, ClusterId = Cluster, Principal = principal, ScopeKind = NodeAclScopeKind.Cluster, PermissionFlags = permissions }; internal void MaterialiseNative() { Sink.EnsureFolder(RawDeviceFolder, null, "dev1", AddressSpaceRealm.Raw); Sink.MaterialiseAlarmCondition(RawAlarmPath, RawDeviceFolder, "temp_hi", "OffNormalAlarm", 700, AddressSpaceRealm.Raw, isNative: true); Sink.EnsureFolder(EqA, null, "A", AddressSpaceRealm.Uns); Sink.EnsureFolder(EqB, null, "B", AddressSpaceRealm.Uns); Sink.WireAlarmNotifiers(RawAlarmPath, AddressSpaceRealm.Raw, [EqA, EqB], AddressSpaceRealm.Uns); } public async ValueTask DisposeAsync() { if (_disposed) return; _disposed = true; try { if (_host is not null) { await _host.DisposeAsync(); Canonical.Ledger.Record("host-stop"); } } finally { if (Directory.Exists(PkiRoot)) Directory.Delete(PkiRoot, true); } } private sealed class Auth : IOpcUaUserAuthenticator { public Task AuthenticateUserNameAsync(string username, string password, CancellationToken ct) => Task.FromResult(username == "apikey" && password.StartsWith($"otua_{KeyId}_", StringComparison.Ordinal) ? OpcUaUserAuthResult.AllowKey(KeyPrincipal, [OpcUaDataPlaneRoles.AlarmAck, OpcUaDataPlaneRoles.WriteOperate], KeyId) : OpcUaUserAuthResult.Deny("Invalid fixture key")); } }