node-opcua-server
Version:
pure nodejs OPCUA SDK - module server
471 lines • 22.8 kB
JavaScript
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
;