@tier0/opcua-client 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -6
- package/lib/client.js +38 -9
- package/lib/compatibility.js +28 -0
- package/lib/security.js +182 -2
- package/nodes/connection.html +19 -9
- package/nodes/connection.js +2 -0
- package/nodes/subscribe.html +2 -0
- package/nodes/subscribe.js +49 -15
- package/package.json +1 -1
- package/prebuilds/linux-x64-musl/opcua_open62541.node +0 -0
- package/src/addon.cc +8 -0
package/README.md
CHANGED
|
@@ -178,19 +178,34 @@ finishes.
|
|
|
178
178
|
|
|
179
179
|
Node editors show only the standard settings by default. Connection security/timeouts/cache, Browse limits/paging, Read/Write batching/output, and Subscribe queue/deadband tuning remain available under a closed **Advanced settings** section. Existing flows retain their stored advanced values.
|
|
180
180
|
|
|
181
|
+
## Server compatibility profiles
|
|
182
|
+
|
|
183
|
+
Connection nodes default to the standard OPC UA profile. Select **Siemens
|
|
184
|
+
WinCC** for classic WinCC or Unified servers that do not advertise usable
|
|
185
|
+
OperationLimits: Read, Write, and MonitoredItem service calls then use a
|
|
186
|
+
conservative fallback of 200 items per request, while advertised non-zero
|
|
187
|
+
server limits still take precedence. Subscribe nodes use at most 1000
|
|
188
|
+
MonitoredItems per Subscription and remove Deadband filters because WinCC does
|
|
189
|
+
not support `ModifyMonitoredItems`/Deadband behavior consistently. The selected
|
|
190
|
+
profile and effective subscription behavior are included in status diagnostics.
|
|
191
|
+
|
|
181
192
|
## Secure connections
|
|
182
193
|
|
|
183
194
|
Connection nodes support `None/None` and `Sign` or `SignAndEncrypt` with
|
|
184
|
-
`Basic256Sha256
|
|
185
|
-
|
|
195
|
+
`Basic256Sha256`, `Aes128_Sha256_RsaOaep`, or `Aes256_Sha256_RsaPss`. Secure
|
|
196
|
+
connections require a client certificate, its private key, and at least one
|
|
197
|
+
trusted Server or CA certificate. Trust is strict: an
|
|
186
198
|
empty trust list is rejected instead of silently accepting every Server
|
|
187
199
|
certificate. Optional CRL files can be supplied as a newline-separated list.
|
|
188
200
|
Password-protected PEM private keys use the separate Node-RED credential field,
|
|
189
201
|
so the password is not stored in the normal Flow configuration.
|
|
190
202
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
203
|
+
Before opening the native client, the package checks certificate validity,
|
|
204
|
+
certificate/private-key matching, RSA key type and minimum 2048-bit strength,
|
|
205
|
+
trusted-certificate validity, and Application URI consistency. When the client
|
|
206
|
+
certificate contains exactly one URI, leaving Application URI empty selects it
|
|
207
|
+
automatically. Username authentication uses the selected secure endpoint and
|
|
208
|
+
its advertised UserTokenPolicy.
|
|
194
209
|
|
|
195
210
|
The Connection editor can call `GetEndpoints`, filters the response to the
|
|
196
211
|
implemented policies, and selects an endpoint compatible with Anonymous or
|
|
@@ -212,7 +227,7 @@ Node-RED Alpine image, the pinned open62541 v1.4.16 source archive, and its
|
|
|
212
227
|
not contact npm or GitHub and does not invoke a compiler:
|
|
213
228
|
|
|
214
229
|
```bash
|
|
215
|
-
npm install ./tier0-opcua-client-0.
|
|
230
|
+
npm install ./tier0-opcua-client-0.6.0.tgz --offline
|
|
216
231
|
```
|
|
217
232
|
|
|
218
233
|
The loader uses the bundled native addon directly. Set
|
package/lib/client.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
const { EventEmitter } = require('events');
|
|
4
4
|
const { chunks } = require('./points');
|
|
5
5
|
const { loadAddon } = require('./native');
|
|
6
|
-
const {
|
|
6
|
+
const { preflightConnectionSecurity } = require('./security');
|
|
7
|
+
const { SIEMENS_WINCC_DEFAULTS, isSiemensWinCC } = require('./compatibility');
|
|
7
8
|
|
|
8
9
|
function normalizeNotificationBatch(notification) {
|
|
9
10
|
if (notification && Array.isArray(notification.changes)) {
|
|
@@ -16,13 +17,17 @@ function normalizeNotificationBatch(notification) {
|
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
function effectiveBatchSize(requested, serverLimit) {
|
|
20
|
+
function effectiveBatchSize(requested, serverLimit, fallbackLimit) {
|
|
20
21
|
const configured = Number.isFinite(Number(requested)) && Number(requested) > 0
|
|
21
22
|
? Math.floor(Number(requested))
|
|
22
23
|
: 500;
|
|
23
24
|
const limit = Number(serverLimit);
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
if (Number.isFinite(limit) && limit > 0) {
|
|
26
|
+
return Math.min(configured, Math.floor(limit));
|
|
27
|
+
}
|
|
28
|
+
const fallback = Number(fallbackLimit);
|
|
29
|
+
return Number.isFinite(fallback) && fallback > 0
|
|
30
|
+
? Math.min(configured, Math.floor(fallback))
|
|
26
31
|
: configured;
|
|
27
32
|
}
|
|
28
33
|
|
|
@@ -55,6 +60,7 @@ class OpcUaClient extends EventEmitter {
|
|
|
55
60
|
operationLimits: {},
|
|
56
61
|
nodeIdCache: {},
|
|
57
62
|
connection: {},
|
|
63
|
+
compatibility: this.#compatibilityDiagnostics(),
|
|
58
64
|
};
|
|
59
65
|
}
|
|
60
66
|
|
|
@@ -85,7 +91,8 @@ class OpcUaClient extends EventEmitter {
|
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
async #connect() {
|
|
88
|
-
const options =
|
|
94
|
+
const options = preflightConnectionSecurity(this.options);
|
|
95
|
+
this.diagnostics.securityPreflight = options.securityPreflight;
|
|
89
96
|
const addon = loadAddon();
|
|
90
97
|
const token = ++this.connectionToken;
|
|
91
98
|
this.connectionState = 'connecting';
|
|
@@ -192,7 +199,8 @@ class OpcUaClient extends EventEmitter {
|
|
|
192
199
|
const results = [];
|
|
193
200
|
await this.connect();
|
|
194
201
|
const effective = effectiveBatchSize(
|
|
195
|
-
batchSize, this.diagnostics.operationLimits?.maxNodesPerRead
|
|
202
|
+
batchSize, this.diagnostics.operationLimits?.maxNodesPerRead,
|
|
203
|
+
this.#compatibilityBatchFallback()
|
|
196
204
|
);
|
|
197
205
|
for (const batch of chunks(points, effective)) {
|
|
198
206
|
results.push(...await this.#invoke('readManyAsync', batch));
|
|
@@ -204,7 +212,8 @@ class OpcUaClient extends EventEmitter {
|
|
|
204
212
|
const results = [];
|
|
205
213
|
await this.connect();
|
|
206
214
|
const effective = effectiveBatchSize(
|
|
207
|
-
batchSize, this.diagnostics.operationLimits?.maxNodesPerWrite
|
|
215
|
+
batchSize, this.diagnostics.operationLimits?.maxNodesPerWrite,
|
|
216
|
+
this.#compatibilityBatchFallback()
|
|
208
217
|
);
|
|
209
218
|
for (const batch of chunks(points, effective)) {
|
|
210
219
|
results.push(...await this.#invoke('writeManyAsync', batch));
|
|
@@ -222,7 +231,8 @@ class OpcUaClient extends EventEmitter {
|
|
|
222
231
|
const results = [];
|
|
223
232
|
await this.connect();
|
|
224
233
|
const effective = effectiveBatchSize(
|
|
225
|
-
batchSize, this.diagnostics.operationLimits?.maxMonitoredItemsPerCall
|
|
234
|
+
batchSize, this.diagnostics.operationLimits?.maxMonitoredItemsPerCall,
|
|
235
|
+
this.#compatibilityBatchFallback()
|
|
226
236
|
);
|
|
227
237
|
for (const batch of chunks(points, effective)) {
|
|
228
238
|
results.push(...await this.#invoke(
|
|
@@ -236,7 +246,8 @@ class OpcUaClient extends EventEmitter {
|
|
|
236
246
|
const results = [];
|
|
237
247
|
await this.connect();
|
|
238
248
|
const effective = effectiveBatchSize(
|
|
239
|
-
batchSize, this.diagnostics.operationLimits?.maxMonitoredItemsPerCall
|
|
249
|
+
batchSize, this.diagnostics.operationLimits?.maxMonitoredItemsPerCall,
|
|
250
|
+
this.#compatibilityBatchFallback()
|
|
240
251
|
);
|
|
241
252
|
for (const batch of chunks(monitoredItemIds, effective)) {
|
|
242
253
|
results.push(...await this.#invoke('removeMonitoredItemsAsync', subscriptionId, batch));
|
|
@@ -261,6 +272,22 @@ class OpcUaClient extends EventEmitter {
|
|
|
261
272
|
return this.diagnostics;
|
|
262
273
|
}
|
|
263
274
|
|
|
275
|
+
#compatibilityBatchFallback() {
|
|
276
|
+
return isSiemensWinCC(this.options.compatibilityProfile)
|
|
277
|
+
? SIEMENS_WINCC_DEFAULTS.requestBatchSize
|
|
278
|
+
: 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
#compatibilityDiagnostics() {
|
|
282
|
+
const siemensWincc = isSiemensWinCC(this.options.compatibilityProfile);
|
|
283
|
+
return {
|
|
284
|
+
profile: siemensWincc ? 'siemens-wincc' : 'standard',
|
|
285
|
+
requestBatchFallback: siemensWincc
|
|
286
|
+
? SIEMENS_WINCC_DEFAULTS.requestBatchSize
|
|
287
|
+
: null,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
264
291
|
#refreshDiagnostics() {
|
|
265
292
|
if (this.handle == null) return;
|
|
266
293
|
const addon = loadAddon();
|
|
@@ -270,6 +297,8 @@ class OpcUaClient extends EventEmitter {
|
|
|
270
297
|
this.diagnostics = {
|
|
271
298
|
...nativeDiagnostics,
|
|
272
299
|
browse: this.diagnostics.browse,
|
|
300
|
+
securityPreflight: this.diagnostics.securityPreflight,
|
|
301
|
+
compatibility: this.#compatibilityDiagnostics(),
|
|
273
302
|
connection: {
|
|
274
303
|
...(nativeDiagnostics.connection || {}),
|
|
275
304
|
supervisor: { ...this.supervisorDiagnostics },
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const COMPATIBILITY_PROFILES = Object.freeze({
|
|
4
|
+
Standard: 'standard',
|
|
5
|
+
SiemensWinCC: 'siemens-wincc',
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const SIEMENS_WINCC_DEFAULTS = Object.freeze({
|
|
9
|
+
requestBatchSize: 200,
|
|
10
|
+
monitoredItemsPerSubscription: 1000,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function normalizeCompatibilityProfile(value) {
|
|
14
|
+
return value === COMPATIBILITY_PROFILES.SiemensWinCC
|
|
15
|
+
? COMPATIBILITY_PROFILES.SiemensWinCC
|
|
16
|
+
: COMPATIBILITY_PROFILES.Standard;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isSiemensWinCC(value) {
|
|
20
|
+
return normalizeCompatibilityProfile(value) === COMPATIBILITY_PROFILES.SiemensWinCC;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
COMPATIBILITY_PROFILES,
|
|
25
|
+
SIEMENS_WINCC_DEFAULTS,
|
|
26
|
+
isSiemensWinCC,
|
|
27
|
+
normalizeCompatibilityProfile,
|
|
28
|
+
};
|
package/lib/security.js
CHANGED
|
@@ -1,18 +1,111 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
|
|
3
6
|
const SECURITY_POLICY_URIS = Object.freeze({
|
|
4
7
|
None: 'http://opcfoundation.org/UA/SecurityPolicy#None',
|
|
5
8
|
Basic256Sha256: 'http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256',
|
|
9
|
+
Aes128_Sha256_RsaOaep: 'http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep',
|
|
10
|
+
Aes256_Sha256_RsaPss: 'http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss',
|
|
6
11
|
});
|
|
7
12
|
|
|
8
13
|
const SECURITY_MODES = new Set(['None', 'Sign', 'SignAndEncrypt']);
|
|
9
14
|
|
|
10
|
-
function configurationError(message) {
|
|
15
|
+
function configurationError(message, code = 'OPCUA_INVALID_SECURITY_CONFIGURATION', cause) {
|
|
11
16
|
const error = new Error(message);
|
|
12
|
-
error.code =
|
|
17
|
+
error.code = code;
|
|
18
|
+
if (cause) error.cause = cause;
|
|
13
19
|
return error;
|
|
14
20
|
}
|
|
15
21
|
|
|
22
|
+
function readSecurityFile(filePath, label, readFileSync = fs.readFileSync) {
|
|
23
|
+
try {
|
|
24
|
+
return readFileSync(filePath);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
throw configurationError(
|
|
27
|
+
`Cannot read OPC UA ${label}: ${filePath}`,
|
|
28
|
+
'OPCUA_SECURITY_FILE_UNREADABLE',
|
|
29
|
+
error
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseCertificate(buffer, label, X509Certificate = crypto.X509Certificate) {
|
|
35
|
+
try {
|
|
36
|
+
return new X509Certificate(buffer);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw configurationError(
|
|
39
|
+
`Invalid OPC UA ${label}`,
|
|
40
|
+
'OPCUA_CERTIFICATE_INVALID',
|
|
41
|
+
error
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function applicationUris(certificate) {
|
|
47
|
+
const output = [];
|
|
48
|
+
const expression = /(?:^|,\s*)URI:((?:"[^"]+")|[^,]+)/g;
|
|
49
|
+
for (const match of String(certificate.subjectAltName || '').matchAll(expression)) {
|
|
50
|
+
const value = match[1].replace(/^"|"$/g, '').trim();
|
|
51
|
+
if (value && !output.includes(value)) output.push(value);
|
|
52
|
+
}
|
|
53
|
+
return output;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validateCertificateTime(certificate, label, now = Date.now()) {
|
|
57
|
+
const validFrom = Date.parse(certificate.validFrom);
|
|
58
|
+
const validTo = Date.parse(certificate.validTo);
|
|
59
|
+
if (Number.isFinite(validFrom) && now < validFrom) {
|
|
60
|
+
throw configurationError(
|
|
61
|
+
`OPC UA ${label} is not valid before ${certificate.validFrom}`,
|
|
62
|
+
'OPCUA_CERTIFICATE_NOT_YET_VALID'
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
if (Number.isFinite(validTo) && now > validTo) {
|
|
66
|
+
throw configurationError(
|
|
67
|
+
`OPC UA ${label} expired at ${certificate.validTo}`,
|
|
68
|
+
'OPCUA_CERTIFICATE_EXPIRED'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function loadPrivateKey(buffer, passphrase, createPrivateKey = crypto.createPrivateKey) {
|
|
74
|
+
const password = passphrase ? String(passphrase) : undefined;
|
|
75
|
+
const attempts = [
|
|
76
|
+
{ key: buffer, passphrase: password },
|
|
77
|
+
{ key: buffer, format: 'der', type: 'pkcs8', passphrase: password },
|
|
78
|
+
{ key: buffer, format: 'der', type: 'pkcs1', passphrase: password },
|
|
79
|
+
];
|
|
80
|
+
let lastError;
|
|
81
|
+
for (const options of attempts) {
|
|
82
|
+
try {
|
|
83
|
+
return createPrivateKey(options);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
lastError = error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
throw configurationError(
|
|
89
|
+
'Invalid OPC UA client private key or key password',
|
|
90
|
+
'OPCUA_PRIVATE_KEY_INVALID',
|
|
91
|
+
lastError
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function certificateSummary(certificate, uri) {
|
|
96
|
+
const publicKey = certificate.publicKey;
|
|
97
|
+
return {
|
|
98
|
+
subject: certificate.subject,
|
|
99
|
+
issuer: certificate.issuer,
|
|
100
|
+
validFrom: certificate.validFrom,
|
|
101
|
+
validTo: certificate.validTo,
|
|
102
|
+
fingerprint256: certificate.fingerprint256,
|
|
103
|
+
applicationUri: uri || null,
|
|
104
|
+
publicKeyType: publicKey?.asymmetricKeyType || null,
|
|
105
|
+
modulusLength: publicKey?.asymmetricKeyDetails?.modulusLength || null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
16
109
|
function normalizePathList(value) {
|
|
17
110
|
const source = Array.isArray(value)
|
|
18
111
|
? value
|
|
@@ -72,6 +165,92 @@ function normalizeConnectionOptions(options = {}) {
|
|
|
72
165
|
return normalized;
|
|
73
166
|
}
|
|
74
167
|
|
|
168
|
+
function preflightConnectionSecurity(options = {}, dependencies = {}) {
|
|
169
|
+
const normalized = normalizeConnectionOptions(options);
|
|
170
|
+
const runtime = {
|
|
171
|
+
readFileSync: dependencies.readFileSync || fs.readFileSync,
|
|
172
|
+
X509Certificate: dependencies.X509Certificate || crypto.X509Certificate,
|
|
173
|
+
createPrivateKey: dependencies.createPrivateKey || crypto.createPrivateKey,
|
|
174
|
+
now: dependencies.now == null ? Date.now() : Number(dependencies.now),
|
|
175
|
+
};
|
|
176
|
+
if (normalized.securityMode === 'None') {
|
|
177
|
+
return {
|
|
178
|
+
...normalized,
|
|
179
|
+
securityPreflight: { enabled: false, policy: normalized.securityPolicy },
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const certificate = parseCertificate(
|
|
184
|
+
readSecurityFile(normalized.certificate, 'client certificate', runtime.readFileSync),
|
|
185
|
+
'client certificate',
|
|
186
|
+
runtime.X509Certificate
|
|
187
|
+
);
|
|
188
|
+
validateCertificateTime(certificate, 'client certificate', runtime.now);
|
|
189
|
+
|
|
190
|
+
const uris = applicationUris(certificate);
|
|
191
|
+
const applicationUri = normalized.applicationUri || (uris.length === 1 ? uris[0] : '');
|
|
192
|
+
if (!applicationUri) {
|
|
193
|
+
throw configurationError(
|
|
194
|
+
'The OPC UA client certificate must contain one Application URI or Application URI must be configured',
|
|
195
|
+
'OPCUA_CERTIFICATE_URI_MISSING'
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
if (!uris.includes(applicationUri)) {
|
|
199
|
+
throw configurationError(
|
|
200
|
+
`Configured Application URI does not match the client certificate: ${applicationUri}`,
|
|
201
|
+
'OPCUA_CERTIFICATE_URI_MISMATCH'
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const privateKey = loadPrivateKey(
|
|
206
|
+
readSecurityFile(normalized.privateKey, 'client private key', runtime.readFileSync),
|
|
207
|
+
normalized.privateKeyPassword,
|
|
208
|
+
runtime.createPrivateKey
|
|
209
|
+
);
|
|
210
|
+
if (!certificate.checkPrivateKey(privateKey)) {
|
|
211
|
+
throw configurationError(
|
|
212
|
+
'The OPC UA client private key does not match the client certificate',
|
|
213
|
+
'OPCUA_PRIVATE_KEY_MISMATCH'
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const keyType = certificate.publicKey?.asymmetricKeyType || '';
|
|
218
|
+
const modulusLength = Number(certificate.publicKey?.asymmetricKeyDetails?.modulusLength) || 0;
|
|
219
|
+
if (!keyType.startsWith('rsa')) {
|
|
220
|
+
throw configurationError(
|
|
221
|
+
`OPC UA ${normalized.securityPolicy} requires an RSA client certificate`,
|
|
222
|
+
'OPCUA_CERTIFICATE_KEY_TYPE_UNSUPPORTED'
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
if (modulusLength > 0 && modulusLength < 2048) {
|
|
226
|
+
throw configurationError(
|
|
227
|
+
`OPC UA client certificate RSA key is too short: ${modulusLength} bits; at least 2048 bits required`,
|
|
228
|
+
'OPCUA_CERTIFICATE_KEY_TOO_SHORT'
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const trust = normalized.trustedCertificates.map((filePath) => {
|
|
233
|
+
const trusted = parseCertificate(
|
|
234
|
+
readSecurityFile(filePath, 'trusted certificate', runtime.readFileSync),
|
|
235
|
+
`trusted certificate: ${filePath}`,
|
|
236
|
+
runtime.X509Certificate
|
|
237
|
+
);
|
|
238
|
+
validateCertificateTime(trusted, `trusted certificate: ${filePath}`, runtime.now);
|
|
239
|
+
return certificateSummary(trusted);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
...normalized,
|
|
244
|
+
applicationUri,
|
|
245
|
+
securityPreflight: {
|
|
246
|
+
enabled: true,
|
|
247
|
+
policy: normalized.securityPolicy,
|
|
248
|
+
clientCertificate: certificateSummary(certificate, applicationUri),
|
|
249
|
+
trustedCertificates: trust,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
75
254
|
function endpointIdentityTypes(endpoint) {
|
|
76
255
|
return [...new Set((endpoint?.userIdentityTokens || []).map((token) => token.tokenType))];
|
|
77
256
|
}
|
|
@@ -92,5 +271,6 @@ module.exports = {
|
|
|
92
271
|
matchingEndpoints,
|
|
93
272
|
normalizeConnectionOptions,
|
|
94
273
|
normalizePathList,
|
|
274
|
+
preflightConnectionSecurity,
|
|
95
275
|
securityPolicyName,
|
|
96
276
|
};
|
package/nodes/connection.html
CHANGED
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
<div class="form-row"><label></label><button type="button" class="red-ui-button" id="node-config-input-discoverEndpoints">Discover endpoints</button> <button type="button" class="red-ui-button" id="node-config-input-testConnection">Test connection</button></div>
|
|
16
16
|
<div class="opcua-discovery-result" id="node-config-input-discoveryResult" style="display:none"><select id="node-config-input-discoveredEndpoint"></select></div>
|
|
17
17
|
<div class="opcua-standard-note" id="node-config-input-testResult"></div>
|
|
18
|
+
<div class="form-row"><label for="node-config-input-compatibilityProfile">Server profile</label><select id="node-config-input-compatibilityProfile"><option value="standard">Standard OPC UA</option><option value="siemens-wincc">Siemens WinCC</option></select></div>
|
|
19
|
+
<div class="opcua-standard-note">Siemens WinCC uses conservative 200-item service batches and disables unsupported Subscription Deadband filters when the Server does not advertise usable limits.</div>
|
|
18
20
|
<div class="form-row"><label for="node-config-input-securityMode">Security</label><select id="node-config-input-securityMode"><option value="None">None</option><option value="Sign">Sign</option><option value="SignAndEncrypt">SignAndEncrypt</option></select></div>
|
|
19
21
|
<div class="opcua-standard-note">Standard defaults include automatic reconnect, connectivity checks, and bounded command/NodeId queues.</div>
|
|
20
22
|
<details class="opcua-advanced">
|
|
21
23
|
<summary>Security and advanced connection settings</summary>
|
|
22
|
-
<div class="form-row"><label for="node-config-input-securityPolicy">Policy</label><select id="node-config-input-securityPolicy"><option value="None">None</option><option value="Basic256Sha256">Basic256Sha256</option></select></div>
|
|
24
|
+
<div class="form-row"><label for="node-config-input-securityPolicy">Policy</label><select id="node-config-input-securityPolicy"><option value="None">None</option><option value="Basic256Sha256">Basic256Sha256</option><option value="Aes128_Sha256_RsaOaep">Aes128_Sha256_RsaOaep</option><option value="Aes256_Sha256_RsaPss">Aes256_Sha256_RsaPss</option></select></div>
|
|
23
25
|
<div class="form-row"><label for="node-config-input-user">User</label><input type="text" id="node-config-input-user"></div>
|
|
24
26
|
<div class="form-row"><label for="node-config-input-password">Password</label><input type="password" id="node-config-input-password"></div>
|
|
25
27
|
<div class="form-row opcua-secure-field"><label for="node-config-input-certificate">Client certificate</label><input type="text" class="opcua-security-path" id="node-config-input-certificate" placeholder="Upload or enter /path/to/client.der"> <button type="button" class="red-ui-button" id="node-config-upload-certificate"><i class="fa fa-upload"></i> Upload</button><input type="file" class="opcua-hidden-file" id="node-config-file-certificate" accept=".der,.pem,.crt,.cer"></div>
|
|
@@ -50,8 +52,9 @@
|
|
|
50
52
|
<li><b>Endpoint</b>:服务器地址,例如 <code>opc.tcp://192.168.1.10:4840</code>。</li>
|
|
51
53
|
<li><b>Discover endpoints</b>:读取服务器公开的安全模式、安全策略和用户认证方式。</li>
|
|
52
54
|
<li><b>Test connection</b>:使用当前配置建立临时连接并验证认证。</li>
|
|
53
|
-
<li><b>
|
|
54
|
-
<li><b
|
|
55
|
+
<li><b>Server profile</b>:默认使用标准 OPC UA 行为;Siemens WinCC 模式在服务器未公布有效 OperationLimits 时按每批 200 点请求,并在订阅中禁用 WinCC 不支持的 Deadband。</li>
|
|
56
|
+
<li><b>Security</b>:支持 None、Sign、SignAndEncrypt,以及 Basic256Sha256、Aes128_Sha256_RsaOaep、Aes256_Sha256_RsaPss;安全模式、策略和认证方式必须与服务器 Endpoint 匹配。</li>
|
|
57
|
+
<li><b>证书</b>:安全连接可上传或指定客户端证书、私钥、信任证书和 CRL。连接前会检查有效期、证书和私钥是否匹配、RSA 密钥强度及 Application URI。若证书只包含一个 URI,可留空并自动采用。</li>
|
|
55
58
|
</ul>
|
|
56
59
|
<h4>建议</h4>
|
|
57
60
|
<p>一个 Connection 配置对应一条客户端 Session。多个业务节点应复用同一个 Connection,避免为相同 Endpoint 重复创建 Session。节点内置自动重连、连通性检查以及有界命令队列。</p>
|
|
@@ -65,8 +68,9 @@
|
|
|
65
68
|
<li><b>Endpoint</b>: Server URL, for example <code>opc.tcp://192.168.1.10:4840</code>.</li>
|
|
66
69
|
<li><b>Discover endpoints</b>: Reads advertised security modes, policies, and user token types.</li>
|
|
67
70
|
<li><b>Test connection</b>: Opens a temporary connection with the current settings and validates authentication.</li>
|
|
68
|
-
<li><b>
|
|
69
|
-
<li><b>
|
|
71
|
+
<li><b>Server profile</b>: Standard uses normal OPC UA behavior. Siemens WinCC falls back to 200-item service batches when usable OperationLimits are not advertised and removes unsupported Subscription Deadband filters.</li>
|
|
72
|
+
<li><b>Security</b>: Supports None, Sign, and SignAndEncrypt with Basic256Sha256, Aes128_Sha256_RsaOaep, or Aes256_Sha256_RsaPss. Mode, policy, and identity must match an advertised endpoint.</li>
|
|
73
|
+
<li><b>Certificates</b>: Secure connections can upload or reference a client certificate, private key, trust list, and CRLs. Validity, key matching, RSA strength, and Application URI are checked before connecting. An empty URI is filled automatically when the certificate contains exactly one URI.</li>
|
|
70
74
|
</ul>
|
|
71
75
|
<h4>Recommendation</h4>
|
|
72
76
|
<p>One Connection configuration owns one client Session. Reuse it across nodes targeting the same server instead of creating duplicate Sessions. Automatic reconnect, connectivity checks, and a bounded command queue are enabled.</p>
|
|
@@ -78,6 +82,7 @@
|
|
|
78
82
|
defaults: {
|
|
79
83
|
endpoint: { value: '', required: true },
|
|
80
84
|
securityMode: { value: 'None' }, securityPolicy: { value: 'None' },
|
|
85
|
+
compatibilityProfile: { value: 'standard' },
|
|
81
86
|
user: { value: '' }, certificate: { value: '' }, privateKey: { value: '' },
|
|
82
87
|
applicationUri: { value: '' }, trustedCertificates: { value: '' }, revocationList: { value: '' },
|
|
83
88
|
requestedSessionTimeout: { value: 60000, validate: RED.validators.number() },
|
|
@@ -207,6 +212,7 @@
|
|
|
207
212
|
endpoint: String($('#node-config-input-endpoint').val() || '').trim(),
|
|
208
213
|
securityMode: mode.val(),
|
|
209
214
|
securityPolicy: policy.val(),
|
|
215
|
+
compatibilityProfile: $('#node-config-input-compatibilityProfile').val(),
|
|
210
216
|
user: $('#node-config-input-user').val(),
|
|
211
217
|
password: $('#node-config-input-password').val(),
|
|
212
218
|
certificate: $('#node-config-input-certificate').val(),
|
|
@@ -245,9 +251,12 @@
|
|
|
245
251
|
const allEndpoints = Array.isArray(response.endpoints) ? response.endpoints : [];
|
|
246
252
|
const identityType = String($('#node-config-input-user').val() || '').trim()
|
|
247
253
|
? 'Username' : 'Anonymous';
|
|
254
|
+
const supportedPolicies = new Set([
|
|
255
|
+
'None', 'Basic256Sha256',
|
|
256
|
+
'Aes128_Sha256_RsaOaep', 'Aes256_Sha256_RsaPss',
|
|
257
|
+
]);
|
|
248
258
|
const supported = allEndpoints.filter(function (item) {
|
|
249
|
-
|
|
250
|
-
return itemPolicy === 'None' || itemPolicy === 'Basic256Sha256';
|
|
259
|
+
return supportedPolicies.has(policyName(item.securityPolicyUri));
|
|
251
260
|
});
|
|
252
261
|
const matchingIdentity = supported.filter(function (item) {
|
|
253
262
|
return (item.userIdentityTokens || []).some(function (token) {
|
|
@@ -271,7 +280,7 @@
|
|
|
271
280
|
.appendTo(discovered);
|
|
272
281
|
});
|
|
273
282
|
if (!endpoints.length) {
|
|
274
|
-
RED.notify('The server returned no supported
|
|
283
|
+
RED.notify('The server returned no endpoint supported by this client', 'warning');
|
|
275
284
|
result.hide();
|
|
276
285
|
return;
|
|
277
286
|
}
|
|
@@ -293,7 +302,8 @@
|
|
|
293
302
|
if (!selected) return;
|
|
294
303
|
const selectedPolicy = policyName(selected.securityPolicyUri);
|
|
295
304
|
mode.val(selected.securityMode);
|
|
296
|
-
if (
|
|
305
|
+
if (['None', 'Basic256Sha256', 'Aes128_Sha256_RsaOaep',
|
|
306
|
+
'Aes256_Sha256_RsaPss'].includes(selectedPolicy)) {
|
|
297
307
|
policy.val(selectedPolicy);
|
|
298
308
|
}
|
|
299
309
|
updateSecurityFields();
|
package/nodes/connection.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { OpcUaClient, discoverEndpoints } = require('../lib/client');
|
|
4
4
|
const { ConnectionRegistry } = require('../lib/connection-registry');
|
|
5
5
|
const { SecurityFileStore } = require('../lib/security-file-store');
|
|
6
|
+
const { normalizeCompatibilityProfile } = require('../lib/compatibility');
|
|
6
7
|
|
|
7
8
|
function pathList(value) {
|
|
8
9
|
const source = Array.isArray(value) ? value : String(value || '').split(/\r?\n|;/);
|
|
@@ -14,6 +15,7 @@ function clientOptions(config, credentials = {}) {
|
|
|
14
15
|
endpoint: config.endpoint,
|
|
15
16
|
securityMode: config.securityMode || 'None',
|
|
16
17
|
securityPolicy: config.securityPolicy || 'None',
|
|
18
|
+
compatibilityProfile: normalizeCompatibilityProfile(config.compatibilityProfile),
|
|
17
19
|
user: config.user || '',
|
|
18
20
|
password: credentials.password || '',
|
|
19
21
|
certificate: config.certificate || '',
|
package/nodes/subscribe.html
CHANGED
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
<li>服务器可以把请求间隔修订为它支持的值。</li>
|
|
72
72
|
</ul>
|
|
73
73
|
<p>新节点默认 Server queue size=1、Discard oldest=true、Queue mode=latest,适合实时状态采集。脉冲、报警转换或完整历史不能丢失时,应改为 all 并增大服务器队列。</p>
|
|
74
|
+
<p>Connection 选择 Siemens WinCC 后,如果服务器没有公布有效限制,每个 Subscription 最多建立 1000 个 MonitoredItem,并自动移除 WinCC 不支持的 Deadband 配置;节点状态消息会报告实际兼容策略。</p>
|
|
74
75
|
<h4>输出</h4>
|
|
75
76
|
<ul>
|
|
76
77
|
<li><b>Compact</b>:NodeId/alias、值、状态和源时间戳。</li>
|
|
@@ -93,6 +94,7 @@
|
|
|
93
94
|
<li>The server may revise requested intervals to supported values.</li>
|
|
94
95
|
</ul>
|
|
95
96
|
<p>New nodes default to server queue size 1, discard-oldest, and local latest coalescing for real-time state. Select all and increase the server queue for pulses, alarm transitions, or history where every change must be retained.</p>
|
|
97
|
+
<p>With the Siemens WinCC Connection profile, each Subscription is conservatively capped at 1000 MonitoredItems when the server advertises no usable limit, and unsupported Deadband filters are removed automatically. Status output reports the active compatibility behavior.</p>
|
|
96
98
|
<h4>Output</h4>
|
|
97
99
|
<ul>
|
|
98
100
|
<li><b>Compact</b>: NodeId/alias, value, status, and source timestamp.</li>
|
package/nodes/subscribe.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { looksLikePointList, messagePoints } = require('../lib/points');
|
|
4
|
+
const { SIEMENS_WINCC_DEFAULTS, isSiemensWinCC } = require('../lib/compatibility');
|
|
4
5
|
|
|
5
6
|
const NOTIFICATION_CONFIG_FIELDS = new Set([
|
|
6
7
|
'nodeId',
|
|
@@ -55,19 +56,19 @@ function positiveInteger(value, fallback) {
|
|
|
55
56
|
return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
function monitoringSettings(point, config) {
|
|
59
|
+
function monitoringSettings(point, config, siemensWincc = false) {
|
|
59
60
|
return {
|
|
60
61
|
samplingInterval: Number(point.samplingInterval) || Number(config.samplingInterval) || 1000,
|
|
61
62
|
queueSize: positiveInteger(point.queueSize, positiveInteger(config.queueSize, 1)),
|
|
62
63
|
discardOldest: point.discardOldest !== false,
|
|
63
|
-
deadbandType: point.deadbandType || config.deadbandType || 'none',
|
|
64
|
-
deadbandValue: Number(point.deadbandValue ?? config.deadbandValue) || 0,
|
|
64
|
+
deadbandType: siemensWincc ? 'none' : point.deadbandType || config.deadbandType || 'none',
|
|
65
|
+
deadbandValue: siemensWincc ? 0 : Number(point.deadbandValue ?? config.deadbandValue) || 0,
|
|
65
66
|
};
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
function sameMonitoringSettings(left, right, config) {
|
|
69
|
-
const leftSettings = monitoringSettings(left, config);
|
|
70
|
-
const rightSettings = monitoringSettings(right, config);
|
|
69
|
+
function sameMonitoringSettings(left, right, config, siemensWincc = false) {
|
|
70
|
+
const leftSettings = monitoringSettings(left, config, siemensWincc);
|
|
71
|
+
const rightSettings = monitoringSettings(right, config, siemensWincc);
|
|
71
72
|
return Object.keys(leftSettings).every(
|
|
72
73
|
(key) => leftSettings[key] === rightSettings[key]
|
|
73
74
|
);
|
|
@@ -111,6 +112,7 @@ module.exports = function registerSubscribeNode(RED) {
|
|
|
111
112
|
node.closing = false;
|
|
112
113
|
node.lastDropped = 0;
|
|
113
114
|
node.lastStatusAt = 0;
|
|
115
|
+
node.compatibilityWarningIssued = false;
|
|
114
116
|
const consumer = node.connection?.registerConsumer?.('subscribe', {
|
|
115
117
|
nodeId: config.id, name: config.name,
|
|
116
118
|
});
|
|
@@ -118,7 +120,16 @@ module.exports = function registerSubscribeNode(RED) {
|
|
|
118
120
|
const batchSize = positiveInteger(config.batchSize, 500);
|
|
119
121
|
const callbackBatchSize = positiveInteger(config.callbackBatchSize, 500);
|
|
120
122
|
const callbackFlushIntervalMs = positiveInteger(config.callbackFlushIntervalMs, 50);
|
|
121
|
-
const
|
|
123
|
+
const siemensWincc = isSiemensWinCC(
|
|
124
|
+
node.connection?.client?.options?.compatibilityProfile
|
|
125
|
+
);
|
|
126
|
+
const requestedSubscriptionCapacity = positiveInteger(config.maxItemsPerSubscription, 5000);
|
|
127
|
+
const configuredSubscriptionCapacity = siemensWincc
|
|
128
|
+
? Math.min(
|
|
129
|
+
requestedSubscriptionCapacity,
|
|
130
|
+
SIEMENS_WINCC_DEFAULTS.monitoredItemsPerSubscription
|
|
131
|
+
)
|
|
132
|
+
: requestedSubscriptionCapacity;
|
|
122
133
|
// Undefined preserves the descriptor-rich output used by Flow JSON created
|
|
123
134
|
// before resultDetail existed. New editor nodes explicitly save compact.
|
|
124
135
|
const resultDetail = ['compact', 'value', 'full'].includes(config.resultDetail)
|
|
@@ -129,6 +140,24 @@ module.exports = function registerSubscribeNode(RED) {
|
|
|
129
140
|
return [...node.subscriptions.keys()];
|
|
130
141
|
}
|
|
131
142
|
|
|
143
|
+
function applyCompatibility(points) {
|
|
144
|
+
if (!siemensWincc) return points;
|
|
145
|
+
let removedDeadband = false;
|
|
146
|
+
const compatible = points.map((point) => {
|
|
147
|
+
const configuredType = point.deadbandType || config.deadbandType || 'none';
|
|
148
|
+
if (configuredType === 'none') return point;
|
|
149
|
+
removedDeadband = true;
|
|
150
|
+
return { ...point, deadbandType: 'none', deadbandValue: 0 };
|
|
151
|
+
});
|
|
152
|
+
if (removedDeadband && !node.compatibilityWarningIssued) {
|
|
153
|
+
node.compatibilityWarningIssued = true;
|
|
154
|
+
node.warn(
|
|
155
|
+
'Siemens WinCC does not support OPC UA DataChange Deadband filters; Deadband was disabled.'
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return compatible;
|
|
159
|
+
}
|
|
160
|
+
|
|
132
161
|
function primarySubscriptionId() {
|
|
133
162
|
return subscriptionIds()[0] ?? null;
|
|
134
163
|
}
|
|
@@ -325,8 +354,8 @@ module.exports = function registerSubscribeNode(RED) {
|
|
|
325
354
|
samplingInterval: Number(config.samplingInterval) || 1000,
|
|
326
355
|
queueSize: Number(config.queueSize) || 10,
|
|
327
356
|
discardOldest: config.discardOldest !== false,
|
|
328
|
-
deadbandType: config.deadbandType || 'none',
|
|
329
|
-
deadbandValue: Number(config.deadbandValue) || 0,
|
|
357
|
+
deadbandType: siemensWincc ? 'none' : config.deadbandType || 'none',
|
|
358
|
+
deadbandValue: siemensWincc ? 0 : Number(config.deadbandValue) || 0,
|
|
330
359
|
timestampMode: config.timestampMode || 'source',
|
|
331
360
|
},
|
|
332
361
|
batchSize
|
|
@@ -497,6 +526,11 @@ module.exports = function registerSubscribeNode(RED) {
|
|
|
497
526
|
queueSizeMin: revisedQueues.minimum,
|
|
498
527
|
queueSizeMax: revisedQueues.maximum,
|
|
499
528
|
},
|
|
529
|
+
compatibility: {
|
|
530
|
+
profile: siemensWincc ? 'siemens-wincc' : 'standard',
|
|
531
|
+
deadbandSupported: !siemensWincc,
|
|
532
|
+
monitoredItemsPerSubscription: configuredSubscriptionCapacity,
|
|
533
|
+
},
|
|
500
534
|
connectionGovernance: node.connection?.getConnectionGovernance?.(),
|
|
501
535
|
connectionUsage: node.connection?.getConnectionUsage?.(),
|
|
502
536
|
};
|
|
@@ -563,7 +597,7 @@ module.exports = function registerSubscribeNode(RED) {
|
|
|
563
597
|
for (const [nodeId, monitored] of node.monitored) {
|
|
564
598
|
const previous = node.desired.get(nodeId);
|
|
565
599
|
const next = nextDesired.get(nodeId);
|
|
566
|
-
if (!next || !previous || !sameMonitoringSettings(previous, next, config)) {
|
|
600
|
+
if (!next || !previous || !sameMonitoringSettings(previous, next, config, siemensWincc)) {
|
|
567
601
|
changed.add(nodeId);
|
|
568
602
|
}
|
|
569
603
|
}
|
|
@@ -626,14 +660,14 @@ module.exports = function registerSubscribeNode(RED) {
|
|
|
626
660
|
const configuredPayload = !looksLikePointList(msg.payload) && !Array.isArray(msg.items) &&
|
|
627
661
|
!dynamicNodeId ? config.pointList : '';
|
|
628
662
|
const staticNodeId = dynamicNodeId ? '' : config.nodeId;
|
|
629
|
-
const points = command === 'clear' ? [] : messagePoints(msg, staticNodeId, {
|
|
663
|
+
const points = command === 'clear' ? [] : applyCompatibility(messagePoints(msg, staticNodeId, {
|
|
630
664
|
samplingInterval: Number(config.samplingInterval) || 1000,
|
|
631
665
|
queueSize: Number(config.queueSize) || 10,
|
|
632
666
|
discardOldest: config.discardOldest !== false,
|
|
633
667
|
}, {
|
|
634
668
|
configuredPayload,
|
|
635
669
|
format: 'auto',
|
|
636
|
-
});
|
|
670
|
+
}));
|
|
637
671
|
|
|
638
672
|
node.status({ fill: 'blue', shape: 'dot', text: `${command} ${points.length}` });
|
|
639
673
|
await runSerialized(async () => {
|
|
@@ -642,7 +676,7 @@ module.exports = function registerSubscribeNode(RED) {
|
|
|
642
676
|
const changed = new Set();
|
|
643
677
|
points.forEach((point) => {
|
|
644
678
|
const previous = node.desired.get(point.nodeId);
|
|
645
|
-
if (previous && !sameMonitoringSettings(previous, point, config)) {
|
|
679
|
+
if (previous && !sameMonitoringSettings(previous, point, config, siemensWincc)) {
|
|
646
680
|
changed.add(point.nodeId);
|
|
647
681
|
}
|
|
648
682
|
node.desired.set(point.nodeId, point);
|
|
@@ -710,14 +744,14 @@ module.exports = function registerSubscribeNode(RED) {
|
|
|
710
744
|
if (node.closing) return;
|
|
711
745
|
let points;
|
|
712
746
|
try {
|
|
713
|
-
points = messagePoints({}, config.nodeId, {
|
|
747
|
+
points = applyCompatibility(messagePoints({}, config.nodeId, {
|
|
714
748
|
samplingInterval: Number(config.samplingInterval) || 1000,
|
|
715
749
|
queueSize: Number(config.queueSize) || 10,
|
|
716
750
|
discardOldest: config.discardOldest !== false,
|
|
717
751
|
}, {
|
|
718
752
|
configuredPayload: config.pointList,
|
|
719
753
|
format: 'auto',
|
|
720
|
-
});
|
|
754
|
+
}));
|
|
721
755
|
} catch (error) {
|
|
722
756
|
node.status({ fill: 'red', shape: 'ring', text: error.message });
|
|
723
757
|
node.warn(`OPC UA saved subscription point list is invalid: ${error.message}`);
|
package/package.json
CHANGED
|
Binary file
|
package/src/addon.cc
CHANGED
|
@@ -49,6 +49,14 @@ std::string SecurityPolicyUri(const std::string& policy) {
|
|
|
49
49
|
policy == "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256") {
|
|
50
50
|
return "http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256";
|
|
51
51
|
}
|
|
52
|
+
if (policy == "Aes128_Sha256_RsaOaep" ||
|
|
53
|
+
policy == "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep") {
|
|
54
|
+
return "http://opcfoundation.org/UA/SecurityPolicy#Aes128_Sha256_RsaOaep";
|
|
55
|
+
}
|
|
56
|
+
if (policy == "Aes256_Sha256_RsaPss" ||
|
|
57
|
+
policy == "http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss") {
|
|
58
|
+
return "http://opcfoundation.org/UA/SecurityPolicy#Aes256_Sha256_RsaPss";
|
|
59
|
+
}
|
|
52
60
|
return "";
|
|
53
61
|
}
|
|
54
62
|
|