UNPKG

node-opcua-server

Version:

pure nodejs OPCUA SDK - module server

471 lines 22.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.OPCUABaseServer = void 0; /** * @module node-opcua-server */ // tslint:disable:no-console const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const os_1 = __importDefault(require("os")); const util_1 = require("util"); const async_1 = __importDefault(require("async")); const chalk_1 = __importDefault(require("chalk")); const node_opcua_assert_1 = require("node-opcua-assert"); const global_mutex_1 = require("@ster5/global-mutex"); const node_opcua_certificate_manager_1 = require("node-opcua-certificate-manager"); const node_opcua_common_1 = require("node-opcua-common"); const node_opcua_data_model_1 = require("node-opcua-data-model"); const node_opcua_date_time_1 = require("node-opcua-date-time"); const node_opcua_debug_1 = require("node-opcua-debug"); const node_opcua_debug_2 = require("node-opcua-debug"); const node_opcua_hostname_1 = require("node-opcua-hostname"); const node_opcua_service_discovery_1 = require("node-opcua-service-discovery"); const node_opcua_service_endpoints_1 = require("node-opcua-service-endpoints"); const node_opcua_service_endpoints_2 = require("node-opcua-service-endpoints"); const node_opcua_service_secure_channel_1 = require("node-opcua-service-secure-channel"); const node_opcua_status_code_1 = require("node-opcua-status-code"); const node_opcua_utils_1 = require("node-opcua-utils"); const node_opcua_client_1 = require("node-opcua-client"); const doDebug = (0, node_opcua_debug_1.checkDebugFlag)(__filename); const debugLog = (0, node_opcua_debug_1.make_debugLog)(__filename); const errorLog = (0, node_opcua_debug_1.make_errorLog)(__filename); const warningLog = errorLog; const default_server_info = { // The globally unique identifier for the application instance. This URI is used as // ServerUri in Services if the application is a Server. applicationUri: (0, node_opcua_common_1.makeApplicationUrn)(os_1.default.hostname(), "NodeOPCUA-Server"), // The globally unique identifier for the product. productUri: "NodeOPCUA-Server", // A localized descriptive name for the application. applicationName: { text: "NodeOPCUA", locale: "en" }, applicationType: node_opcua_service_endpoints_1.ApplicationType.Server, gatewayServerUri: "", discoveryProfileUri: "", discoveryUrls: [] }; function cleanupEndpoint(endpoint) { if (endpoint._on_new_channel) { (0, node_opcua_assert_1.assert)(typeof endpoint._on_new_channel === "function"); endpoint.removeListener("newChannel", endpoint._on_new_channel); endpoint._on_new_channel = undefined; } if (endpoint._on_close_channel) { (0, node_opcua_assert_1.assert)(typeof endpoint._on_close_channel === "function"); endpoint.removeListener("closeChannel", endpoint._on_close_channel); endpoint._on_close_channel = undefined; } if (endpoint._on_connectionRefused) { (0, node_opcua_assert_1.assert)(typeof endpoint._on_connectionRefused === "function"); endpoint.removeListener("connectionRefused", endpoint._on_connectionRefused); endpoint._on_connectionRefused = undefined; } if (endpoint._on_openSecureChannelFailure) { (0, node_opcua_assert_1.assert)(typeof endpoint._on_openSecureChannelFailure === "function"); endpoint.removeListener("openSecureChannelFailure", endpoint._on_openSecureChannelFailure); endpoint._on_openSecureChannelFailure = undefined; } } const emptyCallback = () => { /* empty */ }; class OPCUABaseServer extends node_opcua_common_1.OPCUASecureObject { /** * The type of server */ get serverType() { return this.serverInfo.applicationType; } constructor(options) { options = options || {}; if (!options.serverCertificateManager) { options.serverCertificateManager = (0, node_opcua_certificate_manager_1.getDefaultCertificateManager)("PKI"); } options.privateKeyFile = options.privateKeyFile || options.serverCertificateManager.privateKey; options.certificateFile = options.certificateFile || path_1.default.join(options.serverCertificateManager.rootDir, "own/certs/certificate.pem"); super(options); this.serverCertificateManager = options.serverCertificateManager; this.capabilitiesForMDNS = []; this.endpoints = []; this.options = options; this._preInitTask = []; const serverInfo = { ...default_server_info, ...options.serverInfo }; serverInfo.applicationName = (0, node_opcua_data_model_1.coerceLocalizedText)(serverInfo.applicationName); this.serverInfo = new node_opcua_service_endpoints_2.ApplicationDescription(serverInfo); if (this.serverInfo.applicationName.toString().match(/urn:/)) { errorLog("[NODE-OPCUA-E06] application name cannot be a urn", this.serverInfo.applicationName.toString()); } this.serverInfo.applicationName.locale = this.serverInfo.applicationName?.locale || "en"; if (!this.serverInfo.applicationName?.locale) { warningLog("[NODE-OPCUA-W24] the server applicationName must have a valid locale : ", this.serverInfo.applicationName.toString()); } const __applicationUri = serverInfo.applicationUri || ""; this.serverInfo.__defineGetter__("applicationUri", () => (0, node_opcua_hostname_1.resolveFullyQualifiedDomainName)(__applicationUri)); this._preInitTask.push(async () => { const fqdn = await (0, node_opcua_hostname_1.extractFullyQualifiedDomainName)(); }); this._preInitTask.push(async () => { await this.initializeCM(); }); } async createDefaultCertificate() { if (fs_1.default.existsSync(this.certificateFile)) { return; } // collect all hostnames const hostnames = []; for (const e of this.endpoints) { for (const ee of e.endpointDescriptions()) { /* to do */ } } if (!fs_1.default.existsSync(this.certificateFile)) { await (0, global_mutex_1.withLock)({ fileToLock: this.certificateFile + ".mutex" }, async () => { if (fs_1.default.existsSync(this.certificateFile)) { return; } const applicationUri = this.serverInfo.applicationUri; const fqdn = (0, node_opcua_hostname_1.getFullyQualifiedDomainName)(); const hostname = (0, node_opcua_hostname_1.getHostname)(); const dns = [...new Set([fqdn, hostname])]; await this.serverCertificateManager.createSelfSignedCertificate({ applicationUri, dns, // ip: await getIpAddresses(), outputFile: this.certificateFile, subject: (0, node_opcua_certificate_manager_1.makeSubject)(this.serverInfo.applicationName.text, hostname), startDate: new Date(), validity: 365 * 10 // 10 years }); }); } } async initializeCM() { await this.serverCertificateManager.initialize(); await this.createDefaultCertificate(); debugLog("privateKey = ", this.privateKeyFile, this.serverCertificateManager.privateKey); debugLog("certificateFile = ", this.certificateFile); await (0, node_opcua_client_1.performCertificateSanityCheck)(this, "server", this.serverCertificateManager, this.serverInfo.applicationUri); } /** * start all registered endPoint, in parallel, and call done when all endPoints are listening. */ start(done) { (0, node_opcua_assert_1.assert)(typeof done === "function"); this.startAsync() .then(() => done(null)) .catch((err) => done(err)); } async performPreInitialization() { const tasks = this._preInitTask; this._preInitTask = []; for (const task of tasks) { await task(); } } async startAsync() { await this.performPreInitialization(); (0, node_opcua_assert_1.assert)(Array.isArray(this.endpoints)); (0, node_opcua_assert_1.assert)(this.endpoints.length > 0, "We need at least one end point"); (0, node_opcua_date_time_1.installPeriodicClockAdjustment)(); // eslint-disable-next-line @typescript-eslint/no-this-alias const server = this; const _on_new_channel = function (channel) { server.emit("newChannel", channel, this); }; const _on_close_channel = function (channel) { server.emit("closeChannel", channel, this); }; const _on_connectionRefused = function (socketData) { server.emit("connectionRefused", socketData, this); }; const _on_openSecureChannelFailure = function (socketData, channelData) { server.emit("openSecureChannelFailure", socketData, channelData, this); }; const promises = []; for (const endpoint of this.endpoints) { (0, node_opcua_assert_1.assert)(!endpoint._on_close_channel); endpoint._on_new_channel = _on_new_channel; endpoint.on("newChannel", endpoint._on_new_channel); endpoint._on_close_channel = _on_close_channel; endpoint.on("closeChannel", endpoint._on_close_channel); endpoint._on_connectionRefused = _on_connectionRefused; endpoint.on("connectionRefused", endpoint._on_connectionRefused); endpoint._on_openSecureChannelFailure = _on_openSecureChannelFailure; endpoint.on("openSecureChannelFailure", endpoint._on_openSecureChannelFailure); promises.push(new Promise((resolve, reject) => endpoint.start((err) => (err ? reject(err) : resolve())))); } await Promise.all(promises); } /** * shutdown all server endPoints */ shutdown(done) { (0, node_opcua_assert_1.assert)(typeof done === "function"); (0, node_opcua_date_time_1.uninstallPeriodicClockAdjustment)(); this.serverCertificateManager.dispose().then(() => { debugLog("OPCUABaseServer#shutdown starting"); async_1.default.forEach(this.endpoints, (endpoint, callback) => { cleanupEndpoint(endpoint); endpoint.shutdown(callback); }, (err) => { debugLog("shutdown completed"); done(err); }); }); } shutdownChannels(callback) { (0, node_opcua_assert_1.assert)(typeof callback === "function"); debugLog("OPCUABaseServer#shutdownChannels"); async_1.default.forEach(this.endpoints, (endpoint, inner_callback) => { debugLog(" shutting down endpoint ", endpoint.endpointDescriptions()[0].endpointUrl); async_1.default.series([ // xx (callback2: (err?: Error| null) => void) => { // xx endpoint.suspendConnection(callback2); // xx }, (callback2) => { endpoint.abruptlyInterruptChannels(); endpoint.shutdown(callback2); } // xx (callback2: (err?: Error| null) => void) => { // xx endpoint.restoreConnection(callback2); // xx } ], inner_callback); }, callback); } /** * @private */ on_request(message, channel) { (0, node_opcua_assert_1.assert)(message.request); (0, node_opcua_assert_1.assert)(message.requestId !== 0); const request = message.request; // install channel._on_response so we can intercept its call and emit the "response" event. if (!channel._on_response) { channel._on_response = (msg, response1 /*, inner_message: Message*/) => { this.emit("response", response1, channel); }; } // prepare request this.prepare(message, channel); if (doDebug) { debugLog(chalk_1.default.green.bold("--------------------------------------------------------"), channel.channelId, request.schema.name); } let errMessage; let response; this.emit("request", request, channel); try { // handler must be named _on_ActionRequest() const handler = this["_on_" + request.schema.name]; if (typeof handler === "function") { // eslint-disable-next-line prefer-rest-params handler.apply(this, arguments); } else { errMessage = "[NODE-OPCUA-W07] Unsupported Service : " + request.schema.name; warningLog(errMessage); debugLog(chalk_1.default.red.bold(errMessage)); response = makeServiceFault(node_opcua_status_code_1.StatusCodes.BadServiceUnsupported, [errMessage]); channel.send_response("MSG", response, message, emptyCallback); } } catch (err) { /* istanbul ignore if */ const errMessage1 = "[NODE-OPCUA-W08] EXCEPTION CAUGHT WHILE PROCESSING REQUEST !! " + request.schema.name; warningLog(chalk_1.default.red.bold(errMessage1)); warningLog(request.toString()); (0, node_opcua_debug_2.displayTraceFromThisProjectOnly)(err); let additional_messages = []; additional_messages.push("EXCEPTION CAUGHT WHILE PROCESSING REQUEST !!! " + request.schema.name); if (util_1.types.isNativeError(err)) { additional_messages.push(err.message); if (err.stack) { additional_messages = additional_messages.concat(err.stack.split("\n")); } } response = makeServiceFault(node_opcua_status_code_1.StatusCodes.BadInternalError, additional_messages); channel.send_response("MSG", response, message, emptyCallback); } } /** * @private */ _get_endpoints(endpointUrl) { let endpoints = []; for (const endPoint of this.endpoints) { const ep = endPoint.endpointDescriptions(); const epFiltered = endpointUrl ? ep.filter((e) => (0, node_opcua_utils_1.matchUri)(e.endpointUrl, endpointUrl)) : ep; endpoints = endpoints.concat(epFiltered); } return endpoints; } /** * get one of the possible endpointUrl */ getEndpointUrl() { return this._get_endpoints()[0].endpointUrl; } getDiscoveryUrls() { const discoveryUrls = this.endpoints.map((e) => { return e.endpointDescriptions()[0].endpointUrl; }); return discoveryUrls; } getServers(channel) { this.serverInfo.discoveryUrls = this.getDiscoveryUrls(); const servers = [this.serverInfo]; return servers; } suspendEndPoints(callback) { /* istanbul ignore next */ if (!callback) { throw new Error("Internal Error"); } async_1.default.forEach(this.endpoints, (ep, _inner_callback) => { /* istanbul ignore next */ if (doDebug) { debugLog("Suspending ", ep.endpointDescriptions()[0].endpointUrl); } ep.suspendConnection((err) => { /* istanbul ignore next */ if (doDebug) { debugLog("Suspended ", ep.endpointDescriptions()[0].endpointUrl); } _inner_callback(err); }); }, (err) => callback(err)); } resumeEndPoints(callback) { async_1.default.forEach(this.endpoints, (ep, _inner_callback) => { ep.restoreConnection(_inner_callback); }, (err) => callback(err)); } prepare(message, channel) { /* empty */ } /** * @private */ _on_GetEndpointsRequest(message, channel) { const request = message.request; (0, node_opcua_assert_1.assert)(request.schema.name === "GetEndpointsRequest"); const response = new node_opcua_service_endpoints_1.GetEndpointsResponse({}); /** * endpointUrl String The network address that the Client used to access the DiscoveryEndpoint. * The Server uses this information for diagnostics and to determine what URLs to return in the response. * The Server should return a suitable default URL if it does not recognize the HostName in the URL * localeIds []LocaleId List of locales to use. * Specifies the locale to use when returning human readable strings. * profileUris [] String List of Transport Profile that the returned Endpoints shall support. * OPC 10000-7 defines URIs for the Transport Profiles. * All Endpoints are returned if the list is empty. * If the URI is a URL, this URL may have a query string appended. * The Transport Profiles that support query strings are defined in OPC 10000-7. */ response.endpoints = this._get_endpoints(null); const e = response.endpoints.map((e) => e.endpointUrl); if (request.endpointUrl) { const filtered = response.endpoints.filter((endpoint) => endpoint.endpointUrl === request.endpointUrl); if (filtered.length > 0) { response.endpoints = filtered; } } response.endpoints = response.endpoints.filter((endpoint) => !endpoint.restricted); // apply filters if (request.profileUris && request.profileUris.length > 0) { response.endpoints = response.endpoints.filter((endpoint) => { return request.profileUris.indexOf(endpoint.transportProfileUri) >= 0; }); } // adjust locale on ApplicationName to match requested local or provide // a string with neutral locale (locale === null) // TODO: find a better way to handle this response.endpoints.forEach((endpoint) => { endpoint.server.applicationName.locale = "en-US"; }); channel.send_response("MSG", response, message, emptyCallback); } /** * @private */ _on_FindServersRequest(message, channel) { // Release 1.02 13 OPC Unified Architecture, Part 4 : // This Service can be used without security and it is therefore vulnerable to Denial Of Service (DOS) // attacks. A Server should minimize the amount of processing required to send the response for this // Service. This can be achieved by preparing the result in advance. The Server should also add a // short delay before starting processing of a request during high traffic conditions. const shortDelay = 100; // milliseconds setTimeout(() => { const request = message.request; (0, node_opcua_assert_1.assert)(request.schema.name === "FindServersRequest"); if (!(request instanceof node_opcua_service_discovery_1.FindServersRequest)) { throw new Error("Invalid request type"); } let servers = this.getServers(channel); // apply filters // TODO / if (request.serverUris && request.serverUris.length > 0) { // A serverUri matches the applicationUri from the ApplicationDescription define servers = servers.filter((inner_Server) => { return request.serverUris.indexOf(inner_Server.applicationUri) >= 0; }); } function adapt(applicationDescription) { return new node_opcua_service_endpoints_2.ApplicationDescription({ applicationName: applicationDescription.applicationName, applicationType: applicationDescription.applicationType, applicationUri: applicationDescription.applicationUri, discoveryProfileUri: applicationDescription.discoveryProfileUri, discoveryUrls: applicationDescription.discoveryUrls, gatewayServerUri: applicationDescription.gatewayServerUri, productUri: applicationDescription.productUri }); } const response = new node_opcua_service_discovery_1.FindServersResponse({ servers: servers.map(adapt) }); channel.send_response("MSG", response, message, emptyCallback); }, shortDelay); } /** * returns a array of currently active channels */ getChannels() { let channels = []; for (const endpoint of this.endpoints) { const c = endpoint.getChannels(); channels = channels.concat(c); } return channels; } } exports.OPCUABaseServer = OPCUABaseServer; OPCUABaseServer.makeServiceFault = makeServiceFault; /** * construct a service Fault response */ function makeServiceFault(statusCode, messages) { const response = new node_opcua_service_secure_channel_1.ServiceFault(); response.responseHeader.serviceResult = statusCode; // xx response.serviceDiagnostics.push( new DiagnosticInfo({ additionalInfo: messages.join("\n")})); (0, node_opcua_assert_1.assert)(Array.isArray(messages)); (0, node_opcua_assert_1.assert)(typeof messages[0] === "string"); response.responseHeader.stringTable = messages; // tslint:disable:no-console warningLog(chalk_1.default.cyan(" messages "), messages.join("\n")); return response; } // tslint:disable:no-var-requires const thenify_ex_1 = require("thenify-ex"); const opts = { multiArgs: false }; OPCUABaseServer.prototype.resumeEndPoints = (0, thenify_ex_1.withCallback)(OPCUABaseServer.prototype.resumeEndPoints, opts); OPCUABaseServer.prototype.suspendEndPoints = (0, thenify_ex_1.withCallback)(OPCUABaseServer.prototype.suspendEndPoints, opts); OPCUABaseServer.prototype.shutdownChannels = (0, thenify_ex_1.withCallback)(OPCUABaseServer.prototype.shutdownChannels, opts); //# sourceMappingURL=base_server.js.map