node-opcua-server
Version:
pure nodejs OPCUA SDK - module server
946 lines (799 loc) • 38.3 kB
text/typescript
/**
* @module node-opcua-server
*/
// tslint:disable:no-console
import { randomBytes } from "crypto";
import { EventEmitter } from "events";
import { assert } from "node-opcua-assert";
import {
addElement,
AddressSpace,
ContinuationPointManager,
createExtObjArrayNode,
ensureObjectIsSecure,
ISessionBase,
removeElement,
UADynamicVariableArray,
UAObject,
UASessionDiagnosticsVariable,
UASessionSecurityDiagnostics,
DTSessionDiagnostics,
DTSessionSecurityDiagnostics,
SessionContext,
IUserManager
} from "node-opcua-address-space";
import { ISessionContext } from "node-opcua-address-space-base";
import { getMinOPCUADate, randomGuid } from "node-opcua-basic-types";
import { SessionDiagnosticsDataType, SessionSecurityDiagnosticsDataType, SubscriptionDiagnosticsDataType } from "node-opcua-common";
import { QualifiedName, NodeClass } from "node-opcua-data-model";
import { checkDebugFlag, make_debugLog, make_errorLog } from "node-opcua-debug";
import { makeNodeId, NodeId, NodeIdType, sameNodeId } from "node-opcua-nodeid";
import { ObjectRegistry } from "node-opcua-object-registry";
import { StatusCode, StatusCodes } from "node-opcua-status-code";
import { WatchDog } from "node-opcua-utils";
import { lowerFirstLetter } from "node-opcua-utils";
import { ISubscriber, IWatchdogData2 } from "node-opcua-utils";
import { IServerSession, IServerSessionBase, ServerSecureChannelLayer } from "node-opcua-secure-channel";
import { ApplicationDescription, UserIdentityToken, CreateSubscriptionRequestOptions, EndpointDescription } from "node-opcua-types";
import { ServerSidePublishEngine } from "./server_publish_engine";
import { Subscription } from "./server_subscription";
import { SubscriptionState } from "./server_subscription";
import { ServerEngine } from "./server_engine";
const debugLog = make_debugLog(__filename);
const errorLog = make_errorLog(__filename);
const doDebug = checkDebugFlag(__filename);
const theWatchDog = new WatchDog();
const registeredNodeNameSpace = 9999;
function on_channel_abort(this: ServerSession) {
debugLog("ON CHANNEL ABORT ON SESSION!!!");
/**
* @event channel_aborted
*/
this.emit("channel_aborted");
}
interface SessionDiagnosticsDataTypeEx extends SessionDiagnosticsDataType {
$session: any;
}
interface SessionSecurityDiagnosticsDataTypeEx extends SessionSecurityDiagnosticsDataType {
$session: any;
}
export type SessionStatus = "new" | "active" | "screwed" | "disposed" | "closed";
/**
*
* A Server session object.
*
* **from OPCUA Spec 1.02:**
*
* * Sessions are created to be independent of the underlying communications connection. Therefore, if a communication
* connection fails, the Session is not immediately affected. The exact mechanism to recover from an underlying
* communication connection error depends on the SecureChannel mapping as described in Part 6.
*
* * Sessions are terminated by the Server automatically if the Client fails to issue a Service request on the Session
* within the timeout period negotiated by the Server in the CreateSession Service response. This protects the Server
* against Client failures and against situations where a failed underlying connection cannot be re-established.
*
* * Clients shall be prepared to submit requests in a timely manner to prevent the Session from closing automatically.
*
* * Clients may explicitly terminate Sessions using the CloseSession Service.
*
* * When a Session is terminated, all outstanding requests on the Session are aborted and BadSessionClosed StatusCodes
* are returned to the Client. In addition, the Server deletes the entry for the Client from its
* SessionDiagnosticsArray Variable and notifies any other Clients who were subscribed to this entry.
*
*/
export class ServerSession extends EventEmitter implements ISubscriber, ISessionBase, IServerSession, IServerSessionBase {
public static registry = new ObjectRegistry();
public static maxPublishRequestInQueue = 100;
public __status: SessionStatus = "new";
public parent: ServerEngine;
public authenticationToken: NodeId;
public nodeId: NodeId;
public sessionName = "";
public publishEngine: ServerSidePublishEngine;
public sessionObject: any;
public readonly creationDate: Date;
public sessionTimeout: number;
public sessionDiagnostics?: UASessionDiagnosticsVariable<DTSessionDiagnostics>;
public sessionSecurityDiagnostics?: UASessionSecurityDiagnostics<DTSessionSecurityDiagnostics>;
public subscriptionDiagnosticsArray?: UADynamicVariableArray<SubscriptionDiagnosticsDataType>;
public channel?: ServerSecureChannelLayer;
public nonce?: Buffer;
public userIdentityToken?: UserIdentityToken;
public clientDescription?: ApplicationDescription;
public channelId?: number | null;
public continuationPointManager: ContinuationPointManager;
public sessionContext: ISessionContext;
// ISubscriber
public _watchDog?: WatchDog;
public _watchDogData?: IWatchdogData2;
keepAlive: () => void = WatchDog.emptyKeepAlive;
private _registeredNodesCounter: number;
private _registeredNodes: any;
private _registeredNodesInv: any;
private _cumulatedSubscriptionCount: number;
private _sessionDiagnostics?: SessionDiagnosticsDataTypeEx;
private _sessionSecurityDiagnostics?: SessionSecurityDiagnosticsDataTypeEx;
private channel_abort_event_handler: any;
constructor(parent: ServerEngine, userManager: IUserManager, sessionTimeout: number) {
super();
this.parent = parent; // SessionEngine
ServerSession.registry.register(this);
this.sessionContext = new SessionContext({
session: this,
server: { userManager }
});
assert(isFinite(sessionTimeout));
assert(sessionTimeout >= 0, " sessionTimeout");
this.sessionTimeout = sessionTimeout;
const authenticationTokenBuf = randomBytes(16);
this.authenticationToken = new NodeId(NodeIdType.BYTESTRING, authenticationTokenBuf);
// the sessionId
const ownNamespaceIndex = 1; // addressSpace.getOwnNamespace().index;
this.nodeId = new NodeId(NodeIdType.GUID, randomGuid(), ownNamespaceIndex);
assert(this.authenticationToken instanceof NodeId);
assert(this.nodeId instanceof NodeId);
this._cumulatedSubscriptionCount = 0;
this.publishEngine = new ServerSidePublishEngine({
maxPublishRequestInQueue: ServerSession.maxPublishRequestInQueue
});
this.publishEngine.setMaxListeners(100);
theWatchDog.addSubscriber(this, this.sessionTimeout);
this.__status = "new";
/**
* the continuation point manager for this session
* @property continuationPointManager
* @type {ContinuationPointManager}
*/
this.continuationPointManager = new ContinuationPointManager();
/**
* @property creationDate
* @type {Date}
*/
this.creationDate = new Date();
this._registeredNodesCounter = 0;
this._registeredNodes = {};
this._registeredNodesInv = {};
}
public getSessionId(): NodeId {
return this.nodeId;
}
public endpoint?: EndpointDescription;
public getEndpointDescription(): EndpointDescription {
return this.endpoint!;
}
public dispose(): void {
debugLog("ServerSession#dispose()");
assert(!this.sessionObject, " sessionObject has not been cleared !");
this.parent = null as any as ServerEngine;
this.authenticationToken = new NodeId();
if (this.publishEngine) {
this.publishEngine.dispose();
(this as any).publishEngine = null;
}
this._sessionDiagnostics = undefined;
this._registeredNodesCounter = 0;
this._registeredNodes = null;
this._registeredNodesInv = null;
(this as any).continuationPointManager = null;
this.removeAllListeners();
this.__status = "disposed";
ServerSession.registry.unregister(this);
}
public get clientConnectionTime(): Date {
return this.creationDate;
}
/**
* return the number of milisecond since last session transaction occurs from client
* the first transaction is the creation of the session
*/
public get clientLastContactTime(): number {
const lastSeen = this._watchDogData ? this._watchDogData.lastSeen : getMinOPCUADate().getTime();
return WatchDog.lastSeenToDuration(lastSeen);
}
public get status(): SessionStatus {
return this.__status;
}
public set status(value: SessionStatus) {
if (value === "active") {
this._createSessionObjectInAddressSpace();
}
if (this.__status !== value) {
this.emit("statusChanged", value);
}
this.__status = value;
}
get addressSpace(): AddressSpace | null {
if (this.parent && this.parent.addressSpace) {
return this.parent.addressSpace!;
}
return null;
}
get currentPublishRequestInQueue(): number {
return this.publishEngine ? this.publishEngine.pendingPublishRequestCount : 0;
}
public updateClientLastContactTime(): void {
if (this._sessionDiagnostics && this._sessionDiagnostics.clientLastContactTime) {
const currentTime = new Date();
// do not record all ticks as this may be overwhelming,
if (currentTime.getTime() - 250 >= this._sessionDiagnostics.clientLastContactTime.getTime()) {
this._sessionDiagnostics.clientLastContactTime = currentTime;
}
}
}
/**
* required for watch dog
* @param currentTime {DateTime}
* @private
*/
public onClientSeen(): void {
this.updateClientLastContactTime();
if (this._sessionDiagnostics) {
// see https://opcfoundation-onlineapplications.org/mantis/view.php?id=4111
assert(Object.prototype.hasOwnProperty.call(this._sessionDiagnostics, "currentMonitoredItemsCount"));
assert(Object.prototype.hasOwnProperty.call(this._sessionDiagnostics, "currentSubscriptionsCount"));
assert(Object.prototype.hasOwnProperty.call(this._sessionDiagnostics, "currentPublishRequestsInQueue"));
// note : https://opcfoundation-onlineapplications.org/mantis/view.php?id=4111
// sessionDiagnostics extension object uses a different spelling
// here with an S !!!!
if (this._sessionDiagnostics.currentMonitoredItemsCount !== this.currentMonitoredItemCount) {
this._sessionDiagnostics.currentMonitoredItemsCount = this.currentMonitoredItemCount;
}
if (this._sessionDiagnostics.currentSubscriptionsCount !== this.currentSubscriptionCount) {
this._sessionDiagnostics.currentSubscriptionsCount = this.currentSubscriptionCount;
}
if (this._sessionDiagnostics.currentPublishRequestsInQueue !== this.currentPublishRequestInQueue) {
this._sessionDiagnostics.currentPublishRequestsInQueue = this.currentPublishRequestInQueue;
}
}
}
public incrementTotalRequestCount(): void {
if (this._sessionDiagnostics && this._sessionDiagnostics.totalRequestCount) {
this._sessionDiagnostics.totalRequestCount.totalCount += 1;
}
}
public incrementRequestTotalCounter(counterName: string): void {
if (this._sessionDiagnostics) {
const propName = lowerFirstLetter(counterName + "Count");
// istanbul ignore next
if (!Object.prototype.hasOwnProperty.call(this._sessionDiagnostics, propName)) {
errorLog("incrementRequestTotalCounter: cannot find", propName);
// xx return;
} else {
(this._sessionDiagnostics as any)[propName].totalCount += 1;
}
}
}
public incrementRequestErrorCounter(counterName: string): void {
this.parent?.incrementRejectedRequestsCount();
if (this._sessionDiagnostics) {
const propName = lowerFirstLetter(counterName + "Count");
// istanbul ignore next
if (!Object.prototype.hasOwnProperty.call(this._sessionDiagnostics, propName)) {
errorLog("incrementRequestErrorCounter: cannot find", propName);
// xx return;
} else {
(this._sessionDiagnostics as any)[propName].errorCount += 1;
}
}
}
/**
* returns rootFolder.objects.server.serverDiagnostics.sessionsDiagnosticsSummary.sessionDiagnosticsArray
*/
public getSessionDiagnosticsArray(): UADynamicVariableArray<SessionDiagnosticsDataType> {
const server = this.addressSpace!.rootFolder.objects.server;
return server.serverDiagnostics.sessionsDiagnosticsSummary.sessionDiagnosticsArray as any;
}
/**
* returns rootFolder.objects.server.serverDiagnostics.sessionsDiagnosticsSummary.sessionSecurityDiagnosticsArray
*/
public getSessionSecurityDiagnosticsArray(): UADynamicVariableArray<SessionSecurityDiagnosticsDataType> {
const server = this.addressSpace!.rootFolder.objects.server;
return server.serverDiagnostics.sessionsDiagnosticsSummary.sessionSecurityDiagnosticsArray as any;
}
/**
* number of active subscriptions
*/
public get currentSubscriptionCount(): number {
return this.publishEngine ? this.publishEngine.subscriptionCount : 0;
}
/**
* number of subscriptions ever created since this object is live
*/
public get cumulatedSubscriptionCount(): number {
return this._cumulatedSubscriptionCount;
}
/**
* number of monitored items
*/
public get currentMonitoredItemCount(): number {
return this.publishEngine ? this.publishEngine.currentMonitoredItemCount : 0;
}
/**
* retrieve an existing subscription by subscriptionId
* @param subscriptionId {Number}
*/
public getSubscription(subscriptionId: number): Subscription | null {
if (!this.publishEngine) return null;
const subscription = this.publishEngine.getSubscriptionById(subscriptionId);
if (subscription && subscription.state === SubscriptionState.CLOSED) {
// subscription is CLOSED but has not been notified yet
// it should be considered as excluded
return null;
}
assert(
!subscription || subscription.state !== SubscriptionState.CLOSED,
"CLOSED subscription shall not be managed by publish engine anymore"
);
return subscription;
}
/**
* @param subscriptionId {Number}
* @return {StatusCode}
*/
public deleteSubscription(subscriptionId: number): StatusCode {
const subscription = this.getSubscription(subscriptionId);
if (!subscription) {
return StatusCodes.BadSubscriptionIdInvalid;
}
// xx this.publishEngine.remove_subscription(subscription);
subscription.terminate();
if (this.currentSubscriptionCount === 0) {
const local_publishEngine = this.publishEngine;
local_publishEngine.cancelPendingPublishRequest();
}
return StatusCodes.Good;
}
/**
* close a ServerSession, this will also delete the subscriptions if the flag is set.
*
* Spec extract:
*
* If a Client invokes the CloseSession Service then all Subscriptions associated with the Session are also deleted
* if the deleteSubscriptions flag is set to TRUE. If a Server terminates a Session for any other reason,
* Subscriptions associated with the Session, are not deleted. Each Subscription has its own lifetime to protect
* against data loss in the case of a Session termination. In these cases, the Subscription can be reassigned to
* another Client before its lifetime expires.
*
* @param deleteSubscriptions : should we delete subscription ?
* @param [reason = "CloseSession"] the reason for closing the session
* (shall be "Timeout", "Terminated" or "CloseSession")
*
*/
public close(deleteSubscriptions: boolean, reason: string): void {
debugLog(" closing session deleteSubscriptions = ", deleteSubscriptions);
if (this.publishEngine) {
this.publishEngine.onSessionClose();
}
theWatchDog.removeSubscriber(this);
// --------------- delete associated subscriptions ---------------------
if (!deleteSubscriptions && this.currentSubscriptionCount !== 0) {
// I don't know what to do yet if deleteSubscriptions is false
errorLog("TO DO : Closing session without deleting subscription not yet implemented");
// to do: Put subscriptions in safe place for future transfer if any
}
this._deleteSubscriptions();
assert(this.currentSubscriptionCount === 0);
// Post-Conditions
assert(this.currentSubscriptionCount === 0);
this.status = "closed";
this._detach_channel();
/**
* @event session_closed
* @param deleteSubscriptions {Boolean}
* @param reason {String}
*/
this.emit("session_closed", this, deleteSubscriptions, reason);
// ---------------- shut down publish engine
if (this.publishEngine) {
// remove subscription
this.publishEngine.shutdown();
assert(this.publishEngine.subscriptionCount === 0);
this.publishEngine.dispose();
this.publishEngine = null as any as ServerSidePublishEngine;
}
this._removeSessionObjectFromAddressSpace();
assert(!this.sessionDiagnostics, "ServerSession#_removeSessionObjectFromAddressSpace must be called");
assert(!this.sessionObject, "ServerSession#_removeSessionObjectFromAddressSpace must be called");
}
public registerNode(nodeId: NodeId): NodeId {
assert(nodeId instanceof NodeId);
if (nodeId.namespace === 0 && nodeId.identifierType === NodeIdType.NUMERIC) {
return nodeId;
}
const key = nodeId.toString();
const registeredNode = this._registeredNodes[key];
if (registeredNode) {
// already registered
return registeredNode;
}
const node = this.addressSpace!.findNode(nodeId);
if (!node) {
return nodeId;
}
this._registeredNodesCounter += 1;
const aliasNodeId = makeNodeId(this._registeredNodesCounter, registeredNodeNameSpace);
this._registeredNodes[key] = aliasNodeId;
this._registeredNodesInv[aliasNodeId.toString()] = node;
return aliasNodeId;
}
public unRegisterNode(aliasNodeId: NodeId): void {
assert(aliasNodeId instanceof NodeId);
if (aliasNodeId.namespace !== registeredNodeNameSpace) {
return; // not a registered Node
}
const node = this._registeredNodesInv[aliasNodeId.toString()];
if (!node) {
return;
}
this._registeredNodesInv[aliasNodeId.toString()] = null;
this._registeredNodes[node.nodeId.toString()] = null;
}
public resolveRegisteredNode(aliasNodeId: NodeId): NodeId {
if (aliasNodeId.namespace !== registeredNodeNameSpace) {
return aliasNodeId; // not a registered Node
}
const node = this._registeredNodesInv[aliasNodeId.toString()];
if (!node) {
return aliasNodeId;
}
return node.nodeId;
}
/**
* true if the underlying channel has been closed or aborted...
*/
public get aborted(): boolean {
if (!this.channel) {
return true;
}
return this.channel.aborted;
}
public createSubscription(parameters: CreateSubscriptionRequestOptions): Subscription {
const subscription = this.parent._createSubscriptionOnSession(this, parameters);
assert(!Object.prototype.hasOwnProperty.call(parameters, "id"));
this.assignSubscription(subscription);
assert(subscription.$session === this);
assert(subscription.sessionId instanceof NodeId);
assert(sameNodeId(subscription.sessionId, this.nodeId));
return subscription;
}
public _attach_channel(channel: ServerSecureChannelLayer): void {
assert(this.nonce && this.nonce instanceof Buffer);
this.channel = channel;
this.channelId = channel.channelId;
const key = this.authenticationToken.toString();
assert(!Object.prototype.hasOwnProperty.call(channel.sessionTokens, key), "channel has already a session");
channel.sessionTokens[key] = this;
// when channel is aborting
this.channel_abort_event_handler = on_channel_abort.bind(this);
channel.on("abort", this.channel_abort_event_handler);
}
public _detach_channel(): void {
const channel = this.channel;
// istanbul ignore next
if (!channel) {
return;
// already detached !
// throw new Error("expecting a valid channel");
}
assert(this.nonce && this.nonce instanceof Buffer);
assert(this.authenticationToken);
const key = this.authenticationToken.toString();
assert(Object.prototype.hasOwnProperty.call(channel.sessionTokens, key));
assert(this.channel);
assert(typeof this.channel_abort_event_handler === "function");
channel.removeListener("abort", this.channel_abort_event_handler);
delete channel.sessionTokens[key];
this.channel = undefined;
this.channelId = undefined;
}
public _exposeSubscriptionDiagnostics(subscription: Subscription): void {
debugLog("ServerSession#_exposeSubscriptionDiagnostics");
assert(subscription.$session === this);
const subscriptionDiagnosticsArray = this._getSubscriptionDiagnosticsArray();
const subscriptionDiagnostics = subscription.subscriptionDiagnostics;
assert(subscriptionDiagnostics.$subscription === subscription);
if (subscriptionDiagnostics && subscriptionDiagnosticsArray) {
// subscription.id,"on session", session.nodeId.toString());
addElement(subscriptionDiagnostics, subscriptionDiagnosticsArray);
}
}
public _unexposeSubscriptionDiagnostics(subscription: Subscription): void {
const subscriptionDiagnosticsArray = this._getSubscriptionDiagnosticsArray();
const subscriptionDiagnostics = subscription.subscriptionDiagnostics;
assert(subscriptionDiagnostics instanceof SubscriptionDiagnosticsDataType);
if (subscriptionDiagnostics && subscriptionDiagnosticsArray) {
// subscription.id,"on session", session.nodeId.toString());
removeElement(subscriptionDiagnosticsArray, (a) => a.subscriptionId === subscription.id);
}
debugLog("ServerSession#_unexposeSubscriptionDiagnostics");
}
/**
* used as a callback for the Watchdog
* @private
*/
public watchdogReset(): void {
debugLog("Session#watchdogReset: the server session has expired and must be removed from the server");
// the server session has expired and must be removed from the server
this.emit("timeout");
}
private _createSessionObjectInAddressSpace() {
if (this.sessionObject) {
return;
}
assert(!this.sessionObject, "ServerSession#_createSessionObjectInAddressSpace already called ?");
this.sessionObject = null;
if (!this.addressSpace) {
debugLog("ServerSession#_createSessionObjectInAddressSpace : no addressSpace");
return; // no addressSpace
}
const root = this.addressSpace.rootFolder;
assert(root, "expecting a root object");
if (!root.objects) {
debugLog("ServerSession#_createSessionObjectInAddressSpace : no object folder");
return false;
}
if (!root.objects.server) {
debugLog("ServerSession#_createSessionObjectInAddressSpace : no server object");
return false;
}
// self.addressSpace.findNode(makeNodeId(ObjectIds.Server_ServerDiagnostics));
const serverDiagnosticsNode = root.objects.server.serverDiagnostics;
if (!serverDiagnosticsNode || !serverDiagnosticsNode.sessionsDiagnosticsSummary) {
debugLog("ServerSession#_createSessionObjectInAddressSpace :" + " no serverDiagnostics.sessionsDiagnosticsSummary");
return false;
}
const sessionDiagnosticsObjectType = this.addressSpace.findObjectType("SessionDiagnosticsObjectType");
const sessionDiagnosticsDataType = this.addressSpace.findDataType("SessionDiagnosticsDataType");
const sessionDiagnosticsVariableType = this.addressSpace.findVariableType("SessionDiagnosticsVariableType");
const sessionSecurityDiagnosticsDataType = this.addressSpace.findDataType("SessionSecurityDiagnosticsDataType");
const sessionSecurityDiagnosticsType = this.addressSpace.findVariableType("SessionSecurityDiagnosticsType");
const namespace = this.addressSpace.getOwnNamespace();
function createSessionDiagnosticsStuff(this: ServerSession) {
if (sessionDiagnosticsDataType && sessionDiagnosticsVariableType) {
// the extension object
this._sessionDiagnostics = this.addressSpace!.constructExtensionObject(
sessionDiagnosticsDataType,
{}
)! as SessionDiagnosticsDataTypeEx;
this._sessionDiagnostics.$session = this;
// install property getter on property that are unlikely to change
if (this.parent.clientDescription) {
this._sessionDiagnostics.clientDescription = this.parent.clientDescription;
}
Object.defineProperty(this._sessionDiagnostics, "clientConnectionTime", {
get(this: any) {
return this.$session.clientConnectionTime;
}
});
Object.defineProperty(this._sessionDiagnostics, "actualSessionTimeout", {
get(this: SessionDiagnosticsDataTypeEx) {
return this.$session?.sessionTimeout;
}
});
Object.defineProperty(this._sessionDiagnostics, "sessionId", {
get(this: SessionDiagnosticsDataTypeEx) {
return this.$session ? this.$session.nodeId : NodeId.nullNodeId;
}
});
Object.defineProperty(this._sessionDiagnostics, "sessionName", {
get(this: SessionDiagnosticsDataTypeEx) {
return this.$session ? this.$session.sessionName.toString() : "";
}
});
this.sessionDiagnostics = sessionDiagnosticsVariableType.instantiate({
browseName: new QualifiedName({ name: "SessionDiagnostics", namespaceIndex: 0 }),
componentOf: this.sessionObject,
extensionObject: this._sessionDiagnostics,
minimumSamplingInterval: 2000 // 2 seconds
}) as UASessionDiagnosticsVariable<DTSessionDiagnostics>;
this._sessionDiagnostics = this.sessionDiagnostics.$extensionObject as SessionDiagnosticsDataTypeEx;
assert(this._sessionDiagnostics.$session === this);
const sessionDiagnosticsArray = this.getSessionDiagnosticsArray();
// add sessionDiagnostics into sessionDiagnosticsArray
addElement<SessionDiagnosticsDataType>(this._sessionDiagnostics, sessionDiagnosticsArray);
}
}
function createSessionSecurityDiagnosticsStuff(this: ServerSession) {
if (sessionSecurityDiagnosticsDataType && sessionSecurityDiagnosticsType) {
// the extension object
this._sessionSecurityDiagnostics = this.addressSpace!.constructExtensionObject(
sessionSecurityDiagnosticsDataType,
{}
)! as SessionSecurityDiagnosticsDataTypeEx;
this._sessionSecurityDiagnostics.$session = this;
/*
sessionId: NodeId;
clientUserIdOfSession: UAString;
clientUserIdHistory: UAString[] | null;
authenticationMechanism: UAString;
encoding: UAString;
transportProtocol: UAString;
securityMode: MessageSecurityMode;
securityPolicyUri: UAString;
clientCertificate: ByteString;
*/
Object.defineProperty(this._sessionSecurityDiagnostics, "sessionId", {
get(this: any) {
return this.$session?.nodeId;
}
});
Object.defineProperty(this._sessionSecurityDiagnostics, "clientUserIdOfSession", {
get(this: any) {
return ""; // UAString // TO DO : implement
}
});
Object.defineProperty(this._sessionSecurityDiagnostics, "clientUserIdHistory", {
get(this: any) {
return []; // UAString[] | null
}
});
Object.defineProperty(this._sessionSecurityDiagnostics, "authenticationMechanism", {
get(this: any) {
return "";
}
});
Object.defineProperty(this._sessionSecurityDiagnostics, "encoding", {
get(this: any) {
return "";
}
});
Object.defineProperty(this._sessionSecurityDiagnostics, "transportProtocol", {
get(this: any) {
return "opc.tcp";
}
});
Object.defineProperty(this._sessionSecurityDiagnostics, "securityMode", {
get(this: any) {
const session: ServerSession = this.$session;
return session?.channel?.securityMode;
}
});
Object.defineProperty(this._sessionSecurityDiagnostics, "securityPolicyUri", {
get(this: any) {
const session: ServerSession = this.$session;
return session?.channel?.securityPolicy;
}
});
Object.defineProperty(this._sessionSecurityDiagnostics, "clientCertificate", {
get(this: any) {
const session: ServerSession = this.$session;
return session?.channel!.clientCertificate;
}
});
this.sessionSecurityDiagnostics = sessionSecurityDiagnosticsType.instantiate({
browseName: new QualifiedName({ name: "SessionSecurityDiagnostics", namespaceIndex: 0 }),
componentOf: this.sessionObject,
extensionObject: this._sessionSecurityDiagnostics,
minimumSamplingInterval: 2000 // 2 seconds
}) as UASessionSecurityDiagnostics<DTSessionSecurityDiagnostics>;
ensureObjectIsSecure(this.sessionSecurityDiagnostics);
this._sessionSecurityDiagnostics = this.sessionSecurityDiagnostics
.$extensionObject as SessionSecurityDiagnosticsDataTypeEx;
assert(this._sessionSecurityDiagnostics.$session === this);
const sessionSecurityDiagnosticsArray = this.getSessionSecurityDiagnosticsArray();
// add sessionDiagnostics into sessionDiagnosticsArray
const node = addElement<SessionSecurityDiagnosticsDataType>(
this._sessionSecurityDiagnostics,
sessionSecurityDiagnosticsArray
);
ensureObjectIsSecure(node);
}
}
function createSessionDiagnosticSummaryUAObject(this: ServerSession) {
const references: any[] = [];
if (sessionDiagnosticsObjectType) {
references.push({
isForward: true,
nodeId: sessionDiagnosticsObjectType,
referenceType: "HasTypeDefinition"
});
}
this.sessionObject = namespace.createNode({
browseName: this.sessionName || "Session-" + this.nodeId.toString(),
componentOf: serverDiagnosticsNode.sessionsDiagnosticsSummary,
nodeClass: NodeClass.Object,
nodeId: this.nodeId,
references,
typeDefinition: sessionDiagnosticsObjectType
}) as UAObject;
createSessionDiagnosticsStuff.call(this);
createSessionSecurityDiagnosticsStuff.call(this);
}
function createSubscriptionDiagnosticsArray(this: ServerSession) {
const subscriptionDiagnosticsArrayType = this.addressSpace!.findVariableType("SubscriptionDiagnosticsArrayType")!;
assert(subscriptionDiagnosticsArrayType.nodeId.toString() === "ns=0;i=2171");
this.subscriptionDiagnosticsArray = createExtObjArrayNode<SubscriptionDiagnosticsDataType>(this.sessionObject, {
browseName: { namespaceIndex: 0, name: "SubscriptionDiagnosticsArray" },
complexVariableType: "SubscriptionDiagnosticsArrayType",
indexPropertyName: "subscriptionId",
minimumSamplingInterval: 2000, // 2 seconds
variableType: "SubscriptionDiagnosticsType"
});
}
createSessionDiagnosticSummaryUAObject.call(this);
createSubscriptionDiagnosticsArray.call(this);
return this.sessionObject;
}
public async resendMonitoredItemInitialValues(): Promise<void> {
for (const subscription of this.publishEngine.subscriptions) {
await subscription.resendInitialValues();
}
}
/**
*
* @private
*/
private _removeSessionObjectFromAddressSpace() {
// todo : dump session statistics in a file or somewhere for deeper diagnostic analysis on closed session
if (!this.addressSpace) {
return;
}
if (this.sessionDiagnostics) {
const sessionDiagnosticsArray = this.getSessionDiagnosticsArray()!;
removeElement(sessionDiagnosticsArray, (a) => sameNodeId(a.sessionId, this.getSessionId()));
this.addressSpace.deleteNode(this.sessionDiagnostics);
assert(this._sessionDiagnostics!.$session === this);
this._sessionDiagnostics!.$session = null;
this._sessionDiagnostics = undefined;
this.sessionDiagnostics = undefined;
}
if (this.sessionSecurityDiagnostics) {
const sessionSecurityDiagnosticsArray = this.getSessionSecurityDiagnosticsArray()!;
removeElement(sessionSecurityDiagnosticsArray, (a) => sameNodeId(a.sessionId, this.getSessionId()));
this.addressSpace.deleteNode(this.sessionSecurityDiagnostics);
assert(this._sessionSecurityDiagnostics!.$session === this);
this._sessionSecurityDiagnostics!.$session = null;
this._sessionSecurityDiagnostics = undefined;
this.sessionSecurityDiagnostics = undefined;
}
if (this.sessionObject) {
this.addressSpace.deleteNode(this.sessionObject);
this.sessionObject = null;
}
}
/**
*
* @private
*/
private _getSubscriptionDiagnosticsArray() {
if (!this.addressSpace) {
// istanbul ignore next
if (doDebug) {
console.warn("ServerSession#_getSubscriptionDiagnosticsArray : no addressSpace");
}
return null; // no addressSpace
}
const subscriptionDiagnosticsArray = this.subscriptionDiagnosticsArray;
if (!subscriptionDiagnosticsArray) {
return null; // no subscriptionDiagnosticsArray
}
assert(subscriptionDiagnosticsArray.browseName.toString() === "SubscriptionDiagnosticsArray");
return subscriptionDiagnosticsArray;
}
private assignSubscription(subscription: Subscription) {
assert(!subscription.$session);
assert(this.nodeId instanceof NodeId);
subscription.$session = this;
assert(subscription.sessionId === this.nodeId);
this._cumulatedSubscriptionCount += 1;
// Notify the owner that a new subscription has been created
// @event new_subscription
// @param {Subscription} subscription
this.emit("new_subscription", subscription);
// add subscription diagnostics to SubscriptionDiagnosticsArray
this._exposeSubscriptionDiagnostics(subscription);
subscription.once("terminated", () => {
// Notify the owner that a new subscription has been terminated
// @event subscription_terminated
// @param {Subscription} subscription
this.emit("subscription_terminated", subscription);
});
}
private _deleteSubscriptions() {
if (!this.publishEngine) return;
const subscriptions = this.publishEngine.subscriptions;
for (const subscription of subscriptions) {
this.deleteSubscription(subscription.id);
}
}
}