node-opcua-server
Version:
pure nodejs OPCUA SDK - module server
1,132 lines (967 loc) • 88.2 kB
text/typescript
/**
* @module node-opcua-server
*/
import { EventEmitter } from "events";
import { types } from "util";
import async from "async";
import chalk from "chalk";
import { assert } from "node-opcua-assert";
import { BinaryStream } from "node-opcua-binary-stream";
import {
addElement,
AddressSpace,
bindExtObjArrayNode,
ensureObjectIsSecure,
MethodFunctor,
removeElement,
SessionContext,
UADynamicVariableArray,
UAMethod,
UAObject,
UAServerDiagnosticsSummary,
UAServerStatus,
UAVariable,
UAServerDiagnostics,
BindVariableOptions,
ISessionContext,
DTServerStatus,
IServerBase
} from "node-opcua-address-space";
import { generateAddressSpace } from "node-opcua-address-space/nodeJS";
import { DataValue } from "node-opcua-data-value";
import {
ServerDiagnosticsSummaryDataType,
ServerState,
ServerStatusDataType,
SubscriptionDiagnosticsDataType
} from "node-opcua-common";
import { AttributeIds, coerceLocalizedText, LocalizedTextLike, makeAccessLevelFlag, NodeClass } from "node-opcua-data-model";
import { coerceNodeId, makeNodeId, NodeId, NodeIdLike, NodeIdType, resolveNodeId } from "node-opcua-nodeid";
import { BrowseResult } from "node-opcua-service-browse";
import { UInt32 } from "node-opcua-basic-types";
import { CreateSubscriptionRequestLike } from "node-opcua-client";
import { DataTypeIds, MethodIds, ObjectIds, VariableIds } from "node-opcua-constants";
import { getCurrentClock, getMinOPCUADate } from "node-opcua-date-time";
import { checkDebugFlag, make_debugLog, make_errorLog, make_warningLog, traceFromThisProjectOnly } from "node-opcua-debug";
import { nodesets } from "node-opcua-nodesets";
import { ObjectRegistry } from "node-opcua-object-registry";
import { CallMethodResult } from "node-opcua-service-call";
import { TransferResult } from "node-opcua-service-subscription";
import { ApplicationDescription } from "node-opcua-service-endpoints";
import { HistoryReadRequest, HistoryReadResult, HistoryReadValueId } from "node-opcua-service-history";
import { StatusCode, StatusCodes, CallbackT } from "node-opcua-status-code";
import {
BrowseDescription,
BrowsePath,
BrowsePathResult,
BuildInfo,
BuildInfoOptions,
SessionDiagnosticsDataType,
SessionSecurityDiagnosticsDataType,
WriteValue,
ReadValueId,
TimeZoneDataType,
ProgramDiagnosticDataType,
CallMethodResultOptions,
ReadRequestOptions,
BrowseDescriptionOptions,
CallMethodRequest,
ApplicationType
} from "node-opcua-types";
import { DataType, isValidVariant, Variant, VariantArrayType } from "node-opcua-variant";
import { HistoryServerCapabilities, HistoryServerCapabilitiesOptions } from "./history_server_capabilities";
import { MonitoredItem } from "./monitored_item";
import { ServerCapabilities, ServerCapabilitiesOptions, ServerOperationLimits, defaultServerCapabilities } from "./server_capabilities";
import { ServerSidePublishEngine } from "./server_publish_engine";
import { ServerSidePublishEngineForOrphanSubscription } from "./server_publish_engine_for_orphan_subscriptions";
import { ServerSession } from "./server_session";
import { Subscription } from "./server_subscription";
import { sessionsCompatibleForTransfer } from "./sessions_compatible_for_transfer";
import { OPCUAServerOptions } from "./opcua_server";
import { IAddressSpaceAccessor } from "./i_address_space_accessor";
import { AddressSpaceAccessor } from "./addressSpace_accessor";
const debugLog = make_debugLog(__filename);
const errorLog = make_errorLog(__filename);
const warningLog = make_warningLog(__filename);
const doDebug = checkDebugFlag(__filename);
function upperCaseFirst(str: string) {
return str.slice(0, 1).toUpperCase() + str.slice(1);
}
async function shutdownAndDisposeAddressSpace(this: ServerEngine) {
if (this.addressSpace) {
await this.addressSpace.shutdown();
this.addressSpace.dispose();
delete (this as any).addressSpace;
}
}
function setSubscriptionDurable(
this: ServerEngine,
inputArguments: Variant[],
context: ISessionContext,
callback: CallbackT<CallMethodResultOptions>
) {
// see https://reference.opcfoundation.org/v104/Core/docs/Part5/9.3/
// https://reference.opcfoundation.org/v104/Core/docs/Part4/6.8/
assert(typeof callback === "function");
const data = _getSubscription.call(this, inputArguments, context);
if (data.statusCode) return callback(null, { statusCode: data.statusCode });
const { subscription } = data;
const lifetimeInHours = inputArguments[1].value as UInt32;
if (subscription.monitoredItemCount > 0) {
// This is returned when a Subscription already contains MonitoredItems.
return callback(null, { statusCode: StatusCodes.BadInvalidState });
}
/**
* MonitoredItems are used to monitor Variable Values for data changes and event notifier
* Objects for new Events. Subscriptions are used to combine data changes and events of
* the assigned MonitoredItems to an optimized stream of network messages. A reliable
* delivery is ensured as long as the lifetime of the Subscription and the queues in the
* MonitoredItems are long enough for a network interruption between OPC UA Client and
* Server. All queues that ensure reliable delivery are normally kept in memory and a
* Server restart would delete them.
* There are use cases where OPC UA Clients have no permanent network connection to the
* OPC UA Server or where reliable delivery of data changes and events is necessary
* even if the OPC UA Server is restarted or the network connection is interrupted
* for a longer time.
* To ensure this reliable delivery, the OPC UA Server must store collected data and
* events in non-volatile memory until the OPC UA Client has confirmed reception.
* It is possible that there will be data lost if the Server is not shut down gracefully
* or in case of power failure. But the OPC UA Server should store the queues frequently
* even if the Server is not shut down.
* The Method SetSubscriptionDurable defined in OPC 10000-5 is used to set a Subscription
* into this durable mode and to allow much longer lifetimes and queue sizes than for normal
* Subscriptions. The Method shall be called before the MonitoredItems are created in the
* durable Subscription. The Server shall verify that the Method is called within the
* Session context of the Session that owns the Subscription.
*
* A value of 0 for the parameter lifetimeInHours requests the highest lifetime supported by the Server.
*/
const highestLifetimeInHours = 24 * 100;
const revisedLifetimeInHours =
lifetimeInHours === 0 ? highestLifetimeInHours : Math.max(1, Math.min(lifetimeInHours, highestLifetimeInHours));
// also adjust subscription life time
const currentLifeTimeInHours = (subscription.lifeTimeCount * subscription.publishingInterval) / (1000 * 60 * 60);
if (currentLifeTimeInHours < revisedLifetimeInHours) {
const requestedLifetimeCount = Math.ceil((revisedLifetimeInHours * (1000 * 60 * 60)) / subscription.publishingInterval);
subscription.modify({
requestedMaxKeepAliveCount: subscription.maxKeepAliveCount,
requestedPublishingInterval: subscription.publishingInterval,
maxNotificationsPerPublish: subscription.maxNotificationsPerPublish,
priority: subscription.priority,
requestedLifetimeCount
});
}
const callMethodResult = new CallMethodResult({
statusCode: StatusCodes.Good,
outputArguments: [{ dataType: DataType.UInt32, arrayType: VariantArrayType.Scalar, value: revisedLifetimeInHours }]
});
callback(null, callMethodResult);
}
function requestServerStateChange(
this: ServerEngine,
inputArguments: Variant[],
context: ISessionContext,
callback: CallbackT<CallMethodResultOptions>
) {
assert(Array.isArray(inputArguments));
assert(typeof callback === "function");
assert(Object.prototype.hasOwnProperty.call(context, "session"), " expecting a session id in the context object");
const session = context.session as ServerSession;
if (!session) {
return callback(null, { statusCode: StatusCodes.BadInternalError });
}
return callback(null, { statusCode: StatusCodes.BadNotImplemented });
}
function _getSubscription(
this: ServerEngine,
inputArguments: Variant[],
context: ISessionContext
): { subscription: Subscription; statusCode?: never } | { statusCode: StatusCode; subscription?: never } {
assert(Array.isArray(inputArguments));
assert(Object.prototype.hasOwnProperty.call(context, "session"), " expecting a session id in the context object");
const session = context.session as ServerSession;
if (!session) {
return { statusCode: StatusCodes.BadInternalError };
}
const subscriptionId = inputArguments[0].value;
const subscription = session.getSubscription(subscriptionId);
if (!subscription) {
// subscription may belongs to a different session that ours
if (this.findSubscription(subscriptionId)) {
// if yes, then access to Subscription data should be denied
return { statusCode: StatusCodes.BadUserAccessDenied };
}
return { statusCode: StatusCodes.BadSubscriptionIdInvalid };
}
return { subscription };
}
function resendData(
this: ServerEngine,
inputArguments: Variant[],
context: ISessionContext,
callback: CallbackT<CallMethodResultOptions>
): void {
assert(typeof callback === "function");
const data = _getSubscription.call(this, inputArguments, context);
if (data.statusCode) return callback(null, { statusCode: data.statusCode });
const { subscription } = data;
subscription
.resendInitialValues()
.then(() => {
callback(null, { statusCode: StatusCodes.Good });
})
.catch((err) => callback(err));
}
// binding methods
function getMonitoredItemsId(
this: ServerEngine,
inputArguments: Variant[],
context: ISessionContext,
callback: CallbackT<CallMethodResultOptions>
) {
assert(typeof callback === "function");
const data = _getSubscription.call(this, inputArguments, context);
if (data.statusCode) return callback(null, { statusCode: data.statusCode });
const { subscription } = data;
const result = subscription.getMonitoredItems();
assert(result.statusCode);
assert(result.serverHandles.length === result.clientHandles.length);
const callMethodResult = new CallMethodResult({
statusCode: result.statusCode,
outputArguments: [
{ dataType: DataType.UInt32, arrayType: VariantArrayType.Array, value: result.serverHandles },
{ dataType: DataType.UInt32, arrayType: VariantArrayType.Array, value: result.clientHandles }
]
});
callback(null, callMethodResult);
}
function __bindVariable(self: ServerEngine, nodeId: NodeIdLike, options?: BindVariableOptions) {
options = options || {};
const variable = self.addressSpace!.findNode(nodeId) as UAVariable;
if (variable && variable.bindVariable) {
variable.bindVariable(options, true);
assert(typeof variable.asyncRefresh === "function");
assert(typeof (variable as any).refreshFunc === "function");
} else {
warningLog(
"Warning: cannot bind object with id ",
nodeId.toString(),
" please check your nodeset.xml file or add this node programmatically"
);
}
}
// note OPCUA 1.03 part 4 page 76
// The Server-assigned identifier for the Subscription (see 7.14 for IntegerId definition). This identifier shall
// be unique for the entire Server, not just for the Session, in order to allow the Subscription to be transferred
// to another Session using the TransferSubscriptions service.
// After Server start-up the generation of subscriptionIds should start from a random IntegerId or continue from
// the point before the restart.
let next_subscriptionId = Math.ceil(Math.random() * 1000000);
export function setNextSubscriptionId(n: number) {
next_subscriptionId = Math.max(n, 1);
}
function _get_next_subscriptionId() {
debugLog(" next_subscriptionId = ", next_subscriptionId);
return next_subscriptionId++;
}
export type StringGetter = () => string;
export type StringArrayGetter = () => string[];
export type ApplicationTypeGetter = () => ApplicationType;
export type BooleanGetter = () => boolean;
export interface ServerConfigurationOptions {
applicationUri?: string | StringGetter;
applicationType?: ApplicationType | ApplicationTypeGetter; // default "Server"
hasSecureElement?: boolean | BooleanGetter; // default true
multicastDnsEnabled?: boolean | BooleanGetter; // default true
productUri?: string | StringGetter;
// /** @restricted only in professional version */
// resetToServerDefaults: () => Promise<void>;
// /** @restricted only in professional version */
// setAdminPassword?: (password: string) => Promise<void>;
/**
* The SupportedPrivateKeyFormats specifies the PrivateKey formats supported by the Server.
* Possible values include “PEM” (see RFC 5958) or “PFX” (see PKCS #12).
* @default ["PEM"]
*/
supportedPrivateKeyFormat: string[] | StringArrayGetter;
/**
* The ServerCapabilities Property specifies the capabilities from Annex D
* ( see https://reference.opcfoundation.org/GDS/v104/docs/D) which the Server supports. The value is
* the same as the value reported to the LocalDiscoveryServer when the Server calls the RegisterServer2 Service.
*/
serverCapabilities?: string[] | StringArrayGetter; // default|"N/A"]
}
export interface ServerEngineOptions {
applicationUri: string | StringGetter;
buildInfo?: BuildInfoOptions;
isAuditing?: boolean;
/**
* set to true to enable serverDiagnostics
*/
serverDiagnosticsEnabled?: boolean;
serverCapabilities?: ServerCapabilitiesOptions;
historyServerCapabilities?: HistoryServerCapabilitiesOptions;
serverConfiguration?: ServerConfigurationOptions;
}
export interface CreateSessionOption {
clientDescription?: ApplicationDescription;
sessionTimeout?: number;
server?: IServerBase;
}
export type ClosingReason = "Timeout" | "Terminated" | "CloseSession" | "Forcing";
export type ServerEngineShutdownTask = (this: ServerEngine) => void | Promise<void>;
/**
*
*/
export class ServerEngine extends EventEmitter implements IAddressSpaceAccessor {
public static readonly registry = new ObjectRegistry();
public isAuditing: boolean;
public serverDiagnosticsSummary: ServerDiagnosticsSummaryDataType;
public serverDiagnosticsEnabled: boolean;
public serverCapabilities: ServerCapabilities;
public historyServerCapabilities: HistoryServerCapabilities;
public serverConfiguration: ServerConfigurationOptions;
public clientDescription?: ApplicationDescription;
public addressSpace: AddressSpace | null;
public addressSpaceAccessor: IAddressSpaceAccessor | null = null;
// pseudo private
public _internalState: "creating" | "initializing" | "initialized" | "shutdown" | "disposed";
private _sessions: { [key: string]: ServerSession };
private _closedSessions: { [key: string]: ServerSession };
private _orphanPublishEngine?: ServerSidePublishEngineForOrphanSubscription;
private _shutdownTasks: ServerEngineShutdownTask[];
private _applicationUri: string;
private _expectedShutdownTime!: Date;
private _serverStatus: ServerStatusDataType;
private _globalCounter: { totalMonitoredItemCount: number } = { totalMonitoredItemCount: 0 };
constructor(options?: ServerEngineOptions) {
super();
options = options || ({ applicationUri: "" } as ServerEngineOptions);
options.buildInfo = options.buildInfo || {};
ServerEngine.registry.register(this);
this._sessions = {};
this._closedSessions = {};
this._orphanPublishEngine = undefined; // will be constructed on demand
this.isAuditing = typeof options.isAuditing === "boolean" ? options.isAuditing : false;
options.buildInfo.buildDate = options.buildInfo.buildDate || new Date();
// ---------------------------------------------------- ServerStatusDataType
this._serverStatus = new ServerStatusDataType({
buildInfo: options.buildInfo,
currentTime: new Date(),
secondsTillShutdown: 0,
shutdownReason: { text: "" },
startTime: new Date(),
state: ServerState.NoConfiguration
});
// --------------------------------------------------- ServerCapabilities
options.serverCapabilities = options.serverCapabilities || {};
options.serverConfiguration = options.serverConfiguration || {
supportedPrivateKeyFormat: ["PEM"]
};
// https://profiles.opcfoundation.org/profile
options.serverCapabilities.serverProfileArray = options.serverCapabilities.serverProfileArray || [
"http://opcfoundation.org/UA-Profile/Server/Standard", // Standard UA Server Profile",
"http://opcfoundation.org/UA-Profile/Server/DataAccess",
"http://opcfoundation.org/UA-Profile/Server/Events",
"http://opcfoundation.org/UA-Profile/Client/HistoricalAccess",
"http://opcfoundation.org/UA-Profile/Server/Methods",
"http://opcfoundation.org/UA-Profile/Server/StandardEventSubscription",
"http://opcfoundation.org/UA-Profile/Transport/uatcp-uasc-uabinary",
"http://opcfoundation.org/UA-Profile/Server/FileAccess",
"http://opcfoundation.org/UA-Profile/Server/StateMachine"
// "http://opcfoundation.org/UA-Profile/Transport/wss-uajson",
// "http://opcfoundation.org/UA-Profile/Transport/wss-uasc-uabinary"
// "http://opcfoundation.org/UA-Profile/Server/DurableSubscription"
// "http://opcfoundation.org/UA-Profile/Server/ReverseConnect",
// "http://opcfoundation.org/UAProfile/Server/NodeManagement",
// "Embedded UA Server Profile",
// "Micro Embedded Device Server Profile",
// "Nano Embedded Device Server Profile"
];
options.serverCapabilities.localeIdArray = options.serverCapabilities.localeIdArray || ["en-EN", "fr-FR"];
this.serverCapabilities = new ServerCapabilities(options.serverCapabilities);
// to do when spec is clear about what goes here!
// spec 1.04 says (in Part 4 7.33 SignedSoftwareCertificate
// Note: Details on SoftwareCertificates need to be defined in a future version.
this.serverCapabilities.softwareCertificates = [
// new SignedSoftwareCertificate({})
];
// make sure minSupportedSampleRate matches MonitoredItem.minimumSamplingInterval
(this.serverCapabilities as any).__defineGetter__("minSupportedSampleRate", () => {
return options!.serverCapabilities?.minSupportedSampleRate || MonitoredItem.minimumSamplingInterval;
});
this.serverConfiguration = options.serverConfiguration;
this.historyServerCapabilities = new HistoryServerCapabilities(options.historyServerCapabilities);
// --------------------------------------------------- serverDiagnosticsSummary extension Object
this.serverDiagnosticsSummary = new ServerDiagnosticsSummaryDataType();
assert(Object.prototype.hasOwnProperty.call(this.serverDiagnosticsSummary, "currentSessionCount"));
// note spelling is different for serverDiagnosticsSummary.currentSubscriptionCount
// and sessionDiagnostics.currentSubscriptionsCount ( with an s)
assert(Object.prototype.hasOwnProperty.call(this.serverDiagnosticsSummary, "currentSubscriptionCount"));
(this.serverDiagnosticsSummary as any).__defineGetter__("currentSubscriptionCount", () => {
// currentSubscriptionCount returns the total number of subscriptions
// that are currently active on all sessions
let counter = 0;
Object.values(this._sessions).forEach((session: ServerSession) => {
counter += session.currentSubscriptionCount;
});
// we also need to add the orphan subscriptions
counter += this._orphanPublishEngine ? this._orphanPublishEngine.subscriptions.length : 0;
return counter;
});
this._internalState = "creating";
this.setServerState(ServerState.NoConfiguration);
this.addressSpace = null;
this._shutdownTasks = [];
this._applicationUri = "";
if (typeof options.applicationUri === "function") {
(this as any).__defineGetter__("_applicationUri", options.applicationUri);
} else {
this._applicationUri = options.applicationUri || "<unset _applicationUri>";
}
options.serverDiagnosticsEnabled = Object.prototype.hasOwnProperty.call(options, "serverDiagnosticsEnable")
? options.serverDiagnosticsEnabled
: true;
this.serverDiagnosticsEnabled = options.serverDiagnosticsEnabled!;
}
public isStarted(): boolean {
return !!this._serverStatus!;
}
public dispose(): void {
this.addressSpace = null;
assert(Object.keys(this._sessions).length === 0, "ServerEngine#_sessions not empty");
this._sessions = {};
// todo fix me
this._closedSessions = {};
assert(Object.keys(this._closedSessions).length === 0, "ServerEngine#_closedSessions not empty");
this._closedSessions = {};
if (this._orphanPublishEngine) {
this._orphanPublishEngine.dispose();
this._orphanPublishEngine = undefined;
}
this._shutdownTasks = [];
this._serverStatus = null as any as ServerStatusDataType;
this._internalState = "disposed";
this.removeAllListeners();
ServerEngine.registry.unregister(this);
}
public get startTime(): Date {
return this._serverStatus.startTime!;
}
public get currentTime(): Date {
return this._serverStatus.currentTime!;
}
public get buildInfo(): BuildInfo {
return this._serverStatus.buildInfo;
}
/**
* register a function that will be called when the server will perform its shut down.
*/
public registerShutdownTask(task: ServerEngineShutdownTask): void {
assert(typeof task === "function");
this._shutdownTasks.push(task);
}
/**
*/
public async shutdown(): Promise<void> {
debugLog("ServerEngine#shutdown");
this._internalState = "shutdown";
this.setServerState(ServerState.Shutdown);
// delete any existing sessions
const tokens = Object.keys(this._sessions).map((key: string) => {
const session = this._sessions[key];
return session.authenticationToken;
});
// delete and close any orphan subscriptions
if (this._orphanPublishEngine) {
this._orphanPublishEngine.shutdown();
}
for (const token of tokens) {
this.closeSession(token, true, "Terminated");
}
// all sessions must have been terminated
assert(this.currentSessionCount === 0);
// all subscriptions must have been terminated
assert(this.currentSubscriptionCount === 0, "all subscriptions must have been terminated");
this._shutdownTasks.push(shutdownAndDisposeAddressSpace);
// perform registerShutdownTask
for (const task of this._shutdownTasks) {
await task.call(this);
}
this.setServerState(ServerState.Invalid);
this.dispose();
}
/**
* the number of active sessions
*/
public get currentSessionCount(): number {
return this.serverDiagnosticsSummary.currentSessionCount;
}
/**
* the cumulated number of sessions that have been opened since this object exists
*/
public get cumulatedSessionCount(): number {
return this.serverDiagnosticsSummary.cumulatedSessionCount;
}
/**
* the number of active subscriptions.
*/
public get currentSubscriptionCount(): number {
return this.serverDiagnosticsSummary.currentSubscriptionCount;
}
/**
* the cumulated number of subscriptions that have been created since this object exists
*/
public get cumulatedSubscriptionCount(): number {
return this.serverDiagnosticsSummary.cumulatedSubscriptionCount;
}
public get rejectedSessionCount(): number {
return this.serverDiagnosticsSummary.rejectedSessionCount;
}
public get rejectedRequestsCount(): number {
return this.serverDiagnosticsSummary.rejectedRequestsCount;
}
public get sessionAbortCount(): number {
return this.serverDiagnosticsSummary.sessionAbortCount;
}
public get sessionTimeoutCount(): number {
return this.serverDiagnosticsSummary.sessionTimeoutCount;
}
public get publishingIntervalCount(): number {
return this.serverDiagnosticsSummary.publishingIntervalCount;
}
public incrementSessionTimeoutCount(): void {
if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) {
// The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount.
this.serverDiagnosticsSummary.sessionTimeoutCount += 1;
}
}
public incrementSessionAbortCount(): void {
if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) {
// The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount.
this.serverDiagnosticsSummary.sessionAbortCount += 1;
}
}
public incrementRejectedRequestsCount(): void {
if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) {
// The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount.
this.serverDiagnosticsSummary.rejectedRequestsCount += 1;
}
}
/**
* increment rejected session count (also increment rejected requests count)
*/
public incrementRejectedSessionCount(): void {
if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) {
// The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount.
this.serverDiagnosticsSummary.rejectedSessionCount += 1;
}
this.incrementRejectedRequestsCount();
}
public incrementSecurityRejectedRequestsCount(): void {
if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) {
// The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount.
this.serverDiagnosticsSummary.securityRejectedRequestsCount += 1;
}
this.incrementRejectedRequestsCount();
}
/**
* increment rejected session count (also increment rejected requests count)
*/
public incrementSecurityRejectedSessionCount(): void {
if (this.serverDiagnosticsSummary && this.serverDiagnosticsEnabled) {
// The requests include all Services defined in Part 4 of the OPC UA Specification, also requests to create sessions. This number includes the securityRejectedRequestsCount.
this.serverDiagnosticsSummary.securityRejectedSessionCount += 1;
}
this.incrementSecurityRejectedRequestsCount();
}
public setShutdownTime(date: Date): void {
this._expectedShutdownTime = date;
}
public setShutdownReason(reason: LocalizedTextLike): void {
this.addressSpace?.rootFolder.objects.server.serverStatus.shutdownReason.setValueFromSource({
dataType: DataType.LocalizedText,
value: coerceLocalizedText(reason)!
});
}
/**
* @return the approximate number of seconds until the server will be shut down. The
* value is only relevant once the state changes into SHUTDOWN.
*/
public secondsTillShutdown(): number {
if (!this._expectedShutdownTime) {
return 0;
}
// ToDo: implement a correct solution here
const now = Date.now();
return Math.max(0, Math.ceil((this._expectedShutdownTime.getTime() - now) / 1000));
}
/**
* the name of the server
*/
public get serverName(): string {
return this._serverStatus.buildInfo!.productName!;
}
/**
* the server urn
*/
public get serverNameUrn(): string {
return this._applicationUri;
}
/**
* the urn of the server namespace
*/
public get serverNamespaceUrn(): string {
return this._applicationUri; // "urn:" + engine.serverName;
}
public get serverStatus(): ServerStatusDataType {
return this._serverStatus;
}
public setServerState(serverState: ServerState): void {
assert(serverState !== null && serverState !== undefined);
this.addressSpace?.rootFolder?.objects?.server?.serverStatus?.state?.setValueFromSource({
dataType: DataType.Int32,
value: serverState
});
}
public getServerDiagnosticsEnabledFlag(): boolean {
const server = this.addressSpace!.rootFolder.objects.server;
const serverDiagnostics = server.getComponentByName("ServerDiagnostics") as UAVariable;
if (!serverDiagnostics) {
return false;
}
return serverDiagnostics.readValue().value.value;
}
/**
*
*/
public initialize(options: OPCUAServerOptions, callback: (err?: Error | null) => void): void {
assert(!this.addressSpace); // check that 'initialize' has not been already called
this._internalState = "initializing";
options = options || {};
assert(typeof callback === "function");
options.nodeset_filename = options.nodeset_filename || nodesets.standard;
const startTime = new Date();
debugLog("Loading ", options.nodeset_filename, "...");
this.addressSpace = AddressSpace.create();
this.addressSpaceAccessor = new AddressSpaceAccessor(this.addressSpace);
if (!options.skipOwnNamespace) {
// register namespace 1 (our namespace);
const serverNamespace = this.addressSpace.registerNamespace(this.serverNamespaceUrn);
assert(serverNamespace.index === 1);
}
// eslint-disable-next-line max-statements
generateAddressSpace(this.addressSpace, options.nodeset_filename)
.catch((err) => {
console.log(err.message);
callback(err);
})
.then(() => {
/* istanbul ignore next */
if (!this.addressSpace) {
throw new Error("Internal error");
}
const addressSpace = this.addressSpace;
const endTime = new Date();
debugLog("Loading ", options.nodeset_filename, " done : ", endTime.getTime() - startTime.getTime(), " ms");
const bindVariableIfPresent = (nodeId: NodeId, opts: any) => {
assert(!nodeId.isEmpty());
const obj = addressSpace.findNode(nodeId);
if (obj) {
__bindVariable(this, nodeId, opts);
}
return obj;
};
// -------------------------------------------- install default get/put handler
const server_NamespaceArray_Id = makeNodeId(VariableIds.Server_NamespaceArray); // ns=0;i=2255
bindVariableIfPresent(server_NamespaceArray_Id, {
get() {
return new Variant({
arrayType: VariantArrayType.Array,
dataType: DataType.String,
value: addressSpace.getNamespaceArray().map((x) => x.namespaceUri)
});
},
set: null // read only
});
const server_NameUrn_var = new Variant({
arrayType: VariantArrayType.Array,
dataType: DataType.String,
value: [
this.serverNameUrn // this is us !
]
});
const server_ServerArray_Id = makeNodeId(VariableIds.Server_ServerArray); // ns=0;i=2254
bindVariableIfPresent(server_ServerArray_Id, {
get() {
return server_NameUrn_var;
},
set: null // read only
});
// fix DefaultUserRolePermissions and DefaultUserRolePermissions
// of namespaces
const namespaces = makeNodeId(ObjectIds.Server_Namespaces);
const namespacesNode = addressSpace.findNode(namespaces) as UAObject;
if (namespacesNode) {
for (const ns of namespacesNode.getComponents()) {
const defaultUserRolePermissions = ns.getChildByName("DefaultUserRolePermissions") as UAVariable | null;
if (defaultUserRolePermissions) {
defaultUserRolePermissions.setValueFromSource({ dataType: DataType.Null });
}
const defaultRolePermissions = ns.getChildByName("DefaultRolePermissions") as UAVariable | null;
if (defaultRolePermissions) {
defaultRolePermissions.setValueFromSource({ dataType: DataType.Null });
}
}
}
const bindStandardScalar = (
id: number,
dataType: DataType,
func: () => any,
setter_func?: (value: any) => void
) => {
assert(typeof id === "number", "expecting id to be a number");
assert(typeof func === "function");
assert(typeof setter_func === "function" || !setter_func);
assert(dataType !== null); // check invalid dataType
let setter_func2 = null;
if (setter_func) {
setter_func2 = (variant: Variant) => {
const variable2 = !!variant.value;
setter_func(variable2);
return StatusCodes.Good;
};
}
const nodeId = makeNodeId(id);
// make sur the provided function returns a valid value for the variant type
// This test may not be exhaustive but it will detect obvious mistakes.
/* istanbul ignore next */
if (!isValidVariant(VariantArrayType.Scalar, dataType, func())) {
errorLog("func", func());
throw new Error("bindStandardScalar : func doesn't provide an value of type " + DataType[dataType]);
}
return bindVariableIfPresent(nodeId, {
get() {
return new Variant({
arrayType: VariantArrayType.Scalar,
dataType,
value: func()
});
},
set: setter_func2
});
};
const bindStandardArray = (id: number, variantDataType: DataType, dataType: any, func: () => any[]) => {
assert(typeof func === "function");
assert(variantDataType !== null); // check invalid dataType
const nodeId = makeNodeId(id);
// make sur the provided function returns a valid value for the variant type
// This test may not be exhaustive but it will detect obvious mistakes.
assert(isValidVariant(VariantArrayType.Array, variantDataType, func()));
bindVariableIfPresent(nodeId, {
get() {
const value = func();
assert(Array.isArray(value));
return new Variant({
arrayType: VariantArrayType.Array,
dataType: variantDataType,
value
});
},
set: null // read only
});
};
bindStandardScalar(VariableIds.Server_EstimatedReturnTime, DataType.DateTime, () => getMinOPCUADate());
// TimeZoneDataType
const timeZoneDataType = addressSpace.findDataType(resolveNodeId(DataTypeIds.TimeZoneDataType))!;
const timeZone = new TimeZoneDataType({
daylightSavingInOffset: /* boolean*/ false,
offset: /* int16 */ 0
});
bindStandardScalar(VariableIds.Server_LocalTime, DataType.ExtensionObject, () => {
return timeZone;
});
bindStandardScalar(VariableIds.Server_ServiceLevel, DataType.Byte, () => {
return 255;
});
bindStandardScalar(VariableIds.Server_Auditing, DataType.Boolean, () => {
return this.isAuditing;
});
// eslint-disable-next-line @typescript-eslint/no-this-alias
const engine = this;
const makeNotReadableIfEnabledFlagIsFalse = (variable: UAVariable) => {
const originalIsReadable = variable.isReadable;
variable.isUserReadable = checkReadableFlag;
function checkReadableFlag(this: UAVariable, context: SessionContext): boolean {
const isEnabled = engine.serverDiagnosticsEnabled;
return originalIsReadable.call(this, context) && isEnabled;
}
for (const c of variable.getAggregates()) {
if (c.nodeClass === NodeClass.Variable) {
makeNotReadableIfEnabledFlagIsFalse(c as UAVariable);
}
}
};
const bindServerDiagnostics = () => {
bindStandardScalar(
VariableIds.Server_ServerDiagnostics_EnabledFlag,
DataType.Boolean,
() => {
return this.serverDiagnosticsEnabled;
},
(newFlag: boolean) => {
this.serverDiagnosticsEnabled = newFlag;
}
);
const nodeId = makeNodeId(VariableIds.Server_ServerDiagnostics_ServerDiagnosticsSummary);
const serverDiagnosticsSummaryNode = addressSpace.findNode(
nodeId
) as UAServerDiagnosticsSummary<ServerDiagnosticsSummaryDataType>;
if (serverDiagnosticsSummaryNode) {
serverDiagnosticsSummaryNode.bindExtensionObject(this.serverDiagnosticsSummary);
this.serverDiagnosticsSummary = serverDiagnosticsSummaryNode.$extensionObject;
makeNotReadableIfEnabledFlagIsFalse(serverDiagnosticsSummaryNode);
}
};
const bindServerStatus = () => {
const serverStatusNode = addressSpace.findNode(
makeNodeId(VariableIds.Server_ServerStatus)
) as UAServerStatus<DTServerStatus>;
if (!serverStatusNode) {
return;
}
if (serverStatusNode) {
serverStatusNode.bindExtensionObject(this._serverStatus);
serverStatusNode.minimumSamplingInterval = 1000;
}
const currentTimeNode = addressSpace.findNode(
makeNodeId(VariableIds.Server_ServerStatus_CurrentTime)
) as UAVariable;
if (currentTimeNode) {
currentTimeNode.minimumSamplingInterval = 1000;
}
const secondsTillShutdown = addressSpace.findNode(
makeNodeId(VariableIds.Server_ServerStatus_SecondsTillShutdown)
) as UAVariable;
if (secondsTillShutdown) {
secondsTillShutdown.minimumSamplingInterval = 1000;
}
assert(serverStatusNode.$extensionObject);
serverStatusNode.$extensionObject = new Proxy(serverStatusNode.$extensionObject, {
get(target, prop) {
if (prop === "currentTime") {
serverStatusNode.currentTime.touchValue();
return new Date();
} else if (prop === "secondsTillShutdown") {
serverStatusNode.secondsTillShutdown.touchValue();
return engine.secondsTillShutdown();
}
return (target as any)[prop];
}
});
this._serverStatus = serverStatusNode.$extensionObject;
};
const bindServerCapabilities = () => {
bindStandardArray(
VariableIds.Server_ServerCapabilities_ServerProfileArray,
DataType.String,
DataType.String,
() => {
return this.serverCapabilities.serverProfileArray;
}
);
bindStandardArray(VariableIds.Server_ServerCapabilities_LocaleIdArray, DataType.String, "LocaleId", () => {
return this.serverCapabilities.localeIdArray;
});
bindStandardScalar(VariableIds.Server_ServerCapabilities_MinSupportedSampleRate, DataType.Double, () => {
return Math.max(
this.serverCapabilities.minSupportedSampleRate,
defaultServerCapabilities.minSupportedSampleRate
);
});
bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxBrowseContinuationPoints, DataType.UInt16, () => {
return this.serverCapabilities.maxBrowseContinuationPoints;
});
bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxQueryContinuationPoints, DataType.UInt16, () => {
return this.serverCapabilities.maxQueryContinuationPoints;
});
bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxHistoryContinuationPoints, DataType.UInt16, () => {
return this.serverCapabilities.maxHistoryContinuationPoints;
});
// new in 1.05
bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxSessions, DataType.UInt32, () => {
return this.serverCapabilities.maxSessions;
});
bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxSubscriptions, DataType.UInt32, () => {
return this.serverCapabilities.maxSubscriptions;
});
bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxMonitoredItems, DataType.UInt32, () => {
return this.serverCapabilities.maxMonitoredItems;
});
bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxSubscriptionsPerSession, DataType.UInt32, () => {
return this.serverCapabilities.maxSubscriptionsPerSession;
});
bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxSelectClauseParameters, DataType.UInt32, () => {
return this.serverCapabilities.maxSelectClauseParameters;
});
bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxWhereClauseParameters, DataType.UInt32, () => {
return this.serverCapabilities.maxWhereClauseParameters;
});
//bindStandardArray(VariableIds.Server_ServerCapabilities_ConformanceUnits, DataType.QualifiedName, () => {
// return this.serverCapabilities.conformanceUnits;
//});
bindStandardScalar(
VariableIds.Server_ServerCapabilities_MaxMonitoredItemsPerSubscription,
DataType.UInt32,
() => {
return this.serverCapabilities.maxMonitoredItemsPerSubscription;
}
);
// added by DI : Server-specific period of time in milliseconds until the Server will revoke a lock.
// TODO bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxInactiveLockTime,
// TODO DataType.UInt16, function () {
// TODO return self.serverCapabilities.maxInactiveLockTime;
// TODO });
bindStandardArray(
VariableIds.Server_ServerCapabilities_SoftwareCertificates,
DataType.ExtensionObject,
"SoftwareCertificates",
() => {
return this.serverCapabilities.softwareCertificates;
}
);
bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxArrayLength, DataType.UInt32, () => {
return Math.min(this.serverCapabilities.maxArrayLength, Variant.maxArrayLength);
});
bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxStringLength, DataType.UInt32, () => {
return Math.min(this.serverCapabilities.maxStringLength, BinaryStream.maxStringLength);
});
bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxByteStringLength, DataType.UInt32, () => {
return Math.min(this.serverCapabilities.maxByteStringLength, BinaryStream.maxByteStringLength);
});
bindStandardScalar(VariableIds.Server_ServerCapabilities_MaxMonitoredItemsQueueSize, DataType.UInt32, () => {
return Math.max(1, this.serverCapabilities.maxMonitoredItemsQueueSize);
});
const bindOperationLimits = (operationLimits: ServerOperationLimits) => {
assert(operationLimits !== null && typeof operationLimits === "object");
const keys = Object.keys(operationLimits);
keys.forEach((key: string) => {
const uid = "Server_ServerCapabilities_OperationLimits_" + upperCaseFirst(key);
const nodeId = makeNodeId((VariableIds as any)[uid]);
assert(!nodeId.isEmpty());
bindStandardScalar((VariableIds as any)[uid], DataType.UInt32, () => {
return (operationLimits as any)[key];
});
});
};
bindOperationLimits(this.serverCapabilities.operationLimits);
// i=2399 [ProgramStateMachineType_ProgramDiagnostics];
function fix_ProgramStateMachineType_ProgramDiagnostics() {
const nodeId = coerceNodeId("i=2399"); // ProgramStateMachineType_ProgramDiagnostics
const variable = addressSpace.findNode(nodeId) as UAVariable;
if (variable) {
(variable as any).$extensionObject = new ProgramDiagnosticDataType({});
// variable.setValueFromSource({
// dataType: DataType.ExtensionObject,
// // value: new ProgramDiagnostic2DataType()
// value: new ProgramDiagnosticDataType({}