Linter/formatter pass across the full codebase. Restores required partial keyword on AXAML code-behind classes that the formatter incorrectly removed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
256 lines
11 KiB
C#
256 lines
11 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Threading.Tasks;
|
|
using Opc.Ua;
|
|
using Opc.Ua.Configuration;
|
|
using Serilog;
|
|
using ZB.MOM.WW.LmxOpcUa.Host.Configuration;
|
|
using ZB.MOM.WW.LmxOpcUa.Host.Domain;
|
|
using ZB.MOM.WW.LmxOpcUa.Host.Historian;
|
|
using ZB.MOM.WW.LmxOpcUa.Host.Metrics;
|
|
|
|
namespace ZB.MOM.WW.LmxOpcUa.Host.OpcUa
|
|
{
|
|
/// <summary>
|
|
/// Manages the OPC UA ApplicationInstance lifecycle. Programmatic config, no XML. (OPC-001, OPC-012, OPC-013)
|
|
/// </summary>
|
|
public class OpcUaServerHost : IDisposable
|
|
{
|
|
private static readonly ILogger Log = Serilog.Log.ForContext<OpcUaServerHost>();
|
|
private readonly AuthenticationConfiguration _authConfig;
|
|
private readonly IUserAuthenticationProvider? _authProvider;
|
|
|
|
private readonly OpcUaConfiguration _config;
|
|
private readonly HistorianDataSource? _historianDataSource;
|
|
private readonly PerformanceMetrics _metrics;
|
|
private readonly IMxAccessClient _mxAccessClient;
|
|
private readonly RedundancyConfiguration _redundancyConfig;
|
|
private readonly SecurityProfileConfiguration _securityConfig;
|
|
private ApplicationInstance? _application;
|
|
private LmxOpcUaServer? _server;
|
|
|
|
/// <summary>
|
|
/// Initializes a new host for the Galaxy-backed OPC UA server instance.
|
|
/// </summary>
|
|
/// <param name="config">The endpoint and session settings for the OPC UA host.</param>
|
|
/// <param name="mxAccessClient">The runtime client used by the node manager for live reads, writes, and subscriptions.</param>
|
|
/// <param name="metrics">The metrics collector shared with the node manager and runtime bridge.</param>
|
|
/// <param name="historianDataSource">The optional historian adapter that enables OPC UA history read support.</param>
|
|
public OpcUaServerHost(OpcUaConfiguration config, IMxAccessClient mxAccessClient, PerformanceMetrics metrics,
|
|
HistorianDataSource? historianDataSource = null,
|
|
AuthenticationConfiguration? authConfig = null,
|
|
IUserAuthenticationProvider? authProvider = null,
|
|
SecurityProfileConfiguration? securityConfig = null,
|
|
RedundancyConfiguration? redundancyConfig = null)
|
|
{
|
|
_config = config;
|
|
_mxAccessClient = mxAccessClient;
|
|
_metrics = metrics;
|
|
_historianDataSource = historianDataSource;
|
|
_authConfig = authConfig ?? new AuthenticationConfiguration();
|
|
_authProvider = authProvider;
|
|
_securityConfig = securityConfig ?? new SecurityProfileConfiguration();
|
|
_redundancyConfig = redundancyConfig ?? new RedundancyConfiguration();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the active node manager that holds the published Galaxy namespace.
|
|
/// </summary>
|
|
public LmxNodeManager? NodeManager => _server?.NodeManager;
|
|
|
|
/// <summary>
|
|
/// Gets the number of currently connected OPC UA client sessions.
|
|
/// </summary>
|
|
public int ActiveSessionCount => _server?.ActiveSessionCount ?? 0;
|
|
|
|
/// <summary>
|
|
/// Gets a value indicating whether the OPC UA server has been started and not yet stopped.
|
|
/// </summary>
|
|
public bool IsRunning => _server != null;
|
|
|
|
/// <summary>
|
|
/// Stops the host and releases server resources.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
Stop();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the OPC UA ServiceLevel based on current runtime health.
|
|
/// </summary>
|
|
public void UpdateServiceLevel(bool mxAccessConnected, bool dbConnected)
|
|
{
|
|
_server?.UpdateServiceLevel(mxAccessConnected, dbConnected);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the OPC UA application instance, prepares certificates, and binds the Galaxy namespace to the configured
|
|
/// endpoint.
|
|
/// </summary>
|
|
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 certOk = await _application.CheckApplicationInstanceCertificate(false, minKeySize);
|
|
if (!certOk)
|
|
{
|
|
Log.Warning("Application certificate check failed, attempting to create...");
|
|
certOk = await _application.CheckApplicationInstanceCertificate(false, minKeySize);
|
|
}
|
|
|
|
_server = new LmxOpcUaServer(_config.GalaxyName, _mxAccessClient, _metrics, _historianDataSource,
|
|
_config.AlarmTrackingEnabled, _authConfig, _authProvider, _redundancyConfig, applicationUri);
|
|
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)
|
|
{
|
|
if (_securityConfig.AutoAcceptClientCertificates)
|
|
{
|
|
e.Accept = true;
|
|
Log.Debug("Client certificate auto-accepted: {Subject}", e.Certificate?.Subject);
|
|
}
|
|
else
|
|
{
|
|
Log.Warning("Client certificate validation: {Error} for {Subject} — Accepted={Accepted}",
|
|
e.Error?.StatusCode, e.Certificate?.Subject, e.Accept);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops the OPC UA application instance and releases its in-memory server objects.
|
|
/// </summary>
|
|
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));
|
|
|
|
if (policies.Count == 0)
|
|
{
|
|
Log.Warning("No authentication methods configured — adding Anonymous as fallback");
|
|
policies.Add(new UserTokenPolicy(UserTokenType.Anonymous));
|
|
}
|
|
|
|
return policies;
|
|
}
|
|
}
|
|
} |