@tier0/opcua-client 0.1.0 → 0.4.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/BENCHMARK.md +53 -2
- package/README.md +180 -14
- package/THIRD_PARTY_NOTICES.md +20 -0
- package/binding.gyp +1 -1
- package/lib/client.js +48 -7
- package/lib/connection-registry.js +68 -0
- package/lib/native.js +25 -10
- package/lib/points.js +187 -21
- package/lib/prebuild.js +42 -0
- package/lib/security-file-store.js +119 -0
- package/lib/security.js +96 -0
- package/node_modules/node-addon-api/LICENSE.md +9 -0
- package/node_modules/node-addon-api/README.md +95 -0
- package/node_modules/node-addon-api/common.gypi +21 -0
- package/node_modules/node-addon-api/except.gypi +25 -0
- package/node_modules/node-addon-api/index.js +14 -0
- package/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
- package/node_modules/node-addon-api/napi-inl.h +7244 -0
- package/node_modules/node-addon-api/napi.h +3420 -0
- package/node_modules/node-addon-api/node_addon_api.gyp +42 -0
- package/node_modules/node-addon-api/node_api.gyp +9 -0
- package/node_modules/node-addon-api/noexcept.gypi +26 -0
- package/node_modules/node-addon-api/nothing.c +0 -0
- package/node_modules/node-addon-api/package-support.json +21 -0
- package/node_modules/node-addon-api/package.json +481 -0
- package/node_modules/node-addon-api/tools/README.md +73 -0
- package/node_modules/node-addon-api/tools/check-napi.js +99 -0
- package/node_modules/node-addon-api/tools/clang-format.js +71 -0
- package/node_modules/node-addon-api/tools/conversion.js +301 -0
- package/nodes/browse.html +27 -2
- package/nodes/connection.html +293 -6
- package/nodes/connection.js +160 -23
- package/nodes/read.html +42 -3
- package/nodes/read.js +51 -12
- package/nodes/subscribe.html +57 -5
- package/nodes/subscribe.js +386 -99
- package/nodes/write.html +29 -2
- package/nodes/write.js +2 -1
- package/package.json +15 -4
- package/prebuilds/linux-x64-musl/opcua_open62541.node +0 -0
- package/scripts/build-offline-prebuild.sh +15 -0
- package/scripts/build-open62541.sh +14 -4
- package/scripts/build-test-server.sh +23 -0
- package/scripts/install.js +28 -0
- package/scripts/verify-offline-package.js +20 -0
- package/scripts/verify-runtime.js +20 -0
- package/src/addon.cc +707 -119
- package/vendor/open62541-LICENSE +373 -0
- package/vendor/open62541-v1.4.16.tar.gz +0 -0
package/lib/points.js
CHANGED
|
@@ -11,6 +11,32 @@ function positiveNumber(value, fallback) {
|
|
|
11
11
|
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function booleanValue(value, fallback) {
|
|
15
|
+
if (value == null || value === '') return fallback;
|
|
16
|
+
if (typeof value === 'boolean') return value;
|
|
17
|
+
if (value === 1 || /^(true|1)$/i.test(String(value))) return true;
|
|
18
|
+
if (value === 0 || /^(false|0)$/i.test(String(value))) return false;
|
|
19
|
+
throw new Error(`Invalid Boolean value: ${value}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeIndexRange(value, fallback = '') {
|
|
23
|
+
const source = value == null || value === '' ? fallback : value;
|
|
24
|
+
const text = String(source == null ? '' : source).trim();
|
|
25
|
+
if (!text) return '';
|
|
26
|
+
const dimensions = text.split(',').map((dimension) => dimension.trim());
|
|
27
|
+
for (const dimension of dimensions) {
|
|
28
|
+
if (!/^\d+(?::\d+)?$/.test(dimension)) {
|
|
29
|
+
throw new Error(`Invalid OPC UA IndexRange: ${text}`);
|
|
30
|
+
}
|
|
31
|
+
const bounds = dimension.split(':').map(Number);
|
|
32
|
+
if (bounds.some((bound) => !Number.isSafeInteger(bound) || bound > 0xffffffff) ||
|
|
33
|
+
(bounds.length === 2 && bounds[0] > bounds[1])) {
|
|
34
|
+
throw new Error(`Invalid OPC UA IndexRange: ${text}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return dimensions.join(',');
|
|
38
|
+
}
|
|
39
|
+
|
|
14
40
|
function normalizePoint(input, defaults = {}) {
|
|
15
41
|
const source = typeof input === 'string' ? { nodeId: input } : { ...(input || {}) };
|
|
16
42
|
const nodeId = String(source.nodeId || source.topic || '').trim();
|
|
@@ -27,14 +53,44 @@ function normalizePoint(input, defaults = {}) {
|
|
|
27
53
|
source.queueSize,
|
|
28
54
|
positiveNumber(defaults.queueSize, DEFAULTS.queueSize)
|
|
29
55
|
)),
|
|
30
|
-
discardOldest:
|
|
31
|
-
|
|
32
|
-
|
|
56
|
+
discardOldest: booleanValue(
|
|
57
|
+
source.discardOldest,
|
|
58
|
+
booleanValue(defaults.discardOldest, true)
|
|
59
|
+
),
|
|
33
60
|
};
|
|
34
61
|
|
|
62
|
+
const indexRange = normalizeIndexRange(
|
|
63
|
+
source.indexRange ?? source.range,
|
|
64
|
+
defaults.indexRange ?? defaults.range
|
|
65
|
+
);
|
|
66
|
+
if (indexRange) point.indexRange = indexRange;
|
|
67
|
+
else {
|
|
68
|
+
delete point.indexRange;
|
|
69
|
+
}
|
|
70
|
+
delete point.range;
|
|
71
|
+
|
|
35
72
|
if (point.alias != null) point.alias = String(point.alias);
|
|
73
|
+
else delete point.alias;
|
|
36
74
|
if (point.dataType != null) point.dataType = String(point.dataType);
|
|
37
|
-
|
|
75
|
+
else delete point.dataType;
|
|
76
|
+
if (point.deadbandType == null || point.deadbandType === '') delete point.deadbandType;
|
|
77
|
+
else {
|
|
78
|
+
const deadbandType = String(point.deadbandType).toLowerCase();
|
|
79
|
+
const normalizedDeadbandType = deadbandType === 'a' ? 'absolute' :
|
|
80
|
+
(deadbandType === 'p' ? 'percent' : deadbandType);
|
|
81
|
+
if (!['none', 'absolute', 'percent'].includes(normalizedDeadbandType)) {
|
|
82
|
+
throw new Error(`Invalid OPC UA deadband type: ${point.deadbandType}`);
|
|
83
|
+
}
|
|
84
|
+
point.deadbandType = normalizedDeadbandType;
|
|
85
|
+
}
|
|
86
|
+
if (point.deadbandValue == null || point.deadbandValue === '') delete point.deadbandValue;
|
|
87
|
+
else {
|
|
88
|
+
point.deadbandValue = Number(point.deadbandValue);
|
|
89
|
+
if (!Number.isFinite(point.deadbandValue) || point.deadbandValue < 0) {
|
|
90
|
+
throw new Error(`Invalid OPC UA deadband value: ${source.deadbandValue}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
point.enabled = booleanValue(point.enabled, true);
|
|
38
94
|
return point;
|
|
39
95
|
}
|
|
40
96
|
|
|
@@ -94,20 +150,38 @@ function parseCsvLine(line) {
|
|
|
94
150
|
return fields;
|
|
95
151
|
}
|
|
96
152
|
|
|
97
|
-
function
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
153
|
+
function forEachCsvLine(text, callback) {
|
|
154
|
+
const source = String(text).replace(/^\uFEFF/, '');
|
|
155
|
+
let start = 0;
|
|
156
|
+
let lineNumber = 0;
|
|
157
|
+
while (start <= source.length) {
|
|
158
|
+
let end = source.indexOf('\n', start);
|
|
159
|
+
if (end === -1) end = source.length;
|
|
160
|
+
const line = source.slice(start, end).replace(/\r$/, '');
|
|
161
|
+
lineNumber += 1;
|
|
162
|
+
if (line.trim() && !line.trimStart().startsWith('#')) callback(line, lineNumber);
|
|
163
|
+
if (end === source.length) break;
|
|
164
|
+
start = end + 1;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
106
167
|
|
|
107
|
-
|
|
168
|
+
function parseCsv(text) {
|
|
169
|
+
let headers;
|
|
170
|
+
const rows = [];
|
|
171
|
+
forEachCsvLine(text, (line) => {
|
|
172
|
+
if (!headers) {
|
|
173
|
+
headers = parseCsvLine(line).map((header) => header.trim());
|
|
174
|
+
if (!headers.includes('nodeId')) throw new Error('CSV header must contain nodeId');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
108
177
|
const values = parseCsvLine(line);
|
|
109
|
-
|
|
178
|
+
const row = {};
|
|
179
|
+
for (let index = 0; index < headers.length; index += 1) {
|
|
180
|
+
row[headers[index]] = values[index] ?? '';
|
|
181
|
+
}
|
|
182
|
+
rows.push(row);
|
|
110
183
|
});
|
|
184
|
+
return rows;
|
|
111
185
|
}
|
|
112
186
|
|
|
113
187
|
function parsePointPayload(payload, format = 'auto') {
|
|
@@ -157,7 +231,17 @@ function messagePoints(msg, staticNodeId, defaults = {}, options = {}) {
|
|
|
157
231
|
|
|
158
232
|
const nodeId = staticNodeId || msg.nodeId || msg.topic;
|
|
159
233
|
if (!nodeId) throw new Error('Missing OPC UA nodeId or point list');
|
|
160
|
-
return [normalizePoint({
|
|
234
|
+
return [normalizePoint({
|
|
235
|
+
nodeId,
|
|
236
|
+
alias: msg.alias,
|
|
237
|
+
dataType: msg.dataType,
|
|
238
|
+
indexRange: msg.indexRange ?? msg.range ?? msg.opcua?.indexRange,
|
|
239
|
+
samplingInterval: msg.samplingInterval ?? msg.interval,
|
|
240
|
+
queueSize: msg.queueSize,
|
|
241
|
+
discardOldest: msg.discardOldest,
|
|
242
|
+
deadbandType: msg.deadbandType,
|
|
243
|
+
deadbandValue: msg.deadbandValue,
|
|
244
|
+
}, defaults)];
|
|
161
245
|
}
|
|
162
246
|
|
|
163
247
|
function coerceWriteValue(value, dataType) {
|
|
@@ -176,20 +260,101 @@ function coerceWriteValue(value, dataType) {
|
|
|
176
260
|
return value;
|
|
177
261
|
}
|
|
178
262
|
|
|
263
|
+
function parseWriteCsvPayload(payload, msg, config) {
|
|
264
|
+
const text = Buffer.isBuffer(payload) ? payload.toString('utf8') : String(payload);
|
|
265
|
+
let headers;
|
|
266
|
+
let nodeIdIndex = -1;
|
|
267
|
+
let valueIndex = -1;
|
|
268
|
+
let dataTypeIndex = -1;
|
|
269
|
+
let indexRangeIndex = -1;
|
|
270
|
+
let invalid = 0;
|
|
271
|
+
const points = new Map();
|
|
272
|
+
|
|
273
|
+
forEachCsvLine(text, (line) => {
|
|
274
|
+
if (!headers) {
|
|
275
|
+
headers = parseCsvLine(line).map((header) => header.trim());
|
|
276
|
+
nodeIdIndex = headers.indexOf('nodeId');
|
|
277
|
+
valueIndex = headers.indexOf('value');
|
|
278
|
+
dataTypeIndex = headers.indexOf('dataType');
|
|
279
|
+
indexRangeIndex = headers.indexOf('indexRange');
|
|
280
|
+
if (indexRangeIndex < 0) indexRangeIndex = headers.indexOf('range');
|
|
281
|
+
if (nodeIdIndex < 0) throw new Error('CSV header must contain nodeId');
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const values = parseCsvLine(line);
|
|
286
|
+
const nodeId = String(values[nodeIdIndex] || '').trim();
|
|
287
|
+
if (!nodeId) {
|
|
288
|
+
invalid += 1;
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const dataType = String(
|
|
292
|
+
(dataTypeIndex >= 0 ? values[dataTypeIndex] : '') ||
|
|
293
|
+
msg.dataType || config.dataType || 'String'
|
|
294
|
+
);
|
|
295
|
+
const value = valueIndex >= 0 ? values[valueIndex] : undefined;
|
|
296
|
+
const defaultIndexRange = msg.indexRange ?? msg.range ??
|
|
297
|
+
msg.opcua?.indexRange ?? config.indexRange;
|
|
298
|
+
const indexRange = normalizeIndexRange(
|
|
299
|
+
indexRangeIndex >= 0 ? values[indexRangeIndex] : undefined,
|
|
300
|
+
defaultIndexRange
|
|
301
|
+
);
|
|
302
|
+
points.set(nodeId, {
|
|
303
|
+
nodeId,
|
|
304
|
+
dataType,
|
|
305
|
+
value: coerceWriteValue(value, dataType),
|
|
306
|
+
...(indexRange ? { indexRange } : {}),
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
if (invalid) throw new Error(`Invalid point list: ${invalid} item(s) failed validation`);
|
|
311
|
+
return [...points.values()];
|
|
312
|
+
}
|
|
313
|
+
|
|
179
314
|
function messageWritePoints(msg, config = {}) {
|
|
180
315
|
let points;
|
|
181
316
|
let configuredPoints = false;
|
|
182
|
-
|
|
183
|
-
|
|
317
|
+
let configuredPointList;
|
|
318
|
+
const defaultIndexRange = msg.indexRange ?? msg.range ??
|
|
319
|
+
msg.opcua?.indexRange ?? config.indexRange;
|
|
320
|
+
if (config.pointList && Array.isArray(msg.payload)) {
|
|
321
|
+
configuredPointList = normalizePointPayload(
|
|
322
|
+
config.pointList, { indexRange: defaultIndexRange }, { format: 'auto' }
|
|
323
|
+
).points;
|
|
324
|
+
}
|
|
325
|
+
const directNodeId = config.nodeId || msg.nodeId || msg.topic;
|
|
326
|
+
const arrayValue = Array.isArray(msg.payload) && msg.opcua?.pointList !== true && (
|
|
327
|
+
msg.opcua?.arrayValue === true || directNodeId ||
|
|
328
|
+
(configuredPointList?.length === 1 && configuredPointList[0].useInput)
|
|
329
|
+
);
|
|
330
|
+
if (!arrayValue && looksLikePointList(msg.payload)) {
|
|
331
|
+
const inputFormat = config.inputFormat || 'auto';
|
|
332
|
+
const hasTextPayload = typeof msg.payload === 'string' || Buffer.isBuffer(msg.payload);
|
|
333
|
+
const text = Buffer.isBuffer(msg.payload)
|
|
334
|
+
? msg.payload.toString('utf8').trimStart()
|
|
335
|
+
: typeof msg.payload === 'string' ? msg.payload.trimStart() : '';
|
|
336
|
+
const isCsv = hasTextPayload && (inputFormat === 'csv' ||
|
|
337
|
+
(inputFormat === 'auto' && text && !text.startsWith('[') && !text.startsWith('{')));
|
|
338
|
+
if (isCsv) return parseWriteCsvPayload(msg.payload, msg, config);
|
|
339
|
+
points = normalizePointPayload(
|
|
340
|
+
msg.payload, { indexRange: defaultIndexRange }, { format: inputFormat }
|
|
341
|
+
).points;
|
|
184
342
|
} else if (msg.payload && typeof msg.payload === 'object' && msg.payload.nodeId) {
|
|
185
|
-
points = normalizePoints([msg.payload]).points;
|
|
343
|
+
points = normalizePoints([msg.payload], { indexRange: defaultIndexRange }).points;
|
|
186
344
|
} else if (config.pointList) {
|
|
187
|
-
points =
|
|
345
|
+
points = configuredPointList ||
|
|
346
|
+
normalizePointPayload(
|
|
347
|
+
config.pointList, { indexRange: defaultIndexRange }, { format: 'auto' }
|
|
348
|
+
).points;
|
|
188
349
|
configuredPoints = true;
|
|
189
350
|
} else {
|
|
190
351
|
const nodeId = config.nodeId || msg.nodeId || msg.topic;
|
|
191
352
|
if (!nodeId) throw new Error('Missing OPC UA nodeId or point list');
|
|
192
|
-
points = normalizePoints([{
|
|
353
|
+
points = normalizePoints([{
|
|
354
|
+
nodeId,
|
|
355
|
+
value: msg.payload,
|
|
356
|
+
indexRange: defaultIndexRange,
|
|
357
|
+
}]).points;
|
|
193
358
|
}
|
|
194
359
|
|
|
195
360
|
return points.map((point) => {
|
|
@@ -223,6 +388,7 @@ module.exports = {
|
|
|
223
388
|
looksLikePointList,
|
|
224
389
|
messagePoints,
|
|
225
390
|
messageWritePoints,
|
|
391
|
+
normalizeIndexRange,
|
|
226
392
|
normalizePoint,
|
|
227
393
|
normalizePointPayload,
|
|
228
394
|
normalizePoints,
|
package/lib/prebuild.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
function linuxLibc(report) {
|
|
7
|
+
if (report?.header?.glibcVersionRuntime || report?.header?.glibcVersionCompiler) {
|
|
8
|
+
return 'glibc';
|
|
9
|
+
}
|
|
10
|
+
return 'musl';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function runtimeReport() {
|
|
14
|
+
try {
|
|
15
|
+
return process.report?.getReport?.();
|
|
16
|
+
} catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function prebuildKey(options = {}) {
|
|
22
|
+
const platform = options.platform || process.platform;
|
|
23
|
+
const arch = options.arch || process.arch;
|
|
24
|
+
if (platform !== 'linux') return `${platform}-${arch}`;
|
|
25
|
+
return `${platform}-${arch}-${linuxLibc(options.report || runtimeReport())}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function prebuildPath(packageRoot, options = {}) {
|
|
29
|
+
return path.join(
|
|
30
|
+
packageRoot,
|
|
31
|
+
'prebuilds',
|
|
32
|
+
prebuildKey(options),
|
|
33
|
+
'opcua_open62541.node'
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function resolvePrebuilt(packageRoot, options = {}) {
|
|
38
|
+
const candidate = prebuildPath(packageRoot, options);
|
|
39
|
+
return fs.existsSync(candidate) ? candidate : undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { linuxLibc, prebuildKey, prebuildPath, resolvePrebuilt };
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs/promises');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const MAX_SECURITY_FILE_SIZE = 2 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
const FILE_KINDS = Object.freeze({
|
|
10
|
+
clientCertificate: {
|
|
11
|
+
prefix: 'client-certificate',
|
|
12
|
+
extensions: new Set(['.der', '.pem', '.crt', '.cer']),
|
|
13
|
+
},
|
|
14
|
+
privateKey: {
|
|
15
|
+
prefix: 'client-private-key',
|
|
16
|
+
extensions: new Set(['.der', '.pem', '.key']),
|
|
17
|
+
},
|
|
18
|
+
trustedCertificate: {
|
|
19
|
+
prefix: 'trusted-certificate',
|
|
20
|
+
extensions: new Set(['.der', '.pem', '.crt', '.cer']),
|
|
21
|
+
},
|
|
22
|
+
revocationList: {
|
|
23
|
+
prefix: 'revocation-list',
|
|
24
|
+
extensions: new Set(['.der', '.pem', '.crl']),
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function storageError(message, code = 'OPCUA_SECURITY_FILE_INVALID') {
|
|
29
|
+
const error = new Error(message);
|
|
30
|
+
error.code = code;
|
|
31
|
+
return error;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function decodeBase64(value) {
|
|
35
|
+
const encoded = String(value || '').trim();
|
|
36
|
+
if (!encoded || encoded.length % 4 !== 0 ||
|
|
37
|
+
!/^[A-Za-z0-9+/]*={0,2}$/.test(encoded)) {
|
|
38
|
+
throw storageError('Security file content must be valid base64');
|
|
39
|
+
}
|
|
40
|
+
const decoded = Buffer.from(encoded, 'base64');
|
|
41
|
+
if (!decoded.length || decoded.toString('base64') !== encoded) {
|
|
42
|
+
throw storageError('Security file content must be valid base64');
|
|
43
|
+
}
|
|
44
|
+
return decoded;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function connectionDirectoryName(connectionId) {
|
|
48
|
+
const value = String(connectionId || '').trim();
|
|
49
|
+
if (!value || value.length > 256) {
|
|
50
|
+
throw storageError('A valid Connection node id is required');
|
|
51
|
+
}
|
|
52
|
+
return crypto.createHash('sha256').update(value).digest('hex').slice(0, 32);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
class SecurityFileStore {
|
|
56
|
+
constructor(options = {}) {
|
|
57
|
+
const userDir = path.resolve(String(options.userDir || process.cwd()));
|
|
58
|
+
this.root = path.join(userDir, 'opcua-pki');
|
|
59
|
+
this.maxFileSize = Number(options.maxFileSize) || MAX_SECURITY_FILE_SIZE;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
connectionDirectory(connectionId) {
|
|
63
|
+
return path.join(this.root, connectionDirectoryName(connectionId));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async save({ connectionId, kind, fileName, contentBase64 }) {
|
|
67
|
+
const definition = FILE_KINDS[kind];
|
|
68
|
+
if (!definition) throw storageError(`Unsupported OPC UA security file kind: ${kind}`);
|
|
69
|
+
|
|
70
|
+
const extension = path.extname(String(fileName || '')).toLowerCase();
|
|
71
|
+
if (!definition.extensions.has(extension)) {
|
|
72
|
+
throw storageError(
|
|
73
|
+
`Unsupported ${kind} file extension; allowed: ${[...definition.extensions].join(', ')}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const content = decodeBase64(contentBase64);
|
|
78
|
+
if (content.length > this.maxFileSize) {
|
|
79
|
+
throw storageError(
|
|
80
|
+
`Security file exceeds the ${this.maxFileSize} byte limit`,
|
|
81
|
+
'OPCUA_SECURITY_FILE_TOO_LARGE'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const directory = this.connectionDirectory(connectionId);
|
|
86
|
+
const sha256 = crypto.createHash('sha256').update(content).digest('hex');
|
|
87
|
+
const managedName = `${definition.prefix}-${sha256.slice(0, 24)}${extension}`;
|
|
88
|
+
const managedPath = path.join(directory, managedName);
|
|
89
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
90
|
+
try {
|
|
91
|
+
await fs.writeFile(managedPath, content, { flag: 'wx', mode: 0o600 });
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (error.code !== 'EEXIST') throw error;
|
|
94
|
+
}
|
|
95
|
+
await fs.chmod(managedPath, 0o600);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
kind,
|
|
99
|
+
name: path.basename(String(fileName || '')),
|
|
100
|
+
path: managedPath,
|
|
101
|
+
size: content.length,
|
|
102
|
+
sha256,
|
|
103
|
+
managed: true,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async removeConnection(connectionId) {
|
|
108
|
+
const directory = this.connectionDirectory(connectionId);
|
|
109
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
FILE_KINDS,
|
|
115
|
+
MAX_SECURITY_FILE_SIZE,
|
|
116
|
+
SecurityFileStore,
|
|
117
|
+
connectionDirectoryName,
|
|
118
|
+
decodeBase64,
|
|
119
|
+
};
|
package/lib/security.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SECURITY_POLICY_URIS = Object.freeze({
|
|
4
|
+
None: 'http://opcfoundation.org/UA/SecurityPolicy#None',
|
|
5
|
+
Basic256Sha256: 'http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256',
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const SECURITY_MODES = new Set(['None', 'Sign', 'SignAndEncrypt']);
|
|
9
|
+
|
|
10
|
+
function configurationError(message) {
|
|
11
|
+
const error = new Error(message);
|
|
12
|
+
error.code = 'OPCUA_INVALID_SECURITY_CONFIGURATION';
|
|
13
|
+
return error;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizePathList(value) {
|
|
17
|
+
const source = Array.isArray(value)
|
|
18
|
+
? value
|
|
19
|
+
: String(value || '').split(/\r?\n|;/);
|
|
20
|
+
return [...new Set(source.map((entry) => String(entry || '').trim()).filter(Boolean))];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function securityPolicyName(value) {
|
|
24
|
+
const policy = String(value || 'None').trim();
|
|
25
|
+
if (SECURITY_POLICY_URIS[policy]) return policy;
|
|
26
|
+
const match = Object.entries(SECURITY_POLICY_URIS).find(([, uri]) => uri === policy);
|
|
27
|
+
return match?.[0] || policy;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeConnectionOptions(options = {}) {
|
|
31
|
+
const normalized = {
|
|
32
|
+
...options,
|
|
33
|
+
endpoint: String(options.endpoint || '').trim(),
|
|
34
|
+
securityMode: String(options.securityMode || 'None').trim(),
|
|
35
|
+
securityPolicy: securityPolicyName(options.securityPolicy),
|
|
36
|
+
certificate: String(options.certificate || '').trim(),
|
|
37
|
+
privateKey: String(options.privateKey || '').trim(),
|
|
38
|
+
applicationUri: String(options.applicationUri || '').trim(),
|
|
39
|
+
trustedCertificates: normalizePathList(
|
|
40
|
+
options.trustedCertificates ?? options.trustList
|
|
41
|
+
),
|
|
42
|
+
revocationList: normalizePathList(options.revocationList),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (!normalized.endpoint) throw configurationError('OPC UA endpoint is required');
|
|
46
|
+
if (!SECURITY_MODES.has(normalized.securityMode)) {
|
|
47
|
+
throw configurationError(`Unsupported OPC UA security mode: ${normalized.securityMode}`);
|
|
48
|
+
}
|
|
49
|
+
if (!SECURITY_POLICY_URIS[normalized.securityPolicy]) {
|
|
50
|
+
throw configurationError(`Unsupported OPC UA security policy: ${normalized.securityPolicy}`);
|
|
51
|
+
}
|
|
52
|
+
if (normalized.securityMode === 'None') {
|
|
53
|
+
if (normalized.securityPolicy !== 'None') {
|
|
54
|
+
throw configurationError('SecurityMode None requires SecurityPolicy None');
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
if (normalized.securityPolicy === 'None') {
|
|
58
|
+
throw configurationError(`${normalized.securityMode} requires a non-None security policy`);
|
|
59
|
+
}
|
|
60
|
+
if (!normalized.certificate) {
|
|
61
|
+
throw configurationError('Secure OPC UA connections require a client certificate');
|
|
62
|
+
}
|
|
63
|
+
if (!normalized.privateKey) {
|
|
64
|
+
throw configurationError('Secure OPC UA connections require a client private key');
|
|
65
|
+
}
|
|
66
|
+
if (!normalized.trustedCertificates.length) {
|
|
67
|
+
throw configurationError('Secure OPC UA connections require at least one trusted server or CA certificate');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
normalized.securityPolicyUri = SECURITY_POLICY_URIS[normalized.securityPolicy];
|
|
72
|
+
return normalized;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function endpointIdentityTypes(endpoint) {
|
|
76
|
+
return [...new Set((endpoint?.userIdentityTokens || []).map((token) => token.tokenType))];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function matchingEndpoints(endpoints, options = {}) {
|
|
80
|
+
const mode = String(options.securityMode || 'None');
|
|
81
|
+
const policy = securityPolicyName(options.securityPolicy);
|
|
82
|
+
const identityType = String(options.identityType || (options.user ? 'Username' : 'Anonymous'));
|
|
83
|
+
return (Array.isArray(endpoints) ? endpoints : []).filter((endpoint) => (
|
|
84
|
+
endpoint.securityMode === mode &&
|
|
85
|
+
securityPolicyName(endpoint.securityPolicyUri || endpoint.securityPolicy) === policy &&
|
|
86
|
+
endpointIdentityTypes(endpoint).includes(identityType)
|
|
87
|
+
));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
SECURITY_POLICY_URIS,
|
|
92
|
+
matchingEndpoints,
|
|
93
|
+
normalizeConnectionOptions,
|
|
94
|
+
normalizePathList,
|
|
95
|
+
securityPolicyName,
|
|
96
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 [Node.js API collaborators](https://github.com/nodejs/node-addon-api#collaborators)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# **node-addon-api module**
|
|
2
|
+
|
|
3
|
+
[](https://app.codecov.io/gh/nodejs/node-addon-api/tree/main)
|
|
4
|
+
|
|
5
|
+
[](https://nodei.co/npm/node-addon-api/) [](https://nodei.co/npm/node-addon-api/)
|
|
6
|
+
|
|
7
|
+
This module contains **header-only C++ wrapper classes** which simplify
|
|
8
|
+
the use of the C based [Node-API](https://nodejs.org/dist/latest/docs/api/n-api.html)
|
|
9
|
+
provided by Node.js when using C++. It provides a C++ object model
|
|
10
|
+
and exception handling semantics with low overhead.
|
|
11
|
+
|
|
12
|
+
- [API References](doc/README.md)
|
|
13
|
+
- [Badges](#badges)
|
|
14
|
+
- [Contributing](#contributing)
|
|
15
|
+
- [License](#license)
|
|
16
|
+
|
|
17
|
+
## API References
|
|
18
|
+
|
|
19
|
+
API references are available in the [doc](doc/README.md) directory.
|
|
20
|
+
|
|
21
|
+
<!-- x-release-please-start-version -->
|
|
22
|
+
## Current version: 8.9.1
|
|
23
|
+
<!-- x-release-please-end -->
|
|
24
|
+
|
|
25
|
+
(See [CHANGELOG.md](CHANGELOG.md) for complete Changelog)
|
|
26
|
+
|
|
27
|
+
node-addon-api is based on [Node-API](https://nodejs.org/api/n-api.html) and supports using different Node-API versions.
|
|
28
|
+
This allows addons built with it to run with Node.js versions which support the targeted Node-API version.
|
|
29
|
+
**However** the node-addon-api support model is to support only the active LTS Node.js versions. This means that
|
|
30
|
+
every year there will be a new major which drops support for the Node.js LTS version which has gone out of service.
|
|
31
|
+
|
|
32
|
+
The oldest Node.js version supported by the current version of node-addon-api is Node.js 18.x.
|
|
33
|
+
|
|
34
|
+
## Badges
|
|
35
|
+
|
|
36
|
+
The use of badges is recommended to indicate the minimum version of Node-API
|
|
37
|
+
required for the module. This helps to determine which Node.js major versions are
|
|
38
|
+
supported. Addon maintainers can consult the [Node-API support matrix][] to determine
|
|
39
|
+
which Node.js versions provide a given Node-API version. The following badges are
|
|
40
|
+
available:
|
|
41
|
+
|
|
42
|
+

|
|
43
|
+

|
|
44
|
+

|
|
45
|
+

|
|
46
|
+

|
|
47
|
+

|
|
48
|
+

|
|
49
|
+

|
|
50
|
+

|
|
51
|
+

|
|
52
|
+
|
|
53
|
+
## Contributing
|
|
54
|
+
|
|
55
|
+
We love contributions from the community to **node-addon-api**!
|
|
56
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for more details on our philosophy around extending this module.
|
|
57
|
+
|
|
58
|
+
## Team members
|
|
59
|
+
|
|
60
|
+
### Active
|
|
61
|
+
|
|
62
|
+
| Name | GitHub Link |
|
|
63
|
+
| ------------------- | ----------------------------------------------------- |
|
|
64
|
+
| Anna Henningsen | [addaleax](https://github.com/addaleax) |
|
|
65
|
+
| Chengzhong Wu | [legendecas](https://github.com/legendecas) |
|
|
66
|
+
| Jack Xia | [JckXia](https://github.com/JckXia) |
|
|
67
|
+
| Kevin Eady | [KevinEady](https://github.com/KevinEady) |
|
|
68
|
+
| Michael Dawson | [mhdawson](https://github.com/mhdawson) |
|
|
69
|
+
| Nicola Del Gobbo | [NickNaso](https://github.com/NickNaso) |
|
|
70
|
+
| Vladimir Morozov | [vmoroz](https://github.com/vmoroz) |
|
|
71
|
+
|
|
72
|
+
<details>
|
|
73
|
+
|
|
74
|
+
<summary>Emeritus</summary>
|
|
75
|
+
|
|
76
|
+
### Emeritus
|
|
77
|
+
|
|
78
|
+
| Name | GitHub Link |
|
|
79
|
+
| ------------------- | ----------------------------------------------------- |
|
|
80
|
+
| Arunesh Chandra | [aruneshchandra](https://github.com/aruneshchandra) |
|
|
81
|
+
| Benjamin Byholm | [kkoopa](https://github.com/kkoopa) |
|
|
82
|
+
| Gabriel Schulhof | [gabrielschulhof](https://github.com/gabrielschulhof) |
|
|
83
|
+
| Hitesh Kanwathirtha | [digitalinfinity](https://github.com/digitalinfinity) |
|
|
84
|
+
| Jason Ginchereau | [jasongin](https://github.com/jasongin) |
|
|
85
|
+
| Jim Schlight | [jschlight](https://github.com/jschlight) |
|
|
86
|
+
| Sampson Gao | [sampsongao](https://github.com/sampsongao) |
|
|
87
|
+
| Taylor Woll | [boingoing](https://github.com/boingoing) |
|
|
88
|
+
|
|
89
|
+
</details>
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
Licensed under [MIT](./LICENSE.md)
|
|
94
|
+
|
|
95
|
+
[Node-API support matrix]: https://nodejs.org/dist/latest/docs/api/n-api.html#node-api-version-matrix
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
'variables': {
|
|
3
|
+
'NAPI_VERSION%': "<!(node -p \"process.env.NAPI_VERSION || process.versions.napi\")",
|
|
4
|
+
'disable_deprecated': "<!(node -p \"process.env['npm_config_disable_deprecated']\")"
|
|
5
|
+
},
|
|
6
|
+
'conditions': [
|
|
7
|
+
['NAPI_VERSION!=""', { 'defines': ['NAPI_VERSION=<@(NAPI_VERSION)'] } ],
|
|
8
|
+
['NAPI_VERSION==2147483647', { 'defines': ['NAPI_EXPERIMENTAL', 'NODE_API_EXPERIMENTAL_NO_WARNING'] } ],
|
|
9
|
+
['disable_deprecated=="true"', {
|
|
10
|
+
'defines': ['NODE_ADDON_API_DISABLE_DEPRECATED']
|
|
11
|
+
}],
|
|
12
|
+
['OS=="mac"', {
|
|
13
|
+
'cflags+': ['-fvisibility=hidden'],
|
|
14
|
+
'xcode_settings': {
|
|
15
|
+
'OTHER_CFLAGS': ['-fvisibility=hidden']
|
|
16
|
+
}
|
|
17
|
+
}]
|
|
18
|
+
],
|
|
19
|
+
'cflags': [ '-Werror', '-Wall', '-Wextra', '-Wpedantic', '-Wunused-parameter' ],
|
|
20
|
+
'cflags_cc': [ '-Werror', '-Wall', '-Wextra', '-Wpedantic', '-Wunused-parameter' ]
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
'defines': [ 'NAPI_CPP_EXCEPTIONS' ],
|
|
3
|
+
'cflags!': [ '-fno-exceptions' ],
|
|
4
|
+
'cflags_cc!': [ '-fno-exceptions' ],
|
|
5
|
+
'conditions': [
|
|
6
|
+
["OS=='win'", {
|
|
7
|
+
"defines": [
|
|
8
|
+
"_HAS_EXCEPTIONS=1"
|
|
9
|
+
],
|
|
10
|
+
"msvs_settings": {
|
|
11
|
+
"VCCLCompilerTool": {
|
|
12
|
+
"ExceptionHandling": 1,
|
|
13
|
+
'EnablePREfast': 'true',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
}],
|
|
17
|
+
["OS=='mac'", {
|
|
18
|
+
'xcode_settings': {
|
|
19
|
+
'GCC_ENABLE_CPP_EXCEPTIONS': 'YES',
|
|
20
|
+
'CLANG_CXX_LIBRARY': 'libc++',
|
|
21
|
+
'MACOSX_DEPLOYMENT_TARGET': '10.7',
|
|
22
|
+
},
|
|
23
|
+
}],
|
|
24
|
+
],
|
|
25
|
+
}
|