@vitormnm/node-red-simple-opcua 1.6.3 → 1.8.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 +104 -136
- package/client/lib/opcua-client-browser.js +254 -11
- package/client/lib/opcua-client-method-service.js +1 -1
- package/client/lib/opcua-client-subscription-service.js +0 -2
- package/client/lib/opcua-client-write-service.js +14 -4
- package/client/opcua-client-config.html +118 -1
- package/client/opcua-client-config.js +112 -9
- package/client/opcua-client-help.html +6 -0
- package/client/opcua-client-utils.js +158 -10
- package/client/opcua-client.html +8 -0
- package/client/opcua-client.js +97 -1
- package/client/view/opcua-client.js +106 -14
- package/examples/flows_simple_opc.json +1 -1
- package/package.json +2 -2
- package/server/lib/opcua-address-space-alarm.js +95 -32
- package/server/lib/opcua-address-space-builder.js +717 -59
- package/server/lib/opcua-config.js +110 -35
- package/server/lib/opcua-server-events-child.js +31 -5
- package/server/lib/opcua-server-runtime-child.js +424 -27
- package/server/lib/opcua-server-runtime.js +52 -5
- package/server/lib/opcua-server-status-child.js +46 -15
- 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 +93 -1
- package/server/opcua-server-io.js +153 -29
- package/server/opcua-server-registry.js +8 -2
- package/server/opcua-server.css +64 -0
- package/server/opcua-server.html +168 -44
- package/server/opcua-server.js +115 -5
- package/server/view/opcua-server.css +100 -6
- package/server/view/opcua-server.js +746 -48
|
@@ -257,18 +257,33 @@ class OpcUaServerProcess {
|
|
|
257
257
|
|
|
258
258
|
|
|
259
259
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
260
|
+
let hasFailures = false;
|
|
261
|
+
if (readArrayResults) {
|
|
262
|
+
const failed = readArrayResults.filter(item => item && item.status !== "Good");
|
|
263
|
+
if (failed.length) {
|
|
264
|
+
hasFailures = true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (result.directError) {
|
|
268
|
+
hasFailures = true;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!hasFailures) {
|
|
272
|
+
process.send({
|
|
273
|
+
type: "send",
|
|
274
|
+
data: msg,
|
|
275
|
+
nodeId: nodeId
|
|
276
|
+
});
|
|
277
|
+
}
|
|
265
278
|
|
|
266
279
|
process.send({
|
|
267
280
|
type: "status",
|
|
268
281
|
data: {
|
|
269
|
-
fill: "green",
|
|
282
|
+
fill: hasFailures ? (result.directError ? "red" : "yellow") : "green",
|
|
270
283
|
shape: "dot",
|
|
271
|
-
text:
|
|
284
|
+
text: hasFailures
|
|
285
|
+
? (result.directError ? "read failed" : "partial read failed")
|
|
286
|
+
: (result.identifiers.length > 1 ? "read " + result.identifiers.length + " tags" : "read " + result.identifiers[0])
|
|
272
287
|
},
|
|
273
288
|
nodeId: nodeId
|
|
274
289
|
});
|
|
@@ -308,6 +323,7 @@ class OpcUaServerProcess {
|
|
|
308
323
|
type: "error",
|
|
309
324
|
data: { fill: "red", shape: "ring", text: "failed read" },
|
|
310
325
|
error: error.message,
|
|
326
|
+
originalMsg: msg,
|
|
311
327
|
nodeId: nodeId
|
|
312
328
|
});
|
|
313
329
|
}
|
|
@@ -362,6 +378,7 @@ class OpcUaServerProcess {
|
|
|
362
378
|
type: "error",
|
|
363
379
|
data: { fill: "red", shape: "ring", text: "failed write" },
|
|
364
380
|
error: error.message,
|
|
381
|
+
originalMsg: msg,
|
|
365
382
|
nodeId: nodeId
|
|
366
383
|
});
|
|
367
384
|
}
|
|
@@ -381,6 +398,14 @@ class OpcUaServerProcess {
|
|
|
381
398
|
const target = msg && msg.opcuaServerIo ? msg.opcuaServerIo : {};
|
|
382
399
|
const identifierType = this.resolveIdentifierType(target);
|
|
383
400
|
|
|
401
|
+
let isSingleTag = false;
|
|
402
|
+
try {
|
|
403
|
+
this.resolveIdentifier(target);
|
|
404
|
+
isSingleTag = true;
|
|
405
|
+
} catch (e) {
|
|
406
|
+
// not a single tag
|
|
407
|
+
}
|
|
408
|
+
|
|
384
409
|
// Buffer serializado pelo IPC
|
|
385
410
|
if (
|
|
386
411
|
payload &&
|
|
@@ -447,7 +472,7 @@ class OpcUaServerProcess {
|
|
|
447
472
|
}
|
|
448
473
|
|
|
449
474
|
// Array de objetos
|
|
450
|
-
else if (Array.isArray(payload)) {
|
|
475
|
+
else if (Array.isArray(payload) && (!isSingleTag || this.isBatchWritePayload(payload))) {
|
|
451
476
|
|
|
452
477
|
if (!payload.length) {
|
|
453
478
|
throw new Error("msg.payload array does not contain any items");
|
|
@@ -524,21 +549,33 @@ class OpcUaServerProcess {
|
|
|
524
549
|
msg.topic = writtenPaths[0];
|
|
525
550
|
}
|
|
526
551
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
552
|
+
let hasFailures = false;
|
|
553
|
+
if (Array.isArray(msg.payload) && msg.payload.length && msg.payload[0] && typeof msg.payload[0].status === "string") {
|
|
554
|
+
const failed = msg.payload.filter(item => item.status !== "Good");
|
|
555
|
+
if (failed.length) {
|
|
556
|
+
hasFailures = true;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (directError) {
|
|
560
|
+
hasFailures = true;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (!hasFailures) {
|
|
564
|
+
process.send({
|
|
565
|
+
type: "send",
|
|
566
|
+
data: msg,
|
|
567
|
+
nodeId
|
|
568
|
+
});
|
|
569
|
+
}
|
|
532
570
|
|
|
533
571
|
process.send({
|
|
534
572
|
type: "status",
|
|
535
573
|
data: {
|
|
536
|
-
fill: "green",
|
|
574
|
+
fill: hasFailures ? (directError ? "red" : "yellow") : "green",
|
|
537
575
|
shape: "dot",
|
|
538
|
-
text:
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
: `write ${writtenPaths[0]}`
|
|
576
|
+
text: hasFailures
|
|
577
|
+
? (directError ? "write failed" : "partial write failed")
|
|
578
|
+
: (writtenPaths.length > 1 ? `write ${writtenPaths.length} tags` : `write ${writtenPaths[0]}`)
|
|
542
579
|
},
|
|
543
580
|
nodeId
|
|
544
581
|
});
|
|
@@ -582,11 +619,24 @@ class OpcUaServerProcess {
|
|
|
582
619
|
text: "failed write"
|
|
583
620
|
},
|
|
584
621
|
error: error.message,
|
|
622
|
+
originalMsg: msg,
|
|
585
623
|
nodeId
|
|
586
624
|
});
|
|
587
625
|
}
|
|
588
626
|
}
|
|
589
627
|
|
|
628
|
+
isBatchWritePayload(payload) {
|
|
629
|
+
if (!Array.isArray(payload) || payload.length === 0) {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
return payload.every(item =>
|
|
633
|
+
item &&
|
|
634
|
+
typeof item === "object" &&
|
|
635
|
+
!Array.isArray(item) &&
|
|
636
|
+
(item.path !== undefined || item.nodeId !== undefined || item.identifier !== undefined)
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
590
640
|
resolveIdentifierType(target) {
|
|
591
641
|
return target && target.identifierType === "nodeId" ? "nodeId" : "path";
|
|
592
642
|
}
|
|
@@ -855,10 +905,12 @@ class OpcUaServerProcess {
|
|
|
855
905
|
|
|
856
906
|
readActiveAlarms(msg, nodeId) {
|
|
857
907
|
try {
|
|
858
|
-
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
}
|
|
908
|
+
const result = registry.getActiveAlarms(this.node);
|
|
909
|
+
const safeResult = Array.isArray(result) ? result : [];
|
|
910
|
+
|
|
911
|
+
const msg2 = Object.assign({}, msg || {}, {
|
|
912
|
+
payload: safeResult
|
|
913
|
+
});
|
|
862
914
|
|
|
863
915
|
process.send({
|
|
864
916
|
type: "send",
|
|
@@ -871,17 +923,336 @@ class OpcUaServerProcess {
|
|
|
871
923
|
data: {
|
|
872
924
|
fill: "green",
|
|
873
925
|
shape: "dot",
|
|
874
|
-
text:
|
|
926
|
+
text: safeResult.length + " active alarm" + (safeResult.length !== 1 ? "s" : "")
|
|
927
|
+
},
|
|
928
|
+
nodeId: nodeId
|
|
929
|
+
});
|
|
930
|
+
} catch (error) {
|
|
931
|
+
process.send({
|
|
932
|
+
type: "error",
|
|
933
|
+
data: error.message,
|
|
934
|
+
nodeId: nodeId
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
async readActiveSessions(msg, nodeId) {
|
|
940
|
+
try {
|
|
941
|
+
await this.ensureReady();
|
|
942
|
+
const server = this.node && this.node.server;
|
|
943
|
+
if (!server || !server.engine) {
|
|
944
|
+
throw new Error("OPC UA server is not available");
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const rawSessions = server.engine._sessions || {};
|
|
948
|
+
const sessions = Object.values(rawSessions).map((session) => buildSessionSnapshot(session));
|
|
949
|
+
|
|
950
|
+
if (msg) {
|
|
951
|
+
const outMsg = Object.assign({}, msg);
|
|
952
|
+
outMsg.payload = sessions;
|
|
953
|
+
|
|
954
|
+
process.send({
|
|
955
|
+
type: "send",
|
|
956
|
+
data: outMsg,
|
|
957
|
+
nodeId: nodeId
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
process.send({
|
|
962
|
+
type: "status",
|
|
963
|
+
data: {
|
|
964
|
+
fill: "green",
|
|
965
|
+
shape: "dot",
|
|
966
|
+
text: sessions.length === 1
|
|
967
|
+
? "1 session"
|
|
968
|
+
: sessions.length + " sessions"
|
|
969
|
+
},
|
|
970
|
+
nodeId: nodeId
|
|
971
|
+
});
|
|
972
|
+
} catch (error) {
|
|
973
|
+
if (msg) {
|
|
974
|
+
process.send({
|
|
975
|
+
type: "error",
|
|
976
|
+
data: { fill: "red", shape: "ring", text: "failed getSessions" },
|
|
977
|
+
error: error.message,
|
|
978
|
+
originalMsg: msg,
|
|
979
|
+
nodeId: nodeId
|
|
980
|
+
});
|
|
981
|
+
} else {
|
|
982
|
+
process.send({
|
|
983
|
+
type: "status",
|
|
984
|
+
data: { fill: "red", shape: "ring", text: "failed getSessions: " + error.message },
|
|
985
|
+
nodeId: nodeId
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
async deleteActiveSessions(msg, nodeId) {
|
|
992
|
+
try {
|
|
993
|
+
await this.ensureReady();
|
|
994
|
+
const server = this.node && this.node.server;
|
|
995
|
+
if (!server || !server.engine) {
|
|
996
|
+
throw new Error("OPC UA server is not available");
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const payload = msg && Array.isArray(msg.payload) ? msg.payload : [];
|
|
1000
|
+
if (!payload.length) {
|
|
1001
|
+
throw new Error("msg.payload must be a non-empty array of { sessionId } objects");
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const engine = server.engine;
|
|
1005
|
+
const rawSessions = engine._sessions || {};
|
|
1006
|
+
|
|
1007
|
+
const results = payload.map((item) => {
|
|
1008
|
+
const requestedId = String(item && item.sessionId || "").trim();
|
|
1009
|
+
if (!requestedId) {
|
|
1010
|
+
return { sessionId: requestedId, status: "error", error: "sessionId is required" };
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Sessions are keyed by authenticationToken; find by matching nodeId (the GUID sessionId)
|
|
1014
|
+
const found = Object.values(rawSessions).find(
|
|
1015
|
+
(s) => safeToString(s.nodeId) === requestedId
|
|
1016
|
+
);
|
|
1017
|
+
|
|
1018
|
+
if (!found) {
|
|
1019
|
+
return { sessionId: requestedId, status: "not_found" };
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
try {
|
|
1023
|
+
engine.closeSession(found.authenticationToken, true, "Forcing");
|
|
1024
|
+
return { sessionId: requestedId, status: "deleted" };
|
|
1025
|
+
} catch (closeError) {
|
|
1026
|
+
return { sessionId: requestedId, status: "error", error: closeError.message };
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
const deletedCount = results.filter((r) => r.status === "deleted").length;
|
|
1031
|
+
const notFoundCount = results.filter((r) => r.status === "not_found").length;
|
|
1032
|
+
const errorCount = results.filter((r) => r.status === "error").length;
|
|
1033
|
+
|
|
1034
|
+
if (msg) {
|
|
1035
|
+
const outMsg = Object.assign({}, msg);
|
|
1036
|
+
outMsg.payload = results;
|
|
1037
|
+
|
|
1038
|
+
process.send({
|
|
1039
|
+
type: "send",
|
|
1040
|
+
data: outMsg,
|
|
1041
|
+
nodeId: nodeId
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const statusParts = [];
|
|
1046
|
+
if (deletedCount) statusParts.push("deleted " + deletedCount);
|
|
1047
|
+
if (notFoundCount) statusParts.push("not found " + notFoundCount);
|
|
1048
|
+
if (errorCount) statusParts.push("error " + errorCount);
|
|
1049
|
+
|
|
1050
|
+
process.send({
|
|
1051
|
+
type: "status",
|
|
1052
|
+
data: {
|
|
1053
|
+
fill: errorCount ? "red" : (notFoundCount ? "yellow" : "green"),
|
|
1054
|
+
shape: "dot",
|
|
1055
|
+
text: statusParts.join(", ") || "no sessions"
|
|
875
1056
|
},
|
|
876
1057
|
nodeId: nodeId
|
|
877
1058
|
});
|
|
878
|
-
} catch {
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
if (msg) {
|
|
1061
|
+
process.send({
|
|
1062
|
+
type: "error",
|
|
1063
|
+
data: { fill: "red", shape: "ring", text: "failed deleteSessions" },
|
|
1064
|
+
error: error.message,
|
|
1065
|
+
originalMsg: msg,
|
|
1066
|
+
nodeId: nodeId
|
|
1067
|
+
});
|
|
1068
|
+
} else {
|
|
1069
|
+
process.send({
|
|
1070
|
+
type: "status",
|
|
1071
|
+
data: { fill: "red", shape: "ring", text: "failed deleteSessions: " + error.message },
|
|
1072
|
+
nodeId: nodeId
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
async validateLogin(msg, nodeId) {
|
|
1079
|
+
try {
|
|
1080
|
+
await this.ensureReady();
|
|
1081
|
+
const runtime = this.runtime;
|
|
1082
|
+
if (!runtime) {
|
|
1083
|
+
throw new Error("OPC UA server is not available");
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
const payload = msg && msg.payload ? msg.payload : {};
|
|
1087
|
+
const username = typeof payload.userName === "string" ? payload.userName : (typeof payload.username === "string" ? payload.username : "");
|
|
1088
|
+
const password = typeof payload.password === "string" ? payload.password : "";
|
|
1089
|
+
|
|
1090
|
+
const normalizedUserName = username.trim();
|
|
1091
|
+
const users = Array.isArray(runtime.users) ? runtime.users : [];
|
|
1092
|
+
const user = users.find((entry) => entry && entry.username === normalizedUserName);
|
|
1093
|
+
|
|
1094
|
+
let isValid = false;
|
|
1095
|
+
if (user) {
|
|
1096
|
+
if (user.password && user.password === password) {
|
|
1097
|
+
isValid = true;
|
|
1098
|
+
} else if (user.passwordHash) {
|
|
1099
|
+
let bcrypt = null;
|
|
1100
|
+
try {
|
|
1101
|
+
bcrypt = require("bcryptjs");
|
|
1102
|
+
} catch (e) {
|
|
1103
|
+
bcrypt = null;
|
|
1104
|
+
}
|
|
1105
|
+
if (bcrypt) {
|
|
1106
|
+
try {
|
|
1107
|
+
isValid = bcrypt.compareSync(password, user.passwordHash);
|
|
1108
|
+
} catch (err) {
|
|
1109
|
+
// ignore comparison error
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
let result;
|
|
1116
|
+
if (isValid) {
|
|
1117
|
+
const groups = typeof user.group === "string"
|
|
1118
|
+
? user.group.split(",").map(g => g.trim()).filter(Boolean)
|
|
1119
|
+
: Array.isArray(user.group)
|
|
1120
|
+
? user.group
|
|
1121
|
+
: [];
|
|
1122
|
+
|
|
1123
|
+
result = {
|
|
1124
|
+
status: "Good",
|
|
1125
|
+
username: user.username,
|
|
1126
|
+
group: user.group,
|
|
1127
|
+
groups: groups
|
|
1128
|
+
};
|
|
1129
|
+
} else {
|
|
1130
|
+
result = {
|
|
1131
|
+
status: "erro",
|
|
1132
|
+
message: "Invalid username or password"
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
if (msg) {
|
|
1137
|
+
const outMsg = Object.assign({}, msg);
|
|
1138
|
+
outMsg.payload = result;
|
|
1139
|
+
|
|
1140
|
+
process.send({
|
|
1141
|
+
type: "send",
|
|
1142
|
+
data: outMsg,
|
|
1143
|
+
nodeId: nodeId
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
879
1146
|
|
|
1147
|
+
process.send({
|
|
1148
|
+
type: "status",
|
|
1149
|
+
data: {
|
|
1150
|
+
fill: isValid ? "green" : "yellow",
|
|
1151
|
+
shape: "dot",
|
|
1152
|
+
text: isValid ? "Login: Good" : "Login: erro"
|
|
1153
|
+
},
|
|
1154
|
+
nodeId: nodeId
|
|
1155
|
+
});
|
|
1156
|
+
} catch (error) {
|
|
1157
|
+
if (msg) {
|
|
1158
|
+
const outMsg = Object.assign({}, msg);
|
|
1159
|
+
outMsg.payload = {
|
|
1160
|
+
status: "erro",
|
|
1161
|
+
message: error.message
|
|
1162
|
+
};
|
|
1163
|
+
process.send({
|
|
1164
|
+
type: "send",
|
|
1165
|
+
data: outMsg,
|
|
1166
|
+
nodeId: nodeId
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
process.send({
|
|
1170
|
+
type: "status",
|
|
1171
|
+
data: { fill: "red", shape: "ring", text: "Login error: " + error.message },
|
|
1172
|
+
nodeId: nodeId
|
|
1173
|
+
});
|
|
880
1174
|
}
|
|
881
1175
|
}
|
|
882
1176
|
|
|
883
1177
|
}
|
|
884
1178
|
|
|
1179
|
+
/**
|
|
1180
|
+
* Serializes a node-opcua ServerSession into a plain, IPC-safe object.
|
|
1181
|
+
*/
|
|
1182
|
+
function buildSessionSnapshot(session) {
|
|
1183
|
+
return {
|
|
1184
|
+
sessionId: safeToString(session.nodeId),
|
|
1185
|
+
sessionName: String(session.sessionName || ""),
|
|
1186
|
+
status: String(session.__status || ""),
|
|
1187
|
+
creationDate: session.creationDate instanceof Date ? session.creationDate.toISOString() : null,
|
|
1188
|
+
sessionTimeout: safeNumber(session.sessionTimeout),
|
|
1189
|
+
clientLastContactTime: safeNumber(session.clientLastContactTime),
|
|
1190
|
+
channelId: session.channelId != null ? session.channelId : null,
|
|
1191
|
+
clientDescription: buildClientDescription(session.clientDescription),
|
|
1192
|
+
userIdentityToken: buildUserIdentityToken(session.userIdentityToken),
|
|
1193
|
+
channel: buildChannelInfo(session.channel),
|
|
1194
|
+
currentSubscriptionCount: safeNumber(session.currentSubscriptionCount),
|
|
1195
|
+
cumulatedSubscriptionCount: safeNumber(session.cumulatedSubscriptionCount),
|
|
1196
|
+
currentMonitoredItemCount: safeNumber(session.currentMonitoredItemCount),
|
|
1197
|
+
aborted: Boolean(session.aborted)
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function safeToString(value) {
|
|
1202
|
+
try {
|
|
1203
|
+
return value != null ? String(value) : null;
|
|
1204
|
+
} catch (_) {
|
|
1205
|
+
return null;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function safeNumber(value) {
|
|
1210
|
+
const n = Number(value);
|
|
1211
|
+
return Number.isFinite(n) ? n : null;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function buildClientDescription(desc) {
|
|
1215
|
+
if (!desc || typeof desc !== "object") {
|
|
1216
|
+
return null;
|
|
1217
|
+
}
|
|
1218
|
+
return {
|
|
1219
|
+
applicationUri: safeToString(desc.applicationUri),
|
|
1220
|
+
productUri: safeToString(desc.productUri),
|
|
1221
|
+
applicationName: desc.applicationName && desc.applicationName.text
|
|
1222
|
+
? String(desc.applicationName.text)
|
|
1223
|
+
: safeToString(desc.applicationName),
|
|
1224
|
+
applicationType: safeToString(desc.applicationType)
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function buildUserIdentityToken(token) {
|
|
1229
|
+
if (!token || typeof token !== "object") {
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
return {
|
|
1233
|
+
policyId: safeToString(token.policyId),
|
|
1234
|
+
userName: safeToString(token.userName),
|
|
1235
|
+
// Never expose passwords or raw credential bytes
|
|
1236
|
+
tokenType: safeToString(token.schema && token.schema.name)
|
|
1237
|
+
};
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function buildChannelInfo(channel) {
|
|
1241
|
+
if (!channel || typeof channel !== "object") {
|
|
1242
|
+
return null;
|
|
1243
|
+
}
|
|
1244
|
+
return {
|
|
1245
|
+
channelId: channel.channelId != null ? channel.channelId : null,
|
|
1246
|
+
remoteAddress: safeToString(channel.remoteAddress),
|
|
1247
|
+
remotePort: safeNumber(channel.remotePort),
|
|
1248
|
+
bytesRead: safeNumber(channel.bytesRead),
|
|
1249
|
+
bytesWritten: safeNumber(channel.bytesWritten),
|
|
1250
|
+
transactionsCount: safeNumber(channel.transactionsCount),
|
|
1251
|
+
securityMode: safeToString(channel.securityMode),
|
|
1252
|
+
securityPolicy: safeToString(channel.securityPolicy)
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
|
|
885
1256
|
/**
|
|
886
1257
|
* Instância única do processo
|
|
887
1258
|
*/
|
|
@@ -932,14 +1303,36 @@ process.on("message", async (msg) => {
|
|
|
932
1303
|
break;
|
|
933
1304
|
|
|
934
1305
|
case "buildServerSnapshot":
|
|
935
|
-
|
|
936
|
-
|
|
1306
|
+
try {
|
|
1307
|
+
await serverProcess.ensureReady();
|
|
1308
|
+
OpcUaServerStatusNode(serverProcess.node, msg.msg, msg.nodeId);
|
|
1309
|
+
} catch (error) {
|
|
1310
|
+
process.send({
|
|
1311
|
+
type: "status",
|
|
1312
|
+
data: {
|
|
1313
|
+
fill: msg.msg ? "red" : "yellow",
|
|
1314
|
+
shape: "ring",
|
|
1315
|
+
text: msg.msg ? "Status: " + error.message : "waiting for server"
|
|
1316
|
+
},
|
|
1317
|
+
nodeId: msg.nodeId
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
break;
|
|
937
1321
|
case "eventsServer":
|
|
938
1322
|
eventsServer(serverProcess.node, msg.node, msg.nodeId)
|
|
939
1323
|
break;
|
|
940
1324
|
case "readActiveAlarms":
|
|
941
1325
|
serverProcess.readActiveAlarms(msg.msg, msg.nodeId)
|
|
942
1326
|
break;
|
|
1327
|
+
case "readActiveSessions":
|
|
1328
|
+
await serverProcess.readActiveSessions(msg.msg, msg.nodeId);
|
|
1329
|
+
break;
|
|
1330
|
+
case "deleteActiveSessions":
|
|
1331
|
+
await serverProcess.deleteActiveSessions(msg.msg, msg.nodeId);
|
|
1332
|
+
break;
|
|
1333
|
+
case "validateLogin":
|
|
1334
|
+
await serverProcess.validateLogin(msg.msg, msg.nodeId);
|
|
1335
|
+
break;
|
|
943
1336
|
|
|
944
1337
|
|
|
945
1338
|
|
|
@@ -977,4 +1370,8 @@ process.on("unhandledRejection", (reason) => {
|
|
|
977
1370
|
data: "Unhandled Rejection: " + (reason?.message || reason),
|
|
978
1371
|
nodeId: serverProcess.node.id
|
|
979
1372
|
});
|
|
980
|
-
});
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
if (require.main !== module) {
|
|
1376
|
+
module.exports = { OpcUaServerProcess };
|
|
1377
|
+
}
|
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
+
const path = require("path");
|
|
5
6
|
const {
|
|
6
7
|
OPCUAServer,
|
|
7
8
|
UserTokenType,
|
|
8
9
|
buildApplicationUri,
|
|
9
10
|
makeRoles,
|
|
10
11
|
WellKnownRoles,
|
|
11
|
-
resolveNodeId
|
|
12
|
+
resolveNodeId,
|
|
13
|
+
OPCUACertificateManager,
|
|
14
|
+
SecurityPolicy,
|
|
15
|
+
MessageSecurityMode
|
|
12
16
|
} = require("./opcua-constants");
|
|
13
17
|
const { OpcUaAddressSpaceBuilder } = require("./opcua-address-space-builder");
|
|
14
18
|
const { OpcUaServerMethods } = require("./opcua-server-methods");
|
|
@@ -29,13 +33,22 @@ class OpcUaServerRuntime {
|
|
|
29
33
|
this.serverName = options.settings.serverName;
|
|
30
34
|
this.port = options.settings.port;
|
|
31
35
|
this.maxConnections = options.settings.maxConnections;
|
|
36
|
+
this.minSessionTimeout = options.settings.minSessionTimeout;
|
|
37
|
+
this.defaultSessionTimeout = options.settings.defaultSessionTimeout;
|
|
38
|
+
this.maxSessionTimeout = options.settings.maxSessionTimeout;
|
|
32
39
|
this.namespaceUri = options.settings.namespaceUri;
|
|
33
40
|
this.resourcePath = options.settings.resourcePath;
|
|
34
41
|
this.allowAnonymous = options.settings.allowAnonymous;
|
|
42
|
+
this.automaticallyAcceptUnknownCertificate = options.settings.automaticallyAcceptUnknownCertificate;
|
|
43
|
+
this.certificatesFolder = options.settings.certificatesFolder;
|
|
35
44
|
this.groups = options.settings.groups;
|
|
36
45
|
this.users = options.settings.users;
|
|
37
|
-
this.
|
|
38
|
-
|
|
46
|
+
this.securityPolicies = Array.isArray(options.settings.securityPolicies) && options.settings.securityPolicies.length > 0
|
|
47
|
+
? options.settings.securityPolicies
|
|
48
|
+
: [SecurityPolicy.None];
|
|
49
|
+
this.securityModes = Array.isArray(options.settings.securityModes) && options.settings.securityModes.length > 0
|
|
50
|
+
? options.settings.securityModes
|
|
51
|
+
: [MessageSecurityMode.None];
|
|
39
52
|
this.treeConfig = options.settings.treeConfig;
|
|
40
53
|
|
|
41
54
|
this.server = null;
|
|
@@ -52,6 +65,28 @@ class OpcUaServerRuntime {
|
|
|
52
65
|
return;
|
|
53
66
|
}
|
|
54
67
|
|
|
68
|
+
const certificateFolder = this.certificatesFolder || path.resolve(__dirname, "..", "..", "certificates");
|
|
69
|
+
this.serverCertificateManager = new OPCUACertificateManager({
|
|
70
|
+
rootFolder: certificateFolder,
|
|
71
|
+
automaticallyAcceptUnknownCertificate: this.automaticallyAcceptUnknownCertificate
|
|
72
|
+
});
|
|
73
|
+
await this.serverCertificateManager.initialize();
|
|
74
|
+
|
|
75
|
+
// Ensure directories exist immediately on startup
|
|
76
|
+
const fs = require("fs");
|
|
77
|
+
try {
|
|
78
|
+
const trustedDir = path.join(certificateFolder, "trusted", "certs");
|
|
79
|
+
const rejectedDir = path.join(certificateFolder, "rejected");
|
|
80
|
+
if (!fs.existsSync(trustedDir)) {
|
|
81
|
+
fs.mkdirSync(trustedDir, { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
if (!fs.existsSync(rejectedDir)) {
|
|
84
|
+
fs.mkdirSync(rejectedDir, { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
// Ignore directory creation errors
|
|
88
|
+
}
|
|
89
|
+
|
|
55
90
|
this.server = new OPCUAServer(this.buildServerOptions());
|
|
56
91
|
await this.server.initialize();
|
|
57
92
|
|
|
@@ -74,6 +109,7 @@ class OpcUaServerRuntime {
|
|
|
74
109
|
|
|
75
110
|
this.addressSpaceBuilder.rebuild(this.treeConfig);
|
|
76
111
|
await this.server.start();
|
|
112
|
+
|
|
77
113
|
this.registry.registerServer(this);
|
|
78
114
|
|
|
79
115
|
//Methods
|
|
@@ -124,6 +160,9 @@ class OpcUaServerRuntime {
|
|
|
124
160
|
this.ensureReady();
|
|
125
161
|
this.syncNamespaces(treeConfig);
|
|
126
162
|
this.treeConfig = treeConfig;
|
|
163
|
+
// Refresh the user list in the builder so newly added/removed users are
|
|
164
|
+
// recognised in access events without requiring a full server restart.
|
|
165
|
+
this.addressSpaceBuilder.updateUsers(this.users);
|
|
127
166
|
this.addressSpaceBuilder.sync(treeConfig);
|
|
128
167
|
}
|
|
129
168
|
|
|
@@ -200,9 +239,17 @@ class OpcUaServerRuntime {
|
|
|
200
239
|
});
|
|
201
240
|
}
|
|
202
241
|
|
|
242
|
+
const certificatesFolder = this.certificatesFolder || path.resolve(__dirname, "..", "..", "certificates");
|
|
243
|
+
|
|
203
244
|
return {
|
|
204
245
|
port: this.port,
|
|
246
|
+
minSessionTimeout: this.minSessionTimeout !== undefined ? this.minSessionTimeout : 100,
|
|
247
|
+
defaultSessionTimeout: this.defaultSessionTimeout !== undefined ? this.defaultSessionTimeout : 30000,
|
|
248
|
+
maxSessionTimeout: this.maxSessionTimeout !== undefined ? this.maxSessionTimeout : 3000000,
|
|
205
249
|
resourcePath: this.resourcePath,
|
|
250
|
+
serverCertificateManager: this.serverCertificateManager,
|
|
251
|
+
certificateFile: path.join(certificatesFolder, "own", "certs", "server_selfsigned_cert_2048.pem"),
|
|
252
|
+
privateKeyFile: path.join(certificatesFolder, "own", "private", "private_key.pem"),
|
|
206
253
|
buildInfo: {
|
|
207
254
|
productName: "opc-ua-server",
|
|
208
255
|
buildNumber: "1",
|
|
@@ -216,8 +263,8 @@ class OpcUaServerRuntime {
|
|
|
216
263
|
applicationUri: buildApplicationUri(this.serverName),
|
|
217
264
|
productUri: "urn:node-red:opc-ua-server"
|
|
218
265
|
},
|
|
219
|
-
securityPolicies:
|
|
220
|
-
securityModes:
|
|
266
|
+
securityPolicies: this.securityPolicies,
|
|
267
|
+
securityModes: this.securityModes,
|
|
221
268
|
allowAnonymous: this.allowAnonymous,
|
|
222
269
|
userManager: {
|
|
223
270
|
isValidUser: (username, password) => this.isValidUser(username, password),
|