node-opcua-server 2.51.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.
Files changed (100) hide show
  1. package/.mocharc.yml +10 -0
  2. package/LICENSE +20 -0
  3. package/dist/base_server.d.ts +110 -0
  4. package/dist/base_server.js +476 -0
  5. package/dist/base_server.js.map +1 -0
  6. package/dist/factory.d.ts +10 -0
  7. package/dist/factory.js +24 -0
  8. package/dist/factory.js.map +1 -0
  9. package/dist/history_server_capabilities.d.ts +35 -0
  10. package/dist/history_server_capabilities.js +44 -0
  11. package/dist/history_server_capabilities.js.map +1 -0
  12. package/dist/i_channel_data.d.ts +13 -0
  13. package/dist/i_channel_data.js +3 -0
  14. package/dist/i_channel_data.js.map +1 -0
  15. package/dist/i_register_server_manager.d.ts +16 -0
  16. package/dist/i_register_server_manager.js +3 -0
  17. package/dist/i_register_server_manager.js.map +1 -0
  18. package/dist/i_server_side_publish_engine.d.ts +36 -0
  19. package/dist/i_server_side_publish_engine.js +50 -0
  20. package/dist/i_server_side_publish_engine.js.map +1 -0
  21. package/dist/i_socket_data.d.ts +11 -0
  22. package/dist/i_socket_data.js +3 -0
  23. package/dist/i_socket_data.js.map +1 -0
  24. package/dist/index.d.ts +14 -0
  25. package/dist/index.js +27 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/monitored_item.d.ts +173 -0
  28. package/dist/monitored_item.js +1006 -0
  29. package/dist/monitored_item.js.map +1 -0
  30. package/dist/node_sampler.d.ts +3 -0
  31. package/dist/node_sampler.js +76 -0
  32. package/dist/node_sampler.js.map +1 -0
  33. package/dist/opcua_server.d.ts +668 -0
  34. package/dist/opcua_server.js +2407 -0
  35. package/dist/opcua_server.js.map +1 -0
  36. package/dist/queue.d.ts +11 -0
  37. package/dist/queue.js +71 -0
  38. package/dist/queue.js.map +1 -0
  39. package/dist/register_server_manager.d.ts +92 -0
  40. package/dist/register_server_manager.js +574 -0
  41. package/dist/register_server_manager.js.map +1 -0
  42. package/dist/register_server_manager_hidden.d.ts +17 -0
  43. package/dist/register_server_manager_hidden.js +28 -0
  44. package/dist/register_server_manager_hidden.js.map +1 -0
  45. package/dist/register_server_manager_mdns_only.d.ts +19 -0
  46. package/dist/register_server_manager_mdns_only.js +58 -0
  47. package/dist/register_server_manager_mdns_only.js.map +1 -0
  48. package/dist/server_capabilities.d.ts +61 -0
  49. package/dist/server_capabilities.js +109 -0
  50. package/dist/server_capabilities.js.map +1 -0
  51. package/dist/server_end_point.d.ts +180 -0
  52. package/dist/server_end_point.js +825 -0
  53. package/dist/server_end_point.js.map +1 -0
  54. package/dist/server_engine.d.ts +311 -0
  55. package/dist/server_engine.js +1659 -0
  56. package/dist/server_engine.js.map +1 -0
  57. package/dist/server_publish_engine.d.ts +109 -0
  58. package/dist/server_publish_engine.js +531 -0
  59. package/dist/server_publish_engine.js.map +1 -0
  60. package/dist/server_publish_engine_for_orphan_subscriptions.d.ts +16 -0
  61. package/dist/server_publish_engine_for_orphan_subscriptions.js +50 -0
  62. package/dist/server_publish_engine_for_orphan_subscriptions.js.map +1 -0
  63. package/dist/server_session.d.ts +176 -0
  64. package/dist/server_session.js +734 -0
  65. package/dist/server_session.js.map +1 -0
  66. package/dist/server_subscription.d.ts +393 -0
  67. package/dist/server_subscription.js +1313 -0
  68. package/dist/server_subscription.js.map +1 -0
  69. package/dist/sessions_compatible_for_transfer.d.ts +2 -0
  70. package/dist/sessions_compatible_for_transfer.js +36 -0
  71. package/dist/sessions_compatible_for_transfer.js.map +1 -0
  72. package/dist/validate_filter.d.ts +5 -0
  73. package/dist/validate_filter.js +64 -0
  74. package/dist/validate_filter.js.map +1 -0
  75. package/package.json +88 -0
  76. package/source/base_server.ts +617 -0
  77. package/source/factory.ts +25 -0
  78. package/source/history_server_capabilities.ts +75 -0
  79. package/source/i_channel_data.ts +17 -0
  80. package/source/i_register_server_manager.ts +24 -0
  81. package/source/i_server_side_publish_engine.ts +77 -0
  82. package/source/i_socket_data.ts +11 -0
  83. package/source/index.ts +14 -0
  84. package/source/monitored_item.ts +1303 -0
  85. package/source/node_sampler.ts +82 -0
  86. package/source/opcua_server.ts +3742 -0
  87. package/source/queue.ts +73 -0
  88. package/source/register_server_manager.ts +744 -0
  89. package/source/register_server_manager_hidden.ts +33 -0
  90. package/source/register_server_manager_mdns_only.ts +69 -0
  91. package/source/server_capabilities.ts +177 -0
  92. package/source/server_end_point.ts +1182 -0
  93. package/source/server_engine.ts +2167 -0
  94. package/source/server_publish_engine.ts +657 -0
  95. package/source/server_publish_engine_for_orphan_subscriptions.ts +52 -0
  96. package/source/server_session.ts +931 -0
  97. package/source/server_subscription.ts +1792 -0
  98. package/source/sessions_compatible_for_transfer.ts +33 -0
  99. package/source/validate_filter.ts +86 -0
  100. package/test_helpers/create_certificates.js +1 -0
@@ -0,0 +1,931 @@
1
+ /**
2
+ * @module node-opcua-server
3
+ */
4
+ // tslint:disable:no-console
5
+
6
+ import * as crypto from "crypto";
7
+ import { EventEmitter } from "events";
8
+
9
+ import {
10
+ addElement,
11
+ AddressSpace,
12
+ ContinuationPointManager,
13
+ createExtObjArrayNode,
14
+ ensureObjectIsSecure,
15
+ ISessionBase,
16
+ removeElement,
17
+ UADynamicVariableArray,
18
+ UAObject,
19
+ UASessionDiagnosticsVariable,
20
+ UASessionSecurityDiagnostics,
21
+ DTSessionDiagnostics,
22
+ DTSessionSecurityDiagnostics
23
+ } from "node-opcua-address-space";
24
+
25
+ import { assert } from "node-opcua-assert";
26
+ import { randomGuid } from "node-opcua-basic-types";
27
+ import { SessionDiagnosticsDataType, SessionSecurityDiagnosticsDataType, SubscriptionDiagnosticsDataType } from "node-opcua-common";
28
+ import { QualifiedName, NodeClass } from "node-opcua-data-model";
29
+ import { checkDebugFlag, make_debugLog } from "node-opcua-debug";
30
+ import { makeNodeId, NodeId, NodeIdType, sameNodeId } from "node-opcua-nodeid";
31
+ import { ObjectRegistry } from "node-opcua-object-registry";
32
+ import { StatusCode, StatusCodes } from "node-opcua-status-code";
33
+ import { WatchDog } from "node-opcua-utils";
34
+ import { lowerFirstLetter } from "node-opcua-utils";
35
+ import { ISubscriber, IWatchdogData2 } from "node-opcua-utils";
36
+
37
+ import { IServerSession, ServerSecureChannelLayer } from "node-opcua-secure-channel";
38
+ import { ApplicationDescription, UserIdentityToken, CreateSubscriptionRequestOptions, EndpointDescription } from "node-opcua-types";
39
+
40
+ import { ServerSidePublishEngine } from "./server_publish_engine";
41
+ import { Subscription } from "./server_subscription";
42
+ import { SubscriptionState } from "./server_subscription";
43
+ import { ServerEngine } from "./server_engine";
44
+
45
+ const debugLog = make_debugLog(__filename);
46
+ const doDebug = checkDebugFlag(__filename);
47
+ const theWatchDog = new WatchDog();
48
+
49
+ const registeredNodeNameSpace = 9999;
50
+
51
+ function compareSessionId(
52
+ sessionDiagnostics1: SessionDiagnosticsDataType | SessionSecurityDiagnosticsDataType,
53
+ sessionDiagnostics2: SessionDiagnosticsDataType | SessionSecurityDiagnosticsDataType
54
+ ) {
55
+ return sessionDiagnostics1.sessionId.toString() === sessionDiagnostics2.sessionId.toString();
56
+ }
57
+
58
+ function on_channel_abort(this: ServerSession) {
59
+ debugLog("ON CHANNEL ABORT ON SESSION!!!");
60
+ /**
61
+ * @event channel_aborted
62
+ */
63
+ this.emit("channel_aborted");
64
+ }
65
+
66
+ interface SessionDiagnosticsDataTypeEx extends SessionDiagnosticsDataType {
67
+ $session: any;
68
+ }
69
+ interface SessionSecurityDiagnosticsDataTypeEx extends SessionSecurityDiagnosticsDataType {
70
+ $session: any;
71
+ }
72
+
73
+ /**
74
+ *
75
+ * A Server session object.
76
+ *
77
+ * **from OPCUA Spec 1.02:**
78
+ *
79
+ * * Sessions are created to be independent of the underlying communications connection. Therefore, if a communication
80
+ * connection fails, the Session is not immediately affected. The exact mechanism to recover from an underlying
81
+ * communication connection error depends on the SecureChannel mapping as described in Part 6.
82
+ *
83
+ * * Sessions are terminated by the Server automatically if the Client fails to issue a Service request on the Session
84
+ * within the timeout period negotiated by the Server in the CreateSession Service response. This protects the Server
85
+ * against Client failures and against situations where a failed underlying connection cannot be re-established.
86
+ *
87
+ * * Clients shall be prepared to submit requests in a timely manner to prevent the Session from closing automatically.
88
+ *
89
+ * * Clients may explicitly terminate Sessions using the CloseSession Service.
90
+ *
91
+ * * When a Session is terminated, all outstanding requests on the Session are aborted and BadSessionClosed StatusCodes
92
+ * are returned to the Client. In addition, the Server deletes the entry for the Client from its
93
+ * SessionDiagnosticsArray Variable and notifies any other Clients who were subscribed to this entry.
94
+ *
95
+ */
96
+ export class ServerSession extends EventEmitter implements ISubscriber, ISessionBase, IServerSession {
97
+ public static registry = new ObjectRegistry();
98
+ public static maxPublishRequestInQueue: number = 100;
99
+
100
+ public __status: string = "";
101
+ public parent: ServerEngine;
102
+ public authenticationToken: NodeId;
103
+ public nodeId: NodeId;
104
+ public sessionName: string = "";
105
+
106
+ public publishEngine: ServerSidePublishEngine;
107
+ public sessionObject: any;
108
+ public readonly creationDate: Date;
109
+ public sessionTimeout: number;
110
+ public sessionDiagnostics?: UASessionDiagnosticsVariable<DTSessionDiagnostics>;
111
+ public sessionSecurityDiagnostics?: UASessionSecurityDiagnostics<DTSessionSecurityDiagnostics>;
112
+ public subscriptionDiagnosticsArray?: UADynamicVariableArray<SubscriptionDiagnosticsDataType>;
113
+ public channel?: ServerSecureChannelLayer;
114
+ public nonce?: Buffer;
115
+ public userIdentityToken?: UserIdentityToken;
116
+ public clientDescription?: ApplicationDescription;
117
+ public channelId?: number | null;
118
+ public continuationPointManager: ContinuationPointManager;
119
+
120
+ // ISubscriber
121
+ public _watchDog?: WatchDog;
122
+ public _watchDogData?: IWatchdogData2;
123
+ keepAlive: () => void = WatchDog.emptyKeepAlive;
124
+
125
+ private _registeredNodesCounter: number;
126
+ private _registeredNodes: any;
127
+ private _registeredNodesInv: any;
128
+ private _cumulatedSubscriptionCount: number;
129
+ private _sessionDiagnostics?: SessionDiagnosticsDataTypeEx;
130
+ private _sessionSecurityDiagnostics?: SessionSecurityDiagnosticsDataTypeEx;
131
+
132
+ private channel_abort_event_handler: any;
133
+
134
+ constructor(parent: ServerEngine, sessionTimeout: number) {
135
+ super();
136
+
137
+ this.parent = parent; // SessionEngine
138
+
139
+ ServerSession.registry.register(this);
140
+
141
+ assert(isFinite(sessionTimeout));
142
+ assert(sessionTimeout >= 0, " sessionTimeout");
143
+ this.sessionTimeout = sessionTimeout;
144
+
145
+ const authenticationTokenBuf = crypto.randomBytes(16);
146
+ this.authenticationToken = new NodeId(NodeIdType.BYTESTRING, authenticationTokenBuf);
147
+
148
+ // the sessionId
149
+ const ownNamespaceIndex = 1; // addressSpace.getOwnNamespace().index;
150
+ this.nodeId = new NodeId(NodeIdType.GUID, randomGuid(), ownNamespaceIndex);
151
+
152
+ assert(this.authenticationToken instanceof NodeId);
153
+ assert(this.nodeId instanceof NodeId);
154
+
155
+ this._cumulatedSubscriptionCount = 0;
156
+
157
+ this.publishEngine = new ServerSidePublishEngine({
158
+ maxPublishRequestInQueue: ServerSession.maxPublishRequestInQueue
159
+ });
160
+
161
+ this.publishEngine.setMaxListeners(100);
162
+
163
+ theWatchDog.addSubscriber(this, this.sessionTimeout);
164
+
165
+ this.__status = "new";
166
+
167
+ /**
168
+ * the continuation point manager for this session
169
+ * @property continuationPointManager
170
+ * @type {ContinuationPointManager}
171
+ */
172
+ this.continuationPointManager = new ContinuationPointManager();
173
+
174
+ /**
175
+ * @property creationDate
176
+ * @type {Date}
177
+ */
178
+ this.creationDate = new Date();
179
+
180
+ this._registeredNodesCounter = 0;
181
+ this._registeredNodes = {};
182
+ this._registeredNodesInv = {};
183
+ }
184
+
185
+ public getSessionId(): NodeId {
186
+ return this.nodeId;
187
+ }
188
+ public endpoint?: EndpointDescription;
189
+ public getEndpointDescription(): EndpointDescription {
190
+ return this.endpoint!;
191
+ }
192
+
193
+ public dispose() {
194
+ debugLog("ServerSession#dispose()");
195
+
196
+ assert(!this.sessionObject, " sessionObject has not been cleared !");
197
+
198
+ this.parent = (null as any) as ServerEngine;
199
+ this.authenticationToken = NodeId.nullNodeId;
200
+
201
+ if (this.publishEngine) {
202
+ this.publishEngine.dispose();
203
+ (this as any).publishEngine = null;
204
+ }
205
+
206
+ this._sessionDiagnostics = undefined;
207
+
208
+ this._registeredNodesCounter = 0;
209
+ this._registeredNodes = null;
210
+ this._registeredNodesInv = null;
211
+ (this as any).continuationPointManager = null;
212
+ this.removeAllListeners();
213
+ this.__status = "disposed";
214
+
215
+ ServerSession.registry.unregister(this);
216
+ }
217
+
218
+ public get clientConnectionTime() {
219
+ return this.creationDate;
220
+ }
221
+
222
+ public get clientLastContactTime() {
223
+ return this._watchDogData!.lastSeen;
224
+ }
225
+
226
+ public get status(): string {
227
+ return this.__status;
228
+ }
229
+
230
+ public set status(value: string) {
231
+ if (value === "active") {
232
+ this._createSessionObjectInAddressSpace();
233
+ }
234
+ this.__status = value;
235
+ }
236
+
237
+ get addressSpace(): AddressSpace | null {
238
+ if (this.parent && this.parent.addressSpace) {
239
+ return this.parent.addressSpace!;
240
+ }
241
+ return null;
242
+ }
243
+
244
+ get currentPublishRequestInQueue(): number {
245
+ return this.publishEngine ? this.publishEngine.pendingPublishRequestCount : 0;
246
+ }
247
+
248
+ public updateClientLastContactTime() {
249
+ const session = this;
250
+ if (session._sessionDiagnostics && session._sessionDiagnostics.clientLastContactTime) {
251
+ const currentTime = new Date();
252
+ // do not record all ticks as this may be overwhelming,
253
+ if (currentTime.getTime() - 250 >= session._sessionDiagnostics.clientLastContactTime.getTime()) {
254
+ session._sessionDiagnostics.clientLastContactTime = currentTime;
255
+ }
256
+ }
257
+ }
258
+
259
+ /**
260
+ * @method onClientSeen
261
+ * required for watch dog
262
+ * @param currentTime {DateTime}
263
+ * @private
264
+ */
265
+ public onClientSeen() {
266
+ this.updateClientLastContactTime();
267
+
268
+ if (this._sessionDiagnostics) {
269
+ // see https://opcfoundation-onlineapplications.org/mantis/view.php?id=4111
270
+ assert(this._sessionDiagnostics.hasOwnProperty("currentMonitoredItemsCount"));
271
+ assert(this._sessionDiagnostics.hasOwnProperty("currentSubscriptionsCount"));
272
+ assert(this._sessionDiagnostics.hasOwnProperty("currentPublishRequestsInQueue"));
273
+
274
+ // note : https://opcfoundation-onlineapplications.org/mantis/view.php?id=4111
275
+ // sessionDiagnostics extension object uses a different spelling
276
+ // here with an S !!!!
277
+ if (this._sessionDiagnostics.currentMonitoredItemsCount !== this.currentMonitoredItemCount) {
278
+ this._sessionDiagnostics.currentMonitoredItemsCount = this.currentMonitoredItemCount;
279
+ }
280
+ if (this._sessionDiagnostics.currentSubscriptionsCount !== this.currentSubscriptionCount) {
281
+ this._sessionDiagnostics.currentSubscriptionsCount = this.currentSubscriptionCount;
282
+ }
283
+ if (this._sessionDiagnostics.currentPublishRequestsInQueue !== this.currentPublishRequestInQueue) {
284
+ this._sessionDiagnostics.currentPublishRequestsInQueue = this.currentPublishRequestInQueue;
285
+ }
286
+ }
287
+ }
288
+
289
+ public incrementTotalRequestCount() {
290
+ if (this._sessionDiagnostics && this._sessionDiagnostics.totalRequestCount) {
291
+ this._sessionDiagnostics.totalRequestCount.totalCount += 1;
292
+ }
293
+ }
294
+
295
+ public incrementRequestTotalCounter(counterName: string) {
296
+ if (this._sessionDiagnostics) {
297
+ const propName = lowerFirstLetter(counterName + "Count");
298
+ if (!this._sessionDiagnostics.hasOwnProperty(propName)) {
299
+ console.log(" cannot find", propName);
300
+ // xx return;
301
+ } else {
302
+ // console.log(self._sessionDiagnostics.toString());
303
+ (this._sessionDiagnostics as any)[propName].totalCount += 1;
304
+ }
305
+ }
306
+ }
307
+
308
+ public incrementRequestErrorCounter(counterName: string) {
309
+ this.parent?.incrementRejectedRequestsCount();
310
+ if (this._sessionDiagnostics) {
311
+ const propName = lowerFirstLetter(counterName + "Count");
312
+ if (!this._sessionDiagnostics.hasOwnProperty(propName)) {
313
+ console.log(" cannot find", propName);
314
+ // xx return;
315
+ } else {
316
+ (this._sessionDiagnostics as any)[propName].errorCount += 1;
317
+ }
318
+ }
319
+ }
320
+
321
+ /**
322
+ * returns rootFolder.objects.server.serverDiagnostics.sessionsDiagnosticsSummary.sessionDiagnosticsArray
323
+ */
324
+ public getSessionDiagnosticsArray(): UADynamicVariableArray<SessionDiagnosticsDataType> {
325
+ const server = this.addressSpace!.rootFolder.objects.server;
326
+ return server.serverDiagnostics.sessionsDiagnosticsSummary.sessionDiagnosticsArray as any;
327
+ }
328
+
329
+ /**
330
+ * returns rootFolder.objects.server.serverDiagnostics.sessionsDiagnosticsSummary.sessionSecurityDiagnosticsArray
331
+ */
332
+ public getSessionSecurityDiagnosticsArray(): UADynamicVariableArray<SessionSecurityDiagnosticsDataType> {
333
+ const server = this.addressSpace!.rootFolder.objects.server;
334
+ return server.serverDiagnostics.sessionsDiagnosticsSummary.sessionSecurityDiagnosticsArray as any;
335
+ }
336
+
337
+ /**
338
+ * number of active subscriptions
339
+ */
340
+ public get currentSubscriptionCount(): number {
341
+ return this.publishEngine ? this.publishEngine.subscriptionCount : 0;
342
+ }
343
+
344
+ /**
345
+ * number of subscriptions ever created since this object is live
346
+ */
347
+ public get cumulatedSubscriptionCount(): number {
348
+ return this._cumulatedSubscriptionCount;
349
+ }
350
+
351
+ /**
352
+ * number of monitored items
353
+ */
354
+ public get currentMonitoredItemCount(): number {
355
+ const self = this;
356
+ return self.publishEngine ? self.publishEngine.currentMonitoredItemCount : 0;
357
+ }
358
+
359
+ /**
360
+ * retrieve an existing subscription by subscriptionId
361
+ * @method getSubscription
362
+ * @param subscriptionId {Number}
363
+ * @return {Subscription}
364
+ */
365
+ public getSubscription(subscriptionId: number): Subscription | null {
366
+ const subscription = this.publishEngine.getSubscriptionById(subscriptionId);
367
+ if (subscription && subscription.state === SubscriptionState.CLOSED) {
368
+ // subscription is CLOSED but has not been notified yet
369
+ // it should be considered as excluded
370
+ return null;
371
+ }
372
+ assert(
373
+ !subscription || subscription.state !== SubscriptionState.CLOSED,
374
+ "CLOSED subscription shall not be managed by publish engine anymore"
375
+ );
376
+ return subscription;
377
+ }
378
+
379
+ /**
380
+ * @method deleteSubscription
381
+ * @param subscriptionId {Number}
382
+ * @return {StatusCode}
383
+ */
384
+ public deleteSubscription(subscriptionId: number): StatusCode {
385
+ const session = this;
386
+ const subscription = session.getSubscription(subscriptionId);
387
+ if (!subscription) {
388
+ return StatusCodes.BadSubscriptionIdInvalid;
389
+ }
390
+
391
+ // xx this.publishEngine.remove_subscription(subscription);
392
+ subscription.terminate();
393
+
394
+ if (session.currentSubscriptionCount === 0) {
395
+ const local_publishEngine = session.publishEngine;
396
+ local_publishEngine.cancelPendingPublishRequest();
397
+ }
398
+ return StatusCodes.Good;
399
+ }
400
+
401
+ /**
402
+ * close a ServerSession, this will also delete the subscriptions if the flag is set.
403
+ *
404
+ * Spec extract:
405
+ *
406
+ * If a Client invokes the CloseSession Service then all Subscriptions associated with the Session are also deleted
407
+ * if the deleteSubscriptions flag is set to TRUE. If a Server terminates a Session for any other reason,
408
+ * Subscriptions associated with the Session, are not deleted. Each Subscription has its own lifetime to protect
409
+ * against data loss in the case of a Session termination. In these cases, the Subscription can be reassigned to
410
+ * another Client before its lifetime expires.
411
+ *
412
+ * @method close
413
+ * @param deleteSubscriptions : should we delete subscription ?
414
+ * @param [reason = "CloseSession"] the reason for closing the session
415
+ * (shall be "Timeout", "Terminated" or "CloseSession")
416
+ *
417
+ */
418
+ public close(deleteSubscriptions: boolean, reason: string): void {
419
+ debugLog(" closing session deleteSubscriptions = ", deleteSubscriptions);
420
+ if (this.publishEngine) {
421
+ this.publishEngine.onSessionClose();
422
+ }
423
+
424
+ theWatchDog.removeSubscriber(this);
425
+ // --------------- delete associated subscriptions ---------------------
426
+
427
+ if (!deleteSubscriptions && this.currentSubscriptionCount !== 0) {
428
+ // I don't know what to do yet if deleteSubscriptions is false
429
+ console.log("TO DO : Closing session without deleting subscription not yet implemented");
430
+ // to do: Put subscriptions in safe place for future transfer if any
431
+ }
432
+
433
+ this._deleteSubscriptions();
434
+
435
+ assert(this.currentSubscriptionCount === 0);
436
+
437
+ // Post-Conditions
438
+ assert(this.currentSubscriptionCount === 0);
439
+
440
+ this.status = "closed";
441
+ /**
442
+ * @event session_closed
443
+ * @param deleteSubscriptions {Boolean}
444
+ * @param reason {String}
445
+ */
446
+ this.emit("session_closed", this, deleteSubscriptions, reason);
447
+
448
+ // ---------------- shut down publish engine
449
+ if (this.publishEngine) {
450
+ // remove subscription
451
+ this.publishEngine.shutdown();
452
+
453
+ assert(this.publishEngine.subscriptionCount === 0);
454
+ this.publishEngine.dispose();
455
+ this.publishEngine = (null as any) as ServerSidePublishEngine;
456
+ }
457
+
458
+ this._removeSessionObjectFromAddressSpace();
459
+
460
+ assert(!this.sessionDiagnostics, "ServerSession#_removeSessionObjectFromAddressSpace must be called");
461
+ assert(!this.sessionObject, "ServerSession#_removeSessionObjectFromAddressSpace must be called");
462
+ }
463
+
464
+ public registerNode(nodeId: NodeId) {
465
+ assert(nodeId instanceof NodeId);
466
+ const session = this;
467
+
468
+ if (nodeId.namespace === 0 && nodeId.identifierType === NodeIdType.NUMERIC) {
469
+ return nodeId;
470
+ }
471
+
472
+ const key = nodeId.toString();
473
+
474
+ const registeredNode = session._registeredNodes[key];
475
+ if (registeredNode) {
476
+ // already registered
477
+ return registeredNode;
478
+ }
479
+
480
+ const node = session.addressSpace!.findNode(nodeId);
481
+ if (!node) {
482
+ return nodeId;
483
+ }
484
+
485
+ session._registeredNodesCounter += 1;
486
+
487
+ const aliasNodeId = makeNodeId(session._registeredNodesCounter, registeredNodeNameSpace);
488
+ session._registeredNodes[key] = aliasNodeId;
489
+ session._registeredNodesInv[aliasNodeId.toString()] = node;
490
+ return aliasNodeId;
491
+ }
492
+
493
+ public unRegisterNode(aliasNodeId: NodeId): void {
494
+ assert(aliasNodeId instanceof NodeId);
495
+ if (aliasNodeId.namespace !== registeredNodeNameSpace) {
496
+ return; // not a registered Node
497
+ }
498
+ const session = this;
499
+
500
+ const node = session._registeredNodesInv[aliasNodeId.toString()];
501
+ if (!node) {
502
+ return;
503
+ }
504
+ session._registeredNodesInv[aliasNodeId.toString()] = null;
505
+ session._registeredNodes[node.nodeId.toString()] = null;
506
+ }
507
+
508
+ public resolveRegisteredNode(aliasNodeId: NodeId): NodeId {
509
+ if (aliasNodeId.namespace !== registeredNodeNameSpace) {
510
+ return aliasNodeId; // not a registered Node
511
+ }
512
+ const node = this._registeredNodesInv[aliasNodeId.toString()];
513
+ if (!node) {
514
+ return aliasNodeId;
515
+ }
516
+ return node.nodeId;
517
+ }
518
+
519
+ /**
520
+ * true if the underlying channel has been closed or aborted...
521
+ */
522
+ public get aborted() {
523
+ if (!this.channel) {
524
+ return true;
525
+ }
526
+ return this.channel.aborted;
527
+ }
528
+
529
+ public createSubscription(parameters: CreateSubscriptionRequestOptions): Subscription {
530
+ const subscription = this.parent._createSubscriptionOnSession(this, parameters);
531
+ assert(!parameters.hasOwnProperty("id"));
532
+ this.assignSubscription(subscription);
533
+ assert(subscription.$session === this);
534
+ assert(subscription.sessionId instanceof NodeId);
535
+ assert(sameNodeId(subscription.sessionId, this.nodeId));
536
+ return subscription;
537
+ }
538
+
539
+ public _attach_channel(channel: ServerSecureChannelLayer) {
540
+ assert(this.nonce && this.nonce instanceof Buffer);
541
+ this.channel = channel;
542
+ this.channelId = channel.channelId;
543
+ const key = this.authenticationToken.toString();
544
+ assert(!channel.sessionTokens.hasOwnProperty(key), "channel has already a session");
545
+
546
+ channel.sessionTokens[key] = this;
547
+
548
+ // when channel is aborting
549
+ this.channel_abort_event_handler = on_channel_abort.bind(this);
550
+ channel.on("abort", this.channel_abort_event_handler);
551
+ }
552
+
553
+ public _detach_channel() {
554
+ const channel = this.channel;
555
+ if (!channel) {
556
+ throw new Error("expecting a valid channel");
557
+ }
558
+ assert(this.nonce && this.nonce instanceof Buffer);
559
+ assert(this.authenticationToken);
560
+ const key = this.authenticationToken.toString();
561
+ assert(channel.sessionTokens.hasOwnProperty(key));
562
+ assert(this.channel);
563
+ assert(typeof this.channel_abort_event_handler === "function");
564
+ channel.removeListener("abort", this.channel_abort_event_handler);
565
+
566
+ delete channel.sessionTokens[key];
567
+ this.channel = undefined;
568
+ this.channelId = undefined;
569
+ }
570
+
571
+ public _exposeSubscriptionDiagnostics(subscription: Subscription) {
572
+ debugLog("ServerSession#_exposeSubscriptionDiagnostics");
573
+ assert(subscription.$session === this);
574
+ const subscriptionDiagnosticsArray = this._getSubscriptionDiagnosticsArray();
575
+ const subscriptionDiagnostics = subscription.subscriptionDiagnostics;
576
+ assert(subscriptionDiagnostics.$subscription === subscription);
577
+
578
+ if (subscriptionDiagnostics && subscriptionDiagnosticsArray) {
579
+ // subscription.id,"on session", session.nodeId.toString());
580
+ addElement(subscriptionDiagnostics, subscriptionDiagnosticsArray);
581
+ }
582
+ }
583
+
584
+ public _unexposeSubscriptionDiagnostics(subscription: Subscription) {
585
+ const subscriptionDiagnosticsArray = this._getSubscriptionDiagnosticsArray();
586
+ const subscriptionDiagnostics = subscription.subscriptionDiagnostics;
587
+ assert(subscriptionDiagnostics instanceof SubscriptionDiagnosticsDataType);
588
+ if (subscriptionDiagnostics && subscriptionDiagnosticsArray) {
589
+ // console.log("GG => ServerSession **Unexposing** subscription diagnostics =>",
590
+ // subscription.id,"on session", session.nodeId.toString());
591
+ removeElement(subscriptionDiagnosticsArray, subscriptionDiagnostics);
592
+ }
593
+ debugLog("ServerSession#_unexposeSubscriptionDiagnostics");
594
+ }
595
+ /**
596
+ * @method watchdogReset
597
+ * used as a callback for the Watchdog
598
+ * @private
599
+ */
600
+ public watchdogReset() {
601
+ debugLog("Session#watchdogReset: the server session has expired and must be removed from the server");
602
+ // the server session has expired and must be removed from the server
603
+ this.emit("timeout");
604
+ }
605
+
606
+ private _createSessionObjectInAddressSpace() {
607
+ if (this.sessionObject) {
608
+ return;
609
+ }
610
+ assert(!this.sessionObject, "ServerSession#_createSessionObjectInAddressSpace already called ?");
611
+
612
+ this.sessionObject = null;
613
+ if (!this.addressSpace) {
614
+ debugLog("ServerSession#_createSessionObjectInAddressSpace : no addressSpace");
615
+ return; // no addressSpace
616
+ }
617
+ const root = this.addressSpace.rootFolder;
618
+ assert(root, "expecting a root object");
619
+
620
+ if (!root.objects) {
621
+ debugLog("ServerSession#_createSessionObjectInAddressSpace : no object folder");
622
+ return false;
623
+ }
624
+ if (!root.objects.server) {
625
+ debugLog("ServerSession#_createSessionObjectInAddressSpace : no server object");
626
+ return false;
627
+ }
628
+
629
+ // self.addressSpace.findNode(makeNodeId(ObjectIds.Server_ServerDiagnostics));
630
+ const serverDiagnosticsNode = root.objects.server.serverDiagnostics;
631
+
632
+ if (!serverDiagnosticsNode || !serverDiagnosticsNode.sessionsDiagnosticsSummary) {
633
+ debugLog("ServerSession#_createSessionObjectInAddressSpace :" + " no serverDiagnostics.sessionsDiagnosticsSummary");
634
+ return false;
635
+ }
636
+
637
+ const sessionDiagnosticsObjectType = this.addressSpace.findObjectType("SessionDiagnosticsObjectType");
638
+
639
+ const sessionDiagnosticsDataType = this.addressSpace.findDataType("SessionDiagnosticsDataType");
640
+ const sessionDiagnosticsVariableType = this.addressSpace.findVariableType("SessionDiagnosticsVariableType");
641
+
642
+ const sessionSecurityDiagnosticsDataType = this.addressSpace.findDataType("SessionSecurityDiagnosticsDataType");
643
+ const sessionSecurityDiagnosticsType = this.addressSpace.findVariableType("SessionSecurityDiagnosticsType");
644
+
645
+ const namespace = this.addressSpace.getOwnNamespace();
646
+
647
+ function createSessionDiagnosticsStuff(this: ServerSession) {
648
+ if (sessionDiagnosticsDataType && sessionDiagnosticsVariableType) {
649
+ // the extension object
650
+ this._sessionDiagnostics = this.addressSpace!.constructExtensionObject(
651
+ sessionDiagnosticsDataType,
652
+ {}
653
+ )! as SessionDiagnosticsDataTypeEx;
654
+ this._sessionDiagnostics.$session = this;
655
+
656
+ // install property getter on property that are unlikely to change
657
+ if (this.parent.clientDescription) {
658
+ this._sessionDiagnostics.clientDescription = this.parent.clientDescription;
659
+ }
660
+
661
+ Object.defineProperty(this._sessionDiagnostics, "clientConnectionTime", {
662
+ get(this: any) {
663
+ return this.$session.clientConnectionTime;
664
+ }
665
+ });
666
+
667
+ Object.defineProperty(this._sessionDiagnostics, "actualSessionTimeout", {
668
+ get(this: SessionDiagnosticsDataTypeEx) {
669
+ return this.$session?.sessionTimeout;
670
+ }
671
+ });
672
+
673
+ Object.defineProperty(this._sessionDiagnostics, "sessionId", {
674
+ get(this: SessionDiagnosticsDataTypeEx) {
675
+ return this.$session?.nodeId;
676
+ }
677
+ });
678
+
679
+ Object.defineProperty(this._sessionDiagnostics, "sessionName", {
680
+ get(this: SessionDiagnosticsDataTypeEx) {
681
+ return this.$session?.sessionName.toString();
682
+ }
683
+ });
684
+
685
+ this.sessionDiagnostics = sessionDiagnosticsVariableType.instantiate({
686
+ browseName: new QualifiedName({ name: "SessionDiagnostics", namespaceIndex: 0 }),
687
+ componentOf: this.sessionObject,
688
+ extensionObject: this._sessionDiagnostics,
689
+ minimumSamplingInterval: 2000 // 2 seconds
690
+ }) as UASessionDiagnosticsVariable<DTSessionDiagnostics>;
691
+
692
+ this._sessionDiagnostics = this.sessionDiagnostics.$extensionObject as SessionDiagnosticsDataTypeEx;
693
+ assert(this._sessionDiagnostics.$session === this);
694
+
695
+ const sessionDiagnosticsArray = this.getSessionDiagnosticsArray();
696
+
697
+ // add sessionDiagnostics into sessionDiagnosticsArray
698
+ addElement<SessionDiagnosticsDataType>(this._sessionDiagnostics, sessionDiagnosticsArray);
699
+ }
700
+ }
701
+ function createSessionSecurityDiagnosticsStuff(this: ServerSession) {
702
+ if (sessionSecurityDiagnosticsDataType && sessionSecurityDiagnosticsType) {
703
+ // the extension object
704
+ this._sessionSecurityDiagnostics = this.addressSpace!.constructExtensionObject(
705
+ sessionSecurityDiagnosticsDataType,
706
+ {}
707
+ )! as SessionSecurityDiagnosticsDataTypeEx;
708
+ this._sessionSecurityDiagnostics.$session = this;
709
+
710
+ /*
711
+ sessionId: NodeId;
712
+ clientUserIdOfSession: UAString;
713
+ clientUserIdHistory: UAString[] | null;
714
+ authenticationMechanism: UAString;
715
+ encoding: UAString;
716
+ transportProtocol: UAString;
717
+ securityMode: MessageSecurityMode;
718
+ securityPolicyUri: UAString;
719
+ clientCertificate: ByteString;
720
+ */
721
+ Object.defineProperty(this._sessionSecurityDiagnostics, "sessionId", {
722
+ get(this: any) {
723
+ return this.$session?.nodeId;
724
+ }
725
+ });
726
+
727
+ Object.defineProperty(this._sessionSecurityDiagnostics, "clientUserIdOfSession", {
728
+ get(this: any) {
729
+ return ""; // UAString // TO DO : implement
730
+ }
731
+ });
732
+
733
+ Object.defineProperty(this._sessionSecurityDiagnostics, "clientUserIdHistory", {
734
+ get(this: any) {
735
+ return []; // UAString[] | null
736
+ }
737
+ });
738
+
739
+ Object.defineProperty(this._sessionSecurityDiagnostics, "authenticationMechanism", {
740
+ get(this: any) {
741
+ return "";
742
+ }
743
+ });
744
+ Object.defineProperty(this._sessionSecurityDiagnostics, "encoding", {
745
+ get(this: any) {
746
+ return "";
747
+ }
748
+ });
749
+ Object.defineProperty(this._sessionSecurityDiagnostics, "transportProtocol", {
750
+ get(this: any) {
751
+ return "opc.tcp";
752
+ }
753
+ });
754
+ Object.defineProperty(this._sessionSecurityDiagnostics, "securityMode", {
755
+ get(this: any) {
756
+ const session: ServerSession = this.$session;
757
+ return session?.channel?.securityMode;
758
+ }
759
+ });
760
+ Object.defineProperty(this._sessionSecurityDiagnostics, "securityPolicyUri", {
761
+ get(this: any) {
762
+ const session: ServerSession = this.$session;
763
+ return session?.channel?.securityPolicy;
764
+ }
765
+ });
766
+ Object.defineProperty(this._sessionSecurityDiagnostics, "clientCertificate", {
767
+ get(this: any) {
768
+ const session: ServerSession = this.$session;
769
+ return session?.channel!.clientCertificate;
770
+ }
771
+ });
772
+
773
+ this.sessionSecurityDiagnostics = sessionSecurityDiagnosticsType.instantiate({
774
+ browseName: new QualifiedName({ name: "SessionSecurityDiagnostics", namespaceIndex: 0 }),
775
+ componentOf: this.sessionObject,
776
+ extensionObject: this._sessionSecurityDiagnostics,
777
+ minimumSamplingInterval: 2000 // 2 seconds
778
+ }) as UASessionSecurityDiagnostics<DTSessionSecurityDiagnostics>;
779
+
780
+ ensureObjectIsSecure(this.sessionSecurityDiagnostics);
781
+
782
+ this._sessionSecurityDiagnostics = this.sessionSecurityDiagnostics
783
+ .$extensionObject as SessionSecurityDiagnosticsDataTypeEx;
784
+ assert(this._sessionSecurityDiagnostics.$session === this);
785
+
786
+ const sessionSecurityDiagnosticsArray = this.getSessionSecurityDiagnosticsArray();
787
+
788
+ // add sessionDiagnostics into sessionDiagnosticsArray
789
+ const node = addElement<SessionSecurityDiagnosticsDataType>(
790
+ this._sessionSecurityDiagnostics,
791
+ sessionSecurityDiagnosticsArray
792
+ );
793
+ ensureObjectIsSecure(node);
794
+ }
795
+ }
796
+
797
+ function createSessionDiagnosticSummaryUAObject(this: ServerSession) {
798
+ const references: any[] = [];
799
+ if (sessionDiagnosticsObjectType) {
800
+ references.push({
801
+ isForward: true,
802
+ nodeId: sessionDiagnosticsObjectType,
803
+ referenceType: "HasTypeDefinition"
804
+ });
805
+ }
806
+
807
+ this.sessionObject = namespace.createNode({
808
+ browseName: this.sessionName || "Session-" + this.nodeId.toString(),
809
+ componentOf: serverDiagnosticsNode.sessionsDiagnosticsSummary,
810
+ nodeClass: NodeClass.Object,
811
+ nodeId: this.nodeId,
812
+ references,
813
+ typeDefinition: sessionDiagnosticsObjectType
814
+ }) as UAObject;
815
+
816
+ createSessionDiagnosticsStuff.call(this);
817
+ createSessionSecurityDiagnosticsStuff.call(this);
818
+ }
819
+ function createSubscriptionDiagnosticsArray(this: ServerSession) {
820
+ const subscriptionDiagnosticsArrayType = this.addressSpace!.findVariableType("SubscriptionDiagnosticsArrayType")!;
821
+ assert(subscriptionDiagnosticsArrayType.nodeId.toString() === "ns=0;i=2171");
822
+
823
+ this.subscriptionDiagnosticsArray = createExtObjArrayNode<SubscriptionDiagnosticsDataType>(this.sessionObject, {
824
+ browseName: { namespaceIndex: 0, name: "SubscriptionDiagnosticsArray" },
825
+ complexVariableType: "SubscriptionDiagnosticsArrayType",
826
+ indexPropertyName: "subscriptionId",
827
+ minimumSamplingInterval: 2000, // 2 seconds
828
+ variableType: "SubscriptionDiagnosticsType"
829
+ });
830
+ }
831
+ createSessionDiagnosticSummaryUAObject.call(this);
832
+ createSubscriptionDiagnosticsArray.call(this);
833
+ return this.sessionObject;
834
+ }
835
+
836
+ /**
837
+ *
838
+ * @private
839
+ */
840
+ private _removeSessionObjectFromAddressSpace() {
841
+ // todo : dump session statistics in a file or somewhere for deeper diagnostic analysis on closed session
842
+
843
+ if (!this.addressSpace) {
844
+ return;
845
+ }
846
+ if (this.sessionDiagnostics) {
847
+ const sessionDiagnosticsArray = this.getSessionDiagnosticsArray()!;
848
+ removeElement(sessionDiagnosticsArray, this.sessionDiagnostics.$extensionObject);
849
+
850
+ this.addressSpace.deleteNode(this.sessionDiagnostics);
851
+
852
+ assert(this._sessionDiagnostics!.$session === this);
853
+ this._sessionDiagnostics!.$session = null;
854
+
855
+ this._sessionDiagnostics = undefined;
856
+ this.sessionDiagnostics = undefined;
857
+ }
858
+
859
+ if (this.sessionSecurityDiagnostics) {
860
+ const sessionSecurityDiagnosticsArray = this.getSessionSecurityDiagnosticsArray()!;
861
+ removeElement(sessionSecurityDiagnosticsArray, this.sessionSecurityDiagnostics.$extensionObject);
862
+
863
+ this.addressSpace.deleteNode(this.sessionSecurityDiagnostics);
864
+
865
+ assert(this._sessionSecurityDiagnostics!.$session === this);
866
+ this._sessionSecurityDiagnostics!.$session = null;
867
+
868
+ this._sessionSecurityDiagnostics = undefined;
869
+ this.sessionSecurityDiagnostics = undefined;
870
+ }
871
+
872
+ if (this.sessionObject) {
873
+ this.addressSpace.deleteNode(this.sessionObject);
874
+ this.sessionObject = null;
875
+ }
876
+ }
877
+
878
+ /**
879
+ *
880
+ * @private
881
+ */
882
+ private _getSubscriptionDiagnosticsArray() {
883
+ if (!this.addressSpace) {
884
+ if (doDebug) {
885
+ console.warn("ServerSession#_getSubscriptionDiagnosticsArray : no addressSpace");
886
+ }
887
+ return null; // no addressSpace
888
+ }
889
+
890
+ const subscriptionDiagnosticsArray = this.subscriptionDiagnosticsArray;
891
+ if (!subscriptionDiagnosticsArray) {
892
+ return null; // no subscriptionDiagnosticsArray
893
+ }
894
+ assert(subscriptionDiagnosticsArray.browseName.toString() === "SubscriptionDiagnosticsArray");
895
+ return subscriptionDiagnosticsArray;
896
+ }
897
+
898
+ private assignSubscription(subscription: Subscription) {
899
+ assert(!subscription.$session);
900
+ assert(this.nodeId instanceof NodeId);
901
+
902
+ subscription.$session = this;
903
+
904
+ subscription.sessionId = this.nodeId;
905
+
906
+ this._cumulatedSubscriptionCount += 1;
907
+
908
+ // Notify the owner that a new subscription has been created
909
+ // @event new_subscription
910
+ // @param {Subscription} subscription
911
+ this.emit("new_subscription", subscription);
912
+
913
+ // add subscription diagnostics to SubscriptionDiagnosticsArray
914
+ this._exposeSubscriptionDiagnostics(subscription);
915
+
916
+ subscription.once("terminated", () => {
917
+ // Notify the owner that a new subscription has been terminated
918
+ // @event subscription_terminated
919
+ // @param {Subscription} subscription
920
+ this.emit("subscription_terminated", subscription);
921
+ });
922
+ }
923
+
924
+ private _deleteSubscriptions() {
925
+ assert(this.publishEngine);
926
+ const subscriptions = this.publishEngine.subscriptions;
927
+ for (const subscription of subscriptions) {
928
+ this.deleteSubscription(subscription.id);
929
+ }
930
+ }
931
+ }