node-opcua-server
Version:
pure nodejs OPCUA SDK - module server
1,371 lines (1,240 loc) • 166 kB
text/typescript
/* eslint-disable complexity */
/**
* @module node-opcua-server
*/
import { randomBytes } from "crypto";
import { EventEmitter } from "events";
import { callbackify, types } from "util";
import async from "async";
import chalk from "chalk";
import { extractFullyQualifiedDomainName, getFullyQualifiedDomainName } from "node-opcua-hostname";
import { assert } from "node-opcua-assert";
import { isNullOrUndefined } from "node-opcua-utils";
import {
AddressSpace,
PseudoVariantBoolean,
PseudoVariantByteString,
PseudoVariantDateTime,
PseudoVariantDuration,
PseudoVariantExtensionObject,
PseudoVariantExtensionObjectArray,
PseudoVariantLocalizedText,
PseudoVariantNodeId,
PseudoVariantString,
RaiseEventData,
SessionContext,
UAObject,
UAVariable,
ISessionContext,
UAView,
EventTypeLike,
UAObjectType,
PseudoVariantStringPredefined,
innerBrowse,
innerBrowseNext,
UAEventType
} from "node-opcua-address-space";
import { getDefaultCertificateManager, OPCUACertificateManager } from "node-opcua-certificate-manager";
import { ServerState } from "node-opcua-common";
import { Certificate, exploreCertificate, Nonce } from "node-opcua-crypto/web";
import {
AttributeIds,
filterDiagnosticOperationLevel,
filterDiagnosticServiceLevel,
NodeClass,
RESPONSE_DIAGNOSTICS_MASK_ALL
} from "node-opcua-data-model";
import { DataValue } from "node-opcua-data-value";
import { dump, make_debugLog, make_errorLog, make_warningLog } from "node-opcua-debug";
import { NodeId } from "node-opcua-nodeid";
import { ObjectRegistry } from "node-opcua-object-registry";
import {
AsymmetricAlgorithmSecurityHeader,
coerceSecurityPolicy,
computeSignature,
fromURI,
getCryptoFactory,
Message,
MessageSecurityMode,
nonceAlreadyBeenUsed,
Request,
Response,
SecurityPolicy,
ServerSecureChannelLayer,
SignatureData,
verifySignature
} from "node-opcua-secure-channel";
import { BrowseNextRequest, BrowseNextResponse, BrowseRequest, BrowseResponse } from "node-opcua-service-browse";
import { CallRequest, CallResponse } from "node-opcua-service-call";
import { ApplicationType, UserTokenType } from "node-opcua-service-endpoints";
import { HistoryReadRequest, HistoryReadResponse, HistoryReadResult, HistoryUpdateResponse } from "node-opcua-service-history";
import {
AddNodesResponse,
AddReferencesResponse,
DeleteNodesResponse,
DeleteReferencesResponse
} from "node-opcua-service-node-management";
import { QueryFirstResponse, QueryNextResponse } from "node-opcua-service-query";
import { ReadRequest, ReadResponse, ReadValueId, TimestampsToReturn } from "node-opcua-service-read";
import {
RegisterNodesRequest,
RegisterNodesResponse,
UnregisterNodesRequest,
UnregisterNodesResponse
} from "node-opcua-service-register-node";
import {
ActivateSessionRequest,
ActivateSessionResponse,
AnonymousIdentityToken,
CloseSessionRequest,
CloseSessionResponse,
CreateSessionRequest,
CreateSessionResponse,
UserNameIdentityToken,
X509IdentityToken
} from "node-opcua-service-session";
import {
CreateMonitoredItemsRequest,
CreateMonitoredItemsResponse,
CreateSubscriptionRequest,
CreateSubscriptionResponse,
DeleteMonitoredItemsRequest,
DeleteMonitoredItemsResponse,
DeleteSubscriptionsRequest,
DeleteSubscriptionsResponse,
ModifyMonitoredItemsRequest,
ModifyMonitoredItemsResponse,
ModifySubscriptionRequest,
ModifySubscriptionResponse,
MonitoredItemModifyResult,
PublishRequest,
PublishResponse,
RepublishRequest,
RepublishResponse,
SetMonitoringModeRequest,
SetMonitoringModeResponse,
SetPublishingModeRequest,
SetPublishingModeResponse,
SetTriggeringRequest,
SetTriggeringResponse,
TransferSubscriptionsRequest,
TransferSubscriptionsResponse
} from "node-opcua-service-subscription";
import {
TranslateBrowsePathsToNodeIdsRequest,
TranslateBrowsePathsToNodeIdsResponse
} from "node-opcua-service-translate-browse-path";
import { WriteRequest, WriteResponse } from "node-opcua-service-write";
import { CallbackT, ErrorCallback, StatusCode, StatusCodes } from "node-opcua-status-code";
import {
ApplicationDescriptionOptions,
BrowseResult,
BuildInfo,
CallMethodResultOptions,
CancelResponse,
EndpointDescription,
MonitoredItemModifyRequest,
MonitoringMode,
UserIdentityToken,
UserTokenPolicy,
BuildInfoOptions,
MonitoredItemCreateResult,
IssuedIdentityToken,
BrowseResultOptions,
ServiceFault,
ServerDiagnosticsSummaryDataType,
BrowseDescriptionOptions
} from "node-opcua-types";
import { DataType } from "node-opcua-variant";
import { VariantArrayType } from "node-opcua-variant";
import { matchUri } from "node-opcua-utils";
import { UAString } from "node-opcua-basic-types";
import { ObjectIds, ObjectTypeIds } from "node-opcua-constants";
import { OPCUABaseServer, OPCUABaseServerOptions } from "./base_server";
import { Factory } from "./factory";
import { IRegisterServerManager } from "./i_register_server_manager";
import { MonitoredItem } from "./monitored_item";
import { RegisterServerManager } from "./register_server_manager";
import { RegisterServerManagerHidden } from "./register_server_manager_hidden";
import { RegisterServerManagerMDNSONLY } from "./register_server_manager_mdns_only";
import { ServerCapabilitiesOptions } from "./server_capabilities";
import { EndpointDescriptionEx, IServerTransportSettings, OPCUAServerEndPoint } from "./server_end_point";
import { ClosingReason, CreateSessionOption, ServerEngine } from "./server_engine";
import { ServerSession } from "./server_session";
import { CreateMonitoredItemHook, DeleteMonitoredItemHook, Subscription } from "./server_subscription";
import { ISocketData } from "./i_socket_data";
import { IChannelData } from "./i_channel_data";
import { UAUserManagerBase, makeUserManager, UserManagerOptions } from "./user_manager";
import { bindRoleSet } from "./user_manager_ua";
import { SamplingFunc } from "./sampling_func";
function isSubscriptionIdInvalid(subscriptionId: number): boolean {
return subscriptionId < 0 || subscriptionId >= 0xffffffff;
}
// tslint:disable-next-line:no-var-requires
import { withCallback } from "thenify-ex";
// tslint:disable-next-line:no-var-requires
const package_info = require("../package.json");
const debugLog = make_debugLog(__filename);
const errorLog = make_errorLog(__filename);
const warningLog = make_warningLog(__filename);
const default_maxConnectionsPerEndpoint = 10;
function g_sendError(channel: ServerSecureChannelLayer, message: Message, ResponseClass: any, statusCode: StatusCode): void {
const response = new ServiceFault({
responseHeader: { serviceResult: statusCode }
});
return channel.send_response("MSG", response, message);
}
const default_build_info: BuildInfoOptions = {
manufacturerName: "NodeOPCUA : MIT Licence ( see http://node-opcua.github.io/)",
productName: "NodeOPCUA-Server",
productUri: null, // << should be same as default_server_info.productUri?
softwareVersion: package_info.version,
buildNumber: "0",
buildDate: new Date(2020, 1, 1)
// xx buildDate: fs.statSync(package_json_file).mtime
};
const minSessionTimeout = 100; // 100 milliseconds
const defaultSessionTimeout = 1000 * 30; // 30 seconds
const maxSessionTimeout = 1000 * 60 * 50; // 50 minutes
let unnamed_session_count = 0;
type ResponseClassType =
| typeof BrowseResponse
| typeof BrowseNextResponse
| typeof CallResponse
| typeof CreateMonitoredItemsResponse
| typeof CreateSubscriptionResponse
| typeof DeleteSubscriptionsResponse
| typeof HistoryReadResponse
| typeof ModifyMonitoredItemsResponse
| typeof ModifySubscriptionResponse
| typeof ReadResponse
| typeof RegisterNodesResponse
| typeof RepublishResponse
| typeof SetPublishingModeResponse
| typeof SetTriggeringResponse
| typeof TransferSubscriptionsResponse
| typeof TranslateBrowsePathsToNodeIdsResponse
| typeof UnregisterNodesResponse
| typeof WriteResponse;
function _adjust_session_timeout(sessionTimeout: number) {
let revisedSessionTimeout = sessionTimeout || defaultSessionTimeout;
revisedSessionTimeout = Math.min(revisedSessionTimeout, maxSessionTimeout);
revisedSessionTimeout = Math.max(revisedSessionTimeout, minSessionTimeout);
return revisedSessionTimeout;
}
function channel_has_session(channel: ServerSecureChannelLayer, session: ServerSession): boolean {
if (session.channel === channel) {
assert(Object.prototype.hasOwnProperty.call(channel.sessionTokens, session.authenticationToken.toString()));
return true;
}
return false;
}
function moveSessionToChannel(session: ServerSession, channel: ServerSecureChannelLayer) {
debugLog("moveSessionToChannel sessionId", session.nodeId, " channelId=", channel.channelId);
if (session.publishEngine) {
session.publishEngine.cancelPendingPublishRequestBeforeChannelChange();
}
session._detach_channel();
session._attach_channel(channel);
assert(session.channel!.channelId === channel.channelId);
}
async function _attempt_to_close_some_old_unactivated_session(server: OPCUAServer) {
const session = server.engine!.getOldestInactiveSession();
if (session) {
await server.engine!.closeSession(session.authenticationToken, false, "Forcing");
}
}
function getRequiredEndpointInfo(endpoint: EndpointDescription) {
assert(endpoint instanceof EndpointDescription);
// https://reference.opcfoundation.org/v104/Core/docs/Part4/5.6.2/
// https://reference.opcfoundation.org/v105/Core/docs/Part4/5.6.2/
const e = new EndpointDescription({
endpointUrl: endpoint.endpointUrl,
securityLevel: endpoint.securityLevel,
securityMode: endpoint.securityMode,
securityPolicyUri: endpoint.securityPolicyUri,
server: {
applicationUri: endpoint.server.applicationUri,
applicationType: endpoint.server.applicationType,
applicationName: endpoint.server.applicationName,
productUri: endpoint.server.productUri
},
transportProfileUri: endpoint.transportProfileUri,
userIdentityTokens: endpoint.userIdentityTokens
});
// reduce even further by explicitly setting unwanted members to null
e.server.applicationName = null as any;
// xx e.server.applicationType = null as any;
e.server.gatewayServerUri = null;
e.server.discoveryProfileUri = null;
e.server.discoveryUrls = null;
e.serverCertificate = null as any;
return e;
}
// serverUri String This value is only specified if the EndpointDescription has a gatewayServerUri.
// This value is the applicationUri from the EndpointDescription which is the applicationUri for the
// underlying Server. The type EndpointDescription is defined in 7.10.
function _serverEndpointsForCreateSessionResponse(server: OPCUAServer, endpointUrl: string | null, serverUri: string | null) {
serverUri = null; // unused then
// https://reference.opcfoundation.org/v104/Core/docs/Part4/5.6.2/
// https://reference.opcfoundation.org/v105/Core/docs/Part4/5.6.2/
return server
._get_endpoints(endpointUrl)
.filter((e) => !(e as any).restricted) // remove restricted endpoints
.filter((e) => matchUri(e.endpointUrl, endpointUrl))
.map(getRequiredEndpointInfo);
}
function adjustSecurityPolicy(channel: ServerSecureChannelLayer, userTokenPolicy_securityPolicyUri: UAString): SecurityPolicy {
// check that userIdentityToken
let securityPolicy = fromURI(userTokenPolicy_securityPolicyUri);
// if the security policy is not specified we use the session security policy
if (securityPolicy === SecurityPolicy.Invalid) {
securityPolicy = fromURI(channel.securityPolicy);
assert(securityPolicy !== SecurityPolicy.Invalid);
}
return securityPolicy;
}
function findUserTokenByPolicy(
endpoint_description: EndpointDescription,
userTokenType: UserTokenType,
policyId: SecurityPolicy | string | null
): UserTokenPolicy | null {
assert(endpoint_description instanceof EndpointDescription);
const r = endpoint_description.userIdentityTokens!.filter(
(userIdentity: UserTokenPolicy) =>
userIdentity.tokenType === userTokenType && (!policyId || userIdentity.policyId === policyId)
);
return r.length === 0 ? null : r[0];
}
function findUserTokenPolicy(endpoint_description: EndpointDescription, userTokenType: UserTokenType): UserTokenPolicy | null {
assert(endpoint_description instanceof EndpointDescription);
const r = endpoint_description.userIdentityTokens!.filter((userIdentity: UserTokenPolicy) => {
assert(userIdentity.tokenType !== undefined);
return userIdentity.tokenType === userTokenType;
});
return r.length === 0 ? null : r[0];
}
function createAnonymousIdentityToken(endpoint_desc: EndpointDescription) {
assert(endpoint_desc instanceof EndpointDescription);
const userTokenPolicy = findUserTokenPolicy(endpoint_desc, UserTokenType.Anonymous);
if (!userTokenPolicy) {
throw new Error("Cannot find ANONYMOUS user token policy in end point description");
}
return new AnonymousIdentityToken({ policyId: userTokenPolicy.policyId });
}
function sameIdentityToken(token1: UserIdentityToken, token2: UserIdentityToken): boolean {
if (token1 instanceof UserNameIdentityToken) {
if (!(token2 instanceof UserNameIdentityToken)) {
return false;
}
if (token1.userName !== token2.userName) {
return false;
}
if (token1.password.toString("hex") !== token2.password.toString("hex")) {
// note pasword hash may be different from two request and cannot be verified at this stage
// we assume that we have a valid password
// NOT CALLING return false;
}
return true;
} else if (token1 instanceof AnonymousIdentityToken) {
if (!(token2 instanceof AnonymousIdentityToken)) {
return false;
}
if (token1.policyId !== token2.policyId) {
return false;
}
return true;
}
assert(false, " Not implemented yet");
return false;
}
function getTokenType(userIdentityToken: UserIdentityToken): UserTokenType {
if (userIdentityToken instanceof AnonymousIdentityToken) {
return UserTokenType.Anonymous;
} else if (userIdentityToken instanceof UserNameIdentityToken) {
return UserTokenType.UserName;
} else if (userIdentityToken instanceof IssuedIdentityToken) {
return UserTokenType.IssuedToken;
} else if (userIdentityToken instanceof X509IdentityToken) {
return UserTokenType.Certificate;
}
return UserTokenType.Invalid;
}
function thumbprint(certificate?: Certificate): string {
return certificate ? certificate.toString("base64") : "";
}
/*=== private
*
* perform the read operation on a given node for a monitored item.
* this method DOES NOT apply to Variable Values attribute
*
* @param self
* @param oldValue
* @param node
* @param itemToMonitor
* @private
*/
function monitoredItem_read_and_record_value(
self: MonitoredItem,
context: ISessionContext,
oldValue: DataValue,
node: UAVariable,
itemToMonitor: any,
callback: (err: Error | null, dataValue?: DataValue) => void
) {
assert(self instanceof MonitoredItem);
assert(oldValue instanceof DataValue);
assert(itemToMonitor.attributeId === AttributeIds.Value);
const dataValue = node.readAttribute(context, itemToMonitor.attributeId, itemToMonitor.indexRange, itemToMonitor.dataEncoding);
callback(null, dataValue);
}
/*== private
* this method applies to Variable Values attribute
* @private
*/
function monitoredItem_read_and_record_value_async(
self: MonitoredItem,
context: ISessionContext,
oldValue: DataValue,
node: UAVariable,
itemToMonitor: any,
callback: (err: Error | null, dataValue?: DataValue) => void
) {
assert(context instanceof SessionContext);
assert(itemToMonitor.attributeId === AttributeIds.Value);
assert(self instanceof MonitoredItem);
assert(oldValue instanceof DataValue);
// do it asynchronously ( this is only valid for value attributes )
assert(itemToMonitor.attributeId === AttributeIds.Value);
node.readValueAsync(context, (err: Error | null, dataValue?: DataValue) => {
callback(err, dataValue);
});
}
function build_scanning_node_function(addressSpace: AddressSpace, itemToMonitor: any): SamplingFunc {
assert(itemToMonitor instanceof ReadValueId);
const node = addressSpace.findNode(itemToMonitor.nodeId) as UAVariable;
/* istanbul ignore next */
if (!node) {
errorLog(" INVALID NODE ID , ", itemToMonitor.nodeId.toString());
dump(itemToMonitor);
return (
_sessionContext: ISessionContext,
_oldData: DataValue,
callback: (err: Error | null, dataValue?: DataValue) => void
) => {
callback(
null,
new DataValue({
statusCode: StatusCodes.BadNodeIdUnknown,
value: { dataType: DataType.Null, value: 0 }
})
);
};
}
///// !!monitoredItem.setNode(node);
if (itemToMonitor.attributeId === AttributeIds.Value) {
const monitoredItem_read_and_record_value_func =
itemToMonitor.attributeId === AttributeIds.Value && typeof node.readValueAsync === "function"
? monitoredItem_read_and_record_value_async
: monitoredItem_read_and_record_value;
return function func(
this: MonitoredItem,
sessionContext: ISessionContext,
oldDataValue: DataValue,
callback: (err: Error | null, dataValue?: DataValue) => void
) {
assert(this instanceof MonitoredItem);
assert(oldDataValue instanceof DataValue);
assert(typeof callback === "function");
monitoredItem_read_and_record_value_func(this, sessionContext, oldDataValue, node, itemToMonitor, callback);
};
} else {
// Attributes, other than the Value Attribute, are only monitored for a change in value.
// The filter is not used for these Attributes. Any change in value for these Attributes
// causes a Notification to be generated.
// only record value when it has changed
return function func(
this: MonitoredItem,
sessionContext: ISessionContext,
oldDataValue: DataValue,
callback: (err: Error | null, dataValue?: DataValue) => void
) {
assert(this instanceof MonitoredItem);
assert(oldDataValue instanceof DataValue);
assert(typeof callback === "function");
const newDataValue = node.readAttribute(sessionContext, itemToMonitor.attributeId);
callback(null, newDataValue);
};
}
}
function prepareMonitoredItem(context: ISessionContext, addressSpace: AddressSpace, monitoredItem: MonitoredItem) {
const itemToMonitor = monitoredItem.itemToMonitor;
const readNodeFunc = build_scanning_node_function(addressSpace, itemToMonitor);
monitoredItem.samplingFunc = readNodeFunc;
}
function isMonitoringModeValid(monitoringMode: MonitoringMode): boolean {
assert(MonitoringMode.Invalid !== undefined);
return monitoringMode !== MonitoringMode.Invalid && monitoringMode <= MonitoringMode.Reporting;
}
function _installRegisterServerManager(self: OPCUAServer) {
assert(self instanceof OPCUAServer);
assert(!self.registerServerManager);
/* istanbul ignore next */
if (!self.registerServerMethod) {
throw new Error("Internal Error");
}
switch (self.registerServerMethod) {
case RegisterServerMethod.HIDDEN:
self.registerServerManager = new RegisterServerManagerHidden({
server: self
});
break;
case RegisterServerMethod.MDNS:
self.registerServerManager = new RegisterServerManagerMDNSONLY({
server: self
});
break;
case RegisterServerMethod.LDS:
self.registerServerManager = new RegisterServerManager({
discoveryServerEndpointUrl: self.discoveryServerEndpointUrl,
server: self
});
break;
/* istanbul ignore next */
default:
throw new Error("Invalid switch");
}
self.registerServerManager.on("serverRegistrationPending", () => {
/**
* emitted when the server is trying to registered the LDS
* but when the connection to the lds has failed
* serverRegistrationPending is sent when the backoff signal of the
* connection process is raised
* @event serverRegistrationPending
*/
debugLog("serverRegistrationPending");
self.emit("serverRegistrationPending");
});
self.registerServerManager.on("serverRegistered", () => {
/**
* emitted when the server is successfully registered to the LDS
* @event serverRegistered
*/
debugLog("serverRegistered");
self.emit("serverRegistered");
});
self.registerServerManager.on("serverRegistrationRenewed", () => {
/**
* emitted when the server has successfully renewed its registration to the LDS
* @event serverRegistrationRenewed
*/
debugLog("serverRegistrationRenewed");
self.emit("serverRegistrationRenewed");
});
self.registerServerManager.on("serverUnregistered", () => {
debugLog("serverUnregistered");
/**
* emitted when the server is successfully unregistered to the LDS
* ( for instance during shutdown)
* @event serverUnregistered
*/
self.emit("serverUnregistered");
});
}
function validate_applicationUri(channel: ServerSecureChannelLayer, request: CreateSessionRequest): boolean {
const applicationUri = request.clientDescription.applicationUri!;
const clientCertificate = request.clientCertificate;
// if session is insecure there is no need to check certificate information
if (channel.securityMode === MessageSecurityMode.None) {
return true; // assume correct
}
if (!clientCertificate || clientCertificate.length === 0) {
return true; // can't check
}
const e = exploreCertificate(clientCertificate);
const uniformResourceIdentifier = e.tbsCertificate.extensions!.subjectAltName?.uniformResourceIdentifier ?? null;
const applicationUriFromCert =
uniformResourceIdentifier && uniformResourceIdentifier.length > 0 ? uniformResourceIdentifier[0] : null;
/* istanbul ignore next */
if (applicationUriFromCert !== applicationUri) {
errorLog("BadCertificateUriInvalid!");
errorLog("applicationUri = ", applicationUri);
errorLog("applicationUriFromCert = ", applicationUriFromCert);
}
return applicationUriFromCert === applicationUri;
}
function validate_security_endpoint(
server: OPCUAServer,
request: CreateSessionRequest,
channel: ServerSecureChannelLayer
): {
errCode: StatusCode;
endpoint?: EndpointDescription;
} {
debugLog("validate_security_endpoint = ", request.endpointUrl);
let endpoints = server._get_endpoints(request.endpointUrl);
// endpointUrl String The network address that the Client used to access the Session Endpoint.
// The HostName portion of the URL should be one of the HostNames for the application that are
// specified in the Server’s ApplicationInstanceCertificate (see 7.2). The Server shall raise an
// AuditUrlMismatchEventType event if the URL does not match the Server’s HostNames.
// AuditUrlMismatchEventType event type is defined in Part 5.
// The Server uses this information for diagnostics and to determine the set of
// EndpointDescriptions to return in the response.
// ToDo: check endpointUrl validity and emit an AuditUrlMismatchEventType event if not
// sometime endpoints have a extra leading "/" that can be ignored
// don't be too harsh.
if (endpoints.length === 0 && request.endpointUrl?.endsWith("/")) {
endpoints = server._get_endpoints(request.endpointUrl.slice(0, -1));
}
if (endpoints.length === 0) {
// we have a UrlMismatch here
const ua_server = server.engine.addressSpace!.rootFolder.objects.server;
if (!request.endpointUrl?.match(/localhost/i) || OPCUAServer.requestExactEndpointUrl) {
warningLog("Cannot find suitable endpoints in available endpoints. endpointUri =", request.endpointUrl);
}
ua_server.raiseEvent("AuditUrlMismatchEventType", {
endpointUrl: { dataType: DataType.String, value: request.endpointUrl }
});
if (OPCUAServer.requestExactEndpointUrl) {
return { errCode: StatusCodes.BadServiceUnsupported };
} else {
endpoints = server._get_endpoints(null);
}
}
// ignore restricted endpoints
endpoints = endpoints.filter((e: EndpointDescription) => !(e as EndpointDescriptionEx).restricted);
const endpoints_matching_security_mode = endpoints.filter((e: EndpointDescription) => {
return e.securityMode === channel.securityMode;
});
if (endpoints_matching_security_mode.length === 0) {
return { errCode: StatusCodes.BadSecurityModeRejected };
}
const endpoints_matching_security_policy = endpoints_matching_security_mode.filter((e: EndpointDescription) => {
return e.securityPolicyUri === channel!.securityPolicy;
});
if (endpoints_matching_security_policy.length === 0) {
return { errCode: StatusCodes.BadSecurityPolicyRejected };
}
if (endpoints_matching_security_policy.length !== 1) {
debugLog("endpoints_matching_security_policy= ", endpoints_matching_security_policy.length);
}
return { errCode: StatusCodes.Good, endpoint: endpoints_matching_security_policy[0] };
}
export function filterDiagnosticInfo(returnDiagnostics: number, response: CallResponse): void {
if (RESPONSE_DIAGNOSTICS_MASK_ALL & returnDiagnostics) {
response.responseHeader.serviceDiagnostics = filterDiagnosticServiceLevel(
returnDiagnostics,
response.responseHeader.serviceDiagnostics
);
if (response.diagnosticInfos && response.diagnosticInfos.length > 0) {
response.diagnosticInfos = response.diagnosticInfos.map((d) => filterDiagnosticOperationLevel(returnDiagnostics, d));
} else {
response.diagnosticInfos = [];
}
if (response.results) {
for (const entry of response.results) {
if (entry.inputArgumentDiagnosticInfos && entry.inputArgumentDiagnosticInfos.length > 0) {
entry.inputArgumentDiagnosticInfos = entry.inputArgumentDiagnosticInfos.map((d) =>
filterDiagnosticOperationLevel(returnDiagnostics, d)
);
} else {
entry.inputArgumentDiagnosticInfos = [];
}
}
}
}
}
export enum RegisterServerMethod {
HIDDEN = 1, // the server doesn't expose itself to the external world
MDNS = 2, // the server publish itself to the mDNS Multicast network directly
LDS = 3 // the server registers itself to the LDS or LDS-ME (Local Discovery Server)
}
export interface OPCUAServerEndpointOptions {
/**
* the primary hostname of the endpoint.
* @default getFullyQualifiedDomainName()
*/
hostname?: string;
/**
* Host IP address or hostname where the TCP server listens for connections.
* If omitted, defaults to listening on all network interfaces:
* - Unspecified IPv6 address (::) if IPv6 is available,
* - Unspecified IPv4 address (0.0.0.0) otherwise.
* Use this to bind the server to a specific interface or IP.
*/
host?: string;
/**
* the TCP port to listen to.
* @default 26543
*/
port?: number;
/**
* the possible security policies that the server will expose
* @default [SecurityPolicy.None, SecurityPolicy.Basic128Rsa15, SecurityPolicy.Basic256Sha256, SecurityPolicy.Aes128_Sha256_RsaOaep, SecurityPolicy.Aes256_Sha256_RsaPss ]
*/
securityPolicies?: SecurityPolicy[];
/**
* the possible security mode that the server will expose
* @default [MessageSecurityMode.None, MessageSecurityMode.Sign, MessageSecurityMode.SignAndEncrypt]
*/
securityModes?: MessageSecurityMode[];
/**
* tells if the server default endpoints should allow anonymous connection.
* @default true
*/
allowAnonymous?: boolean;
/** alternate hostname or IP to use */
alternateHostname?: string | string[];
/**
* true, if discovery service on secure channel shall be disabled
*/
disableDiscovery?: boolean;
}
export interface OPCUAServerOptions extends OPCUABaseServerOptions, OPCUAServerEndpointOptions {
/**
* @deprecated
*/
alternateEndpoints?: OPCUAServerEndpointOptions[];
endpoints?: OPCUAServerEndpointOptions[];
/**
* the server certificate full path filename
*
* the certificate should be in PEM format
*/
certificateFile?: string;
/**
* the server private key full path filename
*
* This file should contains the private key that has been used to generate
* the server certificate file.
*
* the private key should be in PEM format
*
*/
privateKeyFile?: string;
/**
* the default secure token life time in ms.
*/
defaultSecureTokenLifetime?: number;
/**
* the HEL/ACK transaction timeout in ms.
*
* Use a large value ( i.e 15000 ms) for slow connections or embedded devices.
* @default 10000
*/
timeout?: number;
/**
* the maximum number of simultaneous sessions allowed.
* @default 10
* @deprecated use serverCapabilities: { maxSessions: } instead
*/
maxAllowedSessionNumber?: number;
/**
* the maximum number authorized simultaneous connections per endpoint
* @default 10
*/
maxConnectionsPerEndpoint?: number;
/**
* the nodeset.xml file(s) to load
*
* node-opcua comes with pre-installed node-set files that can be used
*
* example:
*
* ```javascript
* import { nodesets } from "node-opcua-nodesets";
* const server = new OPCUAServer({
* nodeset_filename: [
* nodesets.standard,
* nodesets.di,
* nodesets.adi,
* nodesets.machinery,
* ],
* });
* ```
*/
nodeset_filename?: string[] | string;
/**
* the server Info
*
* this object contains the value that will populate the
* Root/ObjectS/Server/ServerInfo OPCUA object in the address space.
*/
serverInfo?: ApplicationDescriptionOptions;
/*{
applicationUri?: string;
productUri?: string;
applicationName?: LocalizedTextLike | string;
gatewayServerUri?: string | null;
discoveryProfileUri?: string | null;
discoveryUrls?: string[];
};
*/
buildInfo?: {
productName?: string;
productUri?: string | null; // << should be same as default_server_info.productUri?
manufacturerName?: string;
softwareVersion?: string;
buildNumber?: string;
buildDate?: Date;
};
/**
* an object that implements user authentication methods
*/
userManager?: UserManagerOptions;
/** resource Path is a string added at the end of the url such as "/UA/Server" */
resourcePath?: string;
/**
*
*/
serverCapabilities?: ServerCapabilitiesOptions;
/**
* if server shall raise AuditingEvent
* @default true
*/
isAuditing?: boolean;
/**
* strategy used by the server to declare itself to a discovery server
*
* - HIDDEN: the server doesn't expose itself to the external world
* - MDNS: the server publish itself to the mDNS Multicast network directly
* - LDS: the server registers itself to the LDS or LDS-ME (Local Discovery Server)
*
* @default .HIDDEN - by default the server
* will not register itself to the local discovery server
*
*/
registerServerMethod?: RegisterServerMethod;
/**
*
* @default "opc.tcp://localhost:4840"]
*/
discoveryServerEndpointUrl?: string;
/**
*
* supported server capabilities for the Multicast (mDNS)
* @default ["NA"]
* the possible values are any of node-opcua-discovery.serverCapabilities)
*
*/
capabilitiesForMDNS?: string[];
/**
* user Certificate Manager
* this certificate manager holds the X509 certificates used
* by client that uses X509 certificate token to impersonate a user
*/
userCertificateManager?: OPCUACertificateManager;
/**
* Server Certificate Manager
*
* this certificate manager will be used by the server to access
* and store certificates from the connecting clients
*/
serverCertificateManager?: OPCUACertificateManager;
/**
*
*/
onCreateMonitoredItem?: CreateMonitoredItemHook;
onDeleteMonitoredItem?: DeleteMonitoredItemHook;
/**
* skipOwnNamespace to true, if you don't want the server to create
* a dedicated namespace for its own (namespace=1).
* Use this flag if you intend to load the server own namespace
* from an external source.
* @default false
*/
skipOwnNamespace?: boolean;
transportSettings?: IServerTransportSettings;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
export interface OPCUAServer {
/**
*
*/
engine: ServerEngine;
/**
*
*/
registerServerMethod: RegisterServerMethod;
/**
*
*/
discoveryServerEndpointUrl: string;
/**
*
*/
registerServerManager?: IRegisterServerManager;
/**
*
*/
capabilitiesForMDNS: string[];
/**
*
*/
userCertificateManager: OPCUACertificateManager;
}
const g_requestExactEndpointUrl = !!process.env.NODEOPCUA_SERVER_REQUEST_EXACT_ENDPOINT_URL;
/**
*
*/
export class OPCUAServer extends OPCUABaseServer {
static defaultShutdownTimeout = 100; // 250 ms
/**
* if requestExactEndpointUrl is set to true the server will only accept createSession that have a endpointUrl that strictly matches
* one of the provided endpoint.
* This mean that if the server expose a endpoint with url such as opc.tcp://MYHOSTNAME:1234, client will not be able to reach the server
* with the ip address of the server.
* requestExactEndpointUrl = true => emulates the Prosys Server behavior
* requestExactEndpointUrl = false => emulates the Unified Automation behavior.
*/
static requestExactEndpointUrl: boolean = g_requestExactEndpointUrl;
/**
* total number of bytes written by the server since startup
*/
public get bytesWritten(): number {
return this.endpoints.reduce((accumulated: number, endpoint: OPCUAServerEndPoint) => {
return accumulated + endpoint.bytesWritten;
}, 0);
}
/**
* total number of bytes read by the server since startup
*/
public get bytesRead(): number {
return this.endpoints.reduce((accumulated: number, endpoint: OPCUAServerEndPoint) => {
return accumulated + endpoint.bytesRead;
}, 0);
}
/**
* Number of transactions processed by the server since startup
*/
public get transactionsCount(): number {
return this.endpoints.reduce((accumulated: number, endpoint: OPCUAServerEndPoint) => {
return accumulated + endpoint.transactionsCount;
}, 0);
}
/**
* The server build info
*/
public get buildInfo(): BuildInfo {
return this.engine.buildInfo;
}
/**
* the number of connected channel on all existing end points
*/
public get currentChannelCount(): number {
// TODO : move to base
return this.endpoints.reduce((currentValue: number, endPoint: OPCUAServerEndPoint) => {
return currentValue + endPoint.currentChannelCount;
}, 0);
}
/**
* The number of active subscriptions from all sessions
*/
public get currentSubscriptionCount(): number {
return this.engine ? this.engine.currentSubscriptionCount : 0;
}
/**
* the number of session activation requests that have been rejected
*/
public get rejectedSessionCount(): number {
return this.engine ? this.engine.rejectedSessionCount : 0;
}
/**
* the number of request that have been rejected
*/
public get rejectedRequestsCount(): number {
return this.engine ? this.engine.rejectedRequestsCount : 0;
}
/**
* the number of sessions that have been aborted
*/
public get sessionAbortCount(): number {
return this.engine ? this.engine.sessionAbortCount : 0;
}
/**
* the publishing interval count
*/
public get publishingIntervalCount(): number {
return this.engine ? this.engine.publishingIntervalCount : 0;
}
/**
* the number of sessions currently active
*/
public get currentSessionCount(): number {
return this.engine ? this.engine.currentSessionCount : 0;
}
/**
* true if the server has been initialized
*
*/
public get initialized(): boolean {
return this.engine && this.engine.addressSpace !== null;
}
/**
* is the server auditing ?
*/
public get isAuditing(): boolean {
return this.engine ? this.engine.isAuditing : false;
}
public static registry = new ObjectRegistry();
public static fallbackSessionName = "Client didn't provide a meaningful sessionName ...";
/**
* the maximum number of subscription that can be created per server
* @deprecated
*/
public static deprecated_MAX_SUBSCRIPTION = 50;
/**
* the maximum number of concurrent sessions allowed on the server
*/
public get maxAllowedSessionNumber(): number {
return this.engine.serverCapabilities.maxSessions;
}
/**
* the maximum number for concurrent connection per end point
*/
public maxConnectionsPerEndpoint: number;
/**
* false if anonymous connection are not allowed
*/
public allowAnonymous = false;
/**
* the user manager
*/
public userManager: UAUserManagerBase;
public readonly options: OPCUAServerOptions;
private objectFactory?: Factory;
private _delayInit?: () => Promise<void>;
constructor(options?: OPCUAServerOptions) {
super(options);
this.allowAnonymous = false;
options = options || {};
this.options = options;
if (options.maxAllowedSessionNumber !== undefined) {
warningLog(
"[NODE-OPCUA-W21] maxAllowedSessionNumber property is now deprecated , please use serverCapabilities.maxSessions instead"
);
options.serverCapabilities = options.serverCapabilities || {};
options.serverCapabilities.maxSessions = options.maxAllowedSessionNumber;
}
// adjust securityPolicies if any
if (options.securityPolicies) {
options.securityPolicies = options.securityPolicies.map(coerceSecurityPolicy);
}
/**
* @property maxConnectionsPerEndpoint
*/
this.maxConnectionsPerEndpoint = options.maxConnectionsPerEndpoint || default_maxConnectionsPerEndpoint;
// build Info
const buildInfo: BuildInfoOptions = {
...default_build_info,
...options.buildInfo
};
// repair product name
buildInfo.productUri = buildInfo.productUri || this.serverInfo.productUri;
this.serverInfo.productUri = this.serverInfo.productUri || buildInfo.productUri;
this.userManager = makeUserManager(options.userManager);
options.allowAnonymous = options.allowAnonymous === undefined ? true : !!options.allowAnonymous;
/**
* @property allowAnonymous
*/
this.allowAnonymous = options.allowAnonymous;
this.discoveryServerEndpointUrl = options.discoveryServerEndpointUrl || "opc.tcp://%FQDN%:4840";
assert(typeof this.discoveryServerEndpointUrl === "string");
this.serverInfo.applicationType =
options.serverInfo?.applicationType === undefined ? ApplicationType.Server : options.serverInfo.applicationType;
this.capabilitiesForMDNS = options.capabilitiesForMDNS || ["NA"];
this.registerServerMethod = options.registerServerMethod || RegisterServerMethod.HIDDEN;
_installRegisterServerManager(this);
if (!options.userCertificateManager) {
this.userCertificateManager = getDefaultCertificateManager("UserPKI");
} else {
this.userCertificateManager = options.userCertificateManager;
}
// note: we need to delay initialization of endpoint as certain resources
// such as %FQDN% might not be ready yet at this stage
this._delayInit = async () => {
/* istanbul ignore next */
if (!options) {
throw new Error("Internal Error");
}
// to check => this.serverInfo.applicationName = this.serverInfo.productName || buildInfo.productName;
// note: applicationUri is handled in a special way
this.engine = new ServerEngine({
applicationUri: () => this.serverInfo.applicationUri!,
buildInfo,
isAuditing: options.isAuditing,
serverCapabilities: options.serverCapabilities,
serverConfiguration: {
serverCapabilities: () => {
return this.capabilitiesForMDNS || ["NA"];
},
supportedPrivateKeyFormat: ["PEM"],
applicationType: () => this.serverInfo.applicationType,
applicationUri: () => this.serverInfo.applicationUri || "",
productUri: () => this.serverInfo.productUri || "",
// hasSecureElement: () => false,
multicastDnsEnabled: () => this.registerServerMethod === RegisterServerMethod.MDNS
}
});
this.objectFactory = new Factory(this.engine);
const endpointDefinitions: OPCUAServerEndpointOptions[] = [
...(options.endpoints || []),
...(options.alternateEndpoints || [])
];
const hostname = getFullyQualifiedDomainName();
endpointDefinitions.forEach((endpointDefinition) => {
endpointDefinition.port = endpointDefinition.port === undefined ? 26543 : endpointDefinition.port;
endpointDefinition.hostname = endpointDefinition.hostname || hostname;
});
if (!options.endpoints) {
endpointDefinitions.push({
port: options.port === undefined ? 26543 : options.port,
hostname: options.hostname || hostname,
host: options.host,
allowAnonymous: options.allowAnonymous,
alternateHostname: options.alternateHostname,
disableDiscovery: options.disableDiscovery,
securityModes: options.securityModes,
securityPolicies: options.securityPolicies
});
}
// todo should self.serverInfo.productUri match self.engine.buildInfo.productUri ?
for (const endpointOptions of endpointDefinitions) {
const endPoint = this.createEndpointDescriptions(options!, endpointOptions);
this.endpoints.push(endPoint);
endPoint.on("message", (message: Message, channel: ServerSecureChannelLayer) => {
this.on_request(message, channel);
});
// endPoint.on("error", (err: Error) => {
// errorLog("OPCUAServer endpoint error", err);
// // set serverState to ServerState.Failed;
// this.engine.setServerState(ServerState.Failed);
// this.shutdown(() => {
// /* empty */
// });
// });
}
};
}
/**
* Initialize the server by installing default node set.
*
* and instruct the server to listen to its endpoints.
*
* ```javascript
* const server = new OPCUAServer();
* await server.initialize();
*
* // default server namespace is now initialized
* // it is a good time to create life instance objects
* const namespace = server.engine.addressSpace.getOwnNamespace();
* namespace.addObject({
* browseName: "SomeObject",
* organizedBy: server.engine.addressSpace.rootFolder.objects
* });
*
* // the addressSpace is now complete
* // let's now start listening to clients
* await server.start();
* ```
*/
public initialize(): Promise<void>;
public initialize(done: () => void): void;
public initialize(...args: [any?, ...any[]]): any {
const done = args[0] as (err?: Error) => void;
assert(!this.initialized, "server is already initialized"); // already initialized ?
this._preInitTask.push(async () => {
/* istanbul ignore else */
if (this._delayInit) {
await this._delayInit();
this._delayInit = undefined;
}
});
this.performPreInitialization()
.then(() => {
OPCUAServer.registry.register(this);
this.engine.initialize(this.options, () => {
bindRoleSet(this.userManager, this.engine.addressSpace!);
setImmediate(() => {
this.emit("post_initialize");
done();
});
});
})
.catch((err) => {
done(err);
});
}
/**
* Initiate the server by starting all its endpoints
*/
public start(): Promise<void>;
public start(done: () => void): void;
public start(...args: [any?, ...any[]]): any {
const done = args[0] as () => void;
const tasks: any[] = [];
tasks.push(callbackify(extractFullyQualifiedDomainName));
if (!this.initialized) {
tasks.push((callback: ErrorCallback) => {
this.initialize(callback);
});
}
tasks.push((callback: ErrorCallback) => {
super.start((err?: Error | null) => {
if (err) {
this.shutdown((/*err2*/ err2?: Error) => {
callback(err);
});
} else {
// we start the registration process asynchronously
// as we want to make server immediately available
this.registerServerManager!.start(() => {
/* empty */
});
setImmediate(callback);
}
});
});
async.series(tasks, done);
}
/**
* shutdown all server endpoints
* @param timeout the timeout (in ms) before the server is actually shutdown
*
* @example
*
* ```javascript
* // shutdown immediately
* server.shutdown(function(err) {
* });
* ```
* ```ts
* // in typescript with promises
* server.shutdown(10000).then(()=>{
* console.log("Server has shutdown");
* });
* ```
* ```javascript
* // shutdown within 10 seconds
* server.engine.shutdownReason = coerceLocalizedText("Shutdown for maintenance");
* server.shutdown(10000,function(err) {
* });
* ```
*/
public shutdown(timeout?: number): Promise<void>;
public shutdown(callback: (err?: Error) => void): void;
public shutdown(timeout: number, callback: (err?: Error) => void): void;
public shutdown(...args: [any?, ...any[]]): any {
const timeout = args.length === 1 ? OPCUAServer.defaultShutdownTimeout : (args[0] as number);
const callback = (args.length === 1 ? args[0] : args[1]) as (err?: Error) => void;
assert(typeof callback === "function");
debugLog("OPCUAServer#shutdown (timeout = ", timeout, ")");
/* istanbul ignore next */
if (!this.engine) {
return callback();
}
assert(this.engine);
if (!this.engine.isStarted()) {
// server may have been shot down already , or may have fail to start !!
const err = new Error("OPCUAServer#shutdown failure ! server doesn't seems to be started yet");
r