opcjs-base 0.1.26-alpha → 0.1.32-alpha
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/dist/index.cjs +256 -124
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +85 -55
- package/dist/index.d.ts +85 -55
- package/dist/index.js +256 -124
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
var fastXmlParser = require('fast-xml-parser');
|
|
4
4
|
|
|
5
|
+
// src/types/expandedNodeId.ts
|
|
6
|
+
var ExpandedNodeId = class {
|
|
7
|
+
/** The wrapped NodeId. */
|
|
8
|
+
nodeId;
|
|
9
|
+
/** The server index (optional, for cross-server references). */
|
|
10
|
+
serverIndex;
|
|
11
|
+
/** The namespace URI (optional, alternative to the namespace index on nodeId). */
|
|
12
|
+
namespaceUri;
|
|
13
|
+
/**
|
|
14
|
+
* Create a new ExpandedNodeId.
|
|
15
|
+
*
|
|
16
|
+
* @param nodeId - The wrapped NodeId
|
|
17
|
+
* @param namespaceUri - Optional namespace URI
|
|
18
|
+
* @param serverIndex - Optional server index
|
|
19
|
+
*/
|
|
20
|
+
constructor(nodeId, namespaceUri, serverIndex) {
|
|
21
|
+
this.nodeId = nodeId;
|
|
22
|
+
this.namespaceUri = namespaceUri;
|
|
23
|
+
this.serverIndex = serverIndex;
|
|
24
|
+
}
|
|
25
|
+
/** Convert ExpandedNodeId to string representation. */
|
|
26
|
+
toString() {
|
|
27
|
+
let result = "";
|
|
28
|
+
if (this.serverIndex !== void 0) {
|
|
29
|
+
result += `svr=${this.serverIndex};`;
|
|
30
|
+
}
|
|
31
|
+
if (this.namespaceUri !== void 0) {
|
|
32
|
+
result += `nsu=${this.namespaceUri};`;
|
|
33
|
+
}
|
|
34
|
+
result += this.nodeId.toString();
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
5
39
|
// src/types/nodeId.ts
|
|
6
40
|
var NodeIdType = /* @__PURE__ */ ((NodeIdType2) => {
|
|
7
41
|
NodeIdType2[NodeIdType2["Numeric"] = 0] = "Numeric";
|
|
@@ -172,47 +206,6 @@ var NodeId = class _NodeId {
|
|
|
172
206
|
}
|
|
173
207
|
};
|
|
174
208
|
|
|
175
|
-
// src/types/expandedNodeId.ts
|
|
176
|
-
var ExpandedNodeId = class extends NodeId {
|
|
177
|
-
/**
|
|
178
|
-
* The server index (optional, for cross-server references)
|
|
179
|
-
*/
|
|
180
|
-
serverIndex;
|
|
181
|
-
/**
|
|
182
|
-
* The namespace URI (optional, alternative to namespace index)
|
|
183
|
-
*/
|
|
184
|
-
namespaceUri;
|
|
185
|
-
/**
|
|
186
|
-
* Create a new ExpandedNodeId
|
|
187
|
-
*
|
|
188
|
-
* @param namespace - The namespace index
|
|
189
|
-
* @param identifier - The identifier
|
|
190
|
-
* @param namespaceUri - Optional namespace URI
|
|
191
|
-
* @param serverIndex - Optional server index
|
|
192
|
-
*/
|
|
193
|
-
constructor(namespace = 0, identifier = 0, namespaceUri, serverIndex) {
|
|
194
|
-
super(namespace, identifier);
|
|
195
|
-
this.namespaceUri = namespaceUri;
|
|
196
|
-
this.serverIndex = serverIndex;
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Convert ExpandedNodeId to string representation
|
|
200
|
-
*
|
|
201
|
-
* @returns String representation of the ExpandedNodeId
|
|
202
|
-
*/
|
|
203
|
-
toString() {
|
|
204
|
-
let result = "";
|
|
205
|
-
if (this.serverIndex !== void 0) {
|
|
206
|
-
result += `svr=${this.serverIndex};`;
|
|
207
|
-
}
|
|
208
|
-
if (this.namespaceUri !== void 0) {
|
|
209
|
-
result += `nsu=${this.namespaceUri};`;
|
|
210
|
-
}
|
|
211
|
-
result += super.toString();
|
|
212
|
-
return result;
|
|
213
|
-
}
|
|
214
|
-
};
|
|
215
|
-
|
|
216
209
|
// src/codecs/encoder.ts
|
|
217
210
|
var Encoder = class {
|
|
218
211
|
encoders = /* @__PURE__ */ new Map();
|
|
@@ -230,7 +223,7 @@ var Encoder = class {
|
|
|
230
223
|
}
|
|
231
224
|
const writer = writerFactory();
|
|
232
225
|
const encodingId = value.getBinaryEncodingId();
|
|
233
|
-
const eid = new ExpandedNodeId(0, encodingId);
|
|
226
|
+
const eid = new ExpandedNodeId(new NodeId(0, encodingId));
|
|
234
227
|
writer.writeExpandedNodeId(eid);
|
|
235
228
|
const typeId = value.getTypeId();
|
|
236
229
|
const encodingFunction = this.encoders.get(typeId);
|
|
@@ -277,7 +270,7 @@ var Decoder = class {
|
|
|
277
270
|
}
|
|
278
271
|
const reader = readerFactory(data);
|
|
279
272
|
const eid = reader.readExpandedNodeId();
|
|
280
|
-
return this.decodeWithEncodingId(eid.identifier, reader);
|
|
273
|
+
return this.decodeWithEncodingId(eid.nodeId.identifier, reader);
|
|
281
274
|
}
|
|
282
275
|
decodeWithEncodingId(encodingId, reader) {
|
|
283
276
|
const decodingInfo = this.encodingIdMap.get(encodingId);
|
|
@@ -493,7 +486,7 @@ function decodeExpandedNodeId(reader) {
|
|
|
493
486
|
if (hasServerIndex) {
|
|
494
487
|
serverIndex = reader.readUInt32();
|
|
495
488
|
}
|
|
496
|
-
return new ExpandedNodeId(nodeId
|
|
489
|
+
return new ExpandedNodeId(nodeId, namespaceUri, serverIndex);
|
|
497
490
|
}
|
|
498
491
|
function encodeExpandedNodeId(writer, value) {
|
|
499
492
|
let extraFlags = 0;
|
|
@@ -504,9 +497,9 @@ function encodeExpandedNodeId(writer, value) {
|
|
|
504
497
|
extraFlags |= ExpandedNodeIdMask.ServerIndexFlag;
|
|
505
498
|
}
|
|
506
499
|
if (extraFlags === 0) {
|
|
507
|
-
encodeNodeId(writer, value);
|
|
500
|
+
encodeNodeId(writer, value.nodeId);
|
|
508
501
|
} else {
|
|
509
|
-
encodeNodeIdWithExtraFlags(writer, value, extraFlags);
|
|
502
|
+
encodeNodeIdWithExtraFlags(writer, value.nodeId, extraFlags);
|
|
510
503
|
}
|
|
511
504
|
if (value.namespaceUri !== void 0) {
|
|
512
505
|
writer.writeString(value.namespaceUri);
|
|
@@ -727,7 +720,7 @@ function encodeLocalizedText(writer, value) {
|
|
|
727
720
|
// src/types/extensionObject.ts
|
|
728
721
|
var ExtensionObject = class _ExtensionObject {
|
|
729
722
|
/**
|
|
730
|
-
* The NodeId that identifies the type of structure encoded in the body.
|
|
723
|
+
* The NodeId (or ExpandedNodeId) that identifies the type of structure encoded in the body.
|
|
731
724
|
*/
|
|
732
725
|
typeId;
|
|
733
726
|
/**
|
|
@@ -1056,7 +1049,7 @@ function decodeExtensionObject(reader, decoder) {
|
|
|
1056
1049
|
}
|
|
1057
1050
|
function encodeExtensionObject(writer, value, encoder) {
|
|
1058
1051
|
const typeId = value.typeId;
|
|
1059
|
-
if (
|
|
1052
|
+
if (typeId instanceof ExpandedNodeId) {
|
|
1060
1053
|
encodeExpandedNodeId(writer, typeId);
|
|
1061
1054
|
} else {
|
|
1062
1055
|
encodeNodeId(writer, typeId);
|
|
@@ -1372,7 +1365,7 @@ function StatusCodeToString(statusCode) {
|
|
|
1372
1365
|
if (statusCode === void 0) {
|
|
1373
1366
|
return "Unknown";
|
|
1374
1367
|
}
|
|
1375
|
-
const baseCode = statusCode & 4294901760;
|
|
1368
|
+
const baseCode = (statusCode & 4294901760) >>> 0;
|
|
1376
1369
|
const name = Object.entries(StatusCode).find(([, v]) => v === baseCode)?.[0];
|
|
1377
1370
|
return name ?? `0x${baseCode.toString(16).toUpperCase().padStart(8, "0")}`;
|
|
1378
1371
|
}
|
|
@@ -1395,7 +1388,7 @@ function StatusCodeGetFlagBits(statusCode) {
|
|
|
1395
1388
|
};
|
|
1396
1389
|
}
|
|
1397
1390
|
function StatusCodeIs(statusCode, expected) {
|
|
1398
|
-
return (statusCode & 4294901760) === expected;
|
|
1391
|
+
return (statusCode & 4294901760) >>> 0 === expected;
|
|
1399
1392
|
}
|
|
1400
1393
|
|
|
1401
1394
|
// src/types/dataValue.ts
|
|
@@ -2939,12 +2932,22 @@ var SecureChannelTypeEncoder = class extends TransformStream {
|
|
|
2939
2932
|
}
|
|
2940
2933
|
};
|
|
2941
2934
|
|
|
2935
|
+
// src/secureChannel/messages/msgType.ts
|
|
2936
|
+
var MsgTypeOpenFinal = "O".charCodeAt(0) | "P".charCodeAt(0) << 8 | "N".charCodeAt(0) << 16 | "F".charCodeAt(0) << 24;
|
|
2937
|
+
var MsgTypeOpenChunk = "O".charCodeAt(0) | "P".charCodeAt(0) << 8 | "N".charCodeAt(0) << 16 | "C".charCodeAt(0) << 24;
|
|
2938
|
+
var MsgTypeAbort = "M".charCodeAt(0) | "S".charCodeAt(0) << 8 | "G".charCodeAt(0) << 16 | "A".charCodeAt(0) << 24;
|
|
2939
|
+
var MsgTypeChunk = "M".charCodeAt(0) | "S".charCodeAt(0) << 8 | "G".charCodeAt(0) << 16 | "C".charCodeAt(0) << 24;
|
|
2940
|
+
var MsgTypeFinal = "M".charCodeAt(0) | "S".charCodeAt(0) << 8 | "G".charCodeAt(0) << 16 | "F".charCodeAt(0) << 24;
|
|
2941
|
+
var MsgTypeCloseFinal = "C".charCodeAt(0) | "L".charCodeAt(0) << 8 | "O".charCodeAt(0) << 16 | "F".charCodeAt(0) << 24;
|
|
2942
|
+
|
|
2942
2943
|
// src/secureChannel/secureChannelTypeDecoder.ts
|
|
2943
2944
|
var SecureChannelTypeDecoder = class extends TransformStream {
|
|
2944
2945
|
constructor(decoder) {
|
|
2945
2946
|
super({
|
|
2946
2947
|
transform(msg, controller) {
|
|
2947
|
-
|
|
2948
|
+
if (msg.header.msgType !== MsgTypeAbort) {
|
|
2949
|
+
msg.body = decoder.decode(msg.body, "binary");
|
|
2950
|
+
}
|
|
2948
2951
|
controller.enqueue(msg);
|
|
2949
2952
|
}
|
|
2950
2953
|
});
|
|
@@ -17406,13 +17409,6 @@ var MsgSymmetric = class _MsgSymmetric extends MsgBase2 {
|
|
|
17406
17409
|
}
|
|
17407
17410
|
};
|
|
17408
17411
|
|
|
17409
|
-
// src/secureChannel/messages/msgType.ts
|
|
17410
|
-
var MsgTypeOpenFinal = "O".charCodeAt(0) | "P".charCodeAt(0) << 8 | "N".charCodeAt(0) << 16 | "F".charCodeAt(0) << 24;
|
|
17411
|
-
var MsgTypeAbort = "M".charCodeAt(0) | "S".charCodeAt(0) << 8 | "G".charCodeAt(0) << 16 | "A".charCodeAt(0) << 24;
|
|
17412
|
-
var MsgTypeChunk = "M".charCodeAt(0) | "S".charCodeAt(0) << 8 | "G".charCodeAt(0) << 16 | "C".charCodeAt(0) << 24;
|
|
17413
|
-
var MsgTypeFinal = "M".charCodeAt(0) | "S".charCodeAt(0) << 8 | "G".charCodeAt(0) << 16 | "F".charCodeAt(0) << 24;
|
|
17414
|
-
var MsgTypeCloseFinal = "C".charCodeAt(0) | "L".charCodeAt(0) << 8 | "O".charCodeAt(0) << 16 | "F".charCodeAt(0) << 24;
|
|
17415
|
-
|
|
17416
17412
|
// src/secureChannel/pendingRequests.ts
|
|
17417
17413
|
var PendingRequests = class {
|
|
17418
17414
|
map = /* @__PURE__ */ new Map();
|
|
@@ -17464,6 +17460,7 @@ var Certificate = class {
|
|
|
17464
17460
|
};
|
|
17465
17461
|
|
|
17466
17462
|
// src/secureChannel/secureChannelFacade.ts
|
|
17463
|
+
var TOKEN_RENEW_FRACTION = 0.75;
|
|
17467
17464
|
var SecureChannelFacade = class {
|
|
17468
17465
|
// ─── Constructor ────────────────────────────────────────────────────────────────
|
|
17469
17466
|
constructor(context, readerTransform, writerTransform) {
|
|
@@ -17478,11 +17475,17 @@ var SecureChannelFacade = class {
|
|
|
17478
17475
|
logger = getLogger("secureChannel.SecureChannelFacade");
|
|
17479
17476
|
writer;
|
|
17480
17477
|
reader;
|
|
17478
|
+
/** Timer handle for the scheduled token renewal; undefined when no renewal is pending. */
|
|
17479
|
+
renewalTimer;
|
|
17481
17480
|
/**
|
|
17482
|
-
*
|
|
17483
|
-
*
|
|
17481
|
+
* Builds and sends an OpenSecureChannel request.
|
|
17482
|
+
* Called for both the initial Issue and subsequent Renew requests.
|
|
17483
|
+
*
|
|
17484
|
+
* On success the context `channelId` and `tokenId` are updated and a new
|
|
17485
|
+
* renewal is scheduled at 75 % of the server-revised token lifetime, per
|
|
17486
|
+
* OPC UA Part 4 Section 5.4.1 / Part 6 Section 6.7.3.
|
|
17484
17487
|
*/
|
|
17485
|
-
async
|
|
17488
|
+
async sendOpenSecureChannel(requestType) {
|
|
17486
17489
|
const requestHeader = new RequestHeader();
|
|
17487
17490
|
requestHeader.authenticationToken = NodeId.newTwoByte(0);
|
|
17488
17491
|
requestHeader.timestamp = /* @__PURE__ */ new Date();
|
|
@@ -17494,31 +17497,72 @@ var SecureChannelFacade = class {
|
|
|
17494
17497
|
const request = new OpenSecureChannelRequest();
|
|
17495
17498
|
request.requestHeader = requestHeader;
|
|
17496
17499
|
request.clientProtocolVersion = 0;
|
|
17497
|
-
request.requestType =
|
|
17500
|
+
request.requestType = requestType;
|
|
17498
17501
|
request.securityMode = this.context.securityPolicy.getSecurityMode();
|
|
17499
17502
|
request.clientNonce = null;
|
|
17500
17503
|
request.requestedLifetime = 36e5;
|
|
17501
17504
|
const { sequenceNumber, requestId } = this.context.nextIds();
|
|
17502
|
-
this.context.securityAlgorithm = this.context.securityPolicy.getAlgorithmAsymmetric(
|
|
17505
|
+
this.context.securityAlgorithm = this.context.securityPolicy.getAlgorithmAsymmetric(
|
|
17506
|
+
new Uint8Array(),
|
|
17507
|
+
new Uint8Array()
|
|
17508
|
+
);
|
|
17503
17509
|
const msg = new MsgAsymmetric(
|
|
17504
|
-
new MsgHeader2(MsgTypeOpenFinal, 0,
|
|
17510
|
+
new MsgHeader2(MsgTypeOpenFinal, 0, this.context.channelId),
|
|
17505
17511
|
new MsgSecurityHeaderAsymmetric(
|
|
17506
17512
|
"http://opcfoundation.org/UA/SecurityPolicy#None"
|
|
17507
17513
|
),
|
|
17508
17514
|
new MsgSequenceHeader(sequenceNumber, requestId),
|
|
17509
17515
|
request
|
|
17510
17516
|
);
|
|
17511
|
-
this.
|
|
17512
|
-
|
|
17513
|
-
|
|
17514
|
-
|
|
17515
|
-
|
|
17516
|
-
|
|
17517
|
-
|
|
17518
|
-
|
|
17519
|
-
|
|
17520
|
-
|
|
17521
|
-
|
|
17517
|
+
const response = await this.pushMessage(msg);
|
|
17518
|
+
this.context.channelId = response.securityToken?.channelId;
|
|
17519
|
+
this.context.tokenId = response.securityToken?.tokenId;
|
|
17520
|
+
this.context.securityAlgorithm = this.context.securityPolicy.getAlgorithmSymmetric(
|
|
17521
|
+
new Certificate(),
|
|
17522
|
+
new Certificate()
|
|
17523
|
+
);
|
|
17524
|
+
const revisedLifetimeMs = response.securityToken?.revisedLifetime;
|
|
17525
|
+
this.scheduleRenewal(revisedLifetimeMs);
|
|
17526
|
+
}
|
|
17527
|
+
/**
|
|
17528
|
+
* Schedules a proactive token renewal at {@link TOKEN_RENEW_FRACTION} of
|
|
17529
|
+
* `lifetimeMs`. Any previously scheduled renewal is cancelled first.
|
|
17530
|
+
*/
|
|
17531
|
+
scheduleRenewal(lifetimeMs) {
|
|
17532
|
+
if (this.renewalTimer !== void 0) {
|
|
17533
|
+
clearTimeout(this.renewalTimer);
|
|
17534
|
+
}
|
|
17535
|
+
const delayMs = Math.floor(lifetimeMs * TOKEN_RENEW_FRACTION);
|
|
17536
|
+
this.logger.debug(
|
|
17537
|
+
`Scheduling SecurityToken renewal in ${delayMs} ms (75 % of ${lifetimeMs} ms lifetime).`
|
|
17538
|
+
);
|
|
17539
|
+
this.renewalTimer = setTimeout(() => {
|
|
17540
|
+
this.renewalTimer = void 0;
|
|
17541
|
+
this.logger.info("Renewing SecurityToken...");
|
|
17542
|
+
this.sendOpenSecureChannel(1 /* Renew */).catch((err) => {
|
|
17543
|
+
this.logger.error("SecurityToken renewal failed:", err);
|
|
17544
|
+
});
|
|
17545
|
+
}, delayMs);
|
|
17546
|
+
}
|
|
17547
|
+
/**
|
|
17548
|
+
* Sends the initial OpenSecureChannel request and resolves once the server
|
|
17549
|
+
* replies. Updates `context.channelId` and `context.tokenId` on success
|
|
17550
|
+
* and schedules automatic renewal at 75 % of the token lifetime.
|
|
17551
|
+
*/
|
|
17552
|
+
openSecureChannel() {
|
|
17553
|
+
this.logger.debug("Sending OpenSecureChannelRequest (Issue)...");
|
|
17554
|
+
return this.sendOpenSecureChannel(0 /* Issue */);
|
|
17555
|
+
}
|
|
17556
|
+
/**
|
|
17557
|
+
* Cancels any pending token renewal timer and releases the stream writer.
|
|
17558
|
+
* Call this when the secure channel is no longer needed.
|
|
17559
|
+
*/
|
|
17560
|
+
close() {
|
|
17561
|
+
if (this.renewalTimer !== void 0) {
|
|
17562
|
+
clearTimeout(this.renewalTimer);
|
|
17563
|
+
this.renewalTimer = void 0;
|
|
17564
|
+
}
|
|
17565
|
+
this.writer.releaseLock();
|
|
17522
17566
|
}
|
|
17523
17567
|
// ─── ISecureChannel implementation ─────────────────────────────────────────────
|
|
17524
17568
|
getSecurityPolicy() {
|
|
@@ -17555,14 +17599,20 @@ var SecureChannelFacade = class {
|
|
|
17555
17599
|
if (!value) {
|
|
17556
17600
|
this.logger.error("Received empty frame");
|
|
17557
17601
|
}
|
|
17558
|
-
const
|
|
17559
|
-
if (
|
|
17560
|
-
|
|
17561
|
-
|
|
17562
|
-
|
|
17563
|
-
);
|
|
17602
|
+
const requestId = value.sequenceHeader.requestId;
|
|
17603
|
+
if (value.header.msgType === MsgTypeAbort) {
|
|
17604
|
+
const reader = new BinaryReader(value.body);
|
|
17605
|
+
const statusCode = reader.readUInt32();
|
|
17606
|
+
const reason = reader.readString();
|
|
17607
|
+
this.context.chunkBuffers.delete(requestId);
|
|
17608
|
+
this.pending.fail(requestId, new Error(`Abort 0x${statusCode.toString(16).toUpperCase()}: ${reason}`));
|
|
17564
17609
|
} else {
|
|
17565
|
-
|
|
17610
|
+
const response = value.body;
|
|
17611
|
+
if (response instanceof ServiceFault) {
|
|
17612
|
+
this.pending.fail(requestId, new Error(`ServiceFault: ${JSON.stringify(response)}`));
|
|
17613
|
+
} else {
|
|
17614
|
+
this.pending.settle(requestId, response);
|
|
17615
|
+
}
|
|
17566
17616
|
}
|
|
17567
17617
|
}
|
|
17568
17618
|
} catch (e) {
|
|
@@ -17693,33 +17743,67 @@ var SecurityPolicyNone = class {
|
|
|
17693
17743
|
};
|
|
17694
17744
|
|
|
17695
17745
|
// src/secureChannel/secureChannelContext.ts
|
|
17696
|
-
var
|
|
17746
|
+
var SEQ_WRAP_THRESHOLD = 4294967295 - 1024;
|
|
17747
|
+
var SEQ_WRAP_START = 1;
|
|
17697
17748
|
var SecureChannelContext = class {
|
|
17698
17749
|
constructor(endpointUrl) {
|
|
17699
17750
|
this.endpointUrl = endpointUrl;
|
|
17700
17751
|
}
|
|
17701
|
-
|
|
17702
|
-
|
|
17752
|
+
/**
|
|
17753
|
+
* Next outgoing sequence number. Starts at 1 for non-ECC legacy profiles
|
|
17754
|
+
* per OPC UA Part 6, Section 6.7.2.4. The value 0 is used only as the
|
|
17755
|
+
* LastSequenceNumber sentinel in the OPN request ("no prior sequence").
|
|
17756
|
+
*/
|
|
17757
|
+
sequenceNumber = 1;
|
|
17758
|
+
/** Next outgoing request ID. The value 0 is reserved and must be skipped. */
|
|
17759
|
+
requestId = 1;
|
|
17703
17760
|
channelId = 0;
|
|
17704
17761
|
tokenId = 0;
|
|
17705
|
-
maxSendBufferSize =
|
|
17706
|
-
maxRecvBufferSize =
|
|
17707
|
-
|
|
17762
|
+
maxSendBufferSize = 2147483647;
|
|
17763
|
+
maxRecvBufferSize = 2147483647;
|
|
17764
|
+
/** Pending chunk bodies keyed by requestId, for reassembling multi-chunk messages. */
|
|
17765
|
+
chunkBuffers = /* @__PURE__ */ new Map();
|
|
17708
17766
|
securityAlgorithm;
|
|
17767
|
+
/** Last remote sequence number seen; undefined before the first received message. */
|
|
17768
|
+
lastRemoteSequenceNumber = void 0;
|
|
17709
17769
|
securityPolicy = new SecurityPolicyNone();
|
|
17710
17770
|
/**
|
|
17711
|
-
*
|
|
17712
|
-
*
|
|
17771
|
+
* Returns the current outgoing sequence number then advances it with
|
|
17772
|
+
* UInt32 wrap-around per OPC UA Part 6, Section 6.7.2.4. Only advances
|
|
17773
|
+
* the sequence counter — use when creating additional chunks for the same
|
|
17774
|
+
* message (each chunk needs its own sequence number but the same requestId).
|
|
17775
|
+
*/
|
|
17776
|
+
nextSequenceNumber() {
|
|
17777
|
+
const seq = this.sequenceNumber;
|
|
17778
|
+
if (this.sequenceNumber >= SEQ_WRAP_THRESHOLD) {
|
|
17779
|
+
this.sequenceNumber = SEQ_WRAP_START;
|
|
17780
|
+
} else {
|
|
17781
|
+
this.sequenceNumber++;
|
|
17782
|
+
}
|
|
17783
|
+
return seq;
|
|
17784
|
+
}
|
|
17785
|
+
/**
|
|
17786
|
+
* Returns the next outgoing sequence number and request ID, then advances
|
|
17787
|
+
* both counters. Call once per outgoing message; use {@link nextSequenceNumber}
|
|
17788
|
+
* for additional chunks of the same message.
|
|
17789
|
+
*
|
|
17790
|
+
* Handles UInt32 wrap-around per OPC UA Part 6, Section 6.7.2.4:
|
|
17791
|
+
* sequence numbers reset to 1 after reaching 0xFFFFFFFF-1024; request IDs
|
|
17792
|
+
* skip the reserved value 0 on wrap.
|
|
17713
17793
|
*/
|
|
17714
17794
|
nextIds() {
|
|
17715
|
-
|
|
17716
|
-
|
|
17717
|
-
|
|
17718
|
-
|
|
17795
|
+
const result = { sequenceNumber: this.nextSequenceNumber(), requestId: this.requestId };
|
|
17796
|
+
this.requestId++;
|
|
17797
|
+
if (this.requestId === 0) {
|
|
17798
|
+
this.requestId = 1;
|
|
17799
|
+
}
|
|
17800
|
+
return result;
|
|
17719
17801
|
}
|
|
17720
17802
|
};
|
|
17721
17803
|
|
|
17722
17804
|
// src/secureChannel/secureChannelMessageDecoder.ts
|
|
17805
|
+
var SEQ_WRAP_THRESHOLD2 = 4294967295 - 1024;
|
|
17806
|
+
var SEQ_WRAP_MAX = 1024;
|
|
17723
17807
|
var SecureChannelMessageDecoder = class extends TransformStream {
|
|
17724
17808
|
constructor(context) {
|
|
17725
17809
|
super({
|
|
@@ -17728,12 +17812,34 @@ var SecureChannelMessageDecoder = class extends TransformStream {
|
|
|
17728
17812
|
this.context = context;
|
|
17729
17813
|
}
|
|
17730
17814
|
logger = getLogger("secureChannel.SecureChannelMessageDecoder");
|
|
17815
|
+
/**
|
|
17816
|
+
* Validates that `sequenceNumber` is strictly increasing from the last
|
|
17817
|
+
* seen remote sequence. Allows exactly one UInt32 wrap-around per token.
|
|
17818
|
+
* Returns false and logs an error if the number is a duplicate or out of order.
|
|
17819
|
+
*/
|
|
17820
|
+
validateSequenceNumber(sequenceNumber, controller) {
|
|
17821
|
+
const last = this.context.lastRemoteSequenceNumber;
|
|
17822
|
+
if (last === void 0) {
|
|
17823
|
+
this.context.lastRemoteSequenceNumber = sequenceNumber;
|
|
17824
|
+
return true;
|
|
17825
|
+
}
|
|
17826
|
+
const isIncrement = sequenceNumber === last + 1;
|
|
17827
|
+
const isWrap = last >= SEQ_WRAP_THRESHOLD2 && sequenceNumber < SEQ_WRAP_MAX;
|
|
17828
|
+
if (!isIncrement && !isWrap) {
|
|
17829
|
+
this.logger.error(`Invalid remote sequence number: expected ${last + 1}, got ${sequenceNumber}`);
|
|
17830
|
+
controller.error(new Error(`Invalid remote sequence number: expected ${last + 1}, got ${sequenceNumber}`));
|
|
17831
|
+
return false;
|
|
17832
|
+
}
|
|
17833
|
+
this.context.lastRemoteSequenceNumber = sequenceNumber;
|
|
17834
|
+
return true;
|
|
17835
|
+
}
|
|
17731
17836
|
transform(data, controller) {
|
|
17732
17837
|
const buffer = new BinaryReader(data);
|
|
17733
17838
|
const header = MsgHeader2.decode(buffer);
|
|
17734
17839
|
switch (header.msgType) {
|
|
17735
|
-
case MsgTypeOpenFinal:
|
|
17736
|
-
|
|
17840
|
+
case MsgTypeOpenFinal:
|
|
17841
|
+
case MsgTypeOpenChunk: {
|
|
17842
|
+
this.logger.debug("SecureChannel received OpenFinal/OpenChunk message");
|
|
17737
17843
|
const secHeader = MsgSecurityHeaderAsymmetric.decode(buffer);
|
|
17738
17844
|
const msgAsym = MsgAsymmetric.decode(
|
|
17739
17845
|
buffer,
|
|
@@ -17741,16 +17847,22 @@ var SecureChannelMessageDecoder = class extends TransformStream {
|
|
|
17741
17847
|
secHeader,
|
|
17742
17848
|
this.context.securityAlgorithm
|
|
17743
17849
|
);
|
|
17850
|
+
if (!this.validateSequenceNumber(msgAsym.sequenceHeader.sequenceNumber, controller)) return;
|
|
17744
17851
|
controller.enqueue(msgAsym);
|
|
17745
17852
|
break;
|
|
17746
17853
|
}
|
|
17747
|
-
case MsgTypeAbort:
|
|
17748
|
-
this.logger.warn("
|
|
17854
|
+
case MsgTypeAbort: {
|
|
17855
|
+
this.logger.warn("SecureChannel received Abort message");
|
|
17856
|
+
const secHeader = MsgSecurityHeaderSymmetric.decode(buffer);
|
|
17857
|
+
const msgSym = MsgSymmetric.decode(buffer, header, secHeader, this.context.securityAlgorithm);
|
|
17858
|
+
controller.enqueue(msgSym);
|
|
17749
17859
|
break;
|
|
17860
|
+
}
|
|
17750
17861
|
case MsgTypeChunk: {
|
|
17751
17862
|
this.logger.debug("SecureChannel received Chunk message.");
|
|
17752
17863
|
const secHeader = MsgSecurityHeaderSymmetric.decode(buffer);
|
|
17753
17864
|
const msgSym = MsgSymmetric.decode(buffer, header, secHeader, this.context.securityAlgorithm);
|
|
17865
|
+
if (!this.validateSequenceNumber(msgSym.sequenceHeader.sequenceNumber, controller)) return;
|
|
17754
17866
|
controller.enqueue(msgSym);
|
|
17755
17867
|
break;
|
|
17756
17868
|
}
|
|
@@ -17758,11 +17870,12 @@ var SecureChannelMessageDecoder = class extends TransformStream {
|
|
|
17758
17870
|
this.logger.debug("SecureChannel received Final message");
|
|
17759
17871
|
const secHeader = MsgSecurityHeaderSymmetric.decode(buffer);
|
|
17760
17872
|
const msgSym = MsgSymmetric.decode(buffer, header, secHeader, this.context.securityAlgorithm);
|
|
17873
|
+
if (!this.validateSequenceNumber(msgSym.sequenceHeader.sequenceNumber, controller)) return;
|
|
17761
17874
|
controller.enqueue(msgSym);
|
|
17762
17875
|
break;
|
|
17763
17876
|
}
|
|
17764
17877
|
case MsgTypeCloseFinal:
|
|
17765
|
-
this.logger.warn("Unimplemented
|
|
17878
|
+
this.logger.warn("Unimplemented CloseFinal message.");
|
|
17766
17879
|
break;
|
|
17767
17880
|
default:
|
|
17768
17881
|
this.logger.warn("SecureChannel received unknown message type:", header.msgType);
|
|
@@ -17772,7 +17885,7 @@ var SecureChannelMessageDecoder = class extends TransformStream {
|
|
|
17772
17885
|
};
|
|
17773
17886
|
|
|
17774
17887
|
// src/secureChannel/secureChannelMessageEncoder.ts
|
|
17775
|
-
var
|
|
17888
|
+
var SecureChannelMessageEncoder = class extends TransformStream {
|
|
17776
17889
|
constructor(context) {
|
|
17777
17890
|
super({
|
|
17778
17891
|
transform(msg, controller) {
|
|
@@ -17787,24 +17900,33 @@ var SecureChannelMesssageEncoder = class extends TransformStream {
|
|
|
17787
17900
|
// src/secureChannel/secureChannelChunkReader.ts
|
|
17788
17901
|
var SecureChannelChunkReader = class extends TransformStream {
|
|
17789
17902
|
logger = getLogger("secureChannel.SecureChannelChunkReader");
|
|
17790
|
-
prependChunk(chunk, body) {
|
|
17791
|
-
const result = new Uint8Array(chunk.byteLength + body.byteLength);
|
|
17792
|
-
result.set(chunk, 0);
|
|
17793
|
-
result.set(body, chunk.byteLength);
|
|
17794
|
-
return result;
|
|
17795
|
-
}
|
|
17796
17903
|
transform(msg, context, controller) {
|
|
17904
|
+
const requestId = msg.sequenceHeader.requestId;
|
|
17797
17905
|
switch (msg.header.msgType) {
|
|
17798
|
-
case MsgTypeChunk:
|
|
17906
|
+
case MsgTypeChunk:
|
|
17907
|
+
case MsgTypeOpenChunk: {
|
|
17799
17908
|
this.logger.debug("Received Chunk message");
|
|
17800
|
-
context.chunkBuffers.
|
|
17909
|
+
const chunks = context.chunkBuffers.get(requestId) ?? [];
|
|
17910
|
+
chunks.push(msg.body);
|
|
17911
|
+
context.chunkBuffers.set(requestId, chunks);
|
|
17801
17912
|
break;
|
|
17802
17913
|
}
|
|
17803
|
-
case MsgTypeFinal:
|
|
17914
|
+
case MsgTypeFinal:
|
|
17915
|
+
case MsgTypeOpenFinal: {
|
|
17804
17916
|
this.logger.debug("Received Final message");
|
|
17805
|
-
|
|
17806
|
-
|
|
17807
|
-
|
|
17917
|
+
const chunks = context.chunkBuffers.get(requestId);
|
|
17918
|
+
if (chunks && chunks.length > 0) {
|
|
17919
|
+
const finalBody = msg.body;
|
|
17920
|
+
const totalLength = chunks.reduce((sum, c) => sum + c.byteLength, 0) + finalBody.byteLength;
|
|
17921
|
+
const assembled = new Uint8Array(totalLength);
|
|
17922
|
+
let offset = 0;
|
|
17923
|
+
for (const chunk of chunks) {
|
|
17924
|
+
assembled.set(chunk, offset);
|
|
17925
|
+
offset += chunk.byteLength;
|
|
17926
|
+
}
|
|
17927
|
+
assembled.set(finalBody, offset);
|
|
17928
|
+
msg.body = assembled;
|
|
17929
|
+
context.chunkBuffers.delete(requestId);
|
|
17808
17930
|
}
|
|
17809
17931
|
controller.enqueue(msg);
|
|
17810
17932
|
break;
|
|
@@ -17838,29 +17960,39 @@ var SecureChannelChunkWriter = class extends TransformStream {
|
|
|
17838
17960
|
this.logger.debug("Received Final message");
|
|
17839
17961
|
const securityAlgorithm = this.context.securityAlgorithm;
|
|
17840
17962
|
const maxCipherTextSize = this.context.maxSendBufferSize - MsgHeader2.Size - MsgSecurityHeaderSymmetric.Size;
|
|
17841
|
-
const
|
|
17842
|
-
const maxPayloadSize = maxPlainTextSize - securityAlgorithm.GetSignatureLength() - 1 - MsgSecurityHeaderSymmetric.Size + (securityAlgorithm.IsAuthenticated() ? 1 : 0);
|
|
17963
|
+
const maxPayloadSize = securityAlgorithm.GetMaxPayload(maxCipherTextSize) - MsgSequenceHeader.Size;
|
|
17843
17964
|
const data = msgSymmetric.body;
|
|
17844
|
-
const
|
|
17845
|
-
if (
|
|
17846
|
-
this.logger.debug(`Message body exceeds max chunk size, splitting into ${
|
|
17847
|
-
for (let i = 0; i <
|
|
17965
|
+
const numChunks = Math.ceil(data.byteLength / maxPayloadSize);
|
|
17966
|
+
if (numChunks > 1) {
|
|
17967
|
+
this.logger.debug(`Message body exceeds max chunk size, splitting into ${numChunks} chunks.`);
|
|
17968
|
+
for (let i = 0; i < numChunks - 1; i++) {
|
|
17848
17969
|
const chunkMsg = new MsgSymmetric(
|
|
17849
|
-
|
|
17970
|
+
// messageSize=0 is a placeholder: MsgBase.Encrypt() overwrites it at byte offset 4
|
|
17971
|
+
// with the actual encrypted size before writing to the wire.
|
|
17972
|
+
new MsgHeader2(MsgTypeChunk, 0, msgSymmetric.header.secureChannelId),
|
|
17850
17973
|
msgSymmetric.securityHeader,
|
|
17851
|
-
|
|
17974
|
+
new MsgSequenceHeader(
|
|
17975
|
+
this.context.nextSequenceNumber(),
|
|
17976
|
+
msgSymmetric.sequenceHeader.requestId
|
|
17977
|
+
),
|
|
17852
17978
|
data.subarray(i * maxPayloadSize, (i + 1) * maxPayloadSize)
|
|
17853
17979
|
);
|
|
17854
|
-
this.logger.trace(
|
|
17980
|
+
this.logger.trace(
|
|
17981
|
+
`Enqueuing chunk ${i + 1}/${numChunks} with size ${chunkMsg.body.byteLength} bytes.`
|
|
17982
|
+
);
|
|
17855
17983
|
controller.enqueue(chunkMsg);
|
|
17856
17984
|
}
|
|
17857
|
-
msg.
|
|
17985
|
+
msg.sequenceHeader = new MsgSequenceHeader(
|
|
17986
|
+
this.context.nextSequenceNumber(),
|
|
17987
|
+
msgSymmetric.sequenceHeader.requestId
|
|
17988
|
+
);
|
|
17989
|
+
msg.body = data.subarray((numChunks - 1) * maxPayloadSize);
|
|
17858
17990
|
}
|
|
17859
17991
|
break;
|
|
17860
17992
|
}
|
|
17861
17993
|
}
|
|
17862
17994
|
}
|
|
17863
|
-
this.logger.trace(`Enqueuing
|
|
17995
|
+
this.logger.trace(`Enqueuing final message with body size ${msg.body.byteLength} bytes.`);
|
|
17864
17996
|
controller.enqueue(msg);
|
|
17865
17997
|
}
|
|
17866
17998
|
};
|
|
@@ -18203,7 +18335,7 @@ exports.SecureChannelChunkWriter = SecureChannelChunkWriter;
|
|
|
18203
18335
|
exports.SecureChannelContext = SecureChannelContext;
|
|
18204
18336
|
exports.SecureChannelFacade = SecureChannelFacade;
|
|
18205
18337
|
exports.SecureChannelMessageDecoder = SecureChannelMessageDecoder;
|
|
18206
|
-
exports.
|
|
18338
|
+
exports.SecureChannelMessageEncoder = SecureChannelMessageEncoder;
|
|
18207
18339
|
exports.SecureChannelTypeDecoder = SecureChannelTypeDecoder;
|
|
18208
18340
|
exports.SecureChannelTypeEncoder = SecureChannelTypeEncoder;
|
|
18209
18341
|
exports.SecurityGroupDataType = SecurityGroupDataType;
|