添加OPCUAServer插件,修复个别bug

This commit is contained in:
2248356998 qq.com
2023-03-30 13:10:31 +08:00
parent a32263d838
commit 9119a28141
12 changed files with 1034 additions and 21 deletions

View File

@@ -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,

View File

@@ -274,7 +274,7 @@ namespace ThingsGateway.Mqtt
MqttRpcResult mqttRpcResult = new();
try
{
var result = await _rpcCore.InvokeDeviceMethod(ToString() + "-" + arg.ClientId, rpcData.Adapt<NameVaue>());
var result = await _rpcCore.InvokeDeviceMethod(ToString() + "-" + arg.ClientId, rpcData.Adapt<NameValue>());
mqttRpcResult = new() { Message = result.Message, RpcId = rpcData.RpcId, Success = result.IsSuccess };

View File

@@ -177,7 +177,7 @@ namespace ThingsGateway.Mqtt
MqttRpcResult mqttRpcResult = new();
try
{
var result = await _rpcCore.InvokeDeviceMethod(ToString() + "-" + IdWithName[arg.ClientId], rpcData.Adapt<NameVaue>());
var result = await _rpcCore.InvokeDeviceMethod(ToString() + "-" + IdWithName[arg.ClientId], rpcData.Adapt<NameValue>());
mqttRpcResult = new() { Message = result.Message, RpcId = rpcData.RpcId, Success = result.IsSuccess };

View File

@@ -12,39 +12,84 @@ using TouchSocket.Core;
namespace ThingsGateway.OPCUA
{
/// <summary>
/// OPCUA客户端
/// </summary>
public class OPCUAClient : DriverBase
{
internal Foundation.Adapter.OPCUA.OPCUAClient PLC = null;
internal CollectDeviceRunTime Device;
internal Foundation.Adapter.OPCUA.OPCUAClient PLC = null;
private List<CollectVariableRunTime> _deviceVariables = new();
/// <inheritdoc cref="OPCUAClient"/>
public OPCUAClient(IServiceScopeFactory scopeFactory) : base(scopeFactory)
{
}
/// <summary>
/// 连接Url
/// </summary>
[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; }
/// <summary>
/// 激活订阅
/// </summary>
[DeviceProperty("激活订阅", "")] public bool ActiveSubscribe { get; set; } = true;
/// <summary>
/// 死区
/// </summary>
[DeviceProperty("死区", "")] public float DeadBand { get; set; } = 0;
public override Type DriverImportUI => typeof(ImportVariable);
/// <summary>
/// 自动分组大小
/// </summary>
[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;
/// <inheritdoc/>
public override Type DriverImportUI => typeof(ImportVariable);
/// <summary>
/// 登录账号
/// </summary>
[DeviceProperty("登录账号", "为空时将采用匿名方式登录")] public string UserName { get; set; }
/// <summary>
/// 登录密码
/// </summary>
[DeviceProperty("登录密码", "")] public string Password { get; set; }
/// <summary>
/// 安全策略
/// </summary>
[DeviceProperty("安全策略", "True为使用安全策略False为无")] public bool IsUseSecurity { get; set; } = true;
/// <summary>
/// 重连频率
/// </summary>
[DeviceProperty("重连频率", "")] public int ReconnectPeriod { get; set; } = 5000;
/// <inheritdoc/>
public override ThingsGatewayBitConverter ThingsGatewayBitConverter { get; } = new(EndianType.Little);
/// <summary>
/// 更新频率
/// </summary>
[DeviceProperty("更新频率", "")] public int UpdateRate { get; set; } = 1000;
/// <inheritdoc/>
public override void AfterStop()
{
PLC?.Disconnect();
}
/// <inheritdoc/>
public override async Task BeforStart()
{
await PLC?.ConnectServer();
}
/// <inheritdoc/>
public override void Dispose()
{
if (PLC != null)
@@ -57,11 +102,19 @@ namespace ThingsGateway.OPCUA
}
}
/// <inheritdoc/>
public override bool IsConnected()
{
return PLC.Connected;
}
/// <inheritdoc/>
public override bool IsSupportAddressRequest()
{
return !ActiveSubscribe;
}
/// <inheritdoc/>
public override OperResult<List<DeviceVariableSourceRead>> LoadSourceRead(List<CollectVariableRunTime> deviceVariables)
{
_deviceVariables = deviceVariables;
@@ -85,6 +138,7 @@ namespace ThingsGateway.OPCUA
}
}
/// <inheritdoc/>
public override async Task<OperResult<byte[]>> ReadSourceAsync(DeviceVariableSourceRead deviceVariableSourceRead, CancellationToken cancellationToken)
{
await Task.CompletedTask;
@@ -100,6 +154,7 @@ namespace ThingsGateway.OPCUA
}
}
/// <inheritdoc/>
public override async Task<OperResult> WriteValueAsync(CollectVariableRunTime deviceVariable, string value)
{
await Task.CompletedTask;
@@ -107,6 +162,7 @@ namespace ThingsGateway.OPCUA
return result ? OperResult.CreateSuccessResult() : new OperResult();
}
/// <inheritdoc/>
protected override void Init(CollectDeviceRunTime device, object client = null)
{
Device = device;
@@ -134,15 +190,12 @@ namespace ThingsGateway.OPCUA
PLC.OPCNode = oPCNode;
}
/// <inheritdoc/>
protected override Task<OperResult<byte[]>> 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

View File

@@ -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;
/// <summary>
/// OPCUA服务端
/// </summary>
public partial class OPCUAServer : UpLoadBase
{
private ApplicationInstance m_application;
private ApplicationConfiguration m_configuration;
private ThingsGatewayServer m_server;
/// <inheritdoc cref="OPCUAServer"/>
public OPCUAServer(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory)
{
}
/// <summary>
/// 服务地址
/// </summary>
[DeviceProperty("服务地址", "")]
public string OpcUaStringUrl { get; set; } = "opc.tcp://127.0.0.1:49321";
/// <summary>
/// 安全策略
/// </summary>
[DeviceProperty("安全策略", "")]
public bool SecurityPolicy { get; set; }
/// <inheritdoc/>
public override async Task BeforStart()
{
// 启动服务器。
await m_application.CheckApplicationInstanceCertificate(true, 0);
await m_application.Start(m_server);
}
/// <inheritdoc/>
public override void Dispose()
{
m_server.Stop();
m_server.Dispose();
m_application.Stop();
}
/// <inheritdoc/>
public override OperResult IsConnected()
{
var result = m_server.GetStatus();
return OperResult.CreateSuccessResult(result);
}
/// <inheritdoc/>
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<VariableData> CollectVariableRunTimes { get; set; } = new();
/// <inheritdoc/>
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>();
_globalCollectDeviceData.CollectVariables.ForEach(a =>
{
VariableValueChange(a);
a.VariableValueChange += VariableValueChange;
});
}
private void VariableValueChange(CollectVariableRunTime collectVariableRunTime)
{
CollectVariableRunTimes.Enqueue(collectVariableRunTime.Adapt<VariableData>());
}
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;
}
}

View File

@@ -0,0 +1,13 @@
using Opc.Ua;
namespace ThingsGateway.OPCUA;
internal class OPCUATag : BaseDataVariableState
{
public OPCUATag(NodeState parent) : base(parent)
{
}
/// <summary>
/// 变量Id
/// </summary>
public long Id { get; set; }
}

View File

@@ -0,0 +1,377 @@
using Mapster;
using Microsoft.Extensions.DependencyInjection;
using Opc.Ua;
using Opc.Ua.Server;
using ThingsGateway.Web.Foundation;
namespace ThingsGateway.OPCUA;
/// <summary>
/// 数据节点
/// </summary>
public class ThingsGatewayNodeManager : CustomNodeManager2
{
private const string ReferenceServer = "https://diego2098.gitee.io/thingsgateway/";
private GlobalCollectDeviceData _globalCollectDeviceData;
/// <summary>
/// OPC和网关对应表
/// </summary>
private Dictionary<NodeId, OPCUATag> _idTags = new Dictionary<NodeId, OPCUATag>();
private RpcCore _rpcCore;
private IServiceScope _serviceScope;
/// <inheritdoc cref="ThingsGatewayNodeManager"/>
public ThingsGatewayNodeManager(IServiceScope serviceScope, IServerInternal server, ApplicationConfiguration configuration) : base(server, configuration, ReferenceServer)
{
_serviceScope = serviceScope;
_rpcCore = serviceScope.ServiceProvider.GetService<RpcCore>();
_globalCollectDeviceData = serviceScope.ServiceProvider.GetService<GlobalCollectDeviceData>();
}
/// <summary>
/// 创建服务目录结构
/// </summary>
/// <param name="externalReferences"></param>
public override void CreateAddressSpace(IDictionary<NodeId, IList<IReference>> externalReferences)
{
lock (Lock)
{
IList<IReference> references = null;
if (!externalReferences.TryGetValue(ObjectIds.ObjectsFolder, out references))
{
externalReferences[ObjectIds.ObjectsFolder] = references = new List<IReference>();
}
//首节点
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<List<CollectDevice>>());
// 开始寻找设备信息,并计算一些节点信息
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);
}
}
/// <summary>
/// 读取历史数据
/// </summary>
public override void HistoryRead(OperationContext context, HistoryReadDetails details, TimestampsToReturn timestampsToReturn, bool releaseContinuationPoints, IList<HistoryReadValueId> nodesToRead, IList<HistoryReadResult> results, IList<ServiceResult> 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<ValueHisHostService>();
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<ValueHis>()
.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);
}
/// <inheritdoc/>
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;
}
/// <summary>
/// 更新变量
/// </summary>
/// <param name="variable"></param>
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);
}
}
/// <summary>
/// 添加变量节点
/// </summary>
/// <param name="fs">设备组节点</param>
/// <param name="name">设备名称</param>
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);
}
}
}
/// <summary>
/// 在服务器端直接更改对应数据节点的值,并通知客户端
/// </summary>
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);
}
}
}
/// <summary>
/// 创建文件夹
/// </summary>
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;
}
/// <summary>
/// 创建一个值节点,类型需要在创建的时候指定
/// </summary>
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;
}
/// <summary>
/// 网关转OPC数据类型
/// </summary>
/// <param name="variableRunTime"></param>
/// <returns></returns>
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;
}
}
}

View File

@@ -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;
/// <summary>
/// UAServer核心实现
/// </summary>
public partial class ThingsGatewayServer : StandardServer
{
/// <summary>
/// 自定义节点
/// </summary>
public ThingsGatewayNodeManager NodeManager;
private ILogger _logger;
private IServiceScope _serviceScope;
private ICertificateValidator m_userCertificateValidator;
/// <inheritdoc cref="ThingsGatewayServer"/>
public ThingsGatewayServer(ILogger logger, IServiceScope serviceScope)
{
_logger = logger;
_serviceScope = serviceScope;
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
protected override MasterNodeManager CreateMasterNodeManager(IServerInternal server, ApplicationConfiguration configuration)
{
IList<INodeManager> nodeManagers = new List<INodeManager>();
// 创建自定义节点管理器.
NodeManager = new ThingsGatewayNodeManager(_serviceScope, server, configuration);
nodeManagers.Add(NodeManager);
// 创建主节点管理器.
var masterNodeManager = new MasterNodeManager(server, configuration, null, nodeManagers.ToArray());
return masterNodeManager;
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
protected override void OnServerStarted(IServerInternal server)
{
// 当用户身份改变时请求。
server.SessionManager.ImpersonateUser += SessionManager_ImpersonateUser;
base.OnServerStarted(server);
_logger.LogInformation("OPCUAServer启动成功");
}
/// <inheritdoc/>
protected override void OnServerStarting(ApplicationConfiguration configuration)
{
_logger.LogInformation("OPCUAServer启动中......");
base.OnServerStarting(configuration);
// 由应用程序决定如何验证用户身份令牌。
// 此函数为 X509 身份令牌创建验证器。
CreateUserIdentityValidators(configuration);
}
/// <inheritdoc/>
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);
}
/// <summary>
/// 从第三方用户中校验
/// </summary>
/// <param name="userNameToken"></param>
/// <returns></returns>
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<IOpenApiUserService>();
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)));
}
}
}

View File

@@ -4,7 +4,7 @@
<LangVersion>latestMajor</LangVersion>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<!--<GenerateDocumentationFile>True</GenerateDocumentationFile>-->
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<Version>1.1.0</Version>
<RunPostBuildEvent>OnBuildSuccess</RunPostBuildEvent>
<EnableDynamicLoading>true</EnableDynamicLoading>
@@ -33,6 +33,7 @@
<PackageReference Include="Masa.Blazor" Version="1.0.0-preview.10">
<IncludeAssets>Compile</IncludeAssets>
</PackageReference>
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Server" Version="1.4.371.60" />
</ItemGroup>
</Project>