From 9119a2814196a063333ed4f7baa536ebb9cb9eac Mon Sep 17 00:00:00 2001 From: "2248356998 qq.com" <2248356998@qq.com> Date: Thu, 30 Mar 2023 13:10:31 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0OPCUAServer=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E4=B8=AA=E5=88=ABbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{ => OPCUAClient}/FormUtils.cs | 0 .../{ => OPCUAClient}/OPCNode.cs | 0 .../{ => OPCUAClient}/OPCUAClient.cs | 7 +- .../{ => OPCUAClient}/OPCUAStatusEventArgs.cs | 0 src/Plugins/ThingsGateway.Mqtt/MqttClient.cs | 2 +- src/Plugins/ThingsGateway.Mqtt/MqttServer.cs | 2 +- .../{OPCUA.cs => OPCUAClient.cs} | 81 +++- .../ThingsGateway.OPCUA/OPCUAServer.cs | 257 ++++++++++++ .../OPCUAServer/OPCUATag.cs | 13 + .../OPCUAServer/ThingsGatewayNodeManager.cs | 377 ++++++++++++++++++ .../OPCUAServer/ThingsGatewayServer.cs | 313 +++++++++++++++ .../ThingsGateway.OPCUA.csproj | 3 +- 12 files changed, 1034 insertions(+), 21 deletions(-) rename src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/{ => OPCUAClient}/FormUtils.cs (100%) rename src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/{ => OPCUAClient}/OPCNode.cs (100%) rename src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/{ => OPCUAClient}/OPCUAClient.cs (99%) rename src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/{ => OPCUAClient}/OPCUAStatusEventArgs.cs (100%) rename src/Plugins/ThingsGateway.OPCUA/{OPCUA.cs => OPCUAClient.cs} (88%) create mode 100644 src/Plugins/ThingsGateway.OPCUA/OPCUAServer.cs create mode 100644 src/Plugins/ThingsGateway.OPCUA/OPCUAServer/OPCUATag.cs create mode 100644 src/Plugins/ThingsGateway.OPCUA/OPCUAServer/ThingsGatewayNodeManager.cs create mode 100644 src/Plugins/ThingsGateway.OPCUA/OPCUAServer/ThingsGatewayServer.cs diff --git a/src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/FormUtils.cs b/src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/OPCUAClient/FormUtils.cs similarity index 100% rename from src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/FormUtils.cs rename to src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/OPCUAClient/FormUtils.cs diff --git a/src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/OPCNode.cs b/src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/OPCUAClient/OPCNode.cs similarity index 100% rename from src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/OPCNode.cs rename to src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/OPCUAClient/OPCNode.cs diff --git a/src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/OPCUAClient.cs b/src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/OPCUAClient/OPCUAClient.cs similarity index 99% rename from src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/OPCUAClient.cs rename to src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/OPCUAClient/OPCUAClient.cs index ac430882c..2988aabaa 100644 --- a/src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/OPCUAClient.cs +++ b/src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/OPCUAClient/OPCUAClient.cs @@ -75,7 +75,7 @@ public class OPCUAClient : DisposableObject ApplicationName = OPCUAName, ApplicationType = ApplicationType.Client, CertificateValidator = certificateValidator, - ApplicationUri = "urn:localhost:ThingsGateway:OPCUAClient", + ApplicationUri = Utils.Format(@"urn:{0}:thingsgatewayopcuaclient", System.Net.Dns.GetHostName()), ProductUri = "https://diego2098.gitee.io/thingsgateway/", ServerConfiguration = new ServerConfiguration @@ -85,7 +85,6 @@ public class OPCUAClient : DisposableObject MaxNotificationQueueSize = 1000000, MaxPublishRequestCount = 10000000, - }, SecurityConfiguration = new SecurityConfiguration @@ -99,7 +98,7 @@ public class OPCUAClient : DisposableObject { StoreType = CertificateStoreType.X509Store, StorePath = "CurrentUser\\UA_ThingsGateway", - SubjectName = "CN=ThingsGateway OPCUAClient, C=US, S=Arizona, O=ThingsGateway, DC=localhost", + SubjectName = "CN=ThingsGateway OPCUAClient, C=CN, S=GUANGZHOU, O=ThingsGateway, DC=" + System.Net.Dns.GetHostName(), }, TrustedIssuerCertificates = new CertificateTrustList { @@ -1408,7 +1407,7 @@ public class OPCUAClient : DisposableObject EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(m_configuration); ConfiguredEndpoint endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration); - await m_application.CheckApplicationInstanceCertificate(false, 0); + var d= await m_application.CheckApplicationInstanceCertificate(true, 0); //var x509 = await m_configuration.SecurityConfiguration.ApplicationCertificate.Find(true); m_session = await Opc.Ua.Client.Session.Create( m_configuration, diff --git a/src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/OPCUAStatusEventArgs.cs b/src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/OPCUAClient/OPCUAStatusEventArgs.cs similarity index 100% rename from src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/OPCUAStatusEventArgs.cs rename to src/Plugins/ThingsGateway.Foundation.Adapter.OPCUA/OPCUAClient/OPCUAStatusEventArgs.cs diff --git a/src/Plugins/ThingsGateway.Mqtt/MqttClient.cs b/src/Plugins/ThingsGateway.Mqtt/MqttClient.cs index f26ec0196..939d4225b 100644 --- a/src/Plugins/ThingsGateway.Mqtt/MqttClient.cs +++ b/src/Plugins/ThingsGateway.Mqtt/MqttClient.cs @@ -274,7 +274,7 @@ namespace ThingsGateway.Mqtt MqttRpcResult mqttRpcResult = new(); try { - var result = await _rpcCore.InvokeDeviceMethod(ToString() + "-" + arg.ClientId, rpcData.Adapt()); + var result = await _rpcCore.InvokeDeviceMethod(ToString() + "-" + arg.ClientId, rpcData.Adapt()); mqttRpcResult = new() { Message = result.Message, RpcId = rpcData.RpcId, Success = result.IsSuccess }; diff --git a/src/Plugins/ThingsGateway.Mqtt/MqttServer.cs b/src/Plugins/ThingsGateway.Mqtt/MqttServer.cs index 419408d7e..d474d138d 100644 --- a/src/Plugins/ThingsGateway.Mqtt/MqttServer.cs +++ b/src/Plugins/ThingsGateway.Mqtt/MqttServer.cs @@ -177,7 +177,7 @@ namespace ThingsGateway.Mqtt MqttRpcResult mqttRpcResult = new(); try { - var result = await _rpcCore.InvokeDeviceMethod(ToString() + "-" + IdWithName[arg.ClientId], rpcData.Adapt()); + var result = await _rpcCore.InvokeDeviceMethod(ToString() + "-" + IdWithName[arg.ClientId], rpcData.Adapt()); mqttRpcResult = new() { Message = result.Message, RpcId = rpcData.RpcId, Success = result.IsSuccess }; diff --git a/src/Plugins/ThingsGateway.OPCUA/OPCUA.cs b/src/Plugins/ThingsGateway.OPCUA/OPCUAClient.cs similarity index 88% rename from src/Plugins/ThingsGateway.OPCUA/OPCUA.cs rename to src/Plugins/ThingsGateway.OPCUA/OPCUAClient.cs index d0b573a1f..dc8715ad3 100644 --- a/src/Plugins/ThingsGateway.OPCUA/OPCUA.cs +++ b/src/Plugins/ThingsGateway.OPCUA/OPCUAClient.cs @@ -12,39 +12,84 @@ using TouchSocket.Core; namespace ThingsGateway.OPCUA { + /// + /// OPCUA客户端 + /// public class OPCUAClient : DriverBase { - internal Foundation.Adapter.OPCUA.OPCUAClient PLC = null; - internal CollectDeviceRunTime Device; - + internal Foundation.Adapter.OPCUA.OPCUAClient PLC = null; private List _deviceVariables = new(); - + /// public OPCUAClient(IServiceScopeFactory scopeFactory) : base(scopeFactory) { } + /// + /// 连接Url + /// [DeviceProperty("连接Url", "")] public string OPCURL { get; set; } = "opc.tcp://127.0.0.1:49320"; - [DeviceProperty("登录账号", "为空时将采用匿名方式登录")] public string UserName { get; set; } - [DeviceProperty("登录密码", "")] public string Password { get; set; } + + /// + /// 激活订阅 + /// [DeviceProperty("激活订阅", "")] public bool ActiveSubscribe { get; set; } = true; + + /// + /// 死区 + /// [DeviceProperty("死区", "")] public float DeadBand { get; set; } = 0; - public override Type DriverImportUI => typeof(ImportVariable); + /// + /// 自动分组大小 + /// [DeviceProperty("自动分组大小", "")] public int GroupSize { get; set; } = 500; - public override ThingsGatewayBitConverter ThingsGatewayBitConverter { get; } = new(EndianType.Little); - [DeviceProperty("重连频率", "")] public int ReconnectPeriod { get; set; } = 5000; - [DeviceProperty("更新频率", "")] public int UpdateRate { get; set; } = 1000; + + /// + public override Type DriverImportUI => typeof(ImportVariable); + + + /// + /// 登录账号 + /// + [DeviceProperty("登录账号", "为空时将采用匿名方式登录")] public string UserName { get; set; } + + /// + /// 登录密码 + /// + [DeviceProperty("登录密码", "")] public string Password { get; set; } + + + /// + /// 安全策略 + /// [DeviceProperty("安全策略", "True为使用安全策略,False为无")] public bool IsUseSecurity { get; set; } = true; + + /// + /// 重连频率 + /// + [DeviceProperty("重连频率", "")] public int ReconnectPeriod { get; set; } = 5000; + + /// + public override ThingsGatewayBitConverter ThingsGatewayBitConverter { get; } = new(EndianType.Little); + + /// + /// 更新频率 + /// + [DeviceProperty("更新频率", "")] public int UpdateRate { get; set; } = 1000; + + /// public override void AfterStop() { PLC?.Disconnect(); } + /// public override async Task BeforStart() { await PLC?.ConnectServer(); } + /// public override void Dispose() { if (PLC != null) @@ -57,11 +102,19 @@ namespace ThingsGateway.OPCUA } } + /// + public override bool IsConnected() + { + return PLC.Connected; + } + + /// public override bool IsSupportAddressRequest() { return !ActiveSubscribe; } + /// public override OperResult> LoadSourceRead(List deviceVariables) { _deviceVariables = deviceVariables; @@ -85,6 +138,7 @@ namespace ThingsGateway.OPCUA } } + /// public override async Task> ReadSourceAsync(DeviceVariableSourceRead deviceVariableSourceRead, CancellationToken cancellationToken) { await Task.CompletedTask; @@ -100,6 +154,7 @@ namespace ThingsGateway.OPCUA } } + /// public override async Task WriteValueAsync(CollectVariableRunTime deviceVariable, string value) { await Task.CompletedTask; @@ -107,6 +162,7 @@ namespace ThingsGateway.OPCUA return result ? OperResult.CreateSuccessResult() : new OperResult(); } + /// protected override void Init(CollectDeviceRunTime device, object client = null) { Device = device; @@ -134,15 +190,12 @@ namespace ThingsGateway.OPCUA PLC.OPCNode = oPCNode; } + /// protected override Task> ReadAsync(string address, int length, CancellationToken cancellationToken) { //不走ReadAsync throw new NotImplementedException(); } - public override bool IsConnected() - { - return PLC.Connected; - } private void dataChangedHandler(List<(MonitoredItem monitoredItem, MonitoredItemNotification monitoredItemNotification)> values) { try diff --git a/src/Plugins/ThingsGateway.OPCUA/OPCUAServer.cs b/src/Plugins/ThingsGateway.OPCUA/OPCUAServer.cs new file mode 100644 index 000000000..aaf10e045 --- /dev/null +++ b/src/Plugins/ThingsGateway.OPCUA/OPCUAServer.cs @@ -0,0 +1,257 @@ +using Mapster; + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using Opc.Ua; +using Opc.Ua.Configuration; + +using System.Collections.Concurrent; + +using ThingsGateway.Foundation; +using ThingsGateway.Foundation.Extension; +using ThingsGateway.Web.Foundation; + +namespace ThingsGateway.OPCUA; + +/// +/// OPCUA服务端 +/// +public partial class OPCUAServer : UpLoadBase +{ + private ApplicationInstance m_application; + private ApplicationConfiguration m_configuration; + private ThingsGatewayServer m_server; + /// + public OPCUAServer(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory) + { + } + /// + /// 服务地址 + /// + [DeviceProperty("服务地址", "")] + public string OpcUaStringUrl { get; set; } = "opc.tcp://127.0.0.1:49321"; + /// + /// 安全策略 + /// + [DeviceProperty("安全策略", "")] + public bool SecurityPolicy { get; set; } + + /// + public override async Task BeforStart() + { + // 启动服务器。 + await m_application.CheckApplicationInstanceCertificate(true, 0); + await m_application.Start(m_server); + } + + /// + public override void Dispose() + { + m_server.Stop(); + m_server.Dispose(); + m_application.Stop(); + } + + /// + public override OperResult IsConnected() + { + var result = m_server.GetStatus(); + + return OperResult.CreateSuccessResult(result); + } + + /// + public override async Task ExecuteAsync(CancellationToken stoppingToken) + { + try + { + ////变化推送 + var varList = CollectVariableRunTimes.ToListWithDequeue(); + if (varList?.Count != 0) + { + foreach (var item in varList) + { + try + { + m_server.NodeManager.UpVariable(item); + } + catch (Exception ex) + { + _logger.LogWarning(ex, ToString()); + } + } + + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, ToString()); + } + await Task.CompletedTask; + } + private ConcurrentQueue CollectVariableRunTimes { get; set; } = new(); + + /// + protected override void Init(UploadDevice device) + { + m_application = new ApplicationInstance(); + m_configuration = GetDefaultConfiguration(); + m_configuration.Validate(ApplicationType.Server).GetAwaiter().GetResult(); + m_application.ApplicationConfiguration = m_configuration; + if (m_configuration.SecurityConfiguration.AutoAcceptUntrustedCertificates) + { + m_configuration.CertificateValidator.CertificateValidation += (s, e) => + { + e.Accept = (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted); + }; + } + m_server = new(_logger, _scopeFactory.CreateScope()); + + using var serviceScope = _scopeFactory.CreateScope(); + var _globalCollectDeviceData = serviceScope.ServiceProvider.GetService(); + + + _globalCollectDeviceData.CollectVariables.ForEach(a => + { + VariableValueChange(a); + a.VariableValueChange += VariableValueChange; + }); + } + + private void VariableValueChange(CollectVariableRunTime collectVariableRunTime) + { + CollectVariableRunTimes.Enqueue(collectVariableRunTime.Adapt()); + } + + + private ApplicationConfiguration GetDefaultConfiguration() + { + ApplicationConfiguration config = new ApplicationConfiguration(); + string url = OpcUaStringUrl; + // 签名及加密验证 + ServerSecurityPolicyCollection policies = new ServerSecurityPolicyCollection(); + if (SecurityPolicy) + { + policies.Add(new ServerSecurityPolicy() + { + SecurityMode = MessageSecurityMode.Sign, + SecurityPolicyUri = SecurityPolicies.Basic128Rsa15 + }); + policies.Add(new ServerSecurityPolicy() + { + SecurityMode = MessageSecurityMode.SignAndEncrypt, + SecurityPolicyUri = SecurityPolicies.Basic128Rsa15 + }); + policies.Add(new ServerSecurityPolicy() + { + SecurityMode = MessageSecurityMode.Sign, + SecurityPolicyUri = SecurityPolicies.Basic256 + }); + policies.Add(new ServerSecurityPolicy() + { + SecurityMode = MessageSecurityMode.SignAndEncrypt, + SecurityPolicyUri = SecurityPolicies.Basic256 + }); + } + else + { + policies.Add(new ServerSecurityPolicy() + { + SecurityMode = MessageSecurityMode.None, + SecurityPolicyUri = SecurityPolicies.None + }); + } + + config.ApplicationName = "ThingsGateway OPCUAServer"; + config.ApplicationType = ApplicationType.Server; + config.ApplicationUri = Utils.Format(@"urn:{0}:thingsgatewayopcuaserver", System.Net.Dns.GetHostName()); + + var userTokens = new UserTokenPolicyCollection(); + userTokens.Add(new UserTokenPolicy(UserTokenType.UserName)); + + config.ServerConfiguration = new ServerConfiguration() + { + // 配置登录的地址 + BaseAddresses = new string[] { url }, + SecurityPolicies = policies, + UserTokenPolicies = userTokens, + ShutdownDelay = 1, + + DiagnosticsEnabled = false, // 是否启用诊断 + MaxSessionCount = 1000, // 最大打开会话数 + MinSessionTimeout = 10000, // 允许该会话在与客户端断开时(单位毫秒)仍然保持连接的最小时间 + MaxSessionTimeout = 60000, // 允许该会话在与客户端断开时(单位毫秒)仍然保持连接的最大时间 + MaxBrowseContinuationPoints = 1000, // 用于Browse / BrowseNext操作的连续点的最大数量。 + MaxQueryContinuationPoints = 1000, // 用于Query / QueryNext操作的连续点的最大数量 + MaxHistoryContinuationPoints = 500, // 用于HistoryRead操作的最大连续点数。 + MaxRequestAge = 1000000, // 传入请求的最大年龄(旧请求被拒绝)。 + MinPublishingInterval = 100, // 服务器支持的最小发布间隔(以毫秒为单位) + MaxPublishingInterval = 3600000, // 服务器支持的最大发布间隔(以毫秒为单位)1小时 + PublishingResolution = 50, // 支持的发布间隔(以毫秒为单位)的最小差异 + MaxSubscriptionLifetime = 3600000, // 订阅将在没有客户端发布的情况下保持打开多长时间 1小时 + MaxMessageQueueSize = 100, // 每个订阅队列中保存的最大消息数 + MaxNotificationQueueSize = 100, // 为每个被监视项目保存在队列中的最大证书数 + MaxNotificationsPerPublish = 1000, // 每次发布的最大通知数 + MinMetadataSamplingInterval = 1000, // 元数据的最小采样间隔 + MaxRegistrationInterval = 30000, // 两次注册尝试之间的最大时间(以毫秒为单位) + + }; + config.SecurityConfiguration = new SecurityConfiguration() + { + AddAppCertToTrustedStore = true, + AutoAcceptUntrustedCertificates = true, + RejectSHA1SignedCertificates = false, + MinimumCertificateKeySize = 1024, + SuppressNonceValidationErrors = true, + ApplicationCertificate = new CertificateIdentifier() + { + StoreType = CertificateStoreType.X509Store, + StorePath = "CurrentUser\\UAServer_ThingsGateway", + SubjectName = "CN=ThingsGateway OPCUAServer, C=CN, S=GUANGZHOU, O=ThingsGateway, DC=" + System.Net.Dns.GetHostName(), + }, + + TrustedPeerCertificates = new CertificateTrustList() + { + StoreType = CertificateStoreType.Directory, + StorePath = "%CommonApplicationData%\\ThingsGateway\\pki\\issuer", + }, + + TrustedIssuerCertificates = new CertificateTrustList() + { + StoreType = CertificateStoreType.Directory, + StorePath = "%CommonApplicationData%\\ThingsGateway\\pki\\issuer", + }, + + RejectedCertificateStore = new CertificateStoreIdentifier() + { + StoreType = CertificateStoreType.Directory, + StorePath = "%CommonApplicationData%\\ThingsGateway\\pki\\rejected", + }, + UserIssuerCertificates = new CertificateTrustList + { + StoreType = CertificateStoreType.Directory, + StorePath = "%CommonApplicationData%\\ThingsGateway\\pki\\issuerUser", + + }, + TrustedUserCertificates = new CertificateTrustList + { + StoreType = CertificateStoreType.Directory, + StorePath = "%CommonApplicationData%\\ThingsGateway\\pki\\trustedUser", + } + }; + + config.TransportConfigurations = new TransportConfigurationCollection(); + config.TransportQuotas = new TransportQuotas { OperationTimeout = 15000 }; + config.ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 }; + config.TraceConfiguration = new TraceConfiguration(); + + + config.CertificateValidator = new CertificateValidator(); + config.CertificateValidator.Update(config); + config.Extensions = new XmlElementCollection(); + + return config; + } + +} diff --git a/src/Plugins/ThingsGateway.OPCUA/OPCUAServer/OPCUATag.cs b/src/Plugins/ThingsGateway.OPCUA/OPCUAServer/OPCUATag.cs new file mode 100644 index 000000000..254db2750 --- /dev/null +++ b/src/Plugins/ThingsGateway.OPCUA/OPCUAServer/OPCUATag.cs @@ -0,0 +1,13 @@ +using Opc.Ua; +namespace ThingsGateway.OPCUA; +internal class OPCUATag : BaseDataVariableState +{ + public OPCUATag(NodeState parent) : base(parent) + { + } + + /// + /// 变量Id + /// + public long Id { get; set; } +} diff --git a/src/Plugins/ThingsGateway.OPCUA/OPCUAServer/ThingsGatewayNodeManager.cs b/src/Plugins/ThingsGateway.OPCUA/OPCUAServer/ThingsGatewayNodeManager.cs new file mode 100644 index 000000000..32a944858 --- /dev/null +++ b/src/Plugins/ThingsGateway.OPCUA/OPCUAServer/ThingsGatewayNodeManager.cs @@ -0,0 +1,377 @@ +using Mapster; + +using Microsoft.Extensions.DependencyInjection; + +using Opc.Ua; +using Opc.Ua.Server; + +using ThingsGateway.Web.Foundation; + +namespace ThingsGateway.OPCUA; + + +/// +/// 数据节点 +/// +public class ThingsGatewayNodeManager : CustomNodeManager2 +{ + private const string ReferenceServer = "https://diego2098.gitee.io/thingsgateway/"; + + private GlobalCollectDeviceData _globalCollectDeviceData; + + /// + /// OPC和网关对应表 + /// + private Dictionary _idTags = new Dictionary(); + private RpcCore _rpcCore; + private IServiceScope _serviceScope; + /// + public ThingsGatewayNodeManager(IServiceScope serviceScope, IServerInternal server, ApplicationConfiguration configuration) : base(server, configuration, ReferenceServer) + { + _serviceScope = serviceScope; + _rpcCore = serviceScope.ServiceProvider.GetService(); + _globalCollectDeviceData = serviceScope.ServiceProvider.GetService(); + } + + + + /// + /// 创建服务目录结构 + /// + /// + public override void CreateAddressSpace(IDictionary> externalReferences) + { + lock (Lock) + { + IList references = null; + if (!externalReferences.TryGetValue(ObjectIds.ObjectsFolder, out references)) + { + externalReferences[ObjectIds.ObjectsFolder] = references = new List(); + } + //首节点 + FolderState rootFolder = CreateFolder(null, "ThingsGateway", "ThingsGateway"); + rootFolder.AddReference(ReferenceTypes.Organizes, true, ObjectIds.ObjectsFolder); + references.Add(new NodeStateReference(ReferenceTypes.Organizes, false, rootFolder.NodeId)); + rootFolder.EventNotifier = EventNotifiers.SubscribeToEvents; + AddRootNotifier(rootFolder); + + //创建设备树 + var _geviceGroup = CollectDeviceService.GetTree(_globalCollectDeviceData.CollectDevices.ToList().Adapt>()); + // 开始寻找设备信息,并计算一些节点信息 + foreach (var item in _geviceGroup) + { + //设备树会有两层 + FolderState fs = CreateFolder(rootFolder, item.Name, item.Name); + fs.AddReference(ReferenceTypes.Organizes, true, ObjectIds.ObjectsFolder); + fs.EventNotifier = EventNotifiers.SubscribeToEvents; + if (item.Childrens?.Count > 0) + { + foreach (var item2 in item.Childrens) + { + AddTagNode(fs, item2.Name); + } + } + else + { + AddTagNode(fs, item.Name); + } + + } + + AddPredefinedNode(SystemContext, rootFolder); + + } + + } + + /// + /// 读取历史数据 + /// + public override void HistoryRead(OperationContext context, HistoryReadDetails details, TimestampsToReturn timestampsToReturn, bool releaseContinuationPoints, IList nodesToRead, IList results, IList errors) + { + ReadProcessedDetails readDetail = details as ReadProcessedDetails; + //必须带有时间范围 + if (readDetail == null || readDetail.StartTime == DateTime.MinValue || readDetail.EndTime == DateTime.MinValue) + { + errors[0] = StatusCodes.BadHistoryOperationUnsupported; + return; + } + var service = _serviceScope.GetBackgroundService(); + if (!service.StatuString.IsSuccess) + { + errors[0] = StatusCodes.BadHistoryOperationUnsupported; + return; + } + + var db = service.HisConfig().GetAwaiter().GetResult(); + if (!db.IsSuccess) + { + errors[0] = StatusCodes.BadHistoryOperationUnsupported; + return; + } + var startTime = readDetail.StartTime; + var endTime = readDetail.EndTime; + + for (int i = 0; i < nodesToRead.Count; i++) + { + var historyRead = nodesToRead[i]; + if (_idTags.TryGetValue(historyRead.NodeId, out OPCUATag tag)) + { + var data = db.Content.Queryable() + .Where(a => a.Name == tag.SymbolicName) + .Where(a => a.CollectTime >= startTime) + .Where(a => a.CollectTime <= endTime) + .ToList(); + + if (data.Count > 0) + { + results[i] = new HistoryReadResult() + { + StatusCode = StatusCodes.Good, + HistoryData = new ExtensionObject(data) + }; + } + else + { + results[i] = new HistoryReadResult() + { + StatusCode = StatusCodes.GoodNoData + }; + } + } + else + { + results[i] = new HistoryReadResult() + { + StatusCode = StatusCodes.BadNotFound + }; + } + } + base.HistoryRead(context, details, timestampsToReturn, releaseContinuationPoints, nodesToRead, results, errors); + } + /// + public override NodeId New(ISystemContext context, NodeState node) + { + BaseInstanceState instance = node as BaseInstanceState; + if (instance != null && instance.Parent != null) + { + string id = instance.Parent.NodeId.Identifier?.ToString(); + if (id != null) + { + //用下划线分割 + return new NodeId(id + "_" + instance.SymbolicName, instance.Parent.NodeId.NamespaceIndex); + } + } + return node.NodeId; + } + + /// + /// 更新变量 + /// + /// + public void UpVariable(VariableData variable) + { + var uaTag = _idTags.Values.FirstOrDefault(it => it.SymbolicName == variable.name); + if (uaTag == null) return; + object initialItemValue = null; + initialItemValue = variable.value; + if (initialItemValue != null) + { + var code = variable.quality == 192 ? StatusCodes.Good : StatusCodes.Bad; + if (uaTag.Value != initialItemValue) + ChangeNodeData(uaTag.NodeId.ToString(), initialItemValue, variable.changeTime); + if (uaTag.StatusCode != code) + uaTag.SetStatusCode(SystemContext, code, variable.changeTime); + } + } + + /// + /// 添加变量节点 + /// + /// 设备组节点 + /// 设备名称 + private void AddTagNode(FolderState fs, string name) + { + var device = _globalCollectDeviceData.CollectDevices.Where(a => a.Name == name).FirstOrDefault(); + if (device != null) + { + foreach (var item in device.DeviceVariableRunTimes) + { + CreateVariable(fs, item); + } + } + } + + /// + /// 在服务器端直接更改对应数据节点的值,并通知客户端 + /// + private void ChangeNodeData(string nodeId, object value, DateTime dateTime) + { + if (_idTags.ContainsKey(nodeId)) + { + lock (Lock) + { + _idTags[nodeId].Value = value; + _idTags[nodeId].Timestamp = dateTime; + _idTags[nodeId].ClearChangeMasks(SystemContext, false); + } + } + } + + /// + /// 创建文件夹 + /// + private FolderState CreateFolder(NodeState parent, string name, string description) + { + FolderState folder = new FolderState(parent); + + folder.SymbolicName = name; + folder.ReferenceTypeId = ReferenceTypes.Organizes; + folder.TypeDefinitionId = ObjectTypeIds.FolderType; + folder.Description = description; + folder.NodeId = new NodeId(name, NamespaceIndex); + folder.BrowseName = new QualifiedName(name, NamespaceIndex); + folder.DisplayName = new LocalizedText(name); + folder.WriteMask = AttributeWriteMask.None; + folder.UserWriteMask = AttributeWriteMask.None; + folder.EventNotifier = EventNotifiers.None; + + if (parent != null) + { + parent.AddChild(folder); + } + + return folder; + } + + /// + /// 创建一个值节点,类型需要在创建的时候指定 + /// + private OPCUATag CreateVariable(NodeState parent, CollectVariableRunTime variableRunTime) + { + OPCUATag variable = new OPCUATag(parent); + + variable.SymbolicName = variableRunTime.Name; + variable.ReferenceTypeId = ReferenceTypes.Organizes; + variable.TypeDefinitionId = VariableTypeIds.BaseDataVariableType; + variable.NodeId = new NodeId(variableRunTime.Name, NamespaceIndex); + variable.Description = variableRunTime.Description; + variable.BrowseName = new QualifiedName(variableRunTime.Name, NamespaceIndex); + variable.DisplayName = new LocalizedText(variableRunTime.Name); + variable.WriteMask = AttributeWriteMask.DisplayName | AttributeWriteMask.Description; + variable.UserWriteMask = AttributeWriteMask.DisplayName | AttributeWriteMask.Description; + variable.ValueRank = ValueRanks.Scalar; + + + variable.Id = variableRunTime.Id; + variable.DataType = DataNodeType(variableRunTime); + var level = ProtectTypeTrans(variableRunTime.ProtectTypeEnum); + variable.AccessLevel = level; + variable.UserAccessLevel = level; + + variable.Historizing = false; + variable.StatusCode = StatusCodes.Good; + variable.Timestamp = DateTime.Now; + variable.Value = Opc.Ua.TypeInfo.GetDefaultValue(variable.DataType, ValueRanks.Scalar, Server.TypeTree); + variable.OnWriteValue = OnWriteDataValue; + if (parent != null) + { + parent.AddChild(variable); + } + _idTags.Add(variable.NodeId, variable); + return variable; + } + + /// + /// 网关转OPC数据类型 + /// + /// + /// + private NodeId DataNodeType(CollectVariableRunTime variableRunTime) + { + var tp = variableRunTime.DataType; + if (tp == typeof(bool)) + return DataTypeIds.Boolean; + if (tp == typeof(byte)) + return DataTypeIds.Byte; + if (tp == typeof(sbyte)) + return DataTypeIds.SByte; + if (tp == typeof(Int16)) + return DataTypeIds.Int16; + if (tp == typeof(UInt16)) + return DataTypeIds.UInt16; + if (tp == typeof(Int32)) + return DataTypeIds.Int32; + if (tp == typeof(UInt32)) + return DataTypeIds.UInt32; + if (tp == typeof(Int64)) + return DataTypeIds.Int64; + if (tp == typeof(UInt64)) + return DataTypeIds.UInt64; + if (tp == typeof(float)) + return DataTypeIds.Float; + if (tp == typeof(Double)) + return DataTypeIds.Double; + if (tp == typeof(String)) + return DataTypeIds.String; + if (tp == typeof(DateTime)) + return DataTypeIds.TimeString; + return DataTypeIds.ObjectNode; + } + + private ServiceResult OnWriteDataValue(ISystemContext context, NodeState node, NumericRange indexRange, QualifiedName dataEncoding, ref object value, ref StatusCode statusCode, ref DateTime timestamp) + { + try + { + OPCUATag variable = node as OPCUATag; + // 验证数据类型。 + Opc.Ua.TypeInfo typeInfo = Opc.Ua.TypeInfo.IsInstanceOfDataType( + value, + variable.DataType, + variable.ValueRank, + context.NamespaceUris, + context.TypeTable); + + if (typeInfo == null || typeInfo == Opc.Ua.TypeInfo.Unknown) + { + return StatusCodes.BadTypeMismatch; + } + // 检查索引范围。 + if (_idTags.TryGetValue(variable.NodeId, out OPCUATag tag)) + { + if (StatusCode.IsGood(variable.StatusCode)) + { + //仅当指定了值时才将值写入 + if (variable.Value != null) + { + var nv = new NameValue() { Name = variable.SymbolicName, Value = value?.ToString() }; + var result = _rpcCore.InvokeDeviceMethod("OPCUASERVER", nv).GetAwaiter().GetResult(); + if (result.IsSuccess) + { + return StatusCodes.Good; + } + } + } + + } + return StatusCodes.BadWaitingForResponse; + } + catch + { + return StatusCodes.BadTypeMismatch; + } + + } + + private byte ProtectTypeTrans(ProtectTypeEnum protectTypeEnum) + { + switch (protectTypeEnum) + { + case ProtectTypeEnum.ReadOnly: return AccessLevels.CurrentRead; + case ProtectTypeEnum.ReadWrite: + return AccessLevels.CurrentReadOrWrite; + default: + return AccessLevels.CurrentRead; + } + } +} diff --git a/src/Plugins/ThingsGateway.OPCUA/OPCUAServer/ThingsGatewayServer.cs b/src/Plugins/ThingsGateway.OPCUA/OPCUAServer/ThingsGatewayServer.cs new file mode 100644 index 000000000..d36147be5 --- /dev/null +++ b/src/Plugins/ThingsGateway.OPCUA/OPCUAServer/ThingsGatewayServer.cs @@ -0,0 +1,313 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +using Opc.Ua; +using Opc.Ua.Server; + +using System.Security.Cryptography.X509Certificates; + +using ThingsGateway.Application; + +namespace ThingsGateway.OPCUA; + +/// +/// UAServer核心实现 +/// +public partial class ThingsGatewayServer : StandardServer +{ + /// + /// 自定义节点 + /// + public ThingsGatewayNodeManager NodeManager; + private ILogger _logger; + private IServiceScope _serviceScope; + private ICertificateValidator m_userCertificateValidator; + /// + public ThingsGatewayServer(ILogger logger, IServiceScope serviceScope) + { + _logger = logger; + _serviceScope = serviceScope; + } + + /// + public override UserTokenPolicyCollection GetUserTokenPolicies(ApplicationConfiguration configuration, EndpointDescription description) + { + var policies = base.GetUserTokenPolicies(configuration, description); + + // 样品如何修改默认用户令牌的政策 + if (description.SecurityPolicyUri == SecurityPolicies.Aes256_Sha256_RsaPss && + description.SecurityMode == MessageSecurityMode.SignAndEncrypt) + { + policies = new UserTokenPolicyCollection(policies.Where(u => u.TokenType != UserTokenType.Certificate)); + } + else if (description.SecurityPolicyUri == SecurityPolicies.Aes128_Sha256_RsaOaep && + description.SecurityMode == MessageSecurityMode.Sign) + { + policies = new UserTokenPolicyCollection(policies.Where(u => u.TokenType != UserTokenType.Anonymous)); + } + else if (description.SecurityPolicyUri == SecurityPolicies.Aes128_Sha256_RsaOaep && + description.SecurityMode == MessageSecurityMode.SignAndEncrypt) + { + policies = new UserTokenPolicyCollection(policies.Where(u => u.TokenType != UserTokenType.UserName)); + } + return policies; + } + + /// + protected override MasterNodeManager CreateMasterNodeManager(IServerInternal server, ApplicationConfiguration configuration) + { + IList nodeManagers = new List(); + // 创建自定义节点管理器. + NodeManager = new ThingsGatewayNodeManager(_serviceScope, server, configuration); + nodeManagers.Add(NodeManager); + // 创建主节点管理器. + var masterNodeManager = new MasterNodeManager(server, configuration, null, nodeManagers.ToArray()); + return masterNodeManager; + } + + /// + protected override ResourceManager CreateResourceManager(IServerInternal server, ApplicationConfiguration configuration) + { + ResourceManager resourceManager = new ResourceManager(server, configuration); + + System.Reflection.FieldInfo[] fields = typeof(StatusCodes).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + + foreach (System.Reflection.FieldInfo field in fields) + { + uint? id = field.GetValue(typeof(StatusCodes)) as uint?; + + if (id != null) + { + resourceManager.Add(id.Value, "en-US", field.Name); + } + } + + resourceManager.Add("InvalidPassword", "zh-cn", "密码验证失败,'{0}'."); + resourceManager.Add("UnexpectedUserTokenError", "zh-cn", "错误的用户令牌。"); + resourceManager.Add("BadUserAccessDenied", "zh-cn", "当前用户名不存在。"); + + return resourceManager; + } + + /// + protected override ServerProperties LoadServerProperties() + { + ServerProperties properties = new ServerProperties + { + ManufacturerName = "Diego", + ProductName = "ThingsGateway OPCUAServer", + ProductUri = "https://diego2098.gitee.io/thingsgateway", + SoftwareVersion = Utils.GetAssemblySoftwareVersion(), + BuildNumber = Utils.GetAssemblyBuildNumber(), + BuildDate = Utils.GetAssemblyTimestamp() + }; + + return properties; + } + + /// + protected override void OnServerStarted(IServerInternal server) + { + // 当用户身份改变时请求。 + server.SessionManager.ImpersonateUser += SessionManager_ImpersonateUser; + base.OnServerStarted(server); + _logger.LogInformation("OPCUAServer启动成功"); + } + + /// + protected override void OnServerStarting(ApplicationConfiguration configuration) + { + _logger.LogInformation("OPCUAServer启动中......"); + base.OnServerStarting(configuration); + + // 由应用程序决定如何验证用户身份令牌。 + // 此函数为 X509 身份令牌创建验证器。 + CreateUserIdentityValidators(configuration); + } + + /// + protected override void OnServerStopping() + { + _logger.LogInformation("OPCUAServer停止中......"); + base.OnServerStopping(); + } + + private void CreateUserIdentityValidators(ApplicationConfiguration configuration) + { + for (int ii = 0; ii < configuration.ServerConfiguration.UserTokenPolicies.Count; ii++) + { + UserTokenPolicy policy = configuration.ServerConfiguration.UserTokenPolicies[ii]; + + // 为证书令牌策略创建验证器。 + if (policy.TokenType == UserTokenType.Certificate) + { + // check if user certificate trust lists are specified in configuration. + if (configuration.SecurityConfiguration.TrustedUserCertificates != null && + configuration.SecurityConfiguration.UserIssuerCertificates != null) + { + CertificateValidator certificateValidator = new CertificateValidator(); + certificateValidator.Update(configuration.SecurityConfiguration).Wait(); + certificateValidator.Update(configuration.SecurityConfiguration.UserIssuerCertificates, + configuration.SecurityConfiguration.TrustedUserCertificates, + configuration.SecurityConfiguration.RejectedCertificateStore); + + // set custom validator for user certificates. + m_userCertificateValidator = certificateValidator.GetChannelValidator(); + } + } + } + } + + private void SessionManager_ImpersonateUser(Session session, ImpersonateEventArgs args) + { + // check for a user name token. + UserNameIdentityToken userNameToken = args.NewIdentity as UserNameIdentityToken; + + if (userNameToken != null) + { + args.Identity = VerifyPassword(userNameToken); + + + // set AuthenticatedUser role for accepted user/password authentication + args.Identity.GrantedRoleIds.Add(ObjectIds.WellKnownRole_AuthenticatedUser); + + if (args.Identity is SystemConfigurationIdentity) + { + // set ConfigureAdmin role for user with permission to configure server + args.Identity.GrantedRoleIds.Add(ObjectIds.WellKnownRole_ConfigureAdmin); + args.Identity.GrantedRoleIds.Add(ObjectIds.WellKnownRole_SecurityAdmin); + } + + return; + } + + // check for x509 user token. + X509IdentityToken x509Token = args.NewIdentity as X509IdentityToken; + + if (x509Token != null) + { + VerifyUserTokenCertificate(x509Token.Certificate); + args.Identity = new UserIdentity(x509Token); + Utils.LogInfo(Utils.TraceMasks.Security, "X509 Token Accepted: {0}", args.Identity?.DisplayName); + + // set AuthenticatedUser role for accepted certificate authentication + args.Identity.GrantedRoleIds.Add(ObjectIds.WellKnownRole_AuthenticatedUser); + + return; + } + + // check for anonymous token. + if (args.NewIdentity is AnonymousIdentityToken || args.NewIdentity == null) + { + // allow anonymous authentication and set Anonymous role for this authentication + args.Identity = new UserIdentity(); + args.Identity.GrantedRoleIds.Add(ObjectIds.WellKnownRole_Anonymous); + + return; + } + + // unsuported identity token type. + throw ServiceResultException.Create(StatusCodes.BadIdentityTokenInvalid, + "Not supported user token type: {0}.", args.NewIdentity); + } + + /// + /// 从第三方用户中校验 + /// + /// + /// + private IUserIdentity VerifyPassword(UserNameIdentityToken userNameToken) + { + var userName = userNameToken.UserName; + var password = userNameToken.DecryptedPassword; + if (string.IsNullOrEmpty(userName)) + { + // an empty username is not accepted. + throw ServiceResultException.Create(StatusCodes.BadIdentityTokenInvalid, + "Security token is not a valid username token. An empty username is not accepted."); + } + + if (string.IsNullOrEmpty(password)) + { + // an empty password is not accepted. + throw ServiceResultException.Create(StatusCodes.BadIdentityTokenRejected, + "Security token is not a valid username token. An empty password is not accepted."); + } + var _openApiUserService = _serviceScope.ServiceProvider.GetService(); + var userInfo = _openApiUserService.GetUserByAccount(userName).GetAwaiter().GetResult();//获取用户信息 + if (userInfo == null) + { + // construct translation object with default text. + TranslationInfo info = new TranslationInfo( + "InvalidPassword", + "en-US", + "Invalid username or password.", + userName); + + // create an exception with a vendor defined sub-code. + throw new ServiceResultException(new ServiceResult( + StatusCodes.BadUserAccessDenied, + "InvalidPassword", + LoadServerProperties().ProductUri, + new LocalizedText(info))); + } + // 有权配置服务器的用户 + if (userName == userInfo.Account && password == userInfo.Password) + { + return new SystemConfigurationIdentity(new UserIdentity(userNameToken)); + } + else + { + return new UserIdentity(userNameToken); + } + + + } + private void VerifyUserTokenCertificate(X509Certificate2 certificate) + { + try + { + if (m_userCertificateValidator != null) + { + m_userCertificateValidator.Validate(certificate); + } + else + { + CertificateValidator.Validate(certificate); + } + } + catch (Exception e) + { + TranslationInfo info; + StatusCode result = StatusCodes.BadIdentityTokenRejected; + ServiceResultException se = e as ServiceResultException; + if (se != null && se.StatusCode == StatusCodes.BadCertificateUseNotAllowed) + { + info = new TranslationInfo( + "InvalidCertificate", + "en-US", + "'{0}' is an invalid user certificate.", + certificate.Subject); + + result = StatusCodes.BadIdentityTokenInvalid; + } + else + { + // construct translation object with default text. + info = new TranslationInfo( + "UntrustedCertificate", + "en-US", + "'{0}' is not a trusted user certificate.", + certificate.Subject); + } + + // create an exception with a vendor defined sub-code. + throw new ServiceResultException(new ServiceResult( + result, + info.Key, + LoadServerProperties().ProductUri, + new LocalizedText(info))); + } + } + +} diff --git a/src/Plugins/ThingsGateway.OPCUA/ThingsGateway.OPCUA.csproj b/src/Plugins/ThingsGateway.OPCUA/ThingsGateway.OPCUA.csproj index 6c1cbebfe..fce93fced 100644 --- a/src/Plugins/ThingsGateway.OPCUA/ThingsGateway.OPCUA.csproj +++ b/src/Plugins/ThingsGateway.OPCUA/ThingsGateway.OPCUA.csproj @@ -4,7 +4,7 @@ latestMajor net6.0;net7.0 enable - + True 1.1.0 OnBuildSuccess true @@ -33,6 +33,7 @@ Compile +