using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Opc.Ua; using Opc.Ua.Configuration; using Serilog; using ZB.MOM.WW.OtOpcUa.Host.Configuration; using ZB.MOM.WW.OtOpcUa.Host.Domain; using ZB.MOM.WW.OtOpcUa.Host.Historian; using ZB.MOM.WW.OtOpcUa.Host.Metrics; namespace ZB.MOM.WW.OtOpcUa.Host.OpcUa { /// /// Manages the OPC UA ApplicationInstance lifecycle. Programmatic config, no XML. (OPC-001, OPC-012, OPC-013) /// public class OpcUaServerHost : IDisposable { private static readonly ILogger Log = Serilog.Log.ForContext(); private readonly AlarmObjectFilter? _alarmObjectFilter; private readonly AuthenticationConfiguration _authConfig; private readonly IUserAuthenticationProvider? _authProvider; private readonly OpcUaConfiguration _config; private readonly IHistorianDataSource? _historianDataSource; private readonly PerformanceMetrics _metrics; private readonly IMxAccessClient _mxAccessClient; private readonly RedundancyConfiguration _redundancyConfig; private readonly SecurityProfileConfiguration _securityConfig; private ApplicationInstance? _application; private LmxOpcUaServer? _server; /// /// Initializes a new host for the Galaxy-backed OPC UA server instance. /// /// The endpoint and session settings for the OPC UA host. /// The runtime client used by the node manager for live reads, writes, and subscriptions. /// The metrics collector shared with the node manager and runtime bridge. /// The optional historian adapter that enables OPC UA history read support. public OpcUaServerHost(OpcUaConfiguration config, IMxAccessClient mxAccessClient, PerformanceMetrics metrics, IHistorianDataSource? historianDataSource = null, AuthenticationConfiguration? authConfig = null, IUserAuthenticationProvider? authProvider = null, SecurityProfileConfiguration? securityConfig = null, RedundancyConfiguration? redundancyConfig = null, AlarmObjectFilter? alarmObjectFilter = null, MxAccessConfiguration? mxAccessConfig = null, HistorianConfiguration? historianConfig = null) { _config = config; _mxAccessClient = mxAccessClient; _metrics = metrics; _historianDataSource = historianDataSource; _authConfig = authConfig ?? new AuthenticationConfiguration(); _authProvider = authProvider; _securityConfig = securityConfig ?? new SecurityProfileConfiguration(); _redundancyConfig = redundancyConfig ?? new RedundancyConfiguration(); _alarmObjectFilter = alarmObjectFilter; _mxAccessConfig = mxAccessConfig ?? new MxAccessConfiguration(); _historianConfig = historianConfig ?? new HistorianConfiguration(); } private readonly MxAccessConfiguration _mxAccessConfig; private readonly HistorianConfiguration _historianConfig; /// /// Gets the active node manager that holds the published Galaxy namespace. /// public LmxNodeManager? NodeManager => _server?.NodeManager; /// /// Gets the number of currently connected OPC UA client sessions. /// public int ActiveSessionCount => _server?.ActiveSessionCount ?? 0; /// /// Gets a value indicating whether the OPC UA server has been started and not yet stopped. /// public bool IsRunning => _server != null; /// /// Gets the list of opc.tcp base addresses the server is currently listening on. /// Returns an empty list when the server has not started. /// public IReadOnlyList BaseAddresses { get { var addrs = _application?.ApplicationConfiguration?.ServerConfiguration?.BaseAddresses; return addrs != null ? addrs.ToList() : Array.Empty(); } } /// /// Gets the list of active security policies advertised to clients (SecurityMode + PolicyUri). /// Returns an empty list when the server has not started. /// public IReadOnlyList SecurityPolicies { get { var policies = _application?.ApplicationConfiguration?.ServerConfiguration?.SecurityPolicies; return policies != null ? policies.ToList() : Array.Empty(); } } /// /// Gets the list of user token policy names advertised to clients (Anonymous, UserName, Certificate). /// Returns an empty list when the server has not started. /// public IReadOnlyList UserTokenPolicies { get { var policies = _application?.ApplicationConfiguration?.ServerConfiguration?.UserTokenPolicies; return policies != null ? policies.Select(p => p.TokenType.ToString()).ToList() : Array.Empty(); } } /// /// Stops the host and releases server resources. /// public void Dispose() { Stop(); } /// /// Updates the OPC UA ServiceLevel based on current runtime health. /// public void UpdateServiceLevel(bool mxAccessConnected, bool dbConnected) { _server?.UpdateServiceLevel(mxAccessConnected, dbConnected); } /// /// Starts the OPC UA application instance, prepares certificates, and binds the Galaxy namespace to the configured /// endpoint. /// public async Task StartAsync() { var namespaceUri = $"urn:{_config.GalaxyName}:LmxOpcUa"; var applicationUri = _config.ApplicationUri ?? namespaceUri; // Resolve configured security profiles var securityPolicies = SecurityProfileResolver.Resolve(_securityConfig.Profiles); foreach (var sp in securityPolicies) Log.Information("Security profile active: {PolicyUri} / {Mode}", sp.SecurityPolicyUri, sp.SecurityMode); // Build PKI paths var pkiRoot = _securityConfig.PkiRootPath ?? Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "OPC Foundation", "pki"); var certSubject = _securityConfig.CertificateSubject ?? $"CN={_config.ServerName}, O=ZB MOM, DC=localhost"; var serverConfig = new ServerConfiguration { BaseAddresses = { $"opc.tcp://{_config.BindAddress}:{_config.Port}{_config.EndpointPath}" }, MaxSessionCount = _config.MaxSessions, MaxSessionTimeout = _config.SessionTimeoutMinutes * 60 * 1000, // ms MinSessionTimeout = 10000, UserTokenPolicies = BuildUserTokenPolicies() }; foreach (var policy in securityPolicies) serverConfig.SecurityPolicies.Add(policy); var secConfig = new SecurityConfiguration { ApplicationCertificate = new CertificateIdentifier { StoreType = CertificateStoreType.Directory, StorePath = Path.Combine(pkiRoot, "own"), SubjectName = certSubject }, 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 = _securityConfig.AutoAcceptClientCertificates, RejectSHA1SignedCertificates = _securityConfig.RejectSHA1Certificates, MinimumCertificateKeySize = (ushort)_securityConfig.MinimumCertificateKeySize }; var appConfig = new ApplicationConfiguration { ApplicationName = _config.ServerName, ApplicationUri = applicationUri, ApplicationType = ApplicationType.Server, ProductUri = namespaceUri, ServerConfiguration = serverConfig, SecurityConfiguration = secConfig, TransportQuotas = new TransportQuotas { OperationTimeout = 120000, MaxStringLength = 4 * 1024 * 1024, MaxByteStringLength = 4 * 1024 * 1024, MaxArrayLength = 65535, MaxMessageSize = 4 * 1024 * 1024, MaxBufferSize = 65535, ChannelLifetime = 600000, SecurityTokenLifetime = 3600000 }, TraceConfiguration = new TraceConfiguration { OutputFilePath = null, TraceMasks = 0 } }; await appConfig.Validate(ApplicationType.Server); // Hook certificate validation logging appConfig.CertificateValidator.CertificateValidation += OnCertificateValidation; _application = new ApplicationInstance { ApplicationName = _config.ServerName, ApplicationType = ApplicationType.Server, ApplicationConfiguration = appConfig }; // Check/create application certificate var minKeySize = (ushort)_securityConfig.MinimumCertificateKeySize; var certLifetimeMonths = (ushort)_securityConfig.CertificateLifetimeMonths; var certOk = await _application.CheckApplicationInstanceCertificate(false, minKeySize, certLifetimeMonths); if (!certOk) { Log.Warning("Application certificate check failed, attempting to create..."); certOk = await _application.CheckApplicationInstanceCertificate(false, minKeySize, certLifetimeMonths); } _server = new LmxOpcUaServer(_config.GalaxyName, _mxAccessClient, _metrics, _historianDataSource, _config.AlarmTrackingEnabled, _authConfig, _authProvider, _redundancyConfig, applicationUri, _alarmObjectFilter, _mxAccessConfig.RuntimeStatusProbesEnabled, _mxAccessConfig.RuntimeStatusUnknownTimeoutSeconds, _mxAccessConfig.RequestTimeoutSeconds, _historianConfig.RequestTimeoutSeconds); await _application.Start(_server); Log.Information( "OPC UA server started on opc.tcp://{BindAddress}:{Port}{EndpointPath} (applicationUri={ApplicationUri}, namespace={Namespace})", _config.BindAddress, _config.Port, _config.EndpointPath, applicationUri, namespaceUri); } private void OnCertificateValidation(CertificateValidator sender, CertificateValidationEventArgs e) { var cert = e.Certificate; var subject = cert?.Subject ?? "Unknown"; var thumbprint = cert?.Thumbprint ?? "N/A"; if (_securityConfig.AutoAcceptClientCertificates) { e.Accept = true; Log.Warning( "Client certificate auto-accepted: Subject={Subject}, Thumbprint={Thumbprint}, ValidTo={ValidTo}", subject, thumbprint, cert?.NotAfter.ToString("yyyy-MM-dd")); } else { Log.Warning( "Client certificate validation: Error={Error}, Subject={Subject}, Thumbprint={Thumbprint}, Accepted={Accepted}", e.Error?.StatusCode, subject, thumbprint, e.Accept); } } /// /// Stops the OPC UA application instance and releases its in-memory server objects. /// public void Stop() { try { _server?.Stop(); Log.Information("OPC UA server stopped"); } catch (Exception ex) { Log.Warning(ex, "Error stopping OPC UA server"); } finally { _server = null; _application = null; } } private UserTokenPolicyCollection BuildUserTokenPolicies() { var policies = new UserTokenPolicyCollection(); if (_authConfig.AllowAnonymous) policies.Add(new UserTokenPolicy(UserTokenType.Anonymous)); if (_authConfig.Ldap.Enabled || _authProvider != null) policies.Add(new UserTokenPolicy(UserTokenType.UserName)); // X.509 certificate authentication is always available when security is configured if (_securityConfig.Profiles.Any(p => !p.Equals("None", StringComparison.OrdinalIgnoreCase))) policies.Add(new UserTokenPolicy(UserTokenType.Certificate)); if (policies.Count == 0) { Log.Warning("No authentication methods configured — adding Anonymous as fallback"); policies.Add(new UserTokenPolicy(UserTokenType.Anonymous)); } return policies; } } }