UNPKG

node-opcua-server

Version:

pure nodejs OPCUA SDK - module server

1,371 lines (1,240 loc) 166 kB
/* 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