@vitormnm/node-red-simple-opcua 1.6.2 → 1.7.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 +89 -136
- package/client/lib/opcua-client-browser.js +238 -10
- package/client/lib/opcua-client-method-service.js +1 -1
- package/client/lib/opcua-client-subscription-service.js +0 -2
- package/client/opcua-client-config.html +118 -1
- package/client/opcua-client-config.js +74 -8
- package/client/opcua-client-help.html +6 -0
- package/client/opcua-client-utils.js +34 -10
- package/client/opcua-client.html +7 -0
- package/client/opcua-client.js +97 -1
- package/examples/flows_simple_opc.json +1 -1
- package/package.json +1 -1
- package/server/lib/opcua-address-space-alarm.js +11 -5
- package/server/lib/opcua-address-space-builder.js +65 -15
- package/server/lib/opcua-config.js +81 -23
- package/server/lib/opcua-server-events-child.js +1 -1
- package/server/lib/opcua-server-runtime-child.js +429 -59
- package/server/lib/opcua-server-runtime.js +49 -5
- package/server/lib/opcua-server-status-child.js +14 -14
- package/server/nodered/simple_opcua/server/certificates/mutex +0 -0
- package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem +25 -0
- package/server/nodered/simple_opcua/server/certificates/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
- package/server/nodered/simple_opcua/server/certificates/own/openssl.cnf +72 -0
- package/server/nodered/simple_opcua/server/certificates/own/private/private_key.pem +28 -0
- package/server/nodered/simple_opcua/server/certificates/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/mutex +0 -0
- package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/own/certs/server_selfsigned_cert_2048.pem.mutex +0 -0
- package/server/nodered/simple_opcua/server/myServer1/own/openssl.cnf +72 -0
- package/server/nodered/simple_opcua/server/myServer1/own/private/private_key.pem +28 -0
- package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[91e520c64ff891c67168f08a46dd194071e15dae].pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[98ae95da627cea4c500753c319161a3554ee38d7].pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[aef8d7a1cfba13d84189a0bcf1694208fc51a7f9].pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[c5a9e20a8b680cdff76aaf0165bb3c9318da37a5].pem +25 -0
- package/server/nodered/simple_opcua/server/myServer1/trusted/certs/NodeOPCUA-Client@tuf[ebdf9acf1d02e347917a14108d3144799c638ea3].pem +25 -0
- package/server/opcua-server-io.html +76 -0
- package/server/opcua-server-io.js +135 -23
- package/server/opcua-server.css +52 -0
- package/server/opcua-server.html +166 -44
- package/server/opcua-server.js +142 -7
- package/server/view/opcua-server.css +89 -6
- package/server/view/opcua-server.js +523 -42
package/server/opcua-server.js
CHANGED
|
@@ -8,12 +8,29 @@ module.exports = function (RED) {
|
|
|
8
8
|
const { fork } = require("child_process");
|
|
9
9
|
const path = require("path");
|
|
10
10
|
|
|
11
|
+
const getCertificatesFolder = (serverName) => {
|
|
12
|
+
const safeServerName = (serverName || "default")
|
|
13
|
+
.replace(/[\\/:\*\?"<>|]/g, "_")
|
|
14
|
+
.replace(/^\.+$/, "");
|
|
15
|
+
try {
|
|
16
|
+
const userDir = (RED.settings && RED.settings.userDir) || path.join(require('os').homedir(), ".node-red");
|
|
17
|
+
let flowFile = (RED.settings && RED.settings.flowFile) || "flows.json";
|
|
18
|
+
if (typeof flowFile !== "string") {
|
|
19
|
+
flowFile = "flows.json";
|
|
20
|
+
}
|
|
21
|
+
const flowFileFolder = path.isAbsolute(flowFile) ? path.dirname(flowFile) : path.join(userDir, path.dirname(flowFile));
|
|
22
|
+
return path.join(flowFileFolder, "simple_opcua", "server", safeServerName);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
return path.join(require('os').homedir(), ".node-red", "simple_opcua", "server", safeServerName);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
11
28
|
function OpcUaServerNode(config) {
|
|
12
29
|
RED.nodes.createNode(this, config);
|
|
13
30
|
const node = this;
|
|
14
31
|
const parser = new OpcUaServerConfigParser(node);
|
|
15
|
-
|
|
16
32
|
const settings = parser.parseNodeConfig(config, this.credentials || {});
|
|
33
|
+
settings.certificatesFolder = getCertificatesFolder(settings.serverName);
|
|
17
34
|
|
|
18
35
|
|
|
19
36
|
|
|
@@ -21,6 +38,7 @@ module.exports = function (RED) {
|
|
|
21
38
|
node.serverName = settings.serverName;
|
|
22
39
|
node.server = null;
|
|
23
40
|
node.namespace = null;
|
|
41
|
+
node.isClosing = false;
|
|
24
42
|
|
|
25
43
|
node.status({ fill: "yellow", shape: "ring", text: "initializing OPC UA server" });
|
|
26
44
|
|
|
@@ -44,12 +62,35 @@ module.exports = function (RED) {
|
|
|
44
62
|
|
|
45
63
|
registry.registerServerNames(node.serverName, node.serverName);
|
|
46
64
|
|
|
47
|
-
|
|
65
|
+
let crashHandled = false;
|
|
66
|
+
function handleUnexpectedExit(code, signal) {
|
|
67
|
+
if (node.isClosing || crashHandled) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
crashHandled = true;
|
|
71
|
+
|
|
72
|
+
const errorDetails = `OPC UA server child process exited unexpectedly with code ${code} and signal ${signal}`;
|
|
73
|
+
node.status({ fill: "red", shape: "dot", text: "Child process crashed" });
|
|
74
|
+
|
|
75
|
+
const catchMsg = {
|
|
76
|
+
topic: node.serverName,
|
|
77
|
+
payload: {
|
|
78
|
+
status: "error",
|
|
79
|
+
error: errorDetails
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
node.send(catchMsg);
|
|
83
|
+
node.error(errorDetails, catchMsg);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
child.on("exit", (code, signal) => {
|
|
48
87
|
registry.unregisterChild(node.serverName, child);
|
|
88
|
+
handleUnexpectedExit(code, signal);
|
|
49
89
|
});
|
|
50
90
|
|
|
51
|
-
child.on("close", () => {
|
|
91
|
+
child.on("close", (code, signal) => {
|
|
52
92
|
registry.unregisterChild(node.serverName, child);
|
|
93
|
+
handleUnexpectedExit(code, signal);
|
|
53
94
|
});
|
|
54
95
|
|
|
55
96
|
|
|
@@ -105,13 +146,16 @@ module.exports = function (RED) {
|
|
|
105
146
|
|
|
106
147
|
done();
|
|
107
148
|
} catch (error) {
|
|
108
|
-
reportError(node, "Input processing failed", error);
|
|
109
|
-
done
|
|
149
|
+
reportError(node, "Input processing failed", error, msg);
|
|
150
|
+
if (done) {
|
|
151
|
+
done();
|
|
152
|
+
}
|
|
110
153
|
}
|
|
111
154
|
});
|
|
112
155
|
|
|
113
156
|
node.on("close", async function (removed, done) {
|
|
114
157
|
try {
|
|
158
|
+
node.isClosing = true;
|
|
115
159
|
registry.unregisterChild(node.serverName, child);
|
|
116
160
|
registry.unregisterServerNames(node.serverName);
|
|
117
161
|
child.kill();
|
|
@@ -127,9 +171,13 @@ module.exports = function (RED) {
|
|
|
127
171
|
|
|
128
172
|
|
|
129
173
|
|
|
130
|
-
function reportError(node, message, error) {
|
|
174
|
+
function reportError(node, message, error, msg) {
|
|
131
175
|
const details = error && error.message ? error.message : String(error);
|
|
132
|
-
|
|
176
|
+
if (msg) {
|
|
177
|
+
node.error(message + ": " + details, msg);
|
|
178
|
+
} else {
|
|
179
|
+
node.error(message + ": " + details);
|
|
180
|
+
}
|
|
133
181
|
node.status({ fill: "red", shape: "ring", text: message });
|
|
134
182
|
}
|
|
135
183
|
|
|
@@ -145,6 +193,93 @@ module.exports = function (RED) {
|
|
|
145
193
|
res.sendFile(jsPath);
|
|
146
194
|
});
|
|
147
195
|
|
|
196
|
+
RED.httpAdmin.get("/opc-ua-server/certificates", function (req, res) {
|
|
197
|
+
const serverName = req.query.serverName || "default";
|
|
198
|
+
const certificatesFolder = getCertificatesFolder(serverName);
|
|
199
|
+
|
|
200
|
+
const fs = require("fs");
|
|
201
|
+
const trustedDir = path.join(certificatesFolder, "trusted", "certs");
|
|
202
|
+
const rejectedDir = path.join(certificatesFolder, "rejected");
|
|
203
|
+
|
|
204
|
+
// Ensure directories exist
|
|
205
|
+
try {
|
|
206
|
+
if (!fs.existsSync(trustedDir)) {
|
|
207
|
+
fs.mkdirSync(trustedDir, { recursive: true });
|
|
208
|
+
}
|
|
209
|
+
if (!fs.existsSync(rejectedDir)) {
|
|
210
|
+
fs.mkdirSync(rejectedDir, { recursive: true });
|
|
211
|
+
}
|
|
212
|
+
} catch (e) {
|
|
213
|
+
// Ignore directory creation errors (fallback to empty)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const listFiles = (dir) => {
|
|
217
|
+
try {
|
|
218
|
+
if (!fs.existsSync(dir)) {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
return fs.readdirSync(dir).filter(file => {
|
|
222
|
+
const stats = fs.statSync(path.join(dir, file));
|
|
223
|
+
return stats.isFile() && (file.endsWith(".der") || file.endsWith(".pem") || file.endsWith(".crt"));
|
|
224
|
+
});
|
|
225
|
+
} catch (err) {
|
|
226
|
+
return [];
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
res.json({
|
|
231
|
+
trusted: listFiles(trustedDir),
|
|
232
|
+
rejected: listFiles(rejectedDir)
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
RED.httpAdmin.post("/opc-ua-server/certificates/move", function (req, res) {
|
|
237
|
+
const { serverName, filename, fromFolder, toFolder } = req.body;
|
|
238
|
+
const certificatesFolder = getCertificatesFolder(serverName);
|
|
239
|
+
|
|
240
|
+
const fs = require("fs");
|
|
241
|
+
|
|
242
|
+
if (!filename || !fromFolder || !toFolder) {
|
|
243
|
+
return res.status(400).json({ error: "Missing required fields" });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const getFolderDir = (folderName) => {
|
|
247
|
+
if (folderName === "trusted") {
|
|
248
|
+
return path.join(certificatesFolder, "trusted", "certs");
|
|
249
|
+
}
|
|
250
|
+
if (folderName === "rejected") {
|
|
251
|
+
return path.join(certificatesFolder, "rejected");
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const srcDir = getFolderDir(fromFolder);
|
|
257
|
+
const destDir = getFolderDir(toFolder);
|
|
258
|
+
|
|
259
|
+
if (!srcDir || !destDir) {
|
|
260
|
+
return res.status(400).json({ error: "Invalid folders specified" });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const srcPath = path.join(srcDir, filename);
|
|
264
|
+
const destPath = path.join(destDir, filename);
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
if (!fs.existsSync(srcPath)) {
|
|
268
|
+
return res.status(404).json({ error: "Source certificate not found" });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Ensure destination directory exists
|
|
272
|
+
if (!fs.existsSync(destDir)) {
|
|
273
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
fs.renameSync(srcPath, destPath);
|
|
277
|
+
res.json({ success: true });
|
|
278
|
+
} catch (err) {
|
|
279
|
+
res.status(500).json({ error: "Failed to move certificate: " + err.message });
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
148
283
|
|
|
149
284
|
|
|
150
285
|
RED.nodes.registerType("opc-ua-server", OpcUaServerNode, {
|
|
@@ -442,6 +442,9 @@ body.opcua-tree-modal-open {
|
|
|
442
442
|
|
|
443
443
|
.opcua-auth-card .form-row {
|
|
444
444
|
margin-bottom: 8px;
|
|
445
|
+
display: flex;
|
|
446
|
+
align-items: center;
|
|
447
|
+
gap: 8px;
|
|
445
448
|
}
|
|
446
449
|
|
|
447
450
|
.opcua-auth-card .form-row:last-child {
|
|
@@ -450,15 +453,18 @@ body.opcua-tree-modal-open {
|
|
|
450
453
|
|
|
451
454
|
.opcua-auth-card label {
|
|
452
455
|
width: 90px;
|
|
456
|
+
margin: 0;
|
|
457
|
+
flex-shrink: 0;
|
|
453
458
|
}
|
|
454
459
|
|
|
455
|
-
.opcua-auth-card input,
|
|
456
|
-
.opcua-auth-card
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
+
.opcua-auth-card input[type="text"],
|
|
461
|
+
.opcua-auth-card input[type="password"],
|
|
462
|
+
.opcua-auth-card input[type="number"],
|
|
463
|
+
.opcua-auth-card select,
|
|
460
464
|
.opcua-auth-card .red-ui-typedInput-container {
|
|
461
|
-
|
|
465
|
+
flex: 1 1 auto;
|
|
466
|
+
margin: 0;
|
|
467
|
+
width: auto !important;
|
|
462
468
|
}
|
|
463
469
|
|
|
464
470
|
@media (max-width: 900px) {
|
|
@@ -493,4 +499,81 @@ body.opcua-tree-modal-open {
|
|
|
493
499
|
.opcua-tree-actions {
|
|
494
500
|
margin-left: 0;
|
|
495
501
|
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.opcua-cert-item {
|
|
505
|
+
padding: 8px 12px;
|
|
506
|
+
border-bottom: 1px solid var(--red-ui-form-input-border-color, #e0e0e0);
|
|
507
|
+
cursor: pointer;
|
|
508
|
+
display: flex;
|
|
509
|
+
align-items: center;
|
|
510
|
+
gap: 8px;
|
|
511
|
+
user-select: none;
|
|
512
|
+
transition: background-color 0.2s;
|
|
513
|
+
}
|
|
514
|
+
.opcua-cert-item:last-child {
|
|
515
|
+
border-bottom: none;
|
|
516
|
+
}
|
|
517
|
+
.opcua-cert-item:hover {
|
|
518
|
+
background: var(--red-ui-list-item-background-hover, #f0f0f0);
|
|
519
|
+
}
|
|
520
|
+
.opcua-cert-item.is-selected {
|
|
521
|
+
background: var(--red-ui-list-item-background-selected, #d9ecff);
|
|
522
|
+
color: var(--red-ui-list-item-color-selected, inherit);
|
|
523
|
+
font-weight: 500;
|
|
524
|
+
}
|
|
525
|
+
.opcua-cert-item i {
|
|
526
|
+
font-size: 14px;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/* Standardized configuration card styling */
|
|
530
|
+
.opcua-config-card {
|
|
531
|
+
padding: 14px;
|
|
532
|
+
background: var(--red-ui-form-input-background, #fcfcfc);
|
|
533
|
+
border: 1px solid var(--red-ui-form-input-border-color, #d9d9d9);
|
|
534
|
+
border-radius: 6px;
|
|
535
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.opcua-config-card .opcua-card-title {
|
|
539
|
+
margin-top: 0;
|
|
540
|
+
margin-bottom: 12px;
|
|
541
|
+
font-weight: bold;
|
|
542
|
+
border-bottom: 1px solid var(--red-ui-form-input-border-color, #eee);
|
|
543
|
+
padding-bottom: 6px;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.opcua-config-form {
|
|
547
|
+
display: flex;
|
|
548
|
+
flex-direction: column;
|
|
549
|
+
gap: 10px;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.opcua-config-row {
|
|
553
|
+
margin: 0 !important;
|
|
554
|
+
display: flex;
|
|
555
|
+
align-items: center;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.opcua-config-row--checkbox {
|
|
559
|
+
height: 34px;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.opcua-config-label {
|
|
563
|
+
width: 240px;
|
|
564
|
+
margin: 0 !important;
|
|
565
|
+
font-weight: 500;
|
|
566
|
+
flex-shrink: 0;
|
|
567
|
+
display: inline-block;
|
|
568
|
+
vertical-align: middle;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.opcua-config-input {
|
|
572
|
+
width: 300px !important;
|
|
573
|
+
margin: 0 !important;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.opcua-config-input[type="checkbox"] {
|
|
577
|
+
width: auto !important;
|
|
578
|
+
display: inline-block;
|
|
496
579
|
}
|